Refactor lyric storage so each song owns its sections instead of sharing global labels. Adds song_sections (per song+label) owning song_slides; labels stay global ProPresenter group tags (name/color/macro). Arrangements now reference sections, so editing/importing one song no longer corrupts others that share a label name. - New: song_sections table + migration with safe backfill; SongSection, SongArrangementSection models; SongSectionController (edit/add/delete sections, immediate persistence) wired into SongEditModal. - Refactor writers/readers: CcliImport, ProImport, SongService, ArrangementController, SongController, ProExport, PDF, Translation (translation reset now section-scoped), CCLI pairing. - CCLI import fixes: parse SongSelect copy-icon format (German "Vers" abbrev + trailing author), fill empty CTS-synced songs instead of blocking as duplicate, distinct label colors per section kind, import&edit/existing-song open the edit modal (no 404/405), teleport paste dialog above assign dialog, preview shows section content, correct SongSelect search URL, copy-icon instructions. - Bookmarklet clicks #generalCopyLyricsButton and captures clipboard; serves correct host from request. - Export: embed key-visual/background under fixed bundle-relative names. - Tests updated for the section model; new section + isolation coverage.
87 lines
2.8 KiB
PHP
87 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
final class CcliLabels
|
|
{
|
|
/**
|
|
* Regex matching CCLI SongSelect section labels (English + German + variants).
|
|
*/
|
|
public const SECTION_LABEL_PATTERN = '/^(Verse|Vers|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)\s*(\d+[a-z]?)?\s*(?:\((?:Repeat|Wdh\.?)\))?\s*(?:[xX]\s*\d+)?$/iu';
|
|
|
|
/**
|
|
* Regex matching CCLI footer/metadata lines (copyright, CCLI number).
|
|
*/
|
|
public const METADATA_PATTERN = '/©|CCLI[\s\-]|ccli\.com|SongSelect|All rights reserved|Alle Rechte vorbehalten/iu';
|
|
|
|
/**
|
|
* Bidirectional English ↔ German label kind mapping.
|
|
*/
|
|
public const LABEL_NAME_MAP = [
|
|
'Vers' => 'Verse',
|
|
'Strophe' => 'Verse',
|
|
'Refrain' => 'Chorus',
|
|
'Brücke' => 'Bridge',
|
|
'Vorrefrain' => 'Pre-Chorus',
|
|
'Schluss' => 'Ending',
|
|
'Zwischenspiel' => 'Interlude',
|
|
];
|
|
|
|
public static function isSectionLabel(string $line): bool
|
|
{
|
|
return (bool) preg_match(self::SECTION_LABEL_PATTERN, trim($line));
|
|
}
|
|
|
|
public static function isMetadataLine(string $line): bool
|
|
{
|
|
return (bool) preg_match(self::METADATA_PATTERN, $line);
|
|
}
|
|
|
|
public static function extractCcliId(string $line): ?string
|
|
{
|
|
if (preg_match('/CCLI\s*(?:License|Lizenz)/iu', $line)) {
|
|
return null;
|
|
}
|
|
|
|
if (! preg_match('/CCLI(?:[\s-]*(?:Song|Lied(?:nummer)?|Nr\.?))?[\s#:\-.]*(\d+)/iu', $line, $matches)) {
|
|
return null;
|
|
}
|
|
|
|
return $matches[1];
|
|
}
|
|
|
|
public static function normalizeLabelName(string $label): string
|
|
{
|
|
$trimmed = trim($label);
|
|
|
|
if (! preg_match('/^(?<kind>Verse|Vers|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)(?<suffix>\s+\d+[a-z]?)?$/iu', $trimmed, $matches)) {
|
|
return $trimmed;
|
|
}
|
|
|
|
$kind = $matches['kind'];
|
|
$suffix = $matches['suffix'] ?? '';
|
|
|
|
return (self::LABEL_NAME_MAP[$kind] ?? $kind).$suffix;
|
|
}
|
|
|
|
/**
|
|
* @return array{kind: string, number: string|null, modifier: string|null}|null
|
|
*/
|
|
public static function parseLabel(string $line): ?array
|
|
{
|
|
$trimmed = trim($line);
|
|
|
|
if (! preg_match('/^(?<kind>Verse|Vers|Chorus|Bridge|Pre-Chorus|Tag|Ending|Intro|Interlude|Outro|Misc|Strophe|Refrain|Brücke|Vorrefrain|Schluss|Zwischenspiel)(?:\s+(?<number>\d+[a-z]?))?(?:\s*\((?<modifier>Repeat|Wdh\.?)\))?(?:\s*[xX]\s*\d+)?$/iu', $trimmed, $matches)) {
|
|
return null;
|
|
}
|
|
|
|
$modifier = $matches['modifier'] ?? null;
|
|
|
|
return [
|
|
'kind' => $matches['kind'],
|
|
'number' => $matches['number'] ?? null,
|
|
'modifier' => $modifier !== null ? rtrim($modifier, '.') : null,
|
|
];
|
|
}
|
|
}
|