lisp-scripts/sync-music/main.lisp
eriedaberrie 1a714ac77a feat!: sync-music: properly utilize cuesheets
Instead of blindly copying over .cue files, attempt to parse them with
cuetools and use the data to split up the associated flac files.  Old behavior
is available under --dumb-cue-copy.
2024-11-07 15:19:59 -08:00

100 lines
3.4 KiB
Common Lisp

(uiop:define-package #:sync-music/cli
(:use #:cl #:alexandria :serapeum #:sync-music)
(:export #:main))
(in-package #:sync-music/cli)
(defvar *dry-run-p*)
(opts:define-opts
(:name :help
:description "print this help message"
:short #\h
:long "help")
(:name :jobs
:description "number of worker threads to spawn"
:short #\j
:long "jobs"
:arg-parser #'parse-integer
:meta-var "JOBS")
(:name :bitrate
:description "the bitrate for opus conversions (ffmpeg -b:a)"
:short #\b
:long "bitrate"
:arg-parser #'identity
:meta-var "BITRATE")
(:name :max-depth
:description "max depth to search directories before signaling an error"
:short #\d
:long "depth"
:arg-parser #'parse-integer
:meta-var "MAX-DEPTH")
(:name :dumb-cue-copy-p
:description "blindly copy cue files instead of parsing them"
:short #\c
:long "dumb-cue-copy")
(:name :dry-run-p
:description "print out actions and exit"
:short #\n
:long "dry-run")
(:name :no-cleanup-p
:description "don't sync deletions"
:short #\k
:long "no-cleanup")
(:name :no-ignore-toplevel-p
:description "don't ignore files in the top level of the directories"
:long "no-ignore-toplevel"))
(defun confirm-handler (copy-actions delete-actions directory-actions)
(let ((all-actions (list directory-actions copy-actions delete-actions)))
(when (every #'zerop (mapcar #'fset:size all-actions))
(format t "No actions to perform!~%")
(return-from confirm-handler))
(format t "Actions:~%")
(dolist (actions all-actions)
(fset:do-set (action actions)
(action-describe action t)))
(unless *dry-run-p*
(format t
"~2&Perform ~A copies and ~A deletions? [y/N] "
(fset:size copy-actions)
(fset:size delete-actions))
(finish-output)
(char-equal (read-char) #\y))))
(defun progress-handler (queue task-count)
(loop :for n :from 1 :upto task-count
:do (format t
"(~A/~A) ~A~%"
n
task-count
(lparallel.queue:pop-queue queue))))
(defun main ()
(multiple-value-bind (options free-args)
(opts:get-opts)
(let ((free-argc (length free-args)))
(cond
((= free-argc 1)
(push (uiop:native-namestring #P"~/Music/") free-args))
((or (getf options :help) (/= free-argc 2))
(opts:describe
:prefix "Sync music files between directories, converting flac to opus."
:suffix "ORIGIN is assumed to be `~/Music/` if not provided."
:usage-of "sync-music"
:args "[ORIGIN] TARGET")
(return-from main)))
(let ((*worker-threads* (getf options :jobs))
(*opus-bitrate* (or (getf options :bitrate)
*opus-bitrate*))
(*max-depth* (or (getf options :max-depth)
*max-depth*))
(*dumb-cue-copy-p* (getf options :dumb-cue-copy-p))
(*dry-run-p* (getf options :dry-run-p))
(*cleanupp* (not (getf options :no-cleanup-p)))
(*ignore-toplevel-p* (not (getf options :no-ignore-toplevel-p))))
(sync-music (uiop:parse-native-namestring (first free-args))
(uiop:parse-native-namestring (second free-args))
:confirmp #'confirm-handler
:progress-handler #'progress-handler)))))