#!/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] -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