Compare commits

..

No commits in common. "527620d2ea488c5a400986247b6e27d9d3ea4a3e" and "b488540ffe32092e092a10c1d89fbac154073325" have entirely different histories.

11 changed files with 117 additions and 440 deletions

View file

@ -25,11 +25,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1731139594, "lastModified": 1730785428,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -35,21 +35,14 @@
devShells = forSystems ( devShells = forSystems (
pkgs: { pkgs: {
default = pkgs.mkShell rec { default = pkgs.mkShell rec {
inputsFrom = builtins.attrValues self.packages.${pkgs.stdenv.system};
nativeBuildInputs = nativeBuildInputs =
[ [
(pkgs.sbcl.withPackages (lib.const (pkgs.sbcl.withPackages (lib.const
(lib.concatMap (p: p.lispLibs) inputsFrom))) (lib.concatMap (p: p.lispLibs) inputsFrom)))
] ]
++ lib.concatMap (p: p.runtimeInputs or []) inputsFrom; ++ lib.concatMap (p: p.runtimeInputs or []) inputsFrom;
LD_LIBRARY_PATH = let inputsFrom = builtins.attrValues self.packages.${pkgs.stdenv.system};
getNativeLibs = d:
d.nativeLibs
or []
++ builtins.concatMap getNativeLibs (d.lispLibs or []);
in lib.makeLibraryPath (lib.lists.unique (builtins.concatMap getNativeLibs inputsFrom));
}; };
} }
); );

View file

@ -2,7 +2,7 @@
sbcl, sbcl,
lib, lib,
writeText, writeText,
libcue, cuetools,
ffmpeg-headless, ffmpeg-headless,
makeWrapper, makeWrapper,
}: let }: let
@ -14,14 +14,11 @@
lispLibs = with sbcl.pkgs; [ lispLibs = with sbcl.pkgs; [
alexandria alexandria
babel babel
cffi
cffi-grovel
cl-ppcre cl-ppcre
fset fset
lparallel lparallel
pathname-utils pathname-utils
serapeum serapeum
trivial-features
uax-15 uax-15
unix-opts unix-opts
]; ];
@ -30,15 +27,8 @@
makeWrapper makeWrapper
]; ];
nativeLibs = [
(libcue.overrideAttrs {
cmakeFlags = [
(lib.cmakeBool "BUILD_SHARED_LIBS" true)
];
})
];
runtimeInputs = [ runtimeInputs = [
cuetools
ffmpeg-headless ffmpeg-headless
]; ];
@ -61,8 +51,7 @@
postFixup = '' postFixup = ''
wrapProgram $out/bin/sync-music \ wrapProgram $out/bin/sync-music \
--prefix PATH : ${lib.makeBinPath runtimeInputs} \ --prefix PATH : ${lib.makeBinPath runtimeInputs}
--prefix LD_LIBRARY_PATH : $LD_LIBRARY_PATH
''; '';
meta.mainProgram = "sync-music"; meta.mainProgram = "sync-music";

View file

