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

9.4 KiB
Raw Blame History

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 1Verse 1 and RefrainChorus 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.