Files
alembic/pipeline/lib/fix-empty-album.sh
T
andrew b135f11557 Security hardening and first-run/portability improvements
Security (P0):
- Remove committed session-secret default; auto-generate and persist a
  random secret to the config volume when SESSION_SECRET is unset
  (prevents forgeable session cookies / auth bypass).
- Validate playlist names to a safe charset and render sldl configs via
  literal Python substitution instead of sed (closes a command-injection
  and path-traversal path through playlist names).
- shlex-quote credential values written to shell-sourced env files, and
  strip newlines from values patched into .conf files.
- Render playlist .conf files 0600; warn at startup if the master key is
  co-located with the config volume; document keeping it separate.

Portability:
- Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton.
- Remove personal defaults (navidrome user "andrew", ephemeral.club URLs).
- Ship generic example seeds; move the cross-album dedup keep-list and the
  legacy playlist import to editable config files; drop the personal
  _upgrade.csv.
- Generic VPN reference in docker-compose.snippet.yml.

First-run experience:
- Redirect to /setup instead of 500 when OIDC is unconfigured; surface a
  missing master key inline; entrypoint exits with an actionable message
  when the config folder is not writable.
- Add unauthenticated /health (JSON) and /setup (checklist) diagnostics.

Docs:
- Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced
  but missing); expand README with ownership, backups, advanced settings,
  and migration guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:25:55 -06:00

110 lines
3.8 KiB
Bash
Executable File

#!/bin/bash
# Find audio files in the library that have no ALBUM tag, set ALBUM to
# "{TITLE} - Single" — matches Spotify / Apple Music's convention for
# single-track releases. Otherwise these tracks land as [Unknown Album]
# in Navidrome.
#
# Usage:
# fix-empty-album.sh # dry run (default)
# fix-empty-album.sh --apply # actually rewrite tags
#
# Bandcamp single-track downloads are the most common source of this
# issue (Bandcamp serves single tracks with no ALBUM tag). New downloads
# via sync-bandcamp.py are auto-tagged; this script handles backfill.
set -u
APPLY=0
[[ "${1:-}" == "--apply" ]] && APPLY=1
LOG=${ALEMBIC_CONFIG_DIR:-/config}/logs/fix-empty-album-$(date +%Y%m%d-%H%M%S).log
mkdir -p "$(dirname "$LOG")"
NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env"
[ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV"
ND_BASE="${ND_BASE:-http://navidrome:4533}"
ND_USER="${ND_USER:-}"
ND_PASS="${ND_PASS:-}"
log() { echo "[$(date -Iseconds)] $*" | tee -a "$LOG"; }
if [[ $APPLY -eq 1 ]]; then
log "=== FIX EMPTY ALBUM (APPLY) ==="
else
log "=== FIX EMPTY ALBUM (DRY RUN — pass --apply to write) ==="
fi
FIXED=0
SKIPPED=0
while IFS= read -r -d '' f; do
ext="${f,,}"; ext="${ext##*.}"
album="" ; title=""
case "$ext" in
flac)
album=$(metaflac --show-tag=ALBUM "$f" 2>/dev/null | sed -n 's/^[^=]*=//p' | head -1)
title=$(metaflac --show-tag=TITLE "$f" 2>/dev/null | sed -n 's/^[^=]*=//p' | head -1)
;;
mp3)
album=$(id3v2 -l "$f" 2>/dev/null | sed -n 's/^TALB[^:]*: //p' | head -1)
title=$(id3v2 -l "$f" 2>/dev/null | sed -n 's/^TIT2[^:]*: //p' | head -1)
;;
*) continue ;;
esac
if [[ -n "$album" ]]; then
continue # already has an album, leave it alone
fi
if [[ -z "$title" ]]; then
log "[skip-no-title] $f"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Some titles have a leading "NN " track-number prefix baked in from a
# bad filename parse (e.g. title="01 Domestic Violence"). If we use the
# title as-is, the album becomes "01 Domestic Violence - Single" which
# is broken. Strip leading 1-3 digits + space, and also fix the title
# tag in place so it isn't shown with the prefix in clients.
clean_title=$(echo "$title" | sed -E 's/^[[:space:]]*[0-9]{1,3}[[:space:]]+//')
if [[ "$clean_title" != "$title" && -n "$clean_title" ]]; then
log " trimmed leading track-number from title: \"$title\" -> \"$clean_title\""
if [[ $APPLY -eq 1 ]]; then
case "$ext" in
flac) metaflac --remove-tag=TITLE --set-tag="TITLE=${clean_title}" "$f" 2>>"$LOG" ;;
mp3) id3v2 --TIT2 "${clean_title}" "$f" 2>>"$LOG" ;;
esac
fi
title="$clean_title"
fi
new_album="${title} - Single"
log "[$( [[ $APPLY -eq 1 ]] && echo SET || echo WOULD-SET )] album=\"${new_album}\" - $f"
if [[ $APPLY -eq 1 ]]; then
case "$ext" in
flac) metaflac --set-tag="ALBUM=${new_album}" "$f" 2>>"$LOG" ;;
mp3) id3v2 --TALB "${new_album}" "$f" 2>>"$LOG" ;;
esac
fi
FIXED=$((FIXED + 1))
done < <(find ${MUSIC_DATA_DIR:-/data/music}/Library -type f \( -iname "*.flac" -o -iname "*.mp3" \) -print0 2>/dev/null)
log ""
log "=== Summary: $FIXED fixed, $SKIPPED skipped (no title) ==="
if [[ $APPLY -eq 1 && $FIXED -gt 0 ]]; then
log "Syncing beets DB from new tags..."
beet update 2>&1 | tail -3 >> "$LOG"
log "Moving files to reflect new album in path templates..."
beet move 2>&1 | tail -3 >> "$LOG"
log "Triggering Navidrome rescan..."
curl -s -G "$ND_BASE/rest/startScan.view" \
--data-urlencode "u=$ND_USER" \
--data-urlencode "p=$ND_PASS" \
--data-urlencode 'v=1.16.0' \
--data-urlencode 'c=fix-empty-album' \
--data-urlencode 'f=json' \
--data-urlencode 'fullScan=true' >> "$LOG" 2>&1
fi
log "=== Done. Log: $LOG ==="