#!/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 ==="