Template
fix: enforce artwork-safe music ingestion
This commit is contained in:
Executable
+104
@@ -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"
|
||||
Reference in New Issue
Block a user