Compare commits
No commits in common. "2a68d098a2361a4471f88558e5f43da1524892ee" and "ea4b86865032e8b26dc011428632cf3e928c5f73" have entirely different histories.
2a68d098a2
...
ea4b868650
|
|
@ -110,10 +110,7 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
|
||||||
$skippedUnmatched++;
|
$skippedUnmatched++;
|
||||||
$warnings[] = "Lied '".$song->title."' übersprungen: keine Inhaltsfolien.";
|
$warnings[] = "Lied '".$song->title."' übersprungen: keine Inhaltsfolien.";
|
||||||
} else {
|
} else {
|
||||||
$selectedArr = $serviceSong->arrangement
|
$proPath = $exportService->generateProFile($song, $service);
|
||||||
?? ($song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first());
|
|
||||||
|
|
||||||
$proPath = $exportService->generateProFile($song, $service, $selectedArr);
|
|
||||||
$proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';
|
$proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';
|
||||||
$destPath = $tempDir.'/'.$proFilename;
|
$destPath = $tempDir.'/'.$proFilename;
|
||||||
rename($proPath, $destPath);
|
rename($proPath, $destPath);
|
||||||
|
|
@ -121,15 +118,11 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
|
||||||
$embeddedFiles[$proFilename] = file_get_contents($destPath);
|
$embeddedFiles[$proFilename] = file_get_contents($destPath);
|
||||||
$this->embedBackground($service, $embeddedFiles);
|
$this->embedBackground($service, $embeddedFiles);
|
||||||
|
|
||||||
$playlistItem = [
|
$playlistItems[] = [
|
||||||
'type' => 'presentation',
|
'type' => 'presentation',
|
||||||
'name' => $song->title,
|
'name' => $song->title,
|
||||||
'path' => $proFilename,
|
'path' => $proFilename,
|
||||||
];
|
];
|
||||||
if ($selectedArr?->name !== null) {
|
|
||||||
$playlistItem['arrangement_name'] = $selectedArr->name;
|
|
||||||
}
|
|
||||||
$playlistItems[] = $playlistItem;
|
|
||||||
|
|
||||||
$realContentEmitted = true;
|
$realContentEmitted = true;
|
||||||
}
|
}
|
||||||
|
|
@ -275,12 +268,7 @@ private function generatePlaylistLegacy(Service $service, bool $preview = false)
|
||||||
$matchedSongs = $service->serviceSongs()
|
$matchedSongs = $service->serviceSongs()
|
||||||
->whereNotNull('song_id')
|
->whereNotNull('song_id')
|
||||||
->orderBy('order')
|
->orderBy('order')
|
||||||
->with(
|
->with('song.arrangements.arrangementSections.section.slides', 'song.arrangements.arrangementSections.section.label')
|
||||||
'song.arrangements.arrangementSections.section.slides',
|
|
||||||
'song.arrangements.arrangementSections.section.label',
|
|
||||||
'arrangement.arrangementSections.section.slides',
|
|
||||||
'arrangement.arrangementSections.section.label',
|
|
||||||
)
|
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
|
$skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
|
||||||
|
|
@ -315,10 +303,7 @@ private function generatePlaylistLegacy(Service $service, bool $preview = false)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedArr = $serviceSong->arrangement
|
$proPath = $exportService->generateProFile($song, $service);
|
||||||
?? ($song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first());
|
|
||||||
|
|
||||||
$proPath = $exportService->generateProFile($song, $service, $selectedArr);
|
|
||||||
$proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';
|
$proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';
|
||||||
$destPath = $tempDir.'/'.$proFilename;
|
$destPath = $tempDir.'/'.$proFilename;
|
||||||
rename($proPath, $destPath);
|
rename($proPath, $destPath);
|
||||||
|
|
@ -326,15 +311,11 @@ private function generatePlaylistLegacy(Service $service, bool $preview = false)
|
||||||
$embeddedFiles[$proFilename] = file_get_contents($destPath);
|
$embeddedFiles[$proFilename] = file_get_contents($destPath);
|
||||||
$this->embedBackground($service, $embeddedFiles);
|
$this->embedBackground($service, $embeddedFiles);
|
||||||
|
|
||||||
$playlistItem = [
|
$playlistItems[] = [
|
||||||
'type' => 'presentation',
|
'type' => 'presentation',
|
||||||
'name' => $song->title,
|
'name' => $song->title,
|
||||||
'path' => $proFilename,
|
'path' => $proFilename,
|
||||||
];
|
];
|
||||||
if ($selectedArr?->name !== null) {
|
|
||||||
$playlistItem['arrangement_name'] = $selectedArr->name;
|
|
||||||
}
|
|
||||||
$playlistItems[] = $playlistItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addSlidePresentation(
|
$this->addSlidePresentation(
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,6 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
|
||||||
'slides',
|
'slides',
|
||||||
'serviceSong.song.arrangements.arrangementSections.section.slides',
|
'serviceSong.song.arrangements.arrangementSections.section.slides',
|
||||||
'serviceSong.song.arrangements.arrangementSections.section.label',
|
'serviceSong.song.arrangements.arrangementSections.section.label',
|
||||||
'serviceSong.arrangement.arrangementSections.section.slides',
|
|
||||||
'serviceSong.arrangement.arrangementSections.section.label',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$title = $agendaItem->title ?: 'Ablauf-Element';
|
$title = $agendaItem->title ?: 'Ablauf-Element';
|
||||||
|
|
@ -58,8 +56,7 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
|
||||||
throw new RuntimeException('Lied "'.$song->title.'" übersprungen: keine Inhaltsfolien.');
|
throw new RuntimeException('Lied "'.$song->title.'" übersprungen: keine Inhaltsfolien.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$selected = $agendaItem->serviceSong->arrangement;
|
$parserSong = app(ProExportService::class)->generateParserSong($song, $agendaItem->service);
|
||||||
$parserSong = app(ProExportService::class)->generateParserSong($song, $agendaItem->service, $selected);
|
|
||||||
$proFilename = self::safeFilename($song->title).'.pro';
|
$proFilename = self::safeFilename($song->title).'.pro';
|
||||||
|
|
||||||
$songMediaFiles = [];
|
$songMediaFiles = [];
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@
|
||||||
|
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\Song;
|
use App\Models\Song;
|
||||||
use App\Models\SongArrangement;
|
|
||||||
use ProPresenter\Parser\ProFileGenerator;
|
use ProPresenter\Parser\ProFileGenerator;
|
||||||
use ProPresenter\Parser\ProFileWriter;
|
|
||||||
|
|
||||||
class ProExportService
|
class ProExportService
|
||||||
{
|
{
|
||||||
|
|
@ -15,72 +13,91 @@ public function __construct(
|
||||||
private readonly ServiceImageResolver $imageResolver,
|
private readonly ServiceImageResolver $imageResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function generateProFile(Song $song, ?Service $service = null, ?SongArrangement $selected = null): string
|
public function generateProFile(Song $song, ?Service $service = null): string
|
||||||
{
|
{
|
||||||
$tempPath = sys_get_temp_dir().'/'.uniqid('pro-export-').'.pro';
|
$tempPath = sys_get_temp_dir().'/'.uniqid('pro-export-').'.pro';
|
||||||
|
|
||||||
$parserSong = $this->generateParserSong($song, $service, $selected);
|
ProFileGenerator::generateAndWrite(
|
||||||
ProFileWriter::write($parserSong, $tempPath);
|
$tempPath,
|
||||||
|
|
||||||
return $tempPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function generateParserSong(Song $song, ?Service $service = null, ?SongArrangement $selected = null): \ProPresenter\Parser\Song
|
|
||||||
{
|
|
||||||
$song->loadMissing(['arrangements.arrangementSections.section.slides', 'arrangements.arrangementSections.section.label']);
|
|
||||||
|
|
||||||
$parserSong = ProFileGenerator::generate(
|
|
||||||
$song->title,
|
$song->title,
|
||||||
$this->buildGroups($song, $service, $selected),
|
$this->buildGroups($song, $service),
|
||||||
$this->buildArrangements($song),
|
$this->buildArrangements($song),
|
||||||
$this->buildCcliMetadata($song),
|
$this->buildCcliMetadata($song),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pre-select the chosen arrangement (protobuf field 10) by name so the export honours the
|
return $tempPath;
|
||||||
// per-service selection while keeping ALL arrangements available in the .pro file.
|
|
||||||
$chosen = $this->chosenArrangement($song, $selected);
|
|
||||||
|
|
||||||
if ($chosen !== null) {
|
|
||||||
$arrangement = $parserSong->getArrangementByName($chosen->name);
|
|
||||||
if ($arrangement !== null) {
|
|
||||||
$parserSong->setSelectedArrangementUuid($arrangement->getUuid());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $parserSong;
|
public function generateParserSong(Song $song, ?Service $service = null): \ProPresenter\Parser\Song
|
||||||
}
|
|
||||||
|
|
||||||
private function buildGroups(Song $song, ?Service $service = null, ?SongArrangement $selected = null): array
|
|
||||||
{
|
{
|
||||||
$selectedArr = $this->chosenArrangement($song, $selected);
|
$song->loadMissing(['arrangements.arrangementSections.section.slides', 'arrangements.arrangementSections.section.label']);
|
||||||
|
|
||||||
if ($selectedArr === null) {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$defaultArr->loadMissing('arrangementSections.section.slides', 'arrangementSections.section.label');
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
|
$seenSectionIds = [];
|
||||||
$background = $this->backgroundData($service);
|
$background = $this->backgroundData($service);
|
||||||
|
|
||||||
// Ordered, de-duplicated sections of the SELECTED arrangement (its play order).
|
// Pre-compute the total slide count across the whole song (same filtering as the loop below)
|
||||||
$selectedSections = $this->uniqueSectionsFor($selectedArr);
|
// so that 'first_slide' and 'last_slide' macro positions refer to the song's very first/last slide.
|
||||||
$selectedSectionIds = array_map(static fn ($section) => $section->id, $selectedSections);
|
|
||||||
|
|
||||||
// Total slide count across the selected arrangement's play order so that 'first_slide'
|
|
||||||
// and 'last_slide' macros anchor to the selected arrangement's very first/last slide.
|
|
||||||
$totalSlidesInSong = 0;
|
$totalSlidesInSong = 0;
|
||||||
foreach ($selectedSections as $section) {
|
$seenForCount = [];
|
||||||
|
foreach ($defaultArr->arrangementSections->sortBy('order') as $arrangementSection) {
|
||||||
|
$section = $arrangementSection->section;
|
||||||
|
if ($section === null || $section->label === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_array($section->id, $seenForCount, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seenForCount[] = $section->id;
|
||||||
$totalSlidesInSong += $section->slides->count();
|
$totalSlidesInSong += $section->slides->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
$groups = [];
|
|
||||||
$globalSlideIndex = 0;
|
$globalSlideIndex = 0;
|
||||||
|
|
||||||
// 1) Emit the selected arrangement's sections first, with position-aware macros.
|
foreach ($defaultArr->arrangementSections->sortBy('order') as $arrangementSection) {
|
||||||
foreach ($selectedSections as $section) {
|
$section = $arrangementSection->section;
|
||||||
$label = $section->label;
|
$label = $section?->label;
|
||||||
$slides = [];
|
|
||||||
|
|
||||||
foreach ($section->slides->sortBy('order')->values() as $slide) {
|
if ($section === null || $label === null) {
|
||||||
$slideData = $this->baseSlideData($slide, $background);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($section->id, $seenSectionIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seenSectionIds[] = $section->id;
|
||||||
|
|
||||||
|
$slides = [];
|
||||||
|
$sectionSlides = $section->slides->sortBy('order')->values();
|
||||||
|
|
||||||
|
foreach ($sectionSlides as $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) {
|
if ($service !== null) {
|
||||||
$macros = $this->macroResolutionService->macrosForSlide(
|
$macros = $this->macroResolutionService->macrosForSlide(
|
||||||
|
|
@ -106,110 +123,9 @@ private function buildGroups(Song $song, ?Service $service = null, ?SongArrangem
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Append sections referenced by OTHER arrangements so every arrangement's group-name refs
|
|
||||||
// resolve in the parser (unknown group names are silently dropped otherwise). These sections
|
|
||||||
// are not part of the selected play order → no position-based macros, no slide-index advance.
|
|
||||||
foreach ($this->appendedSections($song, $selectedSectionIds) as $section) {
|
|
||||||
$slides = [];
|
|
||||||
|
|
||||||
foreach ($section->slides->sortBy('order')->values() as $slide) {
|
|
||||||
$slides[] = $this->baseSlideData($slide, $background);
|
|
||||||
}
|
|
||||||
|
|
||||||
$groups[] = [
|
|
||||||
'name' => $section->label->name,
|
|
||||||
'color' => ProImportService::hexToRgba($section->label->color ?? '#808080'),
|
|
||||||
'slides' => $slides,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $groups;
|
return $groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the chosen arrangement: explicit selection → default (is_default) → first.
|
|
||||||
*/
|
|
||||||
private function chosenArrangement(Song $song, ?SongArrangement $selected): ?SongArrangement
|
|
||||||
{
|
|
||||||
return $selected
|
|
||||||
?? $song->arrangements->firstWhere('is_default', true)
|
|
||||||
?? $song->arrangements->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ordered, de-duplicated valid sections (section + label present) of a single arrangement.
|
|
||||||
*
|
|
||||||
* @return array<int, \App\Models\SongSection>
|
|
||||||
*/
|
|
||||||
private function uniqueSectionsFor(SongArrangement $arrangement): array
|
|
||||||
{
|
|
||||||
$arrangement->loadMissing('arrangementSections.section.slides', 'arrangementSections.section.label');
|
|
||||||
|
|
||||||
$sections = [];
|
|
||||||
$seen = [];
|
|
||||||
|
|
||||||
foreach ($arrangement->arrangementSections->sortBy('order') as $arrangementSection) {
|
|
||||||
$section = $arrangementSection->section;
|
|
||||||
|
|
||||||
if ($section === null || $section->label === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($section->id, $seen, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$seen[] = $section->id;
|
|
||||||
$sections[] = $section;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sections used by any arrangement but NOT already emitted for the selected arrangement.
|
|
||||||
* Stable order: arrangements in song order, sections in each arrangement's play order.
|
|
||||||
*
|
|
||||||
* @param array<int, int> $excludeSectionIds
|
|
||||||
* @return array<int, \App\Models\SongSection>
|
|
||||||
*/
|
|
||||||
private function appendedSections(Song $song, array $excludeSectionIds): array
|
|
||||||
{
|
|
||||||
$sections = [];
|
|
||||||
$seen = $excludeSectionIds;
|
|
||||||
|
|
||||||
foreach ($song->arrangements as $arrangement) {
|
|
||||||
foreach ($this->uniqueSectionsFor($arrangement) as $section) {
|
|
||||||
if (in_array($section->id, $seen, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$seen[] = $section->id;
|
|
||||||
$sections[] = $section;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base slideData shared by selected and appended sections: text, optional translation, optional background.
|
|
||||||
*/
|
|
||||||
private function baseSlideData(object $slide, ?array $background): array
|
|
||||||
{
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $slideData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function backgroundData(?Service $service): ?array
|
private function backgroundData(?Service $service): ?array
|
||||||
{
|
{
|
||||||
if ($service === null) {
|
if ($service === null) {
|
||||||
|
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\Label;
|
|
||||||
use App\Models\Service;
|
|
||||||
use App\Models\ServiceAgendaItem;
|
|
||||||
use App\Models\ServiceSong;
|
|
||||||
use App\Models\Song;
|
|
||||||
use App\Models\SongArrangement;
|
|
||||||
use App\Services\PlaylistExportService;
|
|
||||||
use App\Services\ProBundleExportService;
|
|
||||||
use App\Services\ProExportService;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use ProPresenter\Parser\ProBundleReader;
|
|
||||||
use ProPresenter\Parser\ProPlaylistReader;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export must honour the per-service selected arrangement:
|
|
||||||
* - .pro file: selected_arrangement (protobuf field 10) set to the chosen arrangement by name.
|
|
||||||
* - .proplaylist: presentation item arrangement_name (field 5) set to the chosen arrangement name.
|
|
||||||
* ALL arrangements stay in the .pro; only the pre-selection changes.
|
|
||||||
*/
|
|
||||||
final class ArrangementSelectionExportTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Song with three sections and two arrangements whose section sets DIFFER:
|
|
||||||
* - "Normal" (default): Strophe → Refrain
|
|
||||||
* - "Bene" (chosen): Refrain → Bridge
|
|
||||||
* Union of both arrangements = {Strophe, Refrain, Bridge}.
|
|
||||||
*
|
|
||||||
* @return array{0: Song, 1: SongArrangement, 2: SongArrangement}
|
|
||||||
*/
|
|
||||||
private function createSongWithTwoArrangements(string $title = 'Union Test'): array
|
|
||||||
{
|
|
||||||
$song = Song::create([
|
|
||||||
'title' => $title,
|
|
||||||
'ccli_id' => (string) fake()->unique()->numerify('#####'),
|
|
||||||
'author' => 'Test Author',
|
|
||||||
'copyright_text' => 'Test Publisher',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$verseLabel = Label::firstOrCreate(['name' => 'Strophe 1 - '.$title], ['color' => '#2196F3']);
|
|
||||||
$chorusLabel = Label::firstOrCreate(['name' => 'Refrain - '.$title], ['color' => '#F44336']);
|
|
||||||
$bridgeLabel = Label::firstOrCreate(['name' => 'Bridge - '.$title], ['color' => '#4CAF50']);
|
|
||||||
|
|
||||||
$verse = $song->sections()->create(['label_id' => $verseLabel->id, 'order' => 0]);
|
|
||||||
$verse->slides()->create(['order' => 0, 'text_content' => 'Strophe Zeile']);
|
|
||||||
|
|
||||||
$chorus = $song->sections()->create(['label_id' => $chorusLabel->id, 'order' => 1]);
|
|
||||||
$chorus->slides()->create(['order' => 0, 'text_content' => 'Refrain Zeile']);
|
|
||||||
|
|
||||||
$bridge = $song->sections()->create(['label_id' => $bridgeLabel->id, 'order' => 2]);
|
|
||||||
$bridge->slides()->create(['order' => 0, 'text_content' => 'Bridge Zeile']);
|
|
||||||
|
|
||||||
$normal = $song->arrangements()->create(['name' => 'Normal', 'is_default' => true]);
|
|
||||||
$normal->arrangementSections()->create(['song_section_id' => $verse->id, 'order' => 0]);
|
|
||||||
$normal->arrangementSections()->create(['song_section_id' => $chorus->id, 'order' => 1]);
|
|
||||||
|
|
||||||
$bene = $song->arrangements()->create(['name' => 'Bene', 'is_default' => false]);
|
|
||||||
$bene->arrangementSections()->create(['song_section_id' => $chorus->id, 'order' => 0]);
|
|
||||||
$bene->arrangementSections()->create(['song_section_id' => $bridge->id, 'order' => 1]);
|
|
||||||
|
|
||||||
return [$song->fresh(), $normal, $bene];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_generate_parser_song_mit_gewaehltem_arrangement_setzt_selected_und_behaelt_alle(): void
|
|
||||||
{
|
|
||||||
[$song, $normal, $bene] = $this->createSongWithTwoArrangements();
|
|
||||||
|
|
||||||
$parserSong = app(ProExportService::class)->generateParserSong($song, null, $bene);
|
|
||||||
|
|
||||||
$beneParser = $parserSong->getArrangementByName('Bene');
|
|
||||||
$this->assertNotNull($beneParser, 'Gewähltes Arrangement "Bene" muss im .pro vorhanden sein');
|
|
||||||
$this->assertSame(
|
|
||||||
$beneParser->getUuid(),
|
|
||||||
$parserSong->getSelectedArrangementUuid(),
|
|
||||||
'selected_arrangement muss auf das gewählte Arrangement "Bene" zeigen',
|
|
||||||
);
|
|
||||||
|
|
||||||
$names = array_map(fn ($arr) => $arr->getName(), $parserSong->getArrangements());
|
|
||||||
$this->assertContains('Normal', $names, 'Default-Arrangement muss erhalten bleiben');
|
|
||||||
$this->assertContains('Bene', $names, 'Gewähltes Arrangement muss erhalten bleiben');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_generate_parser_song_ohne_auswahl_nutzt_default_arrangement(): void
|
|
||||||
{
|
|
||||||
[$song, $normal] = $this->createSongWithTwoArrangements();
|
|
||||||
|
|
||||||
$parserSong = app(ProExportService::class)->generateParserSong($song, null, null);
|
|
||||||
|
|
||||||
$normalParser = $parserSong->getArrangementByName('Normal');
|
|
||||||
$this->assertNotNull($normalParser);
|
|
||||||
$this->assertSame(
|
|
||||||
$normalParser->getUuid(),
|
|
||||||
$parserSong->getSelectedArrangementUuid(),
|
|
||||||
'Ohne explizite Auswahl muss das Default-Arrangement vorausgewählt sein',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_union_der_sektionen_laesst_jedes_arrangement_gruppen_rendern(): void
|
|
||||||
{
|
|
||||||
[$song, $normal, $bene] = $this->createSongWithTwoArrangements();
|
|
||||||
|
|
||||||
// Auswahl "Bene" (Refrain, Bridge) — ohne Union würde "Normal" (Strophe) leer/unvollständig rendern.
|
|
||||||
$parserSong = app(ProExportService::class)->generateParserSong($song, null, $bene);
|
|
||||||
|
|
||||||
// Alle drei Sektionen (Strophe, Refrain, Bridge) als Gruppen emittiert.
|
|
||||||
$this->assertCount(3, $parserSong->getGroups(), 'Union muss alle referenzierten Sektionen als Gruppen emittieren');
|
|
||||||
|
|
||||||
foreach ($parserSong->getArrangements() as $arrangement) {
|
|
||||||
$resolved = $parserSong->getGroupsForArrangement($arrangement);
|
|
||||||
$this->assertCount(
|
|
||||||
2,
|
|
||||||
$resolved,
|
|
||||||
"Arrangement '{$arrangement->getName()}' muss alle 2 Gruppen-Referenzen auflösen können",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_playlist_export_setzt_arrangement_name_auf_gewaehltes_arrangement(): void
|
|
||||||
{
|
|
||||||
Storage::fake('public');
|
|
||||||
|
|
||||||
$service = Service::factory()->create(['title' => 'Arrangement Service', 'date' => now()]);
|
|
||||||
[$song, $normal, $bene] = $this->createSongWithTwoArrangements('Playlist Song');
|
|
||||||
|
|
||||||
$serviceSong = ServiceSong::create([
|
|
||||||
'service_id' => $service->id,
|
|
||||||
'song_id' => $song->id,
|
|
||||||
'song_arrangement_id' => $bene->id,
|
|
||||||
'cts_song_name' => 'Playlist Song',
|
|
||||||
'order' => 1,
|
|
||||||
]);
|
|
||||||
ServiceAgendaItem::factory()->create([
|
|
||||||
'service_id' => $service->id,
|
|
||||||
'title' => 'Playlist Song',
|
|
||||||
'service_song_id' => $serviceSong->id,
|
|
||||||
'sort_order' => 1,
|
|
||||||
'is_before_event' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = app(PlaylistExportService::class)->generatePlaylist($service);
|
|
||||||
$playlist = ProPlaylistReader::read($result['path']);
|
|
||||||
|
|
||||||
$songEntry = null;
|
|
||||||
foreach ($playlist->getEntries() as $entry) {
|
|
||||||
if ($entry->getName() === 'Playlist Song') {
|
|
||||||
$songEntry = $entry;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertNotNull($songEntry, 'Song-Eintrag muss in der Playlist vorhanden sein');
|
|
||||||
$this->assertSame('Bene', $songEntry->getArrangementName(), 'arrangement_name muss das gewählte Arrangement sein');
|
|
||||||
|
|
||||||
// Und der eingebettete .pro trägt selected_arrangement = "Bene".
|
|
||||||
$embedded = $playlist->getEmbeddedSong('Playlist Song.pro');
|
|
||||||
$this->assertNotNull($embedded);
|
|
||||||
$this->assertSame(
|
|
||||||
$embedded->getArrangementByName('Bene')->getUuid(),
|
|
||||||
$embedded->getSelectedArrangementUuid(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->cleanupTempDir($result['temp_dir']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_agenda_bundle_export_selektiert_gewaehltes_arrangement(): void
|
|
||||||
{
|
|
||||||
Storage::fake('public');
|
|
||||||
|
|
||||||
$service = Service::factory()->create(['title' => 'Bundle Service', 'date' => now()]);
|
|
||||||
[$song, $normal, $bene] = $this->createSongWithTwoArrangements('Bundle Song');
|
|
||||||
|
|
||||||
$serviceSong = ServiceSong::create([
|
|
||||||
'service_id' => $service->id,
|
|
||||||
'song_id' => $song->id,
|
|
||||||
'song_arrangement_id' => $bene->id,
|
|
||||||
'cts_song_name' => 'Bundle Song',
|
|
||||||
'order' => 1,
|
|
||||||
]);
|
|
||||||
$agendaItem = ServiceAgendaItem::factory()->create([
|
|
||||||
'service_id' => $service->id,
|
|
||||||
'title' => 'Bundle Song',
|
|
||||||
'service_song_id' => $serviceSong->id,
|
|
||||||
'sort_order' => 1,
|
|
||||||
'is_before_event' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$bundlePath = app(ProBundleExportService::class)->generateAgendaItemBundle($agendaItem);
|
|
||||||
$bundleSong = ProBundleReader::read($bundlePath)->getSong();
|
|
||||||
|
|
||||||
$beneParser = $bundleSong->getArrangementByName('Bene');
|
|
||||||
$this->assertNotNull($beneParser, 'Bundle .pro muss das Arrangement "Bene" enthalten');
|
|
||||||
$this->assertSame(
|
|
||||||
$beneParser->getUuid(),
|
|
||||||
$bundleSong->getSelectedArrangementUuid(),
|
|
||||||
'Bundle .pro selected_arrangement muss "Bene" sein',
|
|
||||||
);
|
|
||||||
$this->assertNotNull($bundleSong->getArrangementByName('Normal'), 'Alle Arrangements bleiben erhalten');
|
|
||||||
|
|
||||||
@unlink($bundlePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue