- POST /api/ccli/preview: parse-only endpoint (no DB writes) - POST /api/songs/import-from-ccli-paste: 3 modes (create / pair-with-song / assign-to-service-song) - GET /songs/import-from-ccli-paste: Inertia page with base64 bookmarklet prefill - Routes guarded by auth:sanctum + throttle:30,1 (API); auth + web stack (web) - Maps DuplicateCcliSongException to 409 with existing_song_id and edit_url - Pest tests (10 cases, 63 assertions): preview, all 3 import modes, 409 dup, 422 errors, unauth, prefill happy/error, login redirect
111 lines
7.6 KiB
Markdown
111 lines
7.6 KiB
Markdown
# CCLI SongSelect Import — Learnings
|
|
|
|
## [2026-05-10] Session Start
|
|
|
|
### Architecture Decisions
|
|
- Manual paste + bookmarklet approach (NO server-side scraping — Cloudflare/ToS blocker)
|
|
- CcliPasteParser is closure-injectable (mirrors ChurchToolsService pattern for testability)
|
|
- All songs upserted via CcliImportService mirroring ProImportService::import() shape
|
|
- Translation stored inline on SongSlide.text_content_translated (no separate model)
|
|
- default_translation_language = APP-GLOBAL Setting (not per-user)
|
|
|
|
### Key Codebase Facts
|
|
- Song.ccli_id is UNIQUE indexed nullable — primary CCLI match key
|
|
- SongSlide: text_content (original), text_content_translated (translation)
|
|
- Labels are GLOBAL (shared across all songs) — labels table with name + color
|
|
- ProImportService::import() is the template for upsert (not upsertSong — that method doesn't exist)
|
|
- SettingsController::AGENDA_KEYS constant whitelist for Settings KV
|
|
- TranslationService::importFromText distributes lines preserving local slide line counts
|
|
- ArrangementDialog.vue lines 488-532 = searchable song select (where CCLI buttons go)
|
|
|
|
### CCLI SongSelect "View Lyrics" Page Format
|
|
- Title on first non-empty line
|
|
- Section label as standalone line (e.g., "Verse 1", "Chorus")
|
|
- Lyrics lines under each section
|
|
- Footer: copyright (©), CCLI number (e.g., "CCLI # 1234567"), author
|
|
|
|
### Section Label Regex (English + German + variants)
|
|
```
|
|
/^(Verse|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+)?$/i
|
|
```
|
|
|
|
### Language Mapping (EN ↔ DE)
|
|
- Verse ↔ Strophe
|
|
- Chorus ↔ Refrain
|
|
- Bridge ↔ Brücke
|
|
- Pre-Chorus ↔ Vorrefrain
|
|
- Ending ↔ Schluss
|
|
- Interlude ↔ Zwischenspiel
|
|
|
|
### Test Fixture Format
|
|
Fixtures are synthetic CCLI-format text files. Format:
|
|
```
|
|
Test Song Title
|
|
Test Artist
|
|
|
|
Verse 1
|
|
Line 1 of verse
|
|
Line 2 of verse
|
|
|
|
Chorus
|
|
Chorus line 1
|
|
Chorus line 2
|
|
|
|
© 2024 Test Publishing
|
|
CCLI # 9999001
|
|
```
|
|
|
|
### Fixture Corpus Notes
|
|
- Keep fixture titles/artists anonymized and numeric (`Test Song N`, `Test Artist N`)
|
|
- Include both English and German section labels in the corpus so parser regex coverage stays broad
|
|
- Add edge cases for missing footer pieces, whitespace, repeat markers, and suffix labels (`2a`, `x2`, `(Repeat)`)
|
|
|
|
### 2026-05-10 CCLI Label Utility Notes
|
|
- `CcliLabels` works best with a fixed kind list in regexes; no locale config needed for EN/DE normalization.
|
|
- `normalizeLabelName()` should map only known German kinds and preserve any numeric suffix.
|
|
- `parseLabel()` can stay lightweight by returning `null` for non-labels and a small array for matched labels.
|
|
|
|
### 2026-05-10 Song CCLI Metadata Migration
|
|
- Song CCLI metadata belongs on `songs` as nullable fields: `imported_from_ccli_at` (timestamp) + `ccli_source_url` (string 500).
|
|
- Factory state helpers can stay tiny; `fromCcli()` just seeds timestamp + SongSelect URL.
|
|
- Inference/LSP can lag after edits; a tiny no-op signature change (`fn (): array => [...]`) forced the factory diagnostics to refresh cleanly.
|
|
|
|
### 2026-05-10 Settings Language Seed
|
|
- `SettingsController::AGENDA_KEYS` drives both the index props and the allowed `key` values for PATCH updates.
|
|
- `default_translation_language` should be validated as a whitelist value (`DE|EN|FR|ES|NL|IT`) only when that setting is being updated.
|
|
- `CcliSettingsSeeder` must use `Setting::firstOrCreate()` so reseeding does not overwrite a user-changed language.
|
|
|
|
### 2026-05-10 CcliPasteParser Scaffold
|
|
- Mirror `ChurchToolsService` with nullable `Closure` constructor injections and default `= null` values.
|
|
- This codebase uses `App\Services\DTO\...` namespaces/directories for DTOs, so keep the uppercase `DTO` path aligned with existing services.
|
|
- Scaffold tests can verify Laravel container resolution without adding any service provider binding.
|
|
|
|
### 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.
|
|
- 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.
|
|
|
|
### 2026-05-10 CcliImportService Implementation
|
|
- `CcliImportService` mirrors `ProImportService` by wrapping song metadata, global label resolution, slide replacement, default arrangement upsert, arrangement-label recreation, and `ApiRequestLog` success entry in one `DB::transaction()`.
|
|
- Active duplicate CCLI IDs are blocked with `DuplicateCcliSongException`; trashed matches are restored and updated in-place via `Song::withTrashed()->where('ccli_id', ...)`.
|
|
- CCLI label names should be canonicalized with `CcliLabels::normalizeLabelName($kind.' '.$number)` before `Label::firstOrCreate()`, keeping labels global/shared.
|
|
- Import tests can verify rollback deterministically with a temporary SQLite trigger that aborts `song_slides` insert; this proves song + log rows are not persisted after mid-transaction failure.
|
|
|
|
### 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.
|
|
|
|
### 2026-05-10 CCLI Translation Pairing
|
|
- `CcliTranslationPairingService` returns a review-only mapping and never writes `SongSlide.text_content_translated`; callers remain responsible for persistence.
|
|
- Pairing canonicalizes both local arrangement labels and CCLI sections with `CcliLabels::normalizeLabelName()` + lowercase, so `Strophe 1` ↔ `Verse 1` and `Refrain` ↔ `Chorus` work across languages.
|
|
- Distribution mirrors `TranslationService::importTranslation()` by filling local slide slots in arrangement order using each local slide's original line count; overflow CCLI lines are kept on the final local slide for that section.
|
|
|
|
### 2026-05-11 CcliPasteController (T10)
|
|
- `SongMatchingService::manualAssign(ServiceSong, Song)` takes a **Song object** (not int id) — different from initial task plan.
|
|
- API routes use `auth:sanctum` middleware (not just `auth`); Sanctum's `EnsureFrontendRequestsAreStateful` is prepended globally to API in bootstrap/app.php.
|
|
- Apply `throttle:30,1` via a nested `Route::middleware('throttle:30,1')->group(...)` inside the existing sanctum group; combined middleware shown by `route:list -vv`.
|
|
- `assertInertia()` enforces page-component file existence by default. For pages whose Vue component is created in a later task (T16 `Songs/ImportFromCcliPaste`), pass `$shouldExist=false` to `component()` as second arg.
|
|
- `tests/fixtures/ccli/` (lowercase) is the canonical fixture directory; existing tests already declare a top-level `ccliFixturePath()` helper, so new test files need a uniquely-named helper to avoid `Cannot redeclare function` errors in Pest.
|
|
- Web route `songs.import-from-ccli-paste` needs the `auth` middleware (web-style redirect to login), while the API routes use sanctum (401 JSON response); the difference matters for unauthenticated test assertions (`assertRedirect(route('login'))` vs `assertUnauthorized()`).
|
|
- `CcliImportService::import()` throws `RuntimeException` for missing CCLI id and `InvalidArgumentException` (via parser) for parse failures; controller catches both to return 422 with a German message.
|