(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)))))