feat(export): append looping Abspann presentation with keyvisual and info slides
This commit is contained in:
parent
e47299f90a
commit
1c811a0550
|
|
@ -16,6 +16,10 @@
|
||||||
|
|
||||||
class PlaylistExportService
|
class PlaylistExportService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MacroResolutionService $macroResolutionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** @return array{path: string, filename: string, skipped: int, warnings: array<int, string>} */
|
/** @return array{path: string, filename: string, skipped: int, warnings: array<int, string>} */
|
||||||
public function generatePlaylist(Service $service, bool $preview = false): array
|
public function generatePlaylist(Service $service, bool $preview = false): array
|
||||||
{
|
{
|
||||||
|
|
@ -43,16 +47,7 @@ public function generatePlaylist(Service $service, bool $preview = false): array
|
||||||
*/
|
*/
|
||||||
private function generatePlaylistFromAgenda(Service $service, Collection $agendaItems, bool $preview = false): array
|
private function generatePlaylistFromAgenda(Service $service, Collection $agendaItems, bool $preview = false): array
|
||||||
{
|
{
|
||||||
$informationSlides = Slide::where('type', 'information')
|
$informationSlides = $this->informationSlidesFor($service);
|
||||||
->where(fn ($q) => $q->whereNull('expire_date')->orWhereDate('expire_date', '>=', $service->date))
|
|
||||||
->where(fn ($q) => $q->whereNull('service_id')->orWhere('service_id', $service->id))
|
|
||||||
->whereNull('deleted_at');
|
|
||||||
|
|
||||||
if ($service->date) {
|
|
||||||
$informationSlides->whereDate('uploaded_at', '<=', $service->date);
|
|
||||||
}
|
|
||||||
|
|
||||||
$informationSlides = $informationSlides->orderBy('sort_order')->orderByDesc('uploaded_at')->get();
|
|
||||||
|
|
||||||
$announcementPatterns = Setting::get('agenda_announcement_position');
|
$announcementPatterns = Setting::get('agenda_announcement_position');
|
||||||
$announcementInserted = false;
|
$announcementInserted = false;
|
||||||
|
|
@ -221,6 +216,8 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
|
||||||
|
|
||||||
$this->injectExportProFiles($playlistItems, $embeddedFiles);
|
$this->injectExportProFiles($playlistItems, $embeddedFiles);
|
||||||
|
|
||||||
|
$this->addAbspannPresentation($service, $tempDir, $playlistItems, $embeddedFiles);
|
||||||
|
|
||||||
$dateFormatted = $service->date?->format('Y-m-d') ?? now()->format('Y-m-d');
|
$dateFormatted = $service->date?->format('Y-m-d') ?? now()->format('Y-m-d');
|
||||||
$safeTitle = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $service->title);
|
$safeTitle = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $service->title);
|
||||||
|
|
||||||
|
|
@ -334,6 +331,8 @@ private function generatePlaylistLegacy(Service $service, bool $preview = false)
|
||||||
|
|
||||||
$this->injectExportProFiles($playlistItems, $embeddedFiles);
|
$this->injectExportProFiles($playlistItems, $embeddedFiles);
|
||||||
|
|
||||||
|
$this->addAbspannPresentation($service, $tempDir, $playlistItems, $embeddedFiles);
|
||||||
|
|
||||||
$dateFormatted = $service->date?->format('Y-m-d') ?? now()->format('Y-m-d');
|
$dateFormatted = $service->date?->format('Y-m-d') ?? now()->format('Y-m-d');
|
||||||
$safeTitle = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $service->title);
|
$safeTitle = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $service->title);
|
||||||
|
|
||||||
|
|
@ -710,9 +709,9 @@ private function extractProBundle(string $absPath): ?array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function writeProFile(string $path, string $name, array $groups, array $arrangements): void
|
protected function writeProFile(string $path, string $name, array $groups, array $arrangements, array $options = []): void
|
||||||
{
|
{
|
||||||
ProFileGenerator::generateAndWrite($path, $name, $groups, $arrangements);
|
ProFileGenerator::generateAndWrite($path, $name, $groups, $arrangements, [], $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildModeratorSlideData(Service $service): ?array
|
private function buildModeratorSlideData(Service $service): ?array
|
||||||
|
|
@ -781,6 +780,107 @@ private function addSermonIntroPresentation(Service $service, string $tempDir, a
|
||||||
$playlistItems[] = ['type' => 'presentation', 'name' => $name, 'path' => $filename];
|
$playlistItems[] = ['type' => 'presentation', 'name' => $name, 'path' => $filename];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information slides visible for the given service: type=information, not soft-deleted,
|
||||||
|
* global or this-service, unexpired at (and uploaded on/before) the service date.
|
||||||
|
*
|
||||||
|
* @return Collection<int, Slide>
|
||||||
|
*/
|
||||||
|
private function informationSlidesFor(Service $service): Collection
|
||||||
|
{
|
||||||
|
$query = Slide::where('type', 'information')
|
||||||
|
->where(fn ($q) => $q->whereNull('expire_date')->orWhereDate('expire_date', '>=', $service->date))
|
||||||
|
->where(fn ($q) => $q->whereNull('service_id')->orWhere('service_id', $service->id))
|
||||||
|
->whereNull('deleted_at');
|
||||||
|
|
||||||
|
if ($service->date) {
|
||||||
|
$query->whereDate('uploaded_at', '<=', $service->date);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->orderBy('sort_order')->orderByDesc('uploaded_at')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the closing "Abspann" presentation as the very last playlist item.
|
||||||
|
*
|
||||||
|
* Slides: key-visual (first) followed by ALL information slides. Every cue
|
||||||
|
* auto-advances after 10s; the last cue loops back to the first ("auto repeat
|
||||||
|
* all"). The presentation uses a Dissolve transition and each cue carries any
|
||||||
|
* configured 'abspann' macro. Emits nothing when there is neither a key-visual
|
||||||
|
* nor an information slide.
|
||||||
|
*/
|
||||||
|
private function addAbspannPresentation(Service $service, string $tempDir, array &$playlistItems, array &$embeddedFiles): void
|
||||||
|
{
|
||||||
|
$informationSlides = $this->informationSlidesFor($service);
|
||||||
|
$kvData = $this->keyVisualData($service);
|
||||||
|
|
||||||
|
if ($kvData === null && $informationSlides->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slides = [];
|
||||||
|
|
||||||
|
if ($kvData !== null) {
|
||||||
|
$this->embedKeyVisual($service, $embeddedFiles);
|
||||||
|
$slides[] = [
|
||||||
|
'imageOnly' => true,
|
||||||
|
'background' => $kvData,
|
||||||
|
'completion' => ['time' => 10.0, 'target' => 'next'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($informationSlides->values() as $index => $slide) {
|
||||||
|
$storedPath = Storage::disk('public')->path($slide->stored_filename);
|
||||||
|
|
||||||
|
if (! file_exists($storedPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageFilename = 'abspann_'.($index + 1).'_'.basename($slide->stored_filename);
|
||||||
|
$destPath = $tempDir.'/'.$imageFilename;
|
||||||
|
copy($storedPath, $destPath);
|
||||||
|
|
||||||
|
$embeddedFiles[$imageFilename] = file_get_contents($destPath);
|
||||||
|
|
||||||
|
$slides[] = [
|
||||||
|
'media' => $imageFilename,
|
||||||
|
'format' => 'JPG',
|
||||||
|
'label' => $slide->original_filename,
|
||||||
|
'bundleRelative' => true,
|
||||||
|
'completion' => ['time' => 10.0, 'target' => 'next'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($slides)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count($slides);
|
||||||
|
foreach ($slides as $i => &$sd) {
|
||||||
|
$macros = $this->macroResolutionService->macrosForSlide($service, 'abspann', [
|
||||||
|
'index' => $i,
|
||||||
|
'total' => $total,
|
||||||
|
'label_id' => null,
|
||||||
|
]);
|
||||||
|
if (! empty($macros)) {
|
||||||
|
$sd['macro'] = $macros[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($sd);
|
||||||
|
|
||||||
|
$slides[$total - 1]['completion']['target'] = 'first';
|
||||||
|
|
||||||
|
$name = 'Abspann';
|
||||||
|
$groups = [['name' => $name, 'color' => [0, 0, 0, 1], 'slides' => $slides]];
|
||||||
|
$arrangements = [['name' => 'normal', 'groupNames' => [$name]]];
|
||||||
|
|
||||||
|
$filename = 'Abspann-'.uniqid().'.pro';
|
||||||
|
$path = $tempDir.'/'.$filename;
|
||||||
|
$this->writeProFile($path, $name, $groups, $arrangements, ['transition' => 'dissolve', 'transitionDuration' => 1.0]);
|
||||||
|
$embeddedFiles[$filename] = file_get_contents($path);
|
||||||
|
$playlistItems[] = ['type' => 'presentation', 'name' => $name, 'path' => $filename];
|
||||||
|
}
|
||||||
|
|
||||||
private function writeProAndEmbed(string $name, array $slideData, string $tempDir, array &$playlistItems, array &$embeddedFiles): void
|
private function writeProAndEmbed(string $name, array $slideData, string $tempDir, array &$playlistItems, array &$embeddedFiles): void
|
||||||
{
|
{
|
||||||
$groups = [['name' => $name, 'color' => [0, 0, 0, 1], 'slides' => [$slideData]]];
|
$groups = [['name' => $name, 'color' => [0, 0, 0, 1], 'slides' => [$slideData]]];
|
||||||
|
|
|
||||||
225
tests/Feature/AbspannExportTest.php
Normal file
225
tests/Feature/AbspannExportTest.php
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Label;
|
||||||
|
use App\Models\Macro;
|
||||||
|
use App\Models\MacroAssignment;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceAgendaItem;
|
||||||
|
use App\Models\ServiceSong;
|
||||||
|
use App\Models\Slide;
|
||||||
|
use App\Models\Song;
|
||||||
|
use App\Services\PlaylistExportService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use ProPresenter\Parser\ProPlaylistReader;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
final class AbspannExportTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Storage::fake('public');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_abspann_is_last_item_with_keyvisual_first_then_information_slides(): void
|
||||||
|
{
|
||||||
|
Storage::disk('public')->put('slides/kv.jpg', 'keyvisual-image');
|
||||||
|
Storage::disk('public')->put('slides/info1.jpg', 'info-image-1');
|
||||||
|
Storage::disk('public')->put('slides/info2.jpg', 'info-image-2');
|
||||||
|
|
||||||
|
$service = $this->serviceWithSong('Abspann Service', 'slides/kv.jpg');
|
||||||
|
$this->createInfoSlide('info1.jpg', 'slides/info1.jpg', 0);
|
||||||
|
$this->createInfoSlide('info2.jpg', 'slides/info2.jpg', 1);
|
||||||
|
|
||||||
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
||||||
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
|
$entries = $playlist->getEntries();
|
||||||
|
$names = array_map(fn ($entry) => $entry->getName(), $entries);
|
||||||
|
|
||||||
|
// Abspann is the very last playlist item and appears exactly once.
|
||||||
|
$last = end($entries);
|
||||||
|
$this->assertSame('Abspann', $last->getName(), 'Abspann must be the last playlist item');
|
||||||
|
$this->assertSame(1, count(array_filter($names, fn ($n) => $n === 'Abspann')));
|
||||||
|
|
||||||
|
// The embedded Abspann .pro has 1 (keyvisual) + 2 (information) = 3 cues.
|
||||||
|
$abspann = $playlist->getEmbeddedSong($last->getDocumentFilename());
|
||||||
|
$this->assertNotNull($abspann);
|
||||||
|
$slides = $this->allParserSlides($abspann);
|
||||||
|
$this->assertCount(3, $slides);
|
||||||
|
|
||||||
|
// First cue is the key-visual (image-only background media).
|
||||||
|
$this->assertTrue($slides[0]->hasBackgroundMedia(), 'First Abspann cue must be the key-visual');
|
||||||
|
$this->assertSame('KEY_VISUAL.jpg', $slides[0]->getBackgroundMediaUrl());
|
||||||
|
$this->assertFalse($slides[0]->hasMedia(), 'Key-visual cue has no foreground media');
|
||||||
|
$this->assertArrayHasKey('KEY_VISUAL.jpg', $playlist->getEmbeddedMediaFiles());
|
||||||
|
|
||||||
|
// Remaining cues are the information slides (foreground media, labelled by filename).
|
||||||
|
$this->assertTrue($slides[1]->hasMedia());
|
||||||
|
$this->assertFalse($slides[1]->hasBackgroundMedia());
|
||||||
|
$this->assertSame('info1.jpg', $slides[1]->getLabel());
|
||||||
|
$this->assertTrue($slides[2]->hasMedia());
|
||||||
|
$this->assertSame('info2.jpg', $slides[2]->getLabel());
|
||||||
|
|
||||||
|
$this->cleanupTempDir($result['temp_dir']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-advance (10s), loop-to-first and the Dissolve transition are emitted into
|
||||||
|
* the slideData by addAbspannPresentation and are covered by the parser repo's
|
||||||
|
* in-memory ProFileGeneratorCompletionTest. They are intentionally NOT asserted
|
||||||
|
* here: the parser's generated protobuf descriptor does not (yet) register the
|
||||||
|
* Cue.completion_* and Presentation.transition fields, so serializeToString()
|
||||||
|
* drops them on the disk round-trip that every export performs. Once the parser
|
||||||
|
* stubs are regenerated these become observable on the exported .proplaylist and
|
||||||
|
* this note can be replaced with real assertions.
|
||||||
|
*/
|
||||||
|
public function test_abspann_cues_carry_configured_abspann_macro(): void
|
||||||
|
{
|
||||||
|
Storage::disk('public')->put('slides/kv.jpg', 'keyvisual-image');
|
||||||
|
Storage::disk('public')->put('slides/info1.jpg', 'info-image-1');
|
||||||
|
|
||||||
|
$macro = Macro::factory()->create(['name' => 'Abspann Macro']);
|
||||||
|
MacroAssignment::create([
|
||||||
|
'part_type' => 'abspann',
|
||||||
|
'macro_id' => $macro->id,
|
||||||
|
'position' => 'all_slides',
|
||||||
|
'order' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = $this->serviceWithSong('Abspann Macro Service', 'slides/kv.jpg');
|
||||||
|
$this->createInfoSlide('info1.jpg', 'slides/info1.jpg', 0);
|
||||||
|
|
||||||
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
||||||
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
|
$entries = $playlist->getEntries();
|
||||||
|
$last = end($entries);
|
||||||
|
|
||||||
|
$abspann = $playlist->getEmbeddedSong($last->getDocumentFilename());
|
||||||
|
$slides = $this->allParserSlides($abspann);
|
||||||
|
$this->assertCount(2, $slides);
|
||||||
|
|
||||||
|
foreach ($slides as $slide) {
|
||||||
|
$this->assertTrue($slide->hasMacro(), 'Every Abspann cue must carry the abspann macro');
|
||||||
|
$this->assertSame('Abspann Macro', $slide->getMacroName());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cleanupTempDir($result['temp_dir']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_no_abspann_when_no_keyvisual_and_no_information_slides(): void
|
||||||
|
{
|
||||||
|
// A service with a song but no key-visual and no information slides.
|
||||||
|
$service = $this->serviceWithSong('Ohne Abspann', null);
|
||||||
|
|
||||||
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
||||||
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
|
$names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries());
|
||||||
|
|
||||||
|
$this->assertNotContains('Abspann', $names, 'No Abspann item without key-visual and information slides');
|
||||||
|
|
||||||
|
$this->cleanupTempDir($result['temp_dir']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serviceWithSong(string $title, ?string $keyVisual): Service
|
||||||
|
{
|
||||||
|
$service = Service::factory()->create([
|
||||||
|
'title' => $title,
|
||||||
|
'date' => now(),
|
||||||
|
'key_visual_filename' => $keyVisual,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$song = $this->createSongWithContent($title.' Lied');
|
||||||
|
$serviceSong = ServiceSong::create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'song_id' => $song->id,
|
||||||
|
'cts_song_name' => $title.' Lied',
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
ServiceAgendaItem::factory()->create([
|
||||||
|
'service_id' => $service->id,
|
||||||
|
'title' => $title.' Lied',
|
||||||
|
'service_song_id' => $serviceSong->id,
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_before_event' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $service;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createInfoSlide(string $original, string $stored, int $sortOrder): Slide
|
||||||
|
{
|
||||||
|
return Slide::factory()->create([
|
||||||
|
'type' => 'information',
|
||||||
|
'service_id' => null,
|
||||||
|
'original_filename' => $original,
|
||||||
|
'stored_filename' => $stored,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'expire_date' => null,
|
||||||
|
'uploaded_at' => now()->subDay(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'],
|
||||||
|
);
|
||||||
|
$section = $song->sections()->create(['label_id' => $label->id, 'order' => 0]);
|
||||||
|
$section->slides()->create(['order' => 0, 'text_content' => 'Erste Zeile']);
|
||||||
|
|
||||||
|
$arrangement = $song->arrangements()->create(['name' => 'normal', 'is_default' => true]);
|
||||||
|
$arrangement->arrangementSections()->create(['song_section_id' => $section->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,8 +48,11 @@ public function test_non_song_agenda_item_without_slides_becomes_headline(): voi
|
||||||
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
||||||
$playlist = ProPlaylistReader::read($result['path']);
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
|
|
||||||
// Content-less item before any real content → HEADER playlist item (no embedded .pro)
|
// Content-less item before any real content → HEADER playlist item (no embedded .pro
|
||||||
$this->assertCount(0, $playlist->getEmbeddedProFiles());
|
// of its own). The only embedded .pro is the trailing looping Abspann key-visual.
|
||||||
|
$proFiles = $playlist->getEmbeddedProFiles();
|
||||||
|
$this->assertCount(1, $proFiles);
|
||||||
|
$this->assertStringStartsWith('Abspann-', array_key_first($proFiles));
|
||||||
$this->assertSame($slideCountBefore, Slide::count());
|
$this->assertSame($slideCountBefore, Slide::count());
|
||||||
|
|
||||||
// Playlist has a header entry named 'Begrüßung'
|
// Playlist has a header entry named 'Begrüßung'
|
||||||
|
|
@ -62,8 +65,8 @@ public function test_non_song_agenda_item_without_slides_becomes_headline(): voi
|
||||||
}
|
}
|
||||||
$this->assertNotNull($headerEntry, 'Expected a HEADER playlist entry for Begrüßung');
|
$this->assertNotNull($headerEntry, 'Expected a HEADER playlist entry for Begrüßung');
|
||||||
|
|
||||||
// No KEY_VISUAL.jpg embedded (header has no background media)
|
// The key-visual is embedded via the trailing Abspann presentation.
|
||||||
$this->assertArrayNotHasKey('KEY_VISUAL.jpg', $playlist->getEmbeddedMediaFiles());
|
$this->assertArrayHasKey('KEY_VISUAL.jpg', $playlist->getEmbeddedMediaFiles());
|
||||||
|
|
||||||
$this->cleanupTempDir($result['temp_dir']);
|
$this->cleanupTempDir($result['temp_dir']);
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +99,8 @@ public function test_song_agenda_item_does_not_get_keyvisual_fallback(): void
|
||||||
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
||||||
$playlist = ProPlaylistReader::read($result['path']);
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
|
|
||||||
$this->assertCount(1, $playlist->getEmbeddedProFiles());
|
// The song .pro plus the trailing looping Abspann key-visual presentation.
|
||||||
|
$this->assertCount(2, $playlist->getEmbeddedProFiles());
|
||||||
$this->assertNotNull($playlist->getEmbeddedSong('Nur ein Lied.pro'));
|
$this->assertNotNull($playlist->getEmbeddedSong('Nur ein Lied.pro'));
|
||||||
$this->assertNull($playlist->getEmbeddedSong('Keyvisual.pro'));
|
$this->assertNull($playlist->getEmbeddedSong('Keyvisual.pro'));
|
||||||
|
|
||||||
|
|
@ -136,7 +140,8 @@ public function test_sermon_agenda_item_with_uploaded_slides_prepends_keyvisual_
|
||||||
$playlist = ProPlaylistReader::read($result['path']);
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
$sermonSong = $playlist->getEmbeddedSong('Predigt.pro');
|
$sermonSong = $playlist->getEmbeddedSong('Predigt.pro');
|
||||||
|
|
||||||
$this->assertCount(2, $playlist->getEmbeddedProFiles());
|
// Keyvisual-Predigt intro + Predigt sermon slides + trailing looping Abspann.
|
||||||
|
$this->assertCount(3, $playlist->getEmbeddedProFiles());
|
||||||
$this->assertNotNull($sermonSong);
|
$this->assertNotNull($sermonSong);
|
||||||
|
|
||||||
$names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries());
|
$names = array_map(fn ($entry) => $entry->getName(), $playlist->getEntries());
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ public function test_sermon_sequence_is_keyvisual_preacher_nametag_then_uploaded
|
||||||
// A moderator name tag may precede the sermon intro when the (factory-random)
|
// A moderator name tag may precede the sermon intro when the (factory-random)
|
||||||
// responsible person resolves to a moderator; skip it if present.
|
// responsible person resolves to a moderator; skip it if present.
|
||||||
$offset = str_starts_with((string) ($names[0] ?? ''), 'Moderator') ? 1 : 0;
|
$offset = str_starts_with((string) ($names[0] ?? ''), 'Moderator') ? 1 : 0;
|
||||||
$this->assertSame(['Prediger - Erika Predigt', 'Predigt'], array_slice($names, $offset));
|
// A looping "Abspann" (key-visual) presentation is appended as the last item.
|
||||||
|
$this->assertSame(['Prediger - Erika Predigt', 'Predigt', 'Abspann'], array_slice($names, $offset));
|
||||||
|
|
||||||
// The sermon intro is ONE presentation with two cues: the key-visual alone,
|
// The sermon intro is ONE presentation with two cues: the key-visual alone,
|
||||||
// then the same key-visual with the preacher name tag rendered on top.
|
// then the same key-visual with the preacher name tag rendered on top.
|
||||||
|
|
@ -161,7 +162,8 @@ public function test_without_macro_configured_no_nametags_are_added_and_sermon_s
|
||||||
$playlist = ProPlaylistReader::read($result['path']);
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
$entries = $playlist->getEntries();
|
$entries = $playlist->getEntries();
|
||||||
|
|
||||||
$this->assertSame(['Keyvisual-Predigt', 'Predigt'], $this->entryNames($playlist));
|
// A looping "Abspann" (key-visual) presentation is appended as the last item.
|
||||||
|
$this->assertSame(['Keyvisual-Predigt', 'Predigt', 'Abspann'], $this->entryNames($playlist));
|
||||||
$this->assertCount(1, $this->slidesForEntry($playlist, $entries[0]));
|
$this->assertCount(1, $this->slidesForEntry($playlist, $entries[0]));
|
||||||
$sermonSlides = $this->slidesForEntry($playlist, $entries[1]);
|
$sermonSlides = $this->slidesForEntry($playlist, $entries[1]);
|
||||||
$this->assertCount(2, $sermonSlides);
|
$this->assertCount(2, $sermonSlides);
|
||||||
|
|
@ -280,8 +282,9 @@ public function test_sermon_item_without_uploaded_slides_still_emits_intro_and_n
|
||||||
$playlist = ProPlaylistReader::read($result['path']);
|
$playlist = ProPlaylistReader::read($result['path']);
|
||||||
$entries = $playlist->getEntries();
|
$entries = $playlist->getEntries();
|
||||||
|
|
||||||
// A sermon item with no uploaded slides emits ONLY the intro presentation.
|
// A sermon item with no uploaded slides emits ONLY the intro presentation,
|
||||||
$this->assertSame(['Prediger - Erika Predigt'], $this->entryNames($playlist));
|
// plus the looping "Abspann" (key-visual) presentation appended as the last item.
|
||||||
|
$this->assertSame(['Prediger - Erika Predigt', 'Abspann'], $this->entryNames($playlist));
|
||||||
|
|
||||||
// The intro .pro carries both cues: key-visual alone + key-visual with name tag.
|
// The intro .pro carries both cues: key-visual alone + key-visual with name tag.
|
||||||
$introSlides = $this->slidesForEntry($playlist, $entries[0]);
|
$introSlides = $this->slidesForEntry($playlist, $entries[0]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue