From 62251d764ed066796ee8af7fc605831414c6f634 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 16 Jul 2026 11:51:55 -0600 Subject: [PATCH] 0.6.9: Finish the Stage 4 path revisit in the three remaining scripts The 2026-07-08 path rewrite changed beets' displayed paths from the transitional /music/... mount to real /data/music/Library/... paths. dedup-library.sh was fixed in 0.6.8; three more scripts still assumed the old prefix, each failing silently: - replace-with-better.sh resolved every library match to a doubled nonexistent path, logged [stale-db], skipped the in-place replace, and then imported the staged FLAC as a NEW track via the leftover-import step. Net effect: the mp3-to-flac upgrade flow produced flac+mp3 twin pairs in the library (surfacing in the dedup queue) instead of replacing the MP3 in place. - fix-track-metadata.py queried beets only by the legacy /music/... form, matched nothing, and silently skipped beet update/move after retagging. - export-laptop-playlists.py filtered out every library track (no displayed path starts with /music/ anymore), producing empty exports. All three now use the real displayed path and keep the /music/... form only as a legacy fallback. Co-Authored-By: Claude Fable 5 --- README.md | 6 +++--- pipeline/lib/export-laptop-playlists.py | 18 +++++++++------- pipeline/lib/fix-track-metadata.py | 28 +++++++++++++++---------- pipeline/lib/replace-with-better.sh | 13 +++++++++++- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c7fe2ba..68d6f87 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ Optional, add these later if you want them: Pull the prebuilt image onto your Docker host: ```bash -docker pull git.kretzer.club/andrew/alembic:0.6.8 +docker pull git.kretzer.club/andrew/alembic:0.6.9 ``` -That is the whole install. You do not need to download the source or build anything. The `0.6.8` is the version; you can pin to it so nothing changes under you, or use `latest` to always get the newest. +That is the whole install. You do not need to download the source or build anything. The `0.6.9` is the version; you can pin to it so nothing changes under you, or use `latest` to always get the newest. (If you would rather build it yourself from source, you can, but you do not need to.) @@ -136,7 +136,7 @@ Create a file called `docker-compose.yml` on your server (put it wherever you ke ```yaml services: alembic: - image: git.kretzer.club/andrew/alembic:0.6.8 + image: git.kretzer.club/andrew/alembic:0.6.9 container_name: alembic ports: - "8420:8420" diff --git a/pipeline/lib/export-laptop-playlists.py b/pipeline/lib/export-laptop-playlists.py index 52ab5d7..906f99a 100755 --- a/pipeline/lib/export-laptop-playlists.py +++ b/pipeline/lib/export-laptop-playlists.py @@ -57,11 +57,12 @@ OUT_DIR = f"{_MUSIC_DATA_DIR}/playlists-laptop" # Absolute Windows path to the laptop's library root. LIBRARY_LAPTOP_ROOT = r"C:\Music\Library" -# Beets stores paths with this prefix (the container-side mount of the -# library); strip it to get the path relative to the library root. This is -# still "/music/" through the migration's Stage 0-3 transitional mount; -# revisit at Stage 4 once beets' directory: becomes MUSIC_DATA_DIR/Library. -BEETS_LIBRARY_PREFIX = "/music/" +# Prefixes beets may display library paths under, tried in order: the real +# library dir (everything since the 2026-07-08 path rewrite) and the legacy +# transitional mount (any stale pre-rewrite entry). Strip whichever matches +# to get the path relative to the library root; when neither matches the +# track is outside the library and is skipped. +BEETS_LIBRARY_PREFIXES = (f"{_MUSIC_DATA_DIR}/Library/", "/music/") M3U_EXT = ".m3u" # Older formats from earlier iterations of this script — swept by reconcile. @@ -157,9 +158,12 @@ def build_beets_index() -> dict[tuple[str, str], list[tuple[str, str, str, str]] if len(parts) != 4: continue aa, alb, ti, path = parts - if not path.startswith(BEETS_LIBRARY_PREFIX): + rel = next( + (path[len(p):] for p in BEETS_LIBRARY_PREFIXES if path.startswith(p)), + None, + ) + if rel is None: continue - rel = path[len(BEETS_LIBRARY_PREFIX):] idx.setdefault((_normkey(alb), _normkey(ti)), []).append((aa, alb, ti, rel)) return idx diff --git a/pipeline/lib/fix-track-metadata.py b/pipeline/lib/fix-track-metadata.py index a2e89d2..cf08dba 100755 --- a/pipeline/lib/fix-track-metadata.py +++ b/pipeline/lib/fix-track-metadata.py @@ -49,8 +49,10 @@ _ALEMBIC_CONFIG_DIR = os.environ.get("ALEMBIC_CONFIG_DIR", "/config") _MUSIC_DATA_DIR = os.environ.get("MUSIC_DATA_DIR", "/data/music") LIBRARY = f"{_MUSIC_DATA_DIR}/Library" -# Still "/music" through the migration's Stage 0-3 transitional beets mount; -# revisit at Stage 4 once beets' directory: becomes MUSIC_DATA_DIR/Library. +# Legacy prefix from the migration's transitional beets mount. Since the +# 2026-07-08 path rewrite beets displays real LIBRARY paths, so this only +# matters for accepting pasted /music/... paths as input and as a fallback +# beets query form for any stale pre-rewrite DB entry. CONTAINER_PREFIX = "/music" SPOTIFY_ENV = f"{_ALEMBIC_CONFIG_DIR}/pipeline/_spotify.env" SPOTIFY_GENRE = f"{os.environ.get('PIPELINE_DIR', '/app/pipeline')}/lib/spotify-genre.py" @@ -316,15 +318,19 @@ def resolve_input_path(arg): def beets_id_for(host_path): - cp = host_to_container(host_path) - r = subprocess.run( - ["beet", "ls", "-f", "$id|||$path", f"path:{cp}"], - capture_output=True, text=True, check=True) - for line in r.stdout.splitlines(): - if "|||" in line: - id_str, p = line.split("|||", 1) - if p == cp: - return int(id_str) + # Real path first (how beets displays everything since the 2026-07-08 + # path rewrite), legacy /music/... form second for any stale entry. + # Querying only the legacy form (the old behavior) matched nothing after + # the rewrite, so retag-from-url silently skipped beet update/move. + for cp in (host_path, host_to_container(host_path)): + r = subprocess.run( + ["beet", "ls", "-f", "$id|||$path", f"path:{cp}"], + capture_output=True, text=True, check=True) + for line in r.stdout.splitlines(): + if "|||" in line: + id_str, p = line.split("|||", 1) + if p == cp: + return int(id_str) return None diff --git a/pipeline/lib/replace-with-better.sh b/pipeline/lib/replace-with-better.sh index 376a672..9234c5e 100755 --- a/pipeline/lib/replace-with-better.sh +++ b/pipeline/lib/replace-with-better.sh @@ -195,7 +195,18 @@ while IFS= read -r -d '' new_file; do existing_id="${match%%$'\x1f'*}" existing_container="${match#*$'\x1f'}" - existing="${MUSIC_DATA_DIR:-/data/music}/Library${existing_container#/music}" + # beets displays real ${MUSIC_DATA_DIR}/Library/... paths since the + # 2026-07-08 path rewrite -- use them as-is; only a legacy /music/... + # display path still needs the prefix swap. Blindly prepending (the old + # behavior) doubled the prefix, so every match logged [stale-db], the MP3 + # never got replaced, and the leftover-import step at the bottom imported + # the staged FLAC as a NEW track -- producing exactly the flac+mp3 twins + # this script exists to prevent. + if [[ "$existing_container" == /music/* ]]; then + existing="${MUSIC_DATA_DIR:-/data/music}/Library${existing_container#/music}" + else + existing="$existing_container" + fi if [[ ! -f "$existing" ]]; then echo "[stale-db] beets has $existing_container but file missing" | tee -a "$LOG" continue