pp-planer/.sisyphus/notepads/ccli-songselect-import/learnings.md

133 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
## 2026-05-11 Plan Compliance Audit
- CCLI SongSelect Import audit approved: 20/20 must-have checks present, 4/4 forbidden-pattern checks clean.
- Targeted PHP filter run passed: 140 tests, 807 assertions; npm build and Pint check also passed.
- Registered routes verified: api.ccli.preview, api.ccli.import, bookmarklets.ccli, songs.import-from-ccli-paste.
## QA Re-verification (2026-05-11): Sanctum fix confirmed
After applying `pp-planer.ddev.site` to the default stateful domains in `config/sanctum.php` and clearing config cache:
- `sanctum.stateful` resolves to `[pp-planer.test, pp-planer.ddev.site, localhost, localhost:8000, 127.0.0.1, 127.0.0.1:8000, ::1]`
- `/api/ccli/preview` returns 200 JSON when called from an authenticated DDEV browser session (was 401 before)
- End-to-end Vue dialog flow on `/songs`: paste lyrics → click "Vorschau" → preview metadata (CCLI-Nr, sections) renders. No "Netzwerkfehler" message.
- E2E test `paste fixture and preview shows metadata` now PASSES (was silently skipped before the fix).
- Suite result: 11 passed, 3 skipped (vs 10 passed, 4 skipped pre-fix). Net +1 test exercising the API endpoint.
Remaining skipped tests (3):
- `duplicate import shows error with edit link` — guarded skip when initial create import returns non-201. Likely a separate data-state or status-code issue (worth investigating but unrelated to the Sanctum fix).
- 2× CCLI translation pairing tests — were skipped before the fix too; not regressions.
Bookmarklet endpoint still serves the same JS payload, header `Content-Type: text/javascript`, body starts with `javascript:`. Note: the embedded `APP_URL` is still `http://pp-planer.test` (driven by `config('app.url')`), so a DDEV user clicking the bookmarklet on SongSelect still lands on the wrong host. That's an APP_URL / bookmarklet-config concern, not a Sanctum concern.