title, $this->buildGroups($song, $service), $this->buildArrangements($song), $this->buildCcliMetadata($song), ); return $tempPath; } public function generateParserSong(Song $song, ?Service $service = null): \ProPresenter\Parser\Song { $song->loadMissing(['arrangements.arrangementSections.section.slides', 'arrangements.arrangementSections.section.label']); return ProFileGenerator::generate( $song->title, $this->buildGroups($song, $service), $this->buildArrangements($song), $this->buildCcliMetadata($song), ); } private function buildGroups(Song $song, ?Service $service = null): array { $defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first(); if ($defaultArr === null) { return []; } $defaultArr->loadMissing('arrangementSections.section.slides', 'arrangementSections.section.label'); $groups = []; $seenSectionIds = []; $background = $this->backgroundData($service); foreach ($defaultArr->arrangementSections->sortBy('order') as $arrangementSection) { $section = $arrangementSection->section; $label = $section?->label; if ($section === null || $label === null) { continue; } if (in_array($section->id, $seenSectionIds, true)) { continue; } $seenSectionIds[] = $section->id; $slides = []; $sectionSlides = $section->slides->sortBy('order')->values(); $totalSlides = $sectionSlides->count(); foreach ($sectionSlides as $slideIndex => $slide) { $slideData = ['text' => $slide->text_content ?? '']; if ($slide->text_content_translated) { $slideData['translation'] = $slide->text_content_translated; } if ($background !== null && ! $this->isFullCoverImageSlide($slide, $slideData)) { $slideData['background'] = $background; } if ($service !== null) { $macros = $this->macroResolutionService->macrosForSlide( $service, 'song', ['index' => $slideIndex, 'total' => $totalSlides, 'label_id' => $label->id], ); if (! empty($macros)) { // ProPresenter parser currently supports one `macro` entry per slide; keep the first resolved macro until stacked macros are supported. $slideData['macro'] = $macros[0]; } } $slides[] = $slideData; } $groups[] = [ 'name' => $label->name, 'color' => ProImportService::hexToRgba($label->color ?? '#808080'), 'slides' => $slides, ]; } return $groups; } private function backgroundData(?Service $service): ?array { if ($service === null) { return null; } $background = $this->imageResolver->backgroundFor($service); if ($background === null) { return null; } return [ 'path' => ServiceImageResolver::BACKGROUND_EXPORT_NAME, 'format' => 'JPG', 'width' => 1920, 'height' => 1080, 'bundleRelative' => true, ]; } private function isFullCoverImageSlide(object $slide, array $slideData): bool { if (! isset($slideData['media'])) { return false; } return ($slide->cover_mode ?? null) === true; } private function buildArrangements(Song $song): array { $arrangements = []; foreach ($song->arrangements as $arrangement) { $arrangement->loadMissing('arrangementSections.section.label'); $groupNames = $arrangement->arrangementSections ->sortBy('order') ->map(fn ($arrangementSection) => $arrangementSection->section?->label?->name) ->filter() ->values() ->toArray(); $arrangements[] = [ 'name' => $arrangement->name, 'groupNames' => $groupNames, ]; } return $arrangements; } private function buildCcliMetadata(Song $song): array { return array_filter([ 'author' => $song->author, 'song_title' => $song->title, 'copyright_year' => $song->copyright_year, 'publisher' => $song->publisher, 'song_number' => $song->ccli_id ? (int) $song->ccli_id : null, ]); } }