fix(ccli): parse common CCLI metadata
This commit is contained in:
parent
9412ca71c9
commit
e4e5df912e
|
|
@ -84,3 +84,7 @@ ### 2026-05-10 CcliPasteParser Implementation
|
||||||
- Parser trims pasted lines, treats blank lines as separators, extracts first two header lines as title/author, and excludes CCLI metadata from lyric sections.
|
- Parser trims pasted lines, treats blank lines as separators, extracts first two header lines as title/author, and excludes CCLI metadata from lyric sections.
|
||||||
- EN/DE side-by-side imports merge only adjacent labels with different raw kinds but the same `CcliLabels::normalizeLabelName()` canonical kind/number, preserving German lyrics in `linesTranslated`.
|
- EN/DE side-by-side imports merge only adjacent labels with different raw kinds but the same `CcliLabels::normalizeLabelName()` canonical kind/number, preserving German lyrics in `linesTranslated`.
|
||||||
- DDEV/Linux path is `tests/fixtures/ccli` (lowercase); macOS accepted `tests/Fixtures/ccli`, but tests must use lowercase for container portability.
|
- DDEV/Linux path is `tests/fixtures/ccli` (lowercase); macOS accepted `tests/Fixtures/ccli`, but tests must use lowercase for container portability.
|
||||||
|
|
||||||
|
### 2026-05-10 CCLI Parser Review Fixes
|
||||||
|
- CCLI SongSelect metadata can appear as `CCLI Song #`, `CCLI-Nr.` or `CCLI-Liednummer`; extraction must ignore `CCLI License/Lizenz` numbers.
|
||||||
|
- Parsed section `kind` is canonicalized via `CcliLabels::normalizeLabelName()`, while the original pasted label remains available in `label`; translation pairing still compares raw label kinds internally.
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,9 @@ public function parse(string $rawText): ParsedCcliSong
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isMetadataLine($line)) {
|
if ($isMetadataLine($line)) {
|
||||||
if (preg_match('/CCLI[\s#-]*(\d+)/iu', $line, $matches)) {
|
$extractedCcliId = CcliLabels::extractCcliId($line);
|
||||||
$ccliId = $matches[1];
|
if ($extractedCcliId !== null) {
|
||||||
|
$ccliId = $extractedCcliId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_contains($line, '©')) {
|
if (str_contains($line, '©')) {
|
||||||
|
|
@ -90,7 +91,8 @@ public function parse(string $rawText): ParsedCcliSong
|
||||||
|
|
||||||
$current = [
|
$current = [
|
||||||
'label' => $line,
|
'label' => $line,
|
||||||
'kind' => $label['kind'],
|
'kind' => CcliLabels::normalizeLabelName($label['kind']),
|
||||||
|
'rawKind' => $label['kind'],
|
||||||
'number' => $label['number'],
|
'number' => $label['number'],
|
||||||
'modifier' => $label['modifier'],
|
'modifier' => $label['modifier'],
|
||||||
'lines' => [],
|
'lines' => [],
|
||||||
|
|
@ -126,7 +128,7 @@ public function parse(string $rawText): ParsedCcliSong
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array{label: string, kind: string, number: string|null, modifier: string|null, lines: string[]}> $sections
|
* @param array<int, array{label: string, kind: string, rawKind: string, number: string|null, modifier: string|null, lines: string[]}> $sections
|
||||||
* @return ParsedCcliSection[]
|
* @return ParsedCcliSection[]
|
||||||
*/
|
*/
|
||||||
private function mergeTranslatedSections(array $sections): array
|
private function mergeTranslatedSections(array $sections): array
|
||||||
|
|
@ -160,12 +162,12 @@ private function mergeTranslatedSections(array $sections): array
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{kind: string, number: string|null} $section
|
* @param array{kind: string, rawKind: string, number: string|null} $section
|
||||||
* @param array{kind: string, number: string|null} $next
|
* @param array{kind: string, rawKind: string, number: string|null} $next
|
||||||
*/
|
*/
|
||||||
private function isTranslatedPair(array $section, array $next): bool
|
private function isTranslatedPair(array $section, array $next): bool
|
||||||
{
|
{
|
||||||
return mb_strtolower($section['kind']) !== mb_strtolower($next['kind'])
|
return mb_strtolower($section['rawKind']) !== mb_strtolower($next['rawKind'])
|
||||||
&& $this->canonicalLabel($section) === $this->canonicalLabel($next);
|
&& $this->canonicalLabel($section) === $this->canonicalLabel($next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,19 @@ public static function isMetadataLine(string $line): bool
|
||||||
return (bool) preg_match(self::METADATA_PATTERN, $line);
|
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
|
public static function normalizeLabelName(string $label): string
|
||||||
{
|
{
|
||||||
$trimmed = trim($label);
|
$trimmed = trim($label);
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,26 @@ function ccliFixtureContent(string $filename): string
|
||||||
expect($first->linesTranslated)->not->toBeEmpty();
|
expect($first->linesTranslated)->not->toBeEmpty();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('german-only.txt detects German section labels', function (): void {
|
test('german-only.txt detects German labels and normalizes section kind', function (): void {
|
||||||
$parser = new CcliPasteParser();
|
$parser = new CcliPasteParser();
|
||||||
$result = $parser->parse(ccliFixtureContent('german-only.txt'));
|
$result = $parser->parse(ccliFixtureContent('german-only.txt'));
|
||||||
|
|
||||||
|
$labels = array_map(fn (ParsedCcliSection $section): string => $section->label, $result->sections);
|
||||||
$kinds = array_map(fn (ParsedCcliSection $section): string => $section->kind, $result->sections);
|
$kinds = array_map(fn (ParsedCcliSection $section): string => $section->kind, $result->sections);
|
||||||
$hasGermanKind = array_filter($kinds, fn (string $kind): bool => in_array(mb_strtolower($kind), ['strophe', 'refrain', 'brücke'], true));
|
|
||||||
expect(count($hasGermanKind))->toBeGreaterThanOrEqual(1, 'Should detect at least one German section label');
|
expect($labels)->toContain('Strophe 1');
|
||||||
|
expect($kinds)->toContain('Verse');
|
||||||
|
expect($kinds)->toContain('Chorus');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('common CCLI metadata formats extract the song ID but not license numbers', function (): void {
|
||||||
|
$parser = new CcliPasteParser();
|
||||||
|
|
||||||
|
$result = $parser->parse("Test Song\nTest Artist\n\nVerse 1\nLine\n\nCCLI Song # 1234567\nCCLI License # 111222");
|
||||||
|
expect($result->ccliId)->toBe('1234567');
|
||||||
|
|
||||||
|
$result = $parser->parse("Test Song\nTest Artist\n\nStrophe 1\nZeile\n\nCCLI-Nr. 7654321");
|
||||||
|
expect($result->ccliId)->toBe('7654321');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('repeat-marker.txt preserves modifier in section DTO', function (): void {
|
test('repeat-marker.txt preserves modifier in section DTO', function (): void {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,17 @@
|
||||||
'Test Song 1',
|
'Test Song 1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
test('extractCcliId parses song number metadata without license numbers', function (string $line, ?string $expected) {
|
||||||
|
expect(CcliLabels::extractCcliId($line))->toBe($expected);
|
||||||
|
})->with([
|
||||||
|
['CCLI # 4760', '4760'],
|
||||||
|
['CCLI Song # 1234567', '1234567'],
|
||||||
|
['CCLI-Nr. 7654321', '7654321'],
|
||||||
|
['CCLI-Liednummer 9999001', '9999001'],
|
||||||
|
['CCLI License # 123456', null],
|
||||||
|
['CCLI-Lizenz # 123456', null],
|
||||||
|
]);
|
||||||
|
|
||||||
test('normalizeLabelName converts german labels to english', function (string $input, string $expected) {
|
test('normalizeLabelName converts german labels to english', function (string $input, string $expected) {
|
||||||
expect(CcliLabels::normalizeLabelName($input))->toBe($expected);
|
expect(CcliLabels::normalizeLabelName($input))->toBe($expected);
|
||||||
})->with([
|
})->with([
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue