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 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-16 11:51:55 -06:00
parent d756aab7fc
commit 62251d764e
4 changed files with 43 additions and 22 deletions
+3 -3
View File
@@ -73,10 +73,10 @@ Optional, add these later if you want them:
Pull the prebuilt image onto your Docker host: Pull the prebuilt image onto your Docker host:
```bash ```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.) (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 ```yaml
services: services:
alembic: alembic:
image: git.kretzer.club/andrew/alembic:0.6.8 image: git.kretzer.club/andrew/alembic:0.6.9
container_name: alembic container_name: alembic
ports: ports:
- "8420:8420" - "8420:8420"
+11 -7
View File
@@ -57,11 +57,12 @@ OUT_DIR = f"{_MUSIC_DATA_DIR}/playlists-laptop"
# Absolute Windows path to the laptop's library root. # Absolute Windows path to the laptop's library root.
LIBRARY_LAPTOP_ROOT = r"C:\Music\Library" LIBRARY_LAPTOP_ROOT = r"C:\Music\Library"
# Beets stores paths with this prefix (the container-side mount of the # Prefixes beets may display library paths under, tried in order: the real
# library); strip it to get the path relative to the library root. This is # library dir (everything since the 2026-07-08 path rewrite) and the legacy
# still "/music/" through the migration's Stage 0-3 transitional mount; # transitional mount (any stale pre-rewrite entry). Strip whichever matches
# revisit at Stage 4 once beets' directory: becomes MUSIC_DATA_DIR/Library. # to get the path relative to the library root; when neither matches the
BEETS_LIBRARY_PREFIX = "/music/" # track is outside the library and is skipped.
BEETS_LIBRARY_PREFIXES = (f"{_MUSIC_DATA_DIR}/Library/", "/music/")
M3U_EXT = ".m3u" M3U_EXT = ".m3u"
# Older formats from earlier iterations of this script — swept by reconcile. # 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: if len(parts) != 4:
continue continue
aa, alb, ti, path = parts 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 continue
rel = path[len(BEETS_LIBRARY_PREFIX):]
idx.setdefault((_normkey(alb), _normkey(ti)), []).append((aa, alb, ti, rel)) idx.setdefault((_normkey(alb), _normkey(ti)), []).append((aa, alb, ti, rel))
return idx return idx
+17 -11
View File
@@ -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") _MUSIC_DATA_DIR = os.environ.get("MUSIC_DATA_DIR", "/data/music")
LIBRARY = f"{_MUSIC_DATA_DIR}/Library" LIBRARY = f"{_MUSIC_DATA_DIR}/Library"
# Still "/music" through the migration's Stage 0-3 transitional beets mount; # Legacy prefix from the migration's transitional beets mount. Since the
# revisit at Stage 4 once beets' directory: becomes MUSIC_DATA_DIR/Library. # 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" CONTAINER_PREFIX = "/music"
SPOTIFY_ENV = f"{_ALEMBIC_CONFIG_DIR}/pipeline/_spotify.env" SPOTIFY_ENV = f"{_ALEMBIC_CONFIG_DIR}/pipeline/_spotify.env"
SPOTIFY_GENRE = f"{os.environ.get('PIPELINE_DIR', '/app/pipeline')}/lib/spotify-genre.py" 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): def beets_id_for(host_path):
cp = host_to_container(host_path) # Real path first (how beets displays everything since the 2026-07-08
r = subprocess.run( # path rewrite), legacy /music/... form second for any stale entry.
["beet", "ls", "-f", "$id|||$path", f"path:{cp}"], # Querying only the legacy form (the old behavior) matched nothing after
capture_output=True, text=True, check=True) # the rewrite, so retag-from-url silently skipped beet update/move.
for line in r.stdout.splitlines(): for cp in (host_path, host_to_container(host_path)):
if "|||" in line: r = subprocess.run(
id_str, p = line.split("|||", 1) ["beet", "ls", "-f", "$id|||$path", f"path:{cp}"],
if p == cp: capture_output=True, text=True, check=True)
return int(id_str) for line in r.stdout.splitlines():
if "|||" in line:
id_str, p = line.split("|||", 1)
if p == cp:
return int(id_str)
return None return None
+12 -1
View File
@@ -195,7 +195,18 @@ while IFS= read -r -d '' new_file; do
existing_id="${match%%$'\x1f'*}" existing_id="${match%%$'\x1f'*}"
existing_container="${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 if [[ ! -f "$existing" ]]; then
echo "[stale-db] beets has $existing_container but file missing" | tee -a "$LOG" echo "[stale-db] beets has $existing_container but file missing" | tee -a "$LOG"
continue continue