Skip to content

feat(editor): Create New Arrangement redesign (roster, staged import, MusicBrainz match, auto-preview, folder-browser load) - #45

Merged
byrongamatos merged 12 commits into
mainfrom
feat/blank-project-onboarding
Jul 5, 2026
Merged

feat(editor): Create New Arrangement redesign (roster, staged import, MusicBrainz match, auto-preview, folder-browser load)#45
byrongamatos merged 12 commits into
mainfrom
feat/blank-project-onboarding

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Ground-up rework of the "New…" create flow (was "Create New custom song"), built + tested iteratively on the :8000 testbed.

Entry

  • Song Editor with nothing loaded → a Load / Create landing.
  • Load is now a file browser rooted at the DLC/library folder (new POST /browse, single directory level, containment-guarded) with recursive search — opens on the folder, not a blank "type to search".

Create New Arrangement modal (two-column)

  • "What are you arranging?" — click-to-add + drag-to-reorder instrument roster (Vocals / Lead / Rhythm / Keys / Bass / Drums); no string counts (set in the editor). Multi-arrangement create; Vocals seeds a lyrics side-file.
  • Draft-now, audio-later — create with just a title (audio + artist optional); backend writes an audio-less draft the editor can open.
  • Content Import — one staged, role-capped file list (1 master audio, 1 chart, MIDI info row) that no longer drops a file when you add another; a staged master track auto-syncs a Guitar Pro chart in one click.
  • Spec-complete metadata (album_artist, track, disc, genres, ISRC, MBID, language, authors) + non-destructive GP-metadata autofill.
  • MusicBrainz "Match" — structured Artist+Title query (fixes the junk single-field search, Add icon + description to plugin.json for the v3 Pedalboard Plugins page #1) and re-ranks candidates by closeness to the staged master audio's length with a "≈ your audio" tag (editorApplyOffset only shifts current arrangement - poisons dataset.applied for +Keys/+Drums imports #2). Pairs with the core ranking/fingerprint PRs (fix(enrichment): rank the canonical studio take over live/comp versions feedBack#758, #759).
  • Album art promoted to a preview; preview clip auto-generated from the master audio (manual upload removed).
  • GP track picker — drums recolored from alarming red to a neutral role pill (Drums amber / Keys indigo / Bass sky / Guitar muted): it's a role, not a warning.

Backend (routes.py)

create_sloppak multi-arrangement + full metadata + audio-less drafts + lyrics/drum seeds; upload-audio returns duration; auto preview generation (create + replace-audio); folder-browse endpoint.

Tests: create_gate.test.js updated; full editor JS suite green. Draft — still iterating on the testbed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a unified Create New Arrangement flow with Blank / Guitar Pro / EOF XML mode selection and Create & Open in Editor.
    • Improved editor library UX with a DLC folder browser, enhanced load modal, and Album Art “Find cover…” via cached MusicBrainz cover art.
    • Added backend support for DLC browsing, CAA cover retrieval, and draft creation without audio.
  • Bug Fixes
    • Fixed “create from audio alone” so audio is optional for drafts; added best-effort audio duration probing and automatic preview regeneration after audio replacement.
    • Renamed the toolbar action to Build feedpak.
  • Tests
    • Added create-modal gating coverage (title/instruments, GP/EOF enablement).

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR unifies the editor create flow around Blank, Guitar Pro, and EOF XML modes, adds audio-less draft support, expands browse/import/metadata handling, auto-generates previews server-side, and updates the create-gate test coverage and related labels.

Changes

Unified create flow

Layer / File(s) Summary
Browse API and draft pack creation
routes.py
Adds one-level DLC browsing and expands create_sloppak plus pack writing to support audio-less drafts, optional artists/authors, lyrics, optional metadata fields, and empty-stem manifests.
Audio, preview, cover, and GP metadata
routes.py
Probes uploaded audio duration, regenerates previews on replace, returns parsed GP song metadata, and adds CAA cover fetch/use endpoints with cached art paths.
Load and create modal HTML
screen.html
Relabels the load modal for feedpak browsing and replaces the create modal with a two-column arrangement editor including roster, content import, album art, metadata, MusicBrainz matching, and the renamed primary action.
Load browser and start landing
screen.js
Makes the load modal open in folder-browse mode, routes empty searches back to browsing, adds a Blank create option, and shows a start landing modal when the editor is empty.
Create staging and routing
screen.js
Refactors GP staging, create gating, content import, blank creation, EOF selection, autosync refinement, and build persistence into a unified create pipeline without the old audio-mode toggles or preview upload path.
Gate tests and changelog
tests/create_gate.test.js, CHANGELOG.md
Adds _createGateOpen tests for roster, GP, EOF, and null-input cases, and documents the unified create flow and build label rename.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main editor redesign, including the new arrangement flow, staged import, MusicBrainz matching, auto-preview, and folder browser.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/blank-project-onboarding

Comment @coderabbitai help to get the list of available commands.

@ChrisBeWithYou ChrisBeWithYou changed the title feat(editor): unified create modal — first-class Blank (audio-only .feedpak) feat(editor): Create New Arrangement redesign (roster, staged import, MusicBrainz match, auto-preview, folder-browser load) Jul 4, 2026
@ChrisBeWithYou
ChrisBeWithYou marked this pull request as ready for review July 4, 2026 10:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
screen.js (1)

6742-6807: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the dead create-arr/string-count helpers
_populateCreateArrButtons and _populateStringCountButtons no longer have call sites, and the old editor-create-* DOM hooks are gone. editorShowCreateModal now uses the roster palette, so these helpers and their initialArr/stringCount state can be deleted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@screen.js` around lines 6742 - 6807, Remove the now-dead
create-arr/string-count UI helpers and related state from screen.js:
`_populateCreateArrButtons` and `_populateStringCountButtons` are no longer
referenced, and their `createState.initialArr` / `createState.stringCount` logic
is obsolete. Update the create-modal flow in `editorShowCreateModal` and any
`createState` initialization to rely only on the roster palette, and delete the
unused `editor-create-*` DOM wiring these helpers target.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 10-28: The changelog entry is stale and still describes an
intermediate Blank/GP/EOF mode-picker flow that was later removed. Update the
`CHANGELOG.md` “Fixed” note to match the final unified create modal and
`editorDoCreate()` behavior: all options are shown in one menu, routing is based
on the provided inputs, `_createGateOpen` uses the full 5-instrument roster
(Lead/Rhythm/Keys/Bass/Drums), and the gate does not require an artist field.
Also remove the outdated “tracked separately” caveat for Keys/Drums and mention
the start landing page if relevant to the shipped flow.

In `@routes.py`:
- Around line 4195-4199: `replace_audio` is calling `_make_preview_clip()`
synchronously, which can block the event loop while `ffprobe`/`ffmpeg` run.
Update the preview regeneration path in `replace_audio` to offload
`_make_preview_clip` via `run_in_executor()` the same way the upload flow does,
then keep the existing `_pv.exists()` and manifest update logic on the awaited
result.

---

Nitpick comments:
In `@screen.js`:
- Around line 6742-6807: Remove the now-dead create-arr/string-count UI helpers
and related state from screen.js: `_populateCreateArrButtons` and
`_populateStringCountButtons` are no longer referenced, and their
`createState.initialArr` / `createState.stringCount` logic is obsolete. Update
the create-modal flow in `editorShowCreateModal` and any `createState`
initialization to rely only on the roster palette, and delete the unused
`editor-create-*` DOM wiring these helpers target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81d09431-48aa-47a5-96c0-f06be5882481

📥 Commits

Reviewing files that changed from the base of the PR and between 5239d47 and 5279326.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • routes.py
  • screen.html
  • screen.js
  • tests/create_gate.test.js

Comment thread CHANGELOG.md
Comment thread routes.py
Comment on lines +4195 to +4199
# Regenerate the auto preview from the new master audio so a draft
# that had no audio at create (and thus no preview) gets one now.
_pv = _make_preview_clip(dest, source_dir)
if _pv and _pv.exists():
manifest["preview"] = _pv.name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and surrounding symbols first.
ast-grep outline routes.py --view expanded | sed -n '1,220p'

# Find the helper and the two audio paths.
rg -n "_make_preview_clip|replace_audio|upload_audio|run_in_executor|subprocess\.run" routes.py

# Read the relevant section around the reported lines.
sed -n '4140,4235p' routes.py

# Read the helper implementation if it is in the same file.
sed -n '1,260p' routes.py | sed -n '/def _make_preview_clip/,/^\s*def /p'

Repository: got-feedBack/feedBack-plugin-editor

Length of output: 10969


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the upload path that already offloads preview generation.
sed -n '3920,3975p' routes.py

# Inspect the helper implementation and its subprocess calls.
sed -n '5840,5905p' routes.py

# Check whether preview is conditionally preserved anywhere else in the file.
rg -n 'manifest\["preview"\]|preview"\s*=' routes.py

Repository: got-feedBack/feedBack-plugin-editor

Length of output: 5584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the save path that also writes manifest["preview"].
sed -n '6110,6165p' routes.py

# Search for any user-facing/manual preview upload or preservation logic.
rg -n "manual.*preview|Preview Clip|preview_path|manifest\\[\"preview\"\\]|preview_clip" routes.py

Repository: got-feedBack/feedBack-plugin-editor

Length of output: 4040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the endpoint that returns a preview_path.
sed -n '4018,4045p' routes.py

# Inspect the replace-audio request body handling around preview_path/audio_url.
sed -n '5758,5818p' routes.py

# Read any nearby comments that explain preview semantics.
sed -n '5848,5908p' routes.py

Repository: got-feedBack/feedBack-plugin-editor

Length of output: 7187


Offload preview generation in replace_audio
routes.py:4197 calls _make_preview_clip() directly; it shells out to ffprobe and ffmpeg, so this async handler can block the event loop for up to ~75s. Run it through run_in_executor() like the upload path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@routes.py` around lines 4195 - 4199, `replace_audio` is calling
`_make_preview_clip()` synchronously, which can block the event loop while
`ffprobe`/`ffmpeg` run. Update the preview regeneration path in `replace_audio`
to offload `_make_preview_clip` via `run_in_executor()` the same way the upload
flow does, then keep the existing `_pv.exists()` and manifest update logic on
the awaited result.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
routes.py (1)

4046-4057: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching "no cover" results to avoid repeated external fetches.

_caa_cached only persists successful fetches; a release with no art returns None without recording that, so every re-render of the cover grid re-hits coverartarchive.org (10s timeout per tile) for the same art-less MBIDs. A small negative marker (e.g. an empty sentinel file or an in-memory set of known-empty release ids) would short-circuit repeat lookups.

Note: the Ruff S310/ast-grep SSRF hints on _caa_fetch_front are false positives here — release_id is constrained by _CAA_ID_RE at both callers, so no scheme/host injection is possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@routes.py` around lines 4046 - 4057, The _caa_cached helper only caches
successful cover fetches, so repeated requests for art-less releases keep
calling _caa_fetch_front and re-hitting coverartarchive.org. Update _caa_cached
to remember negative results too, using a small sentinel/marker for the
release_id so later calls can return None immediately without another network
fetch; keep the existing success-path cache behavior intact and use the same
_caa_cached/_caa_fetch_front symbols to locate the change.

Source: Linters/SAST tools

screen.js (2)

6853-6869: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Surface pick failures to the user.

On !resp.ok || !data || !data.art_path the function returns silently (Line 6860) leaving the popup open with no feedback, while a thrown error falls through and does close the popup (Line 6868). Since the tile only renders when its image already loaded, a failed use-caa-cover is unexpected and worth signaling. Consider a brief inline error and consistent popup handling across both failure paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@screen.js` around lines 6853 - 6869, The _editorPickCaaCover flow in
screen.js silently returns on failed fetch/invalid response while leaving the
popup open, unlike the thrown-error path that closes it. Update
_editorPickCaaCover to surface a brief inline error or visible feedback when
!resp.ok || !data || !data.art_path, and make popup cleanup in editor-art-popup
consistent across both success and failure paths so users aren’t left without
any indication of the failure.

6803-6830: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Guard against stale/overlapping searches.

_editorArtRunSearch is async and can be triggered repeatedly (Search click + Enter on either input). Because the render at Line 6816 happens after await, a slower earlier search can resolve after a newer one and wipe/replace its results, showing stale covers. Consider tagging each run and bailing if a newer run started.

♻️ Example guard
 async function _editorArtRunSearch(artist, title, grid) {
     grid.replaceChildren();
     if (!artist && !title) { _editorArtMsg(grid, 'Enter an artist or title.'); return; }
     _editorArtMsg(grid, 'Searching for covers…');
+    const token = (grid._artRunToken = (grid._artRunToken || 0) + 1);
     const params = new URLSearchParams();
     ...
     } catch (_) { data = null; }
+    if (grid._artRunToken !== token) return; // a newer search superseded this one
     grid.replaceChildren();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@screen.js` around lines 6803 - 6830, _editorArtRunSearch can render stale
results because overlapping async searches may resolve out of order and
overwrite newer output. Add a per-run guard in _editorArtRunSearch (for example,
a monotonic request token stored outside the function) and check it again after
the fetch/await before calling grid.replaceChildren() and appending tiles. If a
newer search has started, exit early so only the latest artist/title query
updates the grid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@routes.py`:
- Around line 4046-4057: The _caa_cached helper only caches successful cover
fetches, so repeated requests for art-less releases keep calling
_caa_fetch_front and re-hitting coverartarchive.org. Update _caa_cached to
remember negative results too, using a small sentinel/marker for the release_id
so later calls can return None immediately without another network fetch; keep
the existing success-path cache behavior intact and use the same
_caa_cached/_caa_fetch_front symbols to locate the change.

In `@screen.js`:
- Around line 6853-6869: The _editorPickCaaCover flow in screen.js silently
returns on failed fetch/invalid response while leaving the popup open, unlike
the thrown-error path that closes it. Update _editorPickCaaCover to surface a
brief inline error or visible feedback when !resp.ok || !data || !data.art_path,
and make popup cleanup in editor-art-popup consistent across both success and
failure paths so users aren’t left without any indication of the failure.
- Around line 6803-6830: _editorArtRunSearch can render stale results because
overlapping async searches may resolve out of order and overwrite newer output.
Add a per-run guard in _editorArtRunSearch (for example, a monotonic request
token stored outside the function) and check it again after the fetch/await
before calling grid.replaceChildren() and appending tiles. If a newer search has
started, exit early so only the latest artist/title query updates the grid.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ebb27b43-5c82-4ba2-a140-1d4f3a024ea6

📥 Commits

Reviewing files that changed from the base of the PR and between 5279326 and f5429b0.

📒 Files selected for processing (3)
  • routes.py
  • screen.html
  • screen.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • screen.html

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
routes.py (1)

4088-4142: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

MusicBrainz rate limiting on cover-search. _mb_release_group_covers hits musicbrainz.org/ws/2/release-group on every call with no throttle. MusicBrainz enforces roughly one request per second per client and a descriptive User-Agent; a burst of keystroke-driven searches can draw 503s or a temporary IP block. Consider debouncing on the client, or a small server-side cache/rate-limiter on this route (CAA fetches are already cached under STORAGE_DIR, but the release-group search is not).

Note: the Ruff S310/ast-grep SSRF hint at Lines 4112-4113 is a false positive here — the request host is the hardcoded musicbrainz.org; only the query string is derived from input, so the endpoint isn't attacker-redirectable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@routes.py` around lines 4088 - 4142, The `_mb_release_group_covers` helper
and `/api/plugins/editor/cover-search` endpoint call MusicBrainz on every
request without any throttle or caching, which can trigger rate limits during
rapid searches. Add a small server-side cache and/or rate limiter around
`_mb_release_group_covers` (or the `cover_search` route) so repeated
artist/query lookups reuse recent results instead of always hitting
`musicbrainz.org`. Keep the hardcoded MusicBrainz host and `_CAA_UA` usage
as-is; just reduce request frequency for the `cover_search` flow.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@routes.py`:
- Around line 4088-4142: The `_mb_release_group_covers` helper and
`/api/plugins/editor/cover-search` endpoint call MusicBrainz on every request
without any throttle or caching, which can trigger rate limits during rapid
searches. Add a small server-side cache and/or rate limiter around
`_mb_release_group_covers` (or the `cover_search` route) so repeated
artist/query lookups reuse recent results instead of always hitting
`musicbrainz.org`. Keep the hardcoded MusicBrainz host and `_CAA_UA` usage
as-is; just reduce request frequency for the `cover_search` flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ded9c9ca-3343-4b62-8102-f92cb5d300b5

📥 Commits

Reviewing files that changed from the base of the PR and between f5429b0 and 9f339c4.

📒 Files selected for processing (2)
  • routes.py
  • screen.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • screen.js

@byrongamatos

Copy link
Copy Markdown
Collaborator

Review + fix pass (Claude Code). Deep-reviewed the create-flow rework; /browse containment guard (resolve + relative_to) is sound, draft-now audio-optional path is correctly guarded, all JS + 180 pytest green. Pushed two fixes:

1. YouTube URL not autosynced on GP import (screen.js) — file master audio couples into the GP autosync path on selection (via _refreshGpAudioUI), but a pasted YouTube URL only resolved at Create time, so gp8AudioMode stayed none and the chart was attached to the downloaded audio unaligned. Now re-couples + recomputes the mode after resolving the URL so autosync runs (matching file-audio behavior).

2. Legacy init_drum_tab dropped (routes.py) — the roster rework computed init_drums only from 'Drums' in roster, so a back-compat caller sending {initial_arrangement, init_drum_tab: true} lost its drum tab. Now translated into the roster.

Two findings left for you (not auto-fixed):

  • Extended metadata dropped on chart imports — album_artist / track / disc / genres / language / ISRC / MBID / authors are shown in the modal for all create flows but only serialized on the blank-create path; a GP/EOF import silently loses them (convert-gp and the EOF endpoint only accept title/artist/album/year). Real data-loss gap, but wiring it needs both endpoints + both frontend serializers extended — deferred as feature-completion rather than destabilize this PR.
  • GP8 embedded-audio + pasted YouTube URL — the URL is ignored (embedded wins). This looks intentional (the convert-gp comment deliberately drops a user URL in embedded mode so it can't mask/override the extracted OGG), so I left it; flag it if overriding embedded with a URL is desired.

@byrongamatos

Copy link
Copy Markdown
Collaborator

Follow-up: implemented the import-mode metadata persistence I'd flagged (was the larger of the deferred items, but turned out contained). All editor tests green (180 pytest + JS).

What was wrong: the create modal collects album_artist / track / disc / genres / language / ISRC / MBID / authors for every flow, but only blank-create wrote them — a GP or EOF import kept just title/artist/album/year on the session, silently dropping the rest.

What made it tractable: _write_sloppak_pak already writes all these fields from its meta dict, and /build merges session.metadata — so the manifest writer and build path were complete; the only gap was routing the fields into the session.

Fix (both import paths → session.metadata → Build):

  • _extended_manifest_meta() — a lenient normalizer coercing the modal's raw strings into the shapes the writer expects (track/disc→int, genres/authors→deduped lists, ISRC stripped+upper, MBID lower). Malformed fields are skipped, not errors.
  • convert-gp reads them from its JSON body; import-xml-project takes one extended_meta JSON form field; both merge into session.metadata.
  • Frontend sends them via a shared _createExtendedMeta() helper (convert-gp body + EOF FormData). Test: test_extended_manifest_meta_normalizes_import_fields.

@ChrisBeWithYou

Copy link
Copy Markdown
Contributor Author

Integration note — from a full-queue dry-run that merged every open org PR into a next-version test build.

⚠️ Heads-up when merging: this produces a silent duplicate _editorDoBlankCreate with no git conflict. main already carries an interim create-modal that defines _editorDoBlankCreate (the initialArrangement-based version); this PR adds the fuller roster-based _editorDoBlankCreate at a different offset in screen.js. Git merges both cleanly — and the older copy then shadows the roster-based one at runtime, so the redesign silently won't take effect.

Fix on merge: delete the old initialArrangement-based _editorDoBlankCreate so only this PR's roster-based version remains — this PR supersedes that interim modal. After removing the duplicate, screen.js passes node --check and the editor JS + Python suites are green (24/24 JS, 172 py).

ChrisBeWithYou and others added 11 commits July 5, 2026 14:17
The create modal labelled its Guitar Pro / EOF XML inputs "optional" but
hard-disabled Create unless one was picked, and its blank-chart options
(initial arrangement + drum tab) were never wired up -- so an audio-only
feedpak could only be made via a separate "New Sloppak" dialog.

Add an explicit Blank / Guitar Pro / EOF XML mode picker (Blank default):
- Blank shows the initial-arrangement toggle (Lead/Rhythm/Bass) + a drum-tab
  option, enables Create on audio + title + artist, routes to the existing
  create_sloppak backend, and opens the new feedpak in the editor.
- GP / EOF keep their import flows; only the active mode's section shows.
- The "New" chooser's entries now open this one unified modal (Blank / GP
  mode) instead of a parallel dialog.
- Rename the toolbar's "Build Song" button to "Build feedpak".

The Create-button enable logic is factored into a pure _createGateOpen()
covered by tests/create_gate.test.js (per-mode enable). screen.js parses
clean; all 13 JS suites pass.

Deferred (backend work / follow-ups): Keys/Drums-as-arrangement + extended
tunings; removing the now-unreferenced standalone New-Sloppak dialog; the
editor-header trim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR-A of the onboarding charrette train. create_sloppak hard-capped the
initial arrangement's tuning to length 4|6, which feedpak-spec 5.2
forbids ("Readers MUST NOT hard-code length 6") -- so 7/8-string guitars
and 5/6-string basses could only be made via the save-as path, not at
the source, and imported extended-range tunings risked coercion.

Backend: range-check tuning length 4-8 (default 4 for Bass, 6 otherwise);
the writer already derives string count from the tuning array length.
Frontend: a Strings picker in Blank mode (guitar 6/7/8, bass 4/5/6) that
sends a standard tuning array of the chosen length.

Verified against the running backend: create_sloppak now accepts a
7-string Lead and 5-string Bass tuning (previously 400) and rejects
out-of-range (9) with the spec-cited error. screen.js parses; all
editor JS suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the create roster beyond fretted roles. Keys and Drums are
name-recognized by the editor (KEYS_PATTERN / ^drums), so a "Keys"
arrangement opens in piano-roll and a "Drums" arrangement in drum mode.

Backend (create_sloppak): accept initial_arrangement Keys/Drums; branch
the tuning/capo validation to fretted only (Keys/Drums carry no strings);
Keys arrangements get the spec `type: piano`; a Drums pack always seeds
its drum_tab. Verified against the running backend -- Keys/Drums accepted,
Vocals rejected, extended-range fretted unchanged.

Frontend: full roster (Lead/Rhythm, Bass, Keys, Drums) with the string-
count picker hidden for Keys/Drums; Vocals shown but DISABLED with an
honest tooltip -- the editor has no vocals edit mode yet, so offering it
would create a pack you can't edit.

Bass string-default bug: switching to Bass kept the guitar default of 6
(6 is a valid bass count, so the keep-if-valid reset didn't fire) -- now
resets to the role default (Bass 4, guitar 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A ground-up rework of the "New…" create flow (was "Create New custom song"),
built + tested iteratively on the :8000 testbed.

Onboarding / entry
- Entering the Song Editor with nothing loaded shows a Load / Create landing.
- Load is now a file browser rooted at the DLC/library folder (new backend
  POST /browse, single directory level, containment-guarded) with recursive
  search; opens on the folder instead of a blank "type to search" prompt.

Create New Arrangement modal (two-column, wider)
- "What are you arranging?" — click-to-add + drag-to-reorder instrument roster
  (Vocals / Lead / Rhythm / Keys / Bass / Drums); no string counts (set in the
  editor). Multi-arrangement create; Vocals seeds a lyrics side-file.
- Draft-now, audio-later: create with just a title (audio + artist optional);
  backend writes an audio-less draft the editor can open.
- Content Import — one staged, role-capped file list (1 master audio, 1 chart,
  MIDI info row) that no longer drops one file when you add another; Guitar Pro
  audio auto-syncs to a staged master track in one click.
- Spec-complete manifest metadata (album_artist, track, disc, genres, ISRC,
  MBID, language, authors) + non-destructive GP-metadata autofill.
- MusicBrainz "Match" popup: structured Artist+Title query (no more junk
  single-field search), and re-ranks candidates by how close each length is to
  the staged master audio (studio vs live/extended) with a "≈ your audio" tag.
- Album art promoted to a preview; preview clip auto-generated from the master
  audio (manual upload removed).
- GP track picker: drums recolored from alarming red to a neutral role pill
  (Drums amber / Keys indigo / Bass sky / Guitar muted) — it's a role, not a
  warning.

Backend (routes.py): create_sloppak multi-arrangement + full metadata +
audio-less drafts + lyrics/drum seeds; upload-audio returns duration; auto
preview generation (create + replace-audio); folder-browse endpoint.

Tests: create_gate.test.js updated; full editor JS suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover art lives in the Cover Art Archive (CAA), keyed by RELEASE MBID — which
the create modal's MusicBrainz "Match" already carries (candidate.release_id).
A "Find cover…" button next to the album-art file input opens a Plex-style grid
of covers for the song's releases; click one to bake it into the pack.

- routes.py: GET /caa-cover/{release_id} (server-side CAA front-500 fetch,
  cached under STORAGE_DIR, FileResponse; 404 when a release has no art so the
  grid can hide the tile — dodges browser CORS and gives us the bytes) and
  POST /use-caa-cover → an art_path create_sloppak bakes like an upload.
  release_id is regex-validated before URL interpolation.
- screen.*: "Find cover…" button; editorArtSearch popup — prefilled artist/title,
  reuses /api/enrichment/search for candidate release MBIDs, one tile per unique
  release (same-origin <img>, onerror-hidden), pick → art_path + preview.

Verified on :8000: real covers returned per release, 404s hidden, art_path baked.
The art picker reused the RECORDING search, so it inherited the same "dozens of
comp/reissue versions" problem — random covers instead of the canonical album.
Album art is a property of the ALBUM, so search MusicBrainz RELEASE-GROUPS, which
(unlike recording releases) carry reliable Album vs Live/Compilation typing.

- routes.py: GET /cover-search?artist=&query= → release-group search, studio
  album first (primary Album, no Live/Compilation secondary type), earliest.
  caa-cover/use-caa-cover gain a release-group mode (?group=1) since CAA also
  serves release-group front art.
- screen.*: the picker's second field is now "Album (or song)", prefilled from
  the Album field (filled by a MusicBrainz Match) or the title; tiles come from
  cover-search (canonical first), with the recording-release covers as a
  fallback for non-title-track songs searched art-first.

This fixes "art search shows random covers" and makes art-first work: verified
AC/DC "Highway to Hell" returns the 1979 studio album cover as the first tile.
A non-title-track studio recording can sit well down MusicBrainz's flat list
(dozens of comp/reissue takes score identically), so a small limit dropped it
entirely and the duration re-rank had nothing to promote. Fetch the max (25) and
pass the staged audio's duration. Marginal — MB's search order is
non-deterministic, so text matching still can't reliably surface the canonical
non-title-track recording; audio fingerprinting (AcoustID) is the real fix.
…gerprint)

Add an "Identify from audio…" button beside Match: it re-POSTs the staged master
audio to core /api/enrichment/identify and fills the exact recording, reusing the
MB result rows + apply path (identify returns the same candidate shape). Track
the staged File so the bytes can be re-uploaded (core can't read our stored copy).

Self-serve: when AcoustID is off (412 needs_setup), the popup expands an inline
"enable + paste your key" form (application-key guidance + a register link) that
saves to core settings and re-runs — no separate settings screen. A saved-but-
rejected key (503) offers "Change AcoustID key" so it never dead-ends. Autofill
note reflects the real source (fingerprint vs MusicBrainz).
File master audio couples into the GP autosync path on selection (via
_refreshGpAudioUI), but a pasted YouTube URL only resolves at Create time, so
gp8AudioMode stayed 'none' and the chart was attached to the downloaded audio
UNALIGNED (no autosync run). After resolving the URL, re-derive the GP audio
UI and recompute the mode so the autosync path runs — matching file-audio
behavior. Embedded-audio GPs are unaffected (that branch is guarded and still
deliberately ignores a user URL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rework computed init_drums solely from whether the roster contains
'Drums', ignoring the legacy 'init_drum_tab' flag. A back-compat caller sending
{initial_arrangement: 'Lead', init_drum_tab: true} (default was True) no longer
got its drum tab. Translate the legacy flag into the roster (append 'Drums')
so the old single-arrangement + drum-tab contract is preserved; the new
roster-based path is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The create modal collects album_artist / track / disc / genres / language /
isrc / mbid / authors for every flow, but only blank-create wrote them —
a Guitar Pro or EOF import silently dropped them (convert-gp / import-xml-project
kept only title/artist/album/year on the session). _write_sloppak_pak already
writes all these fields from its meta dict and /build merges session.metadata,
so the only gap was routing them in.

New lenient normalizer _extended_manifest_meta() coerces the modal's raw strings
into the shapes the manifest writer expects (track/disc → int, genres/authors →
deduped lists, isrc stripped+upper, mbid lower). convert-gp reads them from its
JSON body; import-xml-project takes one extended_meta JSON form field; both
merge the result into session.metadata so /build persists it. Frontend sends the
fields via a shared _createExtendedMeta() helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos force-pushed the feat/blank-project-onboarding branch from 51893af to 323e68e Compare July 5, 2026 12:18
…boarding

# Conflicts:
#	routes.py
#	screen.js
#	tests/create_gate.test.js
@byrongamatos
byrongamatos merged commit 977ec65 into main Jul 5, 2026
1 check was pending
byrongamatos added a commit that referenced this pull request Jul 9, 2026
…te UI

Copilot, on #173. Three findings, all pre-existing code this move surfaced, all
from the same commit.

- initCreate()'s comment claimed the input listener invalidates a cached upload
  URL. It does not, and never did here: that happens in the audio file/URL
  change handlers, which clear createState.audioUrl directly. Comment corrected.

- _populateCreateArrButtons and _populateStringCountButtons are UNREACHABLE. The
  first is called only by itself (a re-render on click); the second only by the
  first. Nothing outside the pair enters them, which is why lint has warned about
  the first for weeks.

- They read and write createState.initialArr. Nothing else does — the create
  payload sends createState.initialArrangement. Were this UI ever wired up, the
  arrangement the user picked would not reach the server.

Not renamed. Making dead code consistent would change the behaviour of code
nobody has decided to run, and the right field name is a product question. Both
functions arrived with the Create-New redesign (977ec65, #45) — the same commit
whose _editorDoBlankCreate never ran either, for an unrelated reason. That
redesign is half-wired. Recorded at the call site; left exactly as found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 9, 2026
…tep 22) (#173)

* refactor(editor): move the song-creation flow to src/create.js (R2, step 22)

src/main.js 12,738 -> 10,380. Twenty-third module; the graph stays acyclic.
main.js is now 51% of the 21,176 lines this refactor started from.

2,380 lines: the format picker, the sloppak-create modal, the roster, the
MusicBrainz metadata match, the album-art picker, and the `createState` object
every step of the flow reads. They travel together because they ARE one flow
over one object.

main.js keeps the load/audio pipeline (loadCDLC, loadAudio), the library rescan
and the transport readouts — six new host hooks — plus the entry landing, which
is screen-entry UI that merely opens two of these dialogs.

The 22 window.editor* handlers the HTML calls are exported as plain functions
and re-attached by main.js. The module's one import-time side effect, a global
'input' listener, became an exported initCreate() called from init(): a module
must do no work when it is loaded, or its tests cannot import it without a DOM.

FOUND A REAL BUG, and did not fix it here.

main.js contained TWO `async function _editorDoBlankCreate` definitions. Inside
the IIFE that is legal: function declarations hoist, and the LAST one in source
order silently wins. So the version added by the Create-New redesign (#45, Jul 5)
never ran. The one that has been executing came from "restore audio-only project
creation" (3f66bec, Jul 4), and the two differ in behaviour:

  running (kept):  artist REQUIRED, audio REQUIRED, no roster validation
  dead (removed):  audio optional (draft-now, audio-later), roster validated

The collision only surfaced because create.js is a module, where a duplicate
declaration is a SyntaxError rather than a silent overwrite.

This PR preserves runtime behaviour exactly: the dead definition is deleted, NOT
promoted. Restoring the redesign's intent is a product decision and belongs in
its own PR. A comment in create.js records all of this at the call site.

Verified beyond the unit tests, which cannot see the wiring: verify_create.py
opens the modal and types a title. That one keystroke exercises all three things
that can only fail in a browser — the window.* re-attach, initCreate(), and
host.addGlobalListener — because nothing else is listening. Comment out
initCreate() and all 89 unit tests still pass while the Create button never
un-greys.

node --test 89/89, pytest 248/248, npm run lint 0 errors (6 warnings), Codex
clean, all 15 headless harnesses pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(editor): disambiguate the main.js reduction percentage

CodeRabbit, on #173. The sentence said main.js is '51% of what it was', which
reads as 51% of the previous step's 12,738 and is wrong either way.

10,380 / 21,176 = 49% of the ORIGINAL, i.e. a 51% reduction from where this
refactor started. Against the previous step it is 81%. Both numbers are now
stated explicitly rather than left to the reader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(editor): correct a stale comment and mark #45's unreachable create UI

Copilot, on #173. Three findings, all pre-existing code this move surfaced, all
from the same commit.

- initCreate()'s comment claimed the input listener invalidates a cached upload
  URL. It does not, and never did here: that happens in the audio file/URL
  change handlers, which clear createState.audioUrl directly. Comment corrected.

- _populateCreateArrButtons and _populateStringCountButtons are UNREACHABLE. The
  first is called only by itself (a re-render on click); the second only by the
  first. Nothing outside the pair enters them, which is why lint has warned about
  the first for weeks.

- They read and write createState.initialArr. Nothing else does — the create
  payload sends createState.initialArrangement. Were this UI ever wired up, the
  arrangement the user picked would not reach the server.

Not renamed. Making dead code consistent would change the behaviour of code
nobody has decided to run, and the right field name is a product question. Both
functions arrived with the Create-New redesign (977ec65, #45) — the same commit
whose _editorDoBlankCreate never ran either, for an unrelated reason. That
redesign is half-wired. Recorded at the call site; left exactly as found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 9, 2026
…nal (#174)

Typing a title enabled the Create button. Clicking it said "Artist is required."
Supplying an artist then said "Audio is required for an audio-only project",
which made the advertised draft-now-audio-later flow impossible. The roster
chips were ignored — every draft came out as a lone Lead arrangement — and the
eight extended-metadata fields the modal collects were never sent.

Root cause is the duplicate `_editorDoBlankCreate` found while extracting
src/create.js (#173): main.js defined it twice, function declarations hoist, and
the LAST one in source order silently won. So PR #45's redesign never ran, and
the older definition kept sending the server's documented BACK-COMPAT payload
(`initial_arrangement` + `init_drum_tab`) instead of the roster it asks for.

Everything else already agreed, which is what makes this unambiguous rather than
a product call:

  _createGateOpen   enables Create on a title + one instrument (and is unit-tested)
  screen.html       marks only Title "(required)"
  editorDoCreate    comments, right above the call: "only a title is required;
                    audio + artist are optional"
  create_sloppak    "title required"; "Artist is OPTIONAL for a draft"; "Draft-now,
                    audio-later: audio is OPTIONAL"; and it calls
                    initial_arrangement/init_drum_tab the "Legacy shape"

Only the handler disagreed. It now validates a title and a non-empty instrument
roster, treats artist and audio as optional, and sends `arrangements` plus the
spec-complete metadata via the same _createExtendedMeta() helper the Guitar Pro
and EOF paths already use.

Kept from the old function: the art-upload retry (art normally uploads on
selection; this covers a selection whose upload failed). Added: the roster
check re-enables the button on failure, which the redesign's version forgot.

Removed createState.initialArrangement and createState.initDrumTab — nothing
reads them now. The separate, older editorShowCreateSloppakModal dialog still
sends the legacy shape with its own drum checkbox, and the server still accepts
it; untouched.

Codex flagged one apparent regression: that the old payload defaulted
init_drum_tab to true, so a default create used to seed Drums as well. It reads
that way, but the line below the default overwrites it with a lookup for
`#editor-create-drum-tab` — an element the same redesign deleted from
screen.html. Verified against the live DOM: the element does not exist, the
expression is false, and the server appended nothing. The default roster was
['Lead'] before this change and is ['Lead'] after it.

verify_blank_create.py drives the real modal and reads the real POST body: a
title alone POSTs, no artist and no audio are sent or demanded, the roster chips
arrive as `arrangements`, no back-compat keys are sent, and the extended fields
travel. It fails on 6 of 8 checks against the pre-fix code. End to end the
server writes an audio-less draft (`stems: []`) with arrangements/lead.json,
arrangements/drums.json and drum_tab.json when Drums is chosen.

node --test 89/89, pytest 248/248, npm run lint 0 errors, all 16 headless
harnesses pass.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants