220 lines
4.9 KiB
Bash
Executable File
220 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env zsh
|
|
emulate -L zsh
|
|
setopt err_exit pipefail no_unset
|
|
|
|
prune_root="${BEET_PRUNE_ROOT:-/music/pruned}"
|
|
dry_run=0
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage:
|
|
beet-prune-albums [options] [beets-query...]
|
|
|
|
Interactively select albums with fzf, move them to a pruned quarantine folder,
|
|
then remove them from the beets database.
|
|
|
|
Options:
|
|
--dest DIR Quarantine root. Default: /music/pruned
|
|
--dry-run Show what would happen, but do not move or remove anything.
|
|
-h, --help Show this help.
|
|
|
|
Examples:
|
|
beet-prune-albums
|
|
beet-prune-albums albumartist:=~"Taylor Swift"
|
|
beet-prune-albums genre:soundtrack
|
|
beet-prune-albums --dest /music/pruned
|
|
beet-prune-albums --dry-run
|
|
EOF
|
|
}
|
|
|
|
while (( $# )); do
|
|
case "$1" in
|
|
--dest)
|
|
prune_root="${2:?missing argument for --dest}"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
dry_run=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
print -u2 "error: unknown option: $1"
|
|
usage
|
|
exit 2
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
query=( "$@" )
|
|
|
|
if ! command -v fzf >/dev/null 2>&1; then
|
|
print -u2 "error: fzf is required"
|
|
print -u2 "install with: brew install fzf"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v beet >/dev/null 2>&1; then
|
|
print -u2 "error: beet is not in PATH"
|
|
exit 1
|
|
fi
|
|
|
|
timestamp="$(date +%Y%m%d-%H%M%S)"
|
|
batch_dir="$prune_root/$timestamp"
|
|
marker="prune_candidate_$timestamp"
|
|
|
|
tmpdir="$(mktemp -d)"
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
|
|
all="$tmpdir/all.tsv"
|
|
selected="$tmpdir/selected.tsv"
|
|
|
|
# Album list:
|
|
# column 1 = beets album id, hidden from fzf display but used by the script.
|
|
# columns 2+ = human-readable fields.
|
|
fmt=$'$id\t$albumartist\t$album\t$year\t$tracktotal'
|
|
|
|
beet ls -a -f "$fmt" "${query[@]}" > "$all"
|
|
|
|
if [[ ! -s "$all" ]]; then
|
|
print "No albums matched."
|
|
exit 0
|
|
fi
|
|
|
|
fzf \
|
|
--multi \
|
|
--bind 'space:toggle' \
|
|
--delimiter=$'\t' \
|
|
--with-nth='2,3,4,5' \
|
|
--header='SPACE = select albums to PRUNE | ENTER = confirm selection | ESC = cancel' \
|
|
--preview='beet ls -f "$disc-$track $artist - $title" "album_id:{1}" | sed -n "1,200p"' \
|
|
--preview-window='down,60%,border-top' \
|
|
< "$all" > "$selected" || {
|
|
print "Cancelled."
|
|
exit 0
|
|
}
|
|
|
|
if [[ ! -s "$selected" ]]; then
|
|
print "Nothing selected."
|
|
exit 0
|
|
fi
|
|
|
|
print
|
|
print "Selected albums:"
|
|
awk -F '\t' '{ printf " [%s] %s - %s (%s, %s tracks)\n", $1, $2, $3, $4, $5 }' "$selected"
|
|
|
|
print
|
|
print "Marking selected albums with temporary marker: $marker"
|
|
|
|
while IFS=$'\t' read -r album_id _rest; do
|
|
beet modify -a -y "id:$album_id" "$marker=1" >/dev/null
|
|
done < "$selected"
|
|
|
|
cleanup_marker() {
|
|
beet modify -a -y "$marker:1" "$marker!" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
trap 'cleanup_marker; rm -rf "$tmpdir"' EXIT
|
|
|
|
print
|
|
print "Albums marked for pruning:"
|
|
beet ls -a "$marker:1"
|
|
|
|
print
|
|
beet stats "$marker:1" || true
|
|
|
|
print
|
|
print "Quarantine destination:"
|
|
print " $batch_dir"
|
|
|
|
if (( dry_run )); then
|
|
print
|
|
print "Dry run. No files moved and no database rows removed."
|
|
print
|
|
print "Preview move command:"
|
|
print " beet move -a -d ${(q)batch_dir} ${(q)marker:1}"
|
|
print
|
|
print "Preview DB removal command:"
|
|
print " beet remove -a ${(q)marker:1}"
|
|
exit 0
|
|
fi
|
|
|
|
print
|
|
print "This will:"
|
|
print " 1. Move selected albums out of the active library into:"
|
|
print " $batch_dir"
|
|
print " 2. Remove those albums from the beets database."
|
|
print " 3. Leave the moved files in the quarantine folder."
|
|
|
|
print
|
|
read -q "confirm?Proceed? [y/N] "
|
|
print
|
|
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
print "Aborted. Cleaning temporary marker."
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$batch_dir"
|
|
|
|
print
|
|
print "Moving selected albums to quarantine..."
|
|
beet move -a -d "$batch_dir" "$marker:1"
|
|
|
|
print
|
|
print "Verifying moved files exist under quarantine..."
|
|
missing=0
|
|
|
|
while IFS=$'\t' read -r album_id _rest; do
|
|
paths="$(beet ls -f '$path' "album_id:$album_id" || true)"
|
|
|
|
if [[ -z "$paths" ]]; then
|
|
print -u2 "warning: no paths found in DB for album id $album_id after move"
|
|
missing=1
|
|
continue
|
|
fi
|
|
|
|
while IFS= read -r path; do
|
|
[[ -z "$path" ]] && continue
|
|
|
|
if [[ "$path" != "$batch_dir"/* ]]; then
|
|
print -u2 "warning: path is not under quarantine for album id $album_id:"
|
|
print -u2 " $path"
|
|
missing=1
|
|
elif [[ ! -e "$path" ]]; then
|
|
print -u2 "warning: moved path does not exist for album id $album_id:"
|
|
print -u2 " $path"
|
|
missing=1
|
|
fi
|
|
done <<< "$paths"
|
|
done < "$selected"
|
|
|
|
if (( missing )); then
|
|
print -u2
|
|
print -u2 "Refusing to remove albums from the DB because verification failed."
|
|
print -u2 "The DB may now point some selected albums at the quarantine folder."
|
|
print -u2 "Inspect with:"
|
|
print -u2 " beet ls -a '$marker:1'"
|
|
print -u2 " beet ls -f '\$path' '$marker:1'"
|
|
exit 1
|
|
fi
|
|
|
|
print
|
|
print "Removing selected albums from beets DB only..."
|
|
beet remove -a -f "$marker:1"
|
|
|
|
print
|
|
print "Done."
|
|
print "Pruned files are here:"
|
|
print " $batch_dir"
|