fix: enforce artwork-safe music ingestion

This commit is contained in:
2026-07-20 01:56:13 +08:00
parent 4c68e415ee
commit 484c9c0d24
6 changed files with 1223 additions and 78 deletions
+104
View File
@@ -0,0 +1,104 @@
#!/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"