feat!: sync-music: use libcue instead of cuetools
This commit is contained in:
parent
f0e61b33fd
commit
c9779f83cf
11
flake.nix
11
flake.nix
|
@ -35,14 +35,21 @@
|
||||||
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;
|
||||||
|
|
||||||
inputsFrom = builtins.attrValues self.packages.${pkgs.stdenv.system};
|
LD_LIBRARY_PATH = let
|
||||||
|
getNativeLibs = d:
|
||||||
|
d.nativeLibs
|
||||||
|
or []
|
||||||
|
++ builtins.concatMap getNativeLibs (d.lispLibs or []);
|
||||||
|
in lib.makeLibraryPath (lib.lists.unique (builtins.concatMap getNativeLibs inputsFrom));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
sbcl,
|
sbcl,
|
||||||
lib,
|
lib,
|
||||||
writeText,
|
writeText,
|
||||||
cuetools,
|
libcue,
|
||||||
ffmpeg-headless,
|
ffmpeg-headless,
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
}: let
|
}: let
|
||||||
|
@ -14,11 +14,14 @@
|
||||||
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
|
||||||
];
|
];
|
||||||
|
@ -27,8 +30,15 @@
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
|
nativeLibs = [
|
||||||
|
(libcue.overrideAttrs {
|
||||||
|
cmakeFlags = [
|
||||||
|
(lib.cmakeBool "BUILD_SHARED_LIBS" true)
|
||||||
|
];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
runtimeInputs = [
|
runtimeInputs = [
|
||||||
cuetools
|
|
||||||
ffmpeg-headless
|
ffmpeg-headless
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -51,7 +61,8 @@
|
||||||
|
|
||||||
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";
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
(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"))
|
(:file "actions" :depends-on ("package" "utils"))
|
||||||
(:file "sync-music" :depends-on ("package" "utils")))
|
(:file "sync-music" :depends-on ("package" "utils" "libcue"))
|
||||||
|
(: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
|
||||||
|
|
|
@ -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 string null))
|
(start nil :type (or integer null))
|
||||||
(end nil :type (or string null))
|
(length nil :type (or integer 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" ,(convert-timestamp start)))
|
`("-ss" ,(ffmpeg-timestamp start)))
|
||||||
,@(when-let ((end (copy-flac-action-end a)))
|
,@(when-let ((length (copy-flac-action-length a)))
|
||||||
`("-to" ,(convert-timestamp end)))
|
`("-t" ,(ffmpeg-timestamp length)))
|
||||||
,@(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))
|
||||||
|
|
85
sync-music/libcue/ffi.lisp
Normal file
85
sync-music/libcue/ffi.lisp
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
(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))
|
77
sync-music/libcue/grovel.lisp
Normal file
77
sync-music/libcue/grovel.lisp
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
(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")))
|
79
sync-music/libcue/libcue.lisp
Normal file
79
sync-music/libcue/libcue.lisp
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
(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))
|
|
@ -1,6 +1,72 @@
|
||||||
|
(uiop:define-package #:sync-music/libcue-ffi
|
||||||
|
(:use #:cffi)
|
||||||
|
(:export #+unix #:+stderr+
|
||||||
|
#+unix #:+stderr-fileno+
|
||||||
|
#:+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*
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
(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 and TEST for equality.
|
"Create subclass of COPY-ACTION based on the filetype of ORIGIN.
|
||||||
|
|
||||||
Return the action if it exists, or NIL if the file should not be copied."
|
Use TEST for file extension equality. Return the action if it exists, or NIL
|
||||||
|
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
|
||||||
|
@ -23,94 +24,89 @@ Return the action if it exists, or NIL 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))
|
||||||
(when-let* ((cue-file (uiop:native-namestring
|
"Return all cue-based copy actions from ORIGIN to TARGET if applicable.
|
||||||
(find-if (lambda (f)
|
|
||||||
(funcall test (pathname-type f) "cue"))
|
ORIGIN-FILES is just a list of all files in ORIGIN. Use TEST for file
|
||||||
origin-files)))
|
extension equality."
|
||||||
(cue-track-count (parse-integer
|
(when-let ((cue-file (find-if (lambda (f)
|
||||||
(uiop:run-program (list "cueprint"
|
(funcall test (pathname-type f) "cue"))
|
||||||
"-d" "%N"
|
origin-files)))
|
||||||
cue-file)
|
(libcue:with-cue (cd :file cue-file)
|
||||||
:output :string
|
(let ((track-count (libcue:cd-get-track-count cd)))
|
||||||
:ignore-error-status t)
|
(when (> track-count (count-if (lambda (f)
|
||||||
: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 :repeat cue-track-count
|
(loop :with actions := (fset:empty-set)
|
||||||
:with actions := (fset:empty-set)
|
:with track-count-str := (format nil "~2,'0D" track-count)
|
||||||
:with album-data-raw := (uiop:run-program
|
:with cd-rem := (libcue:cd-get-rem cd)
|
||||||
(list "cueprint"
|
:with cd-cd-text := (libcue:cd-get-cd-text cd)
|
||||||
"-d" "%T\\n%02N\\n%P"
|
:with album-date := (libcue:ensure-nonempty
|
||||||
cue-file)
|
(libcue:get-rem :date cd-rem))
|
||||||
:output :lines)
|
:with album-title := (libcue:get-cd-text :title cd-cd-text)
|
||||||
:with track-data-raw := (uiop:run-program
|
:with album-artist := (libcue:ensure-nonempty
|
||||||
(list "cueprint"
|
(libcue:get-cd-text :performer cd-cd-text))
|
||||||
"-t" "%f\\n%t\\n%02n\\n%p\\n%c\\n%g\\n"
|
:with album-composer := (libcue:ensure-nonempty
|
||||||
cue-file)
|
(libcue:get-cd-text :composer cd-cd-text))
|
||||||
:output :lines)
|
:with album-genre := (libcue:ensure-nonempty
|
||||||
:with break-start-data-raw := (uiop:run-program
|
(libcue:get-cd-text :genre cd-cd-text))
|
||||||
(list "cuebreakpoints"
|
:for track-number :from 1 :upto track-count
|
||||||
"--append-gaps"
|
:for track-number-str := (format nil "~2,'0D" track-number)
|
||||||
cue-file)
|
:for track := (libcue:cd-get-track cd track-number)
|
||||||
:output :lines)
|
:for track-rem := (libcue:track-get-rem track)
|
||||||
:with break-end-data-raw := (uiop:run-program
|
:for track-cd-text := (libcue:track-get-cd-text track)
|
||||||
(list "cuebreakpoints"
|
:for track-date := (or (libcue:ensure-nonempty
|
||||||
"--prepend-gaps"
|
(libcue:get-rem :date track-rem))
|
||||||
cue-file)
|
album-date)
|
||||||
:output :lines)
|
:for track-title := (libcue:get-cd-text :title track-cd-text)
|
||||||
;; HACK: Determine if track 1 has pregap based on breakpoint count
|
:for track-artist := (or (libcue:ensure-nonempty
|
||||||
:with track-1-pregap-p := (< (length break-start-data-raw)
|
(libcue:get-cd-text :performer track-cd-text))
|
||||||
(length break-end-data-raw))
|
album-artist)
|
||||||
:for origin-file-name := (let* ((raw-file (pop track-data-raw))
|
:for track-composer := (or (libcue:ensure-nonempty
|
||||||
(dot (position #\. raw-file :from-end t)))
|
(libcue:get-cd-text :composer track-cd-text))
|
||||||
(subseq raw-file 0 dot))
|
album-composer)
|
||||||
:for origin-file := (make-pathname :name origin-file-name
|
:for track-genre := (or (libcue:ensure-nonempty
|
||||||
:type "flac"
|
(libcue:get-cd-text :genre track-cd-text))
|
||||||
:defaults origin)
|
album-genre)
|
||||||
:for track-title := (first track-data-raw)
|
:for origin-file-name := (let* ((raw-file (libcue:track-get-file-name track))
|
||||||
:for track-number := (second track-data-raw)
|
(dot (position #\. raw-file :from-end t)))
|
||||||
:for data-raw := (append album-data-raw
|
(subseq raw-file 0 dot))
|
||||||
(loop :repeat 5
|
:for origin-file := (make-pathname :name origin-file-name
|
||||||
:collect (pop track-data-raw)))
|
:type "flac"
|
||||||
:for metadata := (mapcar #'cons
|
:defaults origin)
|
||||||
'("ALBUM"
|
:for metadata := (let ((metadata))
|
||||||
"TRACKTOTAL"
|
(flet ((add-metadata (field value)
|
||||||
"ALBUMARTIST"
|
(when value
|
||||||
"TITLE"
|
(push (cons field value) metadata))))
|
||||||
"TRACKNUMBER"
|
(add-metadata "ALBUM" album-title)
|
||||||
"ARTIST"
|
(add-metadata "TRACKTOTAL" track-count-str)
|
||||||
"COMPOSER"
|
(add-metadata "ALBUMARTIST" album-artist)
|
||||||
"GENRE")
|
(add-metadata "TITLE" track-title)
|
||||||
data-raw)
|
(add-metadata "TRACKNUMBER" track-number-str)
|
||||||
:for metadata-filtered := (delete-if (compose #'emptyp #'cdr) metadata)
|
(add-metadata "ARTIST" track-artist)
|
||||||
:for previous-action := nil :then current-action
|
(add-metadata "COMPOSER" track-composer)
|
||||||
:for output-file-name := (ppcre:regex-replace-all *invalid-char-scanner*
|
(add-metadata "GENRE" track-genre)
|
||||||
(format nil
|
(add-metadata "DATE" track-date))
|
||||||
"~A - ~A"
|
metadata)
|
||||||
track-number
|
:for output-file-name := (format nil
|
||||||
track-title)
|
"~A - ~A"
|
||||||
"!!!")
|
track-number-str
|
||||||
:for current-action := (make-copy-flac-action
|
(ppcre:regex-replace-all
|
||||||
:origin origin-file
|
*invalid-char-scanner*
|
||||||
:target (pathname-normalize-unicode
|
track-title
|
||||||
(make-pathname :name output-file-name
|
"!!!"))
|
||||||
:type "opus"
|
:for current-action := (make-copy-flac-action
|
||||||
:defaults target))
|
:origin origin-file
|
||||||
:metadata metadata-filtered)
|
:target (pathname-normalize-unicode
|
||||||
:do (fset:adjoinf actions current-action)
|
(make-pathname :name output-file-name
|
||||||
:with previous-file
|
:type "opus"
|
||||||
:do (cond
|
:defaults target))
|
||||||
((equal previous-file origin-file-name)
|
:start (libcue:ensure-nonempty
|
||||||
(setf (copy-flac-action-start current-action) (pop break-start-data-raw))
|
(libcue:track-get-start track))
|
||||||
(setf (copy-flac-action-end previous-action) (pop break-end-data-raw)))
|
:length (libcue:ensure-nonempty
|
||||||
(t
|
(libcue:track-get-length track))
|
||||||
(unless (uiop:file-exists-p origin-file)
|
:metadata metadata)
|
||||||
(return))
|
:do (fset:adjoinf actions current-action)
|
||||||
(when track-1-pregap-p
|
:finally (return actions)))))))
|
||||||
(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.
|
||||||
|
|
|
@ -18,9 +18,6 @@
|
||||||
(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.")
|
||||||
|
|
||||||
|
@ -46,16 +43,13 @@
|
||||||
,(when next-max
|
,(when next-max
|
||||||
`(handle-overflow (,next-unit ,next-max) ,@args)))))
|
`(handle-overflow (,next-unit ,next-max) ,@args)))))
|
||||||
|
|
||||||
(defun convert-timestamp (s)
|
(defun ffmpeg-timestamp (frames)
|
||||||
"Convert cuetools position MM:SS.FF to ffmpeg duration MM:SS.m."
|
"Convert time in CD frames to ffmpeg duration H:MM:SS.m."
|
||||||
(let* ((all-times (nth-value 1 (ppcre:scan-to-strings *timestamp-scanner* s)))
|
(let ((hours 0)
|
||||||
(hours 0)
|
(minutes 0)
|
||||||
(minutes (parse-integer (aref all-times 0)))
|
(seconds 0)
|
||||||
(seconds (parse-integer (aref all-times 1)))
|
(millis (round (* frames 1000) 75)))
|
||||||
(frames (parse-integer (aref all-times 2)))
|
(handle-overflow (millis 1000) (seconds 60) (minutes 60) (hours))
|
||||||
(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)
|
||||||
|
|
Loading…
Reference in a new issue