#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: cleanup-library.sh --library PATH [--dry-run|--execute] Default mode is --dry-run. --execute is required to delete files. The script only handles audio files below PATH and same-basename sidecars. EOF } LIBRARY="" MODE="dry-run" while [[ $# -gt 0 ]]; do case "$1" in --library) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; LIBRARY="$2"; shift 2 ;; --dry-run) MODE="dry-run"; shift ;; --execute) MODE="execute"; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; esac done [[ -n "$LIBRARY" && -d "$LIBRARY" ]] || { echo "Library directory is required" >&2; exit 2; } command -v ffprobe >/dev/null || { echo "ffprobe is required" >&2; exit 2; } command -v ffmpeg >/dev/null || { echo "ffmpeg is required" >&2; exit 2; } is_audio() { local ext="${1##*.}" ext="${ext,,}" case "$ext" in mp3|m4a|mp4|aac|flac|ogg|oga|opus|wma|wav|ape|aiff|aif|wv|tta) return 0 ;; *) return 1 ;; esac } has_cover_file() { [[ -s "$1/cover.jpg" || -s "$1/cover.jpeg" || -s "$1/cover.png" ]] } embedded_stream() { ffprobe -v error -select_streams v \ -show_entries stream=index:stream_disposition=attached_pic \ -of csv=p=0 "$1" 2>/dev/null | awk -F, '$2 == 1 { print $1; exit }' } extract_cover() { local audio="$1" album_dir="$2" stream codec ext target stream="$(embedded_stream "$audio")" [[ -n "$stream" ]] || return 1 codec="$(ffprobe -v error -show_entries stream=index,codec_name -of csv=p=0 "$audio" 2>/dev/null | awk -F, -v idx="$stream" '$1 == idx { print $2; exit }')" case "$codec" in mjpeg|jpeg) ext="jpg"; target="$album_dir/cover.jpg" ;; png) ext="png"; target="$album_dir/cover.png" ;; *) ext="jpg"; target="$album_dir/cover.jpg" ;; esac [[ -e "$target" ]] && return 0 if [[ "$MODE" == "dry-run" ]]; then echo "WOULD EXTRACT $target FROM $audio" return 0 fi if [[ "$ext" == "jpg" && "$codec" =~ ^(mjpeg|jpeg)$ || "$ext" == "png" ]]; then ffmpeg -v error -y -i "$audio" -map "0:$stream" -frames:v 1 -c copy "$target" 2>/dev/null else ffmpeg -v error -y -i "$audio" -map "0:$stream" -frames:v 1 "$target" 2>/dev/null fi [[ -s "$target" ]] } delete_sidecars() { local audio="$1" dir base sidecar ext dir="${audio%/*}"; base="${audio##*/}"; base="${base%.*}" for sidecar in "$dir/$base".*; do [[ -f "$sidecar" && "$sidecar" != "$audio" ]] || continue ext="${sidecar##*.}" case "${ext,,}" in lrc|cue|json|txt|jpg|jpeg|png|webp) [[ "${sidecar##*/}" == cover.jpg || "${sidecar##*/}" == cover.jpeg || "${sidecar##*/}" == cover.png ]] && continue if [[ "$MODE" == "dry-run" ]]; then echo "WOULD DELETE SIDECAR $sidecar"; else rm -f -- "$sidecar"; fi ;; esac done } removed=0 sidecars=0 extracted=0 while IFS= read -r -d '' audio; do album_dir="${audio%/*}" if has_cover_file "$album_dir"; then continue; fi if extract_cover "$audio" "$album_dir"; then ((extracted += 1)) || true continue fi if [[ "$MODE" == "dry-run" ]]; then echo "WOULD DELETE AUDIO $audio" else rm -f -- "$audio" fi delete_sidecars "$audio" ((removed += 1)) || true done < <(find "$LIBRARY" -type f -print0 | while IFS= read -r -d '' f; do is_audio "$f" && printf '%s\0' "$f"; done) echo "mode=$MODE extracted=$extracted audio_removed=$removed"