@ -1,19 +1,11 @@
(asdf:defsystem sync-music (asdf:defsystem sync-music
:pathname #P"sync-music/" :pathname #P"sync-music/"
:defsystem-depends-on (#:cffi-grovel
#:trivial-features)
:components ((:file "package") :components ((:file "package")
(:file "utils" :depends-on ("package")) (:file "utils" :depends-on ("package"))
(:file "actions" :depends-on ("package" "utils")) (:file "actions" :depends-on ("package"))
(:file "sync-music" :depends-on ("package" "utils" "libcue")) (:file "sync-music" :depends-on ("package" "utils")))
(:module "libcue"
:depends-on ("package")
:components ((:cffi-grovel-file "grovel")
(:file "ffi")
(:file "libcue" :depends-on ("grovel" "ffi")))))
:depends-on (#:alexandria :depends-on (#:alexandria
#:babel #:babel
#:cffi
#:cl-ppcre #:cl-ppcre
#:fset #:fset
#:lparallel #:lparallel

View file

@ -5,8 +5,8 @@
(defstruct (copy-action (:include action)) (defstruct (copy-action (:include action))
(origin nil :type pathname)) (origin nil :type pathname))
(defstruct (copy-flac-action (:include copy-action)) (defstruct (copy-flac-action (:include copy-action))
(start nil :type (or integer null)) (start nil :type (or string null))
(length nil :type (or integer null)) (end nil :type (or string null))
(metadata nil :type (soft-alist-of string string))) (metadata nil :type (soft-alist-of string string)))
(defstruct (copy-meta-action (:include copy-action))) (defstruct (copy-meta-action (:include copy-action)))
(defstruct (delete-action (:include action))) (defstruct (delete-action (:include action)))
@ -39,9 +39,9 @@ Pushes status message about this action to QUEUE if given."))
"-filter_threads" "1" "-filter_threads" "1"
"-i" ,(uiop:native-namestring (copy-action-origin a)) "-i" ,(uiop:native-namestring (copy-action-origin a))
,@(when-let ((start (copy-flac-action-start a))) ,@(when-let ((start (copy-flac-action-start a)))
`("-ss" ,(ffmpeg-timestamp start))) `("-ss" ,(convert-timestamp start)))
,@(when-let ((length (copy-flac-action-length a))) ,@(when-let ((end (copy-flac-action-end a)))
`("-t" ,(ffmpeg-timestamp length))) `("-to" ,(convert-timestamp end)))
,@(loop :for (tag . value) :in (copy-flac-action-metadata a) ,@(loop :for (tag . value) :in (copy-flac-action-metadata a)
:collect "-metadata" :collect "-metadata"
:collect (format nil "~A=~A" tag value)) :collect (format nil "~A=~A" tag value))

View file

@ -1,85 +0,0 @@
(in-package #:sync-music/libcue-ffi)
(defctype file-ptr :pointer)
(defcfun (fopen "fopen") file-ptr
(str :string)
(mode :string))
(defcfun (fclose "fclose") :int
(file file-ptr))
#+unix
(cl:progn
(defcfun (freopen "freopen") file-ptr
(str :string)
(mode :string)
(stream file-ptr))
(defcfun (dup "dup") :int
(old-fd :int)))
(define-foreign-library libcue
(:unix (:or "libcue.so.2.3.0" "libcue.so"))
(t (:default "libcue")))
(use-foreign-library libcue)
(defctype cd-ptr :pointer)
(defctype track-ptr :pointer)
(defctype cd-text-ptr :pointer)
(defctype rem-ptr :pointer)
(defcfun (cue-parse-file "cue_parse_file") cd-ptr
(file file-ptr))
(defcfun (cue-parse-string "cue_parse_string") cd-ptr
(str :string))
(defcfun (cd-delete "cd_delete") :void
(cd cd-ptr))
(defcfun (%cd-get-mode "cd_get_mode") :int
(cd cd-ptr))
(defcfun (cd-get-cd-text-file "cd_get_cdtextfile") :string
(cd cd-ptr))
(defcfun (cd-get-track-count "cd_get_ntrack") :int
(cd cd-ptr))
(defcfun (cd-get-cd-text "cd_get_cdtext") cd-text-ptr
(cd cd-ptr))
(defcfun (track-get-cd-text "track_get_cdtext") cd-text-ptr
(track track-ptr))
(defcfun (cd-text-get "cdtext_get") :string
(pti :int)
(cd-text cd-text-ptr))
(defcfun (cd-get-rem "cd_get_rem") rem-ptr
(cd cd-ptr))
(defcfun (track-get-rem "track_get_rem") rem-ptr
(track track-ptr))
(defcfun (rem-get "rem_get") :string
;; NOTE: Defined in libcue.h as unsigned int, but actually takes in enum
(rem-type :int)
(rem rem-ptr))
(defcfun (cd-get-track "cd_get_track") track-ptr
(cd cd-ptr)
(i :int))
(defcfun (track-get-file-name "track_get_filename") :string
(track track-ptr))
(defcfun (track-get-start "track_get_start") :long
(track track-ptr))
(defcfun (track-get-length "track_get_length") :long
(track track-ptr))
(defcfun (%track-get-mode "track_get_mode") :int
(track track-ptr))
(defcfun (%track-get-sub-mode "track_get_sub_mode") :int
(track track-ptr))
(defcfun (%track-flag-set-p "track_is_set_flag") :boolean
(track track-ptr)
(flag :int))
(defcfun (track-get-zero-pre "track_get_zero_pre") :long
(track track-ptr))
(defcfun (track-get-zero-post "track_get_zero_post") :long
(track track-ptr))
(defcfun (track-get-isrc "track_get_isrc") :string
(track track-ptr))
(defcfun (track-get-index "track_get_index") :long
(track track-ptr)
(i :int))

View file

@ -1,77 +0,0 @@
(in-package #:sync-music/libcue-ffi)
#+unix
(progn
(include "stdio.h")
(cvar ("stderr" +stderr+) :pointer
:read-only t)
(include "unistd.h")
(constant (+stderr-fileno+ "STDERR_FILENO"))
(include "errno.h")
(cvar ("errno" +errno+) :int
:read-only t))
(include "libcue.h")
(cenum disc-mode
((:da "MODE_CD_DA"))
((:rom "MODE_CD_ROM"))
((:rom-xa "MODE_CD_ROM_XA")))
(cenum track-mode
((:audio "MODE_AUDIO"))
((:mode-1 "MODE_MODE1"))
((:mode-1-raw "MODE_MODE1_RAW"))
((:mode-2 "MODE_MODE2"))
((:mode-2-form-1 "MODE_MODE2_FORM1"))
((:mode-2-form-2 "MODE_MODE2_FORM2"))
((:mode-2-form-mix "MODE_MODE2_FORM_MIX"))
((:mode-2-raw "MODE_MODE2_RAW")))
(cenum track-sub-mode
((:rw "SUB_MODE_RW"))
((:rw-raw "SUB_MODE_RW_RAW")))
(cenum track-flag
((:none "FLAG_NONE"))
((:pre-emphasis "FLAG_PRE_EMPHASIS"))
((:copy-permitted "FLAG_COPY_PERMITTED"))
((:data "FLAG_DATA"))
((:four-channel "FLAG_FOUR_CHANNEL"))
((:scms "FLAG_SCMS"))
((:any "FLAG_ANY")))
(cenum data-type
((:audio "DATA_AUDIO"))
((:data "DATA_DATA"))
((:fifo "DATA_FIFO"))
((:zero "DATA_ZERO")))
(cenum pti
((:title "PTI_TITLE"))
((:performer "PTI_PERFORMER"))
((:songwriter "PTI_SONGWRITER"))
((:composer "PTI_COMPOSER"))
((:arranger "PTI_ARRANGER"))
((:message "PTI_MESSAGE"))
((:disc-id "PTI_DISC_ID"))
((:genre "PTI_GENRE"))
((:toc-info-1 "PTI_TOC_INFO1"))
((:toc-info-2 "PTI_TOC_INFO2"))
((:reserved-1 "PTI_RESERVED1"))
((:reserved-2 "PTI_RESERVED2"))
((:reserved-3 "PTI_RESERVED3"))
((:reserved-4 "PTI_RESERVED4"))
((:upc-isrc "PTI_UPC_ISRC"))
((:size-info "PTI_SIZE_INFO"))
((:end "PTI_END")))
(cenum rem-type
((:date "REM_DATE"))
((:album-gain "REM_REPLAYGAIN_ALBUM_GAIN"))
((:album-peak "REM_REPLAYGAIN_ALBUM_PEAK"))
((:track-gain "REM_REPLAYGAIN_TRACK_GAIN"))
((:track-peak "REM_REPLAYGAIN_TRACK_PEAK"))
((:end "REM_END")))

View file

@ -1,79 +0,0 @@
(in-package #:sync-music/libcue)
(defmacro with-read-file ((var path) &body body)
"Bind VAR to the result of an fopen on PATH while executing BODY."
(once-only (path)
`(let ((,var (fopen (uiop:native-namestring ,path) "r")))
(declare (dynamic-extent ,var))
(when (cffi:null-pointer-p ,var)
(error "Failed to open file `~A` with errno ~A." ,path +errno+))
(unwind-protect
(progn ,@body)
(fclose ,var)))))
(defmacro with-ignore-stderr (&body body)
"Execute BODY without printing anything into stderr.
Only works on Unix systems with a /dev/fd/ filesystem."
#+unix
(with-gensyms (old-stderr)
`(progn
(finish-output *error-output*)
(let ((,old-stderr (dup +stderr-fileno+)))
(prog1
(progn
(freopen "/dev/null" "w" +stderr+)
,@body)
(freopen (format nil "/dev/fd/~D" ,old-stderr) "a" +stderr+)))))
#-unix
`(progn ,@body))
(defmacro with-cue ((var type data) &body body)
"Bind VAR to the result of a cue_parse_* while executing BODY.
If TYPE is :FILE, use cue_parse_file with fopen on DATA. If TYPE is :STRING,
use cue_parse_string directly."
(once-only (data)
`(let ((,var ,(ecase type
(:file
(with-gensyms (file)
`(with-read-file (,file ,data)
(with-ignore-stderr
(cue-parse-file ,file)))))
(:string
data))))
(declare (dynamic-extent ,var))
(unless (cffi:null-pointer-p ,var)
(unwind-protect
(progn ,@body)
(cd-delete ,var))))))
(defun get-rem (type data)
(rem-get (cffi:foreign-enum-value 'rem-type type) data))
(defun get-cd-text (type data)
(cd-text-get (cffi:foreign-enum-value 'pti type) data))
(defun cd-get-mode (cd)
(cffi:foreign-enum-keyword 'disc-mode
(%cd-get-mode cd)))
(defun track-get-mode (track)
(cffi:foreign-enum-keyword 'track-mode
(%track-get-mode track)))
(defun track-get-sub-mode (track)
(cffi:foreign-enum-keyword 'track-sub-mode
(%track-get-sub-mode track)))
(defun track-flag-set-p (track flag)
(%track-flag-set-p track
(cffi:foreign-enum-keyword 'track-flag flag)))
(defun ensure-nonempty (val)
"Ensure a null value returned by a libcue function is NIL."
(unless (etypecase val
(null t)
(string (emptyp val))
(integer (minusp val)))
val))

View file

@ -1,72 +1,6 @@
(uiop:define-package #:sync-music/libcue-ffi
(:use #:cffi)
(:export #+unix #:+stderr+
#+unix #:+stderr-fileno+
#+unix #:+errno+
#:disc-mode
#:track-mode
#:track-sub-mode
#:track-flag
#:data-type
#:pti
#:rem-type
#:fopen
#:fclose
#+unix #:freopen
#+unix #:dup
#:cue-parse-file
#:cue-parse-string
#:cd-delete
#:%cd-get-mode
#:cd-get-cd-text-file
#:cd-get-track-count
#:cd-get-cd-text
#:track-get-cd-text
#:cd-text-get
#:cd-get-rem
#:track-get-rem
#:rem-get
#:cd-get-track
#:track-get-file-name
#:track-get-start
#:track-get-length
#:%track-get-mode
#:%track-get-sub-mode
#:%track-flag-set-p
#:track-get-zero-pre
#:track-get-zero-post
#:track-get-isrc
#:track-get-index))
(uiop:define-package #:sync-music/libcue
(:use #:cl #:alexandria #:serapeum #:sync-music/libcue-ffi)
(:export #:with-cue
#:get-rem
#:get-cd-text
#:cd-get-mode
#:track-get-mode
#:track-get-sub-mode
#:track-flag-set-p
#:cd-get-cd-text-file
#:cd-get-track-count
#:cd-get-cd-text
#:track-get-cd-text
#:cd-get-rem
#:track-get-rem
#:cd-get-track
#:track-get-file-name
#:track-get-start
#:track-get-length
#:track-get-zero-pre
#:track-get-zero-post
#:track-get-isrc
#:track-get-index
#:ensure-nonempty))
(uiop:define-package #:sync-music (uiop:define-package #:sync-music
(:use #:cl #:alexandria #:serapeum) (:use #:cl #:alexandria #:serapeum)
(:local-nicknames (#:p-utils #:org.shirakumo.pathname-utils) (:local-nicknames (#:p-utils #:org.shirakumo.pathname-utils))
(#:libcue #:sync-music/libcue))
(:export #:*worker-threads* (:export #:*worker-threads*
#:*opus-bitrate* #:*opus-bitrate*
#:*max-depth* #:*max-depth*

View file

@ -1,10 +1,9 @@
(in-package #:sync-music) (in-package #:sync-music)
(defun make-file-copy-action (origin target &optional (test #'string-equal)) (defun make-file-copy-action (origin target &optional (test #'string-equal))
"Create subclass of COPY-ACTION based on the filetype of ORIGIN. "Create subclass of COPY-ACTION based on the filetype of ORIGIN and TEST for equality.
Use TEST for file extension equality. Return the action if it exists, or NIL Return the action if it exists, or NIL if the file should not be copied."
if the file should not be copied."
(when-let* ((type (pathname-type origin)) (when-let* ((type (pathname-type origin))
(make-action-fn (make-action-fn
(cond (cond
@ -24,89 +23,94 @@ if the file should not be copied."
:defaults target))))) :defaults target)))))
(defun get-cue-copy-actions (origin origin-files target &optional (test #'string-equal)) (defun get-cue-copy-actions (origin origin-files target &optional (test #'string-equal))
"Return all cue-based copy actions from ORIGIN to TARGET if applicable. (when-let* ((cue-file (uiop:native-namestring
(find-if (lambda (f)
ORIGIN-FILES is just a list of all files in ORIGIN. Use TEST for file (funcall test (pathname-type f) "cue"))
extension equality." origin-files)))
(when-let ((cue-file (find-if (lambda (f) (cue-track-count (parse-integer
(funcall test (pathname-type f) "cue")) (uiop:run-program (list "cueprint"
origin-files))) "-d" "%N"
(libcue:with-cue (cd :file cue-file) cue-file)
(let ((track-count (libcue:cd-get-track-count cd))) :output :string
(when (> track-count (count-if (lambda (f) :ignore-error-status t)
:junk-allowed t)))
(when (> cue-track-count (count-if (lambda (f)
(funcall test (pathname-type f) "flac")) (funcall test (pathname-type f) "flac"))
origin-files)) origin-files))
(loop :with actions := (fset:empty-set) (loop :repeat cue-track-count
:with track-count-str := (format nil "~2,'0D" track-count) :with actions := (fset:empty-set)
:with cd-rem := (libcue:cd-get-rem cd) :with album-data-raw := (uiop:run-program
:with cd-cd-text := (libcue:cd-get-cd-text cd) (list "cueprint"
:with album-date := (libcue:ensure-nonempty "-d" "%T\\n%02N\\n%P"
(libcue:get-rem :date cd-rem)) cue-file)
:with album-title := (libcue:get-cd-text :title cd-cd-text) :output :lines)
:with album-artist := (libcue:ensure-nonempty :with track-data-raw := (uiop:run-program
(libcue:get-cd-text :performer cd-cd-text)) (list "cueprint"
:with album-composer := (libcue:ensure-nonempty "-t" "%f\\n%t\\n%02n\\n%p\\n%c\\n%g\\n"
(libcue:get-cd-text :composer cd-cd-text)) cue-file)
:with album-genre := (libcue:ensure-nonempty :output :lines)
(libcue:get-cd-text :genre cd-cd-text)) :with break-start-data-raw := (uiop:run-program
:for track-number :from 1 :upto track-count (list "cuebreakpoints"
:for track-number-str := (format nil "~2,'0D" track-number) "--append-gaps"
:for track := (libcue:cd-get-track cd track-number) cue-file)
:for track-rem := (libcue:track-get-rem track) :output :lines)
:for track-cd-text := (libcue:track-get-cd-text track) :with break-end-data-raw := (uiop:run-program
:for track-date := (or (libcue:ensure-nonempty (list "cuebreakpoints"
(libcue:get-rem :date track-rem)) "--prepend-gaps"
album-date) cue-file)
:for track-title := (libcue:get-cd-text :title track-cd-text) :output :lines)
:for track-artist := (or (libcue:ensure-nonempty ;; HACK: Determine if track 1 has pregap based on breakpoint count
(libcue:get-cd-text :performer track-cd-text)) :with track-1-pregap-p := (< (length break-start-data-raw)
album-artist) (length break-end-data-raw))
:for track-composer := (or (libcue:ensure-nonempty :for origin-file-name := (let* ((raw-file (pop track-data-raw))
(libcue:get-cd-text :composer track-cd-text)) (dot (position #\. raw-file :from-end t)))
album-composer) (subseq raw-file 0 dot))
:for track-genre := (or (libcue:ensure-nonempty :for origin-file := (make-pathname :name origin-file-name
(libcue:get-cd-text :genre track-cd-text)) :type "flac"
album-genre) :defaults origin)
:for origin-file-name := (let* ((raw-file (libcue:track-get-file-name track)) :for track-title := (first track-data-raw)
(dot (position #\. raw-file :from-end t))) :for track-number := (second track-data-raw)
(subseq raw-file 0 dot)) :for data-raw := (append album-data-raw
:for origin-file := (make-pathname :name origin-file-name (loop :repeat 5
:type "flac" :collect (pop track-data-raw)))
:defaults origin) :for metadata := (mapcar #'cons
:for metadata := (let ((metadata)) '("ALBUM"
(flet ((add-metadata (field value) "TRACKTOTAL"
(when value "ALBUMARTIST"
(push (cons field value) metadata)))) "TITLE"
(add-metadata "ALBUM" album-title) "TRACKNUMBER"
(add-metadata "TRACKTOTAL" track-count-str) "ARTIST"
(add-metadata "ALBUMARTIST" album-artist) "COMPOSER"
(add-metadata "TITLE" track-title) "GENRE")
(add-metadata "TRACKNUMBER" track-number-str) data-raw)
(add-metadata "ARTIST" track-artist) :for metadata-filtered := (delete-if (compose #'emptyp #'cdr) metadata)
(add-metadata "COMPOSER" track-composer) :for previous-action := nil :then current-action
(add-metadata "GENRE" track-genre) :for output-file-name := (ppcre:regex-replace-all *invalid-char-scanner*
(add-metadata "DATE" track-date)) (format nil
metadata) "~A - ~A"
:for output-file-name := (format nil track-number
"~A - ~A" track-title)
track-number-str "!!!")
(ppcre:regex-replace-all :for current-action := (make-copy-flac-action
*invalid-char-scanner* :origin origin-file
track-title :target (pathname-normalize-unicode
"!!!")) (make-pathname :name output-file-name
:for current-action := (make-copy-flac-action :type "opus"
:origin origin-file :defaults target))
:target (pathname-normalize-unicode :metadata metadata-filtered)
(make-pathname :name output-file-name :do (fset:adjoinf actions current-action)
:type "opus" :with previous-file
:defaults target)) :do (cond
:start (libcue:ensure-nonempty ((equal previous-file origin-file-name)
(libcue:track-get-start track)) (setf (copy-flac-action-start current-action) (pop break-start-data-raw))
:length (libcue:ensure-nonempty (setf (copy-flac-action-end previous-action) (pop break-end-data-raw)))
(libcue:track-get-length track)) (t
:metadata metadata) (unless (uiop:file-exists-p origin-file)
:do (fset:adjoinf actions current-action) (return))
:finally (return actions))))))) (when track-1-pregap-p
(setf (copy-flac-action-start current-action) (pop break-end-data-raw)))
(setf previous-file origin-file-name)))
:finally (return actions)))))
(defun get-all-copy-actions (origin target &optional (depth 0)) (defun get-all-copy-actions (origin target &optional (depth 0))
"Recursively iterate ORIGIN for files to copy. "Recursively iterate ORIGIN for files to copy.

View file

@ -18,6 +18,9 @@
(defparameter *meta-extensions* '("cue" "m3u" "m3u8") (defparameter *meta-extensions* '("cue" "m3u" "m3u8")
"File extensions where *.flac text needs to be replaced.") "File extensions where *.flac text needs to be replaced.")
(defparameter *timestamp-scanner* (ppcre:create-scanner "^(\\d+):(\\d{2})\\.(\\d{2})$")
"Scan for a cuetools position.")
(defparameter *invalid-char-scanner* (ppcre:create-scanner "[\"*/:<>?\\\\|]") (defparameter *invalid-char-scanner* (ppcre:create-scanner "[\"*/:<>?\\\\|]")
"Scan for any characters not allowed in an Android/NTFS file name.") "Scan for any characters not allowed in an Android/NTFS file name.")
@ -43,13 +46,16 @@
,(when next-max ,(when next-max
`(handle-overflow (,next-unit ,next-max) ,@args))))) `(handle-overflow (,next-unit ,next-max) ,@args)))))
(defun ffmpeg-timestamp (frames) (defun convert-timestamp (s)
"Convert time in CD frames to ffmpeg duration H:MM:SS.m." "Convert cuetools position MM:SS.FF to ffmpeg duration MM:SS.m."
(let ((hours 0) (let* ((all-times (nth-value 1 (ppcre:scan-to-strings *timestamp-scanner* s)))
(minutes 0) (hours 0)
(seconds 0) (minutes (parse-integer (aref all-times 0)))
(millis (round (* frames 1000) 75))) (seconds (parse-integer (aref all-times 1)))
(handle-overflow (millis 1000) (seconds 60) (minutes 60) (hours)) (frames (parse-integer (aref all-times 2)))
(millis (round (* frames 1000) 75)))
(handle-overflow (millis 1000) (seconds 60) (minutes))
(handle-overflow (minutes 60) (hours))
(format nil "~D:~2,'0D:~2,'0D.~3,'0D" hours minutes seconds millis))) (format nil "~D:~2,'0D:~2,'0D.~3,'0D" hours minutes seconds millis)))
(defun vector-nconc-extend (v1 v2 &key (start 0) end) (defun vector-nconc-extend (v1 v2 &key (start 0) end)