feat(export): use MacroResolutionService in ProExportService for flexible macro injection

This commit is contained in:
Thorsten Bus 2026-05-03 23:08:22 +02:00
parent 81b2a9caf6
commit cef247336e
5 changed files with 128 additions and 35 deletions

View file

@ -27,7 +27,7 @@ public function importPro(Request $request): JsonResponse
} }
try { try {
$service = new ProImportService; $service = new ProImportService();
$songs = $service->import($file); $songs = $service->import($file);
return response()->json([ return response()->json([
@ -53,7 +53,7 @@ public function downloadPro(Song $song): BinaryFileResponse
abort(422, 'Song hat keine Gruppen oder Slides zum Exportieren.'); abort(422, 'Song hat keine Gruppen oder Slides zum Exportieren.');
} }
$exportService = new ProExportService; $exportService = app(ProExportService::class);
$tempPath = $exportService->generateProFile($song); $tempPath = $exportService->generateProFile($song);
$filename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro'; $filename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro';

View file

@ -53,7 +53,7 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
$announcementPatterns = Setting::get('agenda_announcement_position'); $announcementPatterns = Setting::get('agenda_announcement_position');
$announcementInserted = false; $announcementInserted = false;
$exportService = new ProExportService; $exportService = app(ProExportService::class);
$tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid(); $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
mkdir($tempDir, 0755, true); mkdir($tempDir, 0755, true);
@ -90,7 +90,7 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda
continue; continue;
} }
$proPath = $exportService->generateProFile($song); $proPath = $exportService->generateProFile($song, $service);
$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);
@ -176,7 +176,7 @@ private function generatePlaylistLegacy(Service $service): array
$skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count(); $skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count();
$skippedEmpty = 0; $skippedEmpty = 0;
$exportService = new ProExportService; $exportService = app(ProExportService::class);
$tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid(); $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid();
mkdir($tempDir, 0755, true); mkdir($tempDir, 0755, true);
@ -201,7 +201,7 @@ private function generatePlaylistLegacy(Service $service): array
continue; continue;
} }
$proPath = $exportService->generateProFile($song); $proPath = $exportService->generateProFile($song, $service);
$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);

View file

@ -34,6 +34,7 @@ public function generateBundle(Service $service, string $blockType): string
public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
{ {
$agendaItem->loadMissing([ $agendaItem->loadMissing([
'service',
'slides', 'slides',
'serviceSong.song.arrangements.arrangementLabels.label.songSlides', 'serviceSong.song.arrangements.arrangementLabels.label.songSlides',
]); ]);
@ -52,7 +53,7 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
throw new RuntimeException('Song "'.$song->title.'" hat keine Gruppen.'); throw new RuntimeException('Song "'.$song->title.'" hat keine Gruppen.');
} }
$parserSong = (new ProExportService)->generateParserSong($song); $parserSong = app(ProExportService::class)->generateParserSong($song, $agendaItem->service);
$proFilename = self::safeFilename($song->title).'.pro'; $proFilename = self::safeFilename($song->title).'.pro';
$bundle = new PresentationBundle($parserSong, $proFilename); $bundle = new PresentationBundle($parserSong, $proFilename);

View file

