Build and publish container / build (pull_request) Canceled after 3m8s
tools/sync-to-ipod.sh does the whole transfer to a Rockbox device, so the only manual part left is the disk-mode button sequence. The guards are the substance rather than decoration. rsync --delete is being aimed at a whole filesystem, so the destination must exist, be its own mount point, and be a FAT filesystem; the mirror must be non-empty and must not be the destination. Emptying the wrong directory is not a mistake that announces itself. It also excludes /.rockbox, the scrobbler logs and the usual filesystem metadata directories. The mirror does not contain them, so a sync to the card root would otherwise have deleted the Rockbox installation -- which the first draft of this script would have done. The unmount is why this is a script at all. FAT32 has no journal, the device is reached through the Apple firmware's disk mode because Rockbox's own mass storage is unreliable on an iFlash, and an interrupted write is corruption that needs fsck.vfat from another machine. tools/submit_scrobbles.py sends the Rockbox scrobbler log to Last.fm and sets it aside. Rockbox writes it in AUDIOSCROBBLER 1.1: tab-separated, one line per track, rated L for listened or S for skipped, and only the listened ones are a play. It runs before the copy, because the plays already happened and a failed transfer is no reason to lose them as well. Two things there differ from every other Last.fm call in these projects. Scrobbling is a write method, so it needs the API secret and a session key obtained once through the browser rather than the read-only key. And a target with no real-time clock gets /.scrobbler-timeless.log with every timestamp set to zero; those are counted and reported but never sent, since submitting them would mean inventing when they happened. Signature generation sorts parameter names by the ASCII table rather than numerically, so artist[10] precedes artist[1]. Sorting them the obvious way produces an invalid signature and no other symptom, so there is a test for it. The log is renamed rather than deleted once accepted, so that if Last.fm quietly dropped something the evidence is still on the device.
137 lines
5.0 KiB
Bash
Executable File
137 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copy the mirror onto a Rockbox device, then unmount it cleanly.
|
|
#
|
|
# The device is FAT32 with no journal, reached through the Apple firmware's
|
|
# disk mode because Rockbox's own mass storage is unreliable on an iFlash. An
|
|
# interrupted write is corruption that needs fsck.vfat from another machine, so
|
|
# this syncs and unmounts rather than leaving that to whoever pulls the cable.
|
|
#
|
|
# rsync --delete is pointed at a whole filesystem, so the checks below are the
|
|
# point of the script rather than decoration.
|
|
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat >&2 <<'USAGE'
|
|
usage: sync-to-ipod.sh [options] <mirror> <destination>
|
|
|
|
-n dry run; show what would change and touch nothing
|
|
-f copy even if the FAT32 check finds unacceptable paths
|
|
-S skip submitting the Rockbox scrobbler log to Last.fm
|
|
-U leave the destination mounted afterwards
|
|
|
|
Submitting scrobbles needs LASTFM_API_KEY and LASTFM_API_SECRET; it is skipped
|
|
with a note when they are unset. Scrobbling is a write method and needs the
|
|
secret, unlike the read-only calls elsewhere in these projects.
|
|
|
|
The destination must be a mounted FAT filesystem. Reach it with the Apple
|
|
firmware's disk mode: Menu+Select to reboot, then immediately Select+Play.
|
|
USAGE
|
|
exit 2
|
|
}
|
|
|
|
dry_run=false
|
|
force=false
|
|
unmount=true
|
|
scrobble=true
|
|
while getopts ":nfSUh" option; do
|
|
case "$option" in
|
|
n) dry_run=true ;;
|
|
f) force=true ;;
|
|
S) scrobble=false ;;
|
|
U) unmount=false ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
shift $((OPTIND - 1))
|
|
[ $# -eq 2 ] || usage
|
|
|
|
mirror=${1%/}
|
|
destination=${2%/}
|
|
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
|
|
die() {
|
|
printf 'sync-to-ipod: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
[ -d "$mirror" ] || die "mirror $mirror is not a directory"
|
|
[ -n "$(ls -A "$mirror")" ] || die "mirror $mirror is empty; refusing to mirror nothing"
|
|
[ -d "$destination" ] || die "destination $destination is not a directory"
|
|
|
|
# --delete makes every one of these load-bearing. A destination that is not its
|
|
# own mount point means the path is wrong, and emptying the wrong directory is
|
|
# not a mistake that announces itself.
|
|
case "$destination" in
|
|
"" | "/" | "$HOME") die "refusing to sync onto $destination" ;;
|
|
esac
|
|
[ "$(readlink -f "$mirror")" != "$(readlink -f "$destination")" ] ||
|
|
die "mirror and destination are the same directory"
|
|
mountpoint -q -- "$destination" || die "$destination is not a mount point"
|
|
|
|
filesystem=$(findmnt -no FSTYPE --target "$destination")
|
|
case "$filesystem" in
|
|
vfat | exfat) ;;
|
|
*)
|
|
$force || die "$destination is $filesystem, not FAT; pass -f if that is deliberate"
|
|
printf 'sync-to-ipod: destination is %s, not FAT\n' "$filesystem" >&2
|
|
;;
|
|
esac
|
|
|
|
if $force; then
|
|
printf 'sync-to-ipod: skipping the FAT32 check\n' >&2
|
|
elif ! python3 "$here/check_fat32.py" "$mirror"; then
|
|
die "the mirror holds paths FAT32 will not take; run music-mirror with --fat32-safe"
|
|
fi
|
|
|
|
# Before the copy, not after: the plays already happened, and if the transfer
|
|
# then fails there is no reason to have lost them too.
|
|
if $scrobble; then
|
|
if [ -z "${LASTFM_API_KEY:-}" ] || [ -z "${LASTFM_API_SECRET:-}" ]; then
|
|
printf 'sync-to-ipod: no Last.fm credentials, skipping the scrobbler log\n' >&2
|
|
else
|
|
scrobble_options=()
|
|
$dry_run && scrobble_options+=(--dry-run)
|
|
python3 "$here/submit_scrobbles.py" "${scrobble_options[@]}" "$destination" ||
|
|
die "submitting scrobbles failed; nothing has been copied"
|
|
fi
|
|
fi
|
|
|
|
# -rt rather than -a: owners, groups and permissions mean nothing on FAT, and
|
|
# asking for them produces a screenful of errors and a non-zero exit.
|
|
# --modify-window=2 because FAT stores mtimes to two-second resolution, without
|
|
# which every file looks changed and the whole library is copied every time.
|
|
# --delete removes tracks whose source has gone, which is the point. It would
|
|
# also remove everything on the device that the mirror does not contain -- and
|
|
# if the destination is the card root that means /.rockbox, the Rockbox install
|
|
# itself. Excluded paths are not deleted unless --delete-excluded is given,
|
|
# which it never is here.
|
|
options=(--recursive --times --delete --modify-window=2 --human-readable --info=progress2)
|
|
for owned in "/.rockbox" "/.scrobbler.log" "/.scrobbler.log.*" "/.playlist_control" \
|
|
"/System Volume Information" "/.Spotlight-V100" "/.Trashes" "/.fseventsd"; do
|
|
options+=(--exclude "$owned")
|
|
done
|
|
$dry_run && options+=(--dry-run --verbose)
|
|
|
|
printf 'sync-to-ipod: %s -> %s\n' "$mirror" "$destination" >&2
|
|
rsync "${options[@]}" "$mirror/" "$destination/"
|
|
|
|
if $dry_run; then
|
|
printf 'sync-to-ipod: dry run, nothing was written\n' >&2
|
|
exit 0
|
|
fi
|
|
|
|
sync
|
|
if $unmount; then
|
|
device=$(findmnt -no SOURCE --target "$destination")
|
|
printf 'sync-to-ipod: unmounting %s\n' "$device" >&2
|
|
if command -v udisksctl >/dev/null 2>&1; then
|
|
udisksctl unmount -b "$device"
|
|
else
|
|
umount -- "$destination"
|
|
fi
|
|
printf 'sync-to-ipod: safe to disconnect\n' >&2
|
|
else
|
|
printf 'sync-to-ipod: still mounted; unmount before disconnecting\n' >&2
|
|
fi
|