diff --git a/tests/Feature/FullPlaylistExportTest.php b/tests/Feature/FullPlaylistExportTest.php new file mode 100644 index 0000000..5c089d6 --- /dev/null +++ b/tests/Feature/FullPlaylistExportTest.php @@ -0,0 +1,305 @@ +put('slides/kv.jpg', 'keyvisual-image'); + Storage::disk('public')->put('slides/bg.jpg', 'background-image'); + Storage::disk('public')->put('slides/sermon1.jpg', 'sermon-image-1'); + Storage::disk('public')->put('slides/sermon2.jpg', 'sermon-image-2'); + + $service = Service::factory()->create([ + 'title' => 'Voller Gottesdienst', + 'date' => now(), + 'key_visual_filename' => 'slides/kv.jpg', + 'background_filename' => 'slides/bg.jpg', + 'moderator_name' => 'Moderator Max', + 'preacher_name' => 'Pastor Paul', + 'preacher_name_override' => null, + 'has_agenda' => true, + ]); + + $song = $this->createSongWithContent('Großer Gott'); + $serviceSong = ServiceSong::create([ + 'service_id' => $service->id, + 'song_id' => $song->id, + 'cts_song_name' => 'Großer Gott', + 'order' => 1, + ]); + ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Großer Gott', + 'service_song_id' => $serviceSong->id, + 'sort_order' => 1, + 'is_before_event' => false, + ]); + + $sermonItem = ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Predigt', + 'service_song_id' => null, + 'sort_order' => 2, + 'is_before_event' => false, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $sermonItem->id, + 'type' => 'sermon', + 'original_filename' => 'sermon1.jpg', + 'stored_filename' => 'slides/sermon1.jpg', + 'sort_order' => 0, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $sermonItem->id, + 'type' => 'sermon', + 'original_filename' => 'sermon2.jpg', + 'stored_filename' => 'slides/sermon2.jpg', + 'sort_order' => 1, + ]); + + $slideCountBefore = Slide::count(); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + $playlist = ProPlaylistReader::read($result['path']); + $names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); + + $moderatorIndex = array_search('Moderator', $names, true); + $songIndex = array_search('Großer Gott', $names, true); + $kvIndex = array_search('Keyvisual-Predigt', $names, true); + $preacherIndex = array_search('Predigername', $names, true); + $sermonIndex = array_search('Predigt', $names, true); + + $this->assertNotFalse($moderatorIndex, 'Moderator nametag missing'); + $this->assertNotFalse($songIndex, 'Song presentation missing'); + $this->assertNotFalse($kvIndex, 'Keyvisual-Predigt entry missing'); + $this->assertNotFalse($preacherIndex, 'Predigername (preacher nametag) missing'); + $this->assertNotFalse($sermonIndex, 'Predigt (sermon slides) missing'); + + $this->assertSame(0, $moderatorIndex, 'Moderator nametag must be first'); + $this->assertLessThan($songIndex, $moderatorIndex); + $this->assertLessThan($kvIndex, $songIndex); + $this->assertLessThan($preacherIndex, $kvIndex, 'Keyvisual must come before preacher nametag'); + $this->assertLessThan($sermonIndex, $preacherIndex, 'Preacher nametag must come before sermon slides'); + + $songParser = $playlist->getEmbeddedSong('Großer Gott.pro'); + $this->assertNotNull($songParser, 'Embedded song .pro missing'); + $songSlides = $this->allParserSlides($songParser); + $this->assertNotEmpty($songSlides); + foreach ($songSlides as $slide) { + $this->assertTrue($slide->hasBackgroundMedia(), 'Song slide must have background media'); + $this->assertSame( + Storage::disk('public')->path('slides/bg.jpg'), + $slide->getBackgroundMediaUrl(), + ); + } + + $sermonParser = $playlist->getEmbeddedSong('Predigt.pro'); + $this->assertNotNull($sermonParser, 'Embedded sermon .pro missing'); + $sermonSlides = $this->allParserSlides($sermonParser); + $this->assertCount(2, $sermonSlides); + foreach ($sermonSlides as $slide) { + $this->assertTrue($slide->hasBackgroundMedia(), 'Sermon slide must have background media'); + } + + $this->assertSame($slideCountBefore, Slide::count()); + + $this->cleanupTempDir($result['temp_dir']); + } + + public function test_without_macro_no_nametags_and_sermon_keeps_keyvisual_and_slides(): void + { + Setting::set('namenseinblender_macro_name', ''); + + Storage::disk('public')->put('slides/kv.jpg', 'keyvisual-image'); + Storage::disk('public')->put('slides/sermon1.jpg', 'sermon-image-1'); + Storage::disk('public')->put('slides/sermon2.jpg', 'sermon-image-2'); + + $service = Service::factory()->create([ + 'title' => 'Ohne Macro', + 'date' => now(), + 'key_visual_filename' => 'slides/kv.jpg', + 'background_filename' => null, + 'moderator_name' => 'Moderator Max', + 'preacher_name' => 'Pastor Paul', + 'preacher_name_override' => null, + 'has_agenda' => true, + ]); + + $sermonItem = ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Predigt', + 'service_song_id' => null, + 'sort_order' => 1, + 'is_before_event' => false, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $sermonItem->id, + 'type' => 'sermon', + 'original_filename' => 'sermon1.jpg', + 'stored_filename' => 'slides/sermon1.jpg', + 'sort_order' => 0, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $sermonItem->id, + 'type' => 'sermon', + 'original_filename' => 'sermon2.jpg', + 'stored_filename' => 'slides/sermon2.jpg', + 'sort_order' => 1, + ]); + + $slideCountBefore = Slide::count(); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + $playlist = ProPlaylistReader::read($result['path']); + $names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); + + $this->assertNotContains('Moderator', $names); + $this->assertNotContains('Predigername', $names); + + $kvIndex = array_search('Keyvisual-Predigt', $names, true); + $sermonIndex = array_search('Predigt', $names, true); + $this->assertNotFalse($kvIndex, 'Keyvisual-Predigt entry missing'); + $this->assertNotFalse($sermonIndex, 'Predigt entry missing'); + $this->assertLessThan($sermonIndex, $kvIndex, 'Keyvisual must come before sermon slides'); + + $sermonParser = $playlist->getEmbeddedSong('Predigt.pro'); + $this->assertNotNull($sermonParser); + $this->assertCount(2, $this->allParserSlides($sermonParser)); + + $this->assertSame($slideCountBefore, Slide::count()); + + $this->cleanupTempDir($result['temp_dir']); + } + + public function test_service_without_agenda_falls_back_to_legacy_export_without_crash(): void + { + Setting::set('namenseinblender_macro_name', 'Namenseinblender'); + + $service = Service::factory()->create([ + 'title' => 'Ohne Agenda', + 'date' => now(), + 'key_visual_filename' => null, + 'background_filename' => null, + 'has_agenda' => false, + ]); + + $song = $this->createSongWithContent('Legacy Lied'); + ServiceSong::create([ + 'service_id' => $service->id, + 'song_id' => $song->id, + 'cts_song_name' => 'Legacy Lied', + 'order' => 1, + ]); + + $slideCountBefore = Slide::count(); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + + $this->assertArrayHasKey('path', $result); + $this->assertArrayHasKey('filename', $result); + $this->assertFileExists($result['path']); + + $playlist = ProPlaylistReader::read($result['path']); + $names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries()); + + $this->assertNotContains('Moderator', $names); + $this->assertNotContains('Predigername', $names); + $this->assertNotContains('Keyvisual-Predigt', $names); + + $this->assertNotNull($playlist->getEmbeddedSong('Legacy Lied.pro')); + + $this->assertSame($slideCountBefore, Slide::count()); + + $this->cleanupTempDir($result['temp_dir']); + } + + private function createSongWithContent(string $title): Song + { + $song = Song::create([ + 'title' => $title, + 'ccli_id' => fake()->unique()->numerify('#####'), + 'author' => 'Test Author', + 'copyright_text' => 'Test Publisher', + ]); + + $label = Label::firstOrCreate( + ['name' => 'Verse 1 - '.$title], + ['color' => '#2196F3'], + ); + $label->songSlides()->create(['order' => 0, 'text_content' => 'Erste Zeile']); + + $arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]); + $arrangement->arrangementLabels()->create(['label_id' => $label->id, 'order' => 0]); + + return $song; + } + + private function allParserSlides(\ProPresenter\Parser\Song $parserSong): array + { + $slides = []; + + foreach ($parserSong->getGroups() as $group) { + foreach ($parserSong->getSlidesForGroup($group) as $slide) { + $slides[] = $slide; + } + } + + return $slides; + } + + private function cleanupTempDir(string $dir): void + { + if (! is_dir($dir)) { + return; + } + + $items = scandir($dir); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir.'/'.$item; + is_dir($path) ? $this->cleanupTempDir($path) : unlink($path); + } + + rmdir($dir); + } +}