@ -2,20 +2,25 @@
namespace App\Services; namespace App\Services;
use App\Models\Setting; use App\Models\Service;
use App\Models\Song; use App\Models\Song;
use ProPresenter\Parser\ProFileGenerator; use ProPresenter\Parser\ProFileGenerator;
class ProExportService class ProExportService
{ {
public function generateProFile(Song $song): string public function __construct(
private readonly MacroResolutionService $macroResolutionService,
) {
}
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';
ProFileGenerator::generateAndWrite( ProFileGenerator::generateAndWrite(
$tempPath, $tempPath,
$song->title, $song->title,
$this->buildGroups($song), $this->buildGroups($song, $service),
$this->buildArrangements($song), $this->buildArrangements($song),
$this->buildCcliMetadata($song), $this->buildCcliMetadata($song),
); );
@ -23,19 +28,19 @@ public function generateProFile(Song $song): string
return $tempPath; return $tempPath;
} }
public function generateParserSong(Song $song): \ProPresenter\Parser\Song public function generateParserSong(Song $song, ?Service $service = null): \ProPresenter\Parser\Song
{ {
$song->loadMissing(['arrangements.arrangementLabels.label.songSlides']); $song->loadMissing(['arrangements.arrangementLabels.label.songSlides']);
return ProFileGenerator::generate( return ProFileGenerator::generate(
$song->title, $song->title,
$this->buildGroups($song), $this->buildGroups($song, $service),
$this->buildArrangements($song), $this->buildArrangements($song),
$this->buildCcliMetadata($song), $this->buildCcliMetadata($song),
); );
} }
private function buildGroups(Song $song): array private function buildGroups(Song $song, ?Service $service = null): array
{ {
$defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first(); $defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first();
@ -45,7 +50,6 @@ private function buildGroups(Song $song): array
$defaultArr->loadMissing('arrangementLabels.label.songSlides'); $defaultArr->loadMissing('arrangementLabels.label.songSlides');
$macroData = $this->buildMacroData();
$groups = []; $groups = [];
$seenLabelIds = []; $seenLabelIds = [];
@ -61,18 +65,28 @@ private function buildGroups(Song $song): array
} }
$seenLabelIds[] = $label->id; $seenLabelIds[] = $label->id;
$isCopyrightGroup = strcasecmp($label->name, 'COPYRIGHT') === 0;
$slides = []; $slides = [];
$labelSlides = $label->songSlides->sortBy('order')->values();
$totalSlides = $labelSlides->count();
foreach ($label->songSlides->sortBy('order') as $slide) { foreach ($labelSlides as $slideIndex => $slide) {
$slideData = ['text' => $slide->text_content ?? '']; $slideData = ['text' => $slide->text_content ?? ''];
if ($slide->text_content_translated) { if ($slide->text_content_translated) {
$slideData['translation'] = $slide->text_content_translated; $slideData['translation'] = $slide->text_content_translated;
} }
if ($isCopyrightGroup && $macroData) { if ($service !== null) {
$slideData['macro'] = $macroData; $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; $slides[] = $slideData;
@ -88,23 +102,6 @@ private function buildGroups(Song $song): array
return $groups; return $groups;
} }
private function buildMacroData(): ?array
{
$name = Setting::get('macro_name');
$uuid = Setting::get('macro_uuid');
if (! $name || ! $uuid) {
return null;
}
return [
'name' => $name,
'uuid' => $uuid,
'collectionName' => Setting::get('macro_collection_name', '--MAIN--'),
'collectionUuid' => Setting::get('macro_collection_uuid', '8D02FC57-83F8-4042-9B90-81C229728426'),
];
}
private function buildArrangements(Song $song): array private function buildArrangements(Song $song): array
{ {
$arrangements = []; $arrangements = [];

View file

@ -3,8 +3,13 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\Label; use App\Models\Label;
use App\Models\Macro;
use App\Models\MacroAssignment;
use App\Models\MacroCollection;
use App\Models\Service;
use App\Models\Song; use App\Models\Song;
use App\Models\User; use App\Models\User;
use App\Services\ProExportService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -202,6 +207,96 @@ public function test_download_pro_roundtrip_preserves_content(): void
} }
} }
public function test_export_ohne_service_context_enthaelt_keine_macros(): void
{
$song = $this->createSongWithContent();
$macro = $this->createMacroForExport('Service Macro');
MacroAssignment::create([
'part_type' => 'song',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$parserSong = app(ProExportService::class)->generateParserSong($song);
foreach ($this->allParserSlides($parserSong) as $slide) {
$this->assertFalse($slide->hasMacro());
}
}
public function test_export_mit_globaler_song_zuweisung_enthaelt_macro_auf_allen_slides(): void
{
$service = Service::factory()->create();
$song = $this->createSongWithContent();
$macro = $this->createMacroForExport('Alle Folien Macro');
MacroAssignment::create([
'part_type' => 'song',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$parserSong = app(ProExportService::class)->generateParserSong($song, $service);
$slides = $this->allParserSlides($parserSong);
$this->assertNotEmpty($slides);
foreach ($slides as $slide) {
$this->assertTrue($slide->hasMacro());
$this->assertSame('Alle Folien Macro', $slide->getMacroName());
$this->assertSame($macro->uuid, $slide->getMacroUuid());
$this->assertSame('Export Collection', $slide->getMacroCollectionName());
}
}
public function test_export_mit_ausgeblendeter_macro_enthaelt_keine_macro(): void
{
$service = Service::factory()->create();
$song = $this->createSongWithContent();
$macro = $this->createMacroForExport('Ausgeblendete Macro', ['hidden_at' => now()]);
MacroAssignment::create([
'part_type' => 'song',
'macro_id' => $macro->id,
'position' => 'all_slides',
'order' => 0,
]);
$parserSong = app(ProExportService::class)->generateParserSong($song, $service);
foreach ($this->allParserSlides($parserSong) as $slide) {
$this->assertFalse($slide->hasMacro());
}
}
private function createMacroForExport(string $name, array $attributes = []): Macro
{
$macro = Macro::factory()->create(array_merge([
'uuid' => '11111111-2222-4333-8444-555555555555',
'name' => $name,
], $attributes));
$collection = MacroCollection::create([
'uuid' => '99999999-8888-4777-8666-555555555555',
'name' => 'Export Collection',
]);
$collection->macros()->attach($macro->id, ['order' => 0]);
return $macro;
}
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 assertStringContains(string $needle, ?string $haystack): void private function assertStringContains(string $needle, ?string $haystack): void
{ {
$this->assertNotNull($haystack); $this->assertNotNull($haystack);