Compare commits
No commits in common. "527620d2ea488c5a400986247b6e27d9d3ea4a3e" and "b488540ffe32092e092a10c1d89fbac154073325" have entirely different histories.
527620d2ea
...
b488540ffe
|
@ -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": {
|
||||||
|
|
|
@ -35,8 +35,6 @@
|
||||||
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
|
||||||
|
@ -44,12 +42,7 @@
|
||||||
]
|
]
|
||||||
++ 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));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
|
|
@ -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")))
|
|
|
@ -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))
|
|
|
@ -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*
|
||||||
|
|
|
@ -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
|
|
||||||
extension equality."
|
|
||||||
(when-let ((cue-file (find-if (lambda (f)
|
|
||||||
(funcall test (pathname-type f) "cue"))
|
(funcall test (pathname-type f) "cue"))
|
||||||
origin-files)))
|
origin-files)))
|
||||||
(libcue:with-cue (cd :file cue-file)
|
(cue-track-count (parse-integer
|
||||||
(let ((track-count (libcue:cd-get-track-count cd)))
|
(uiop:run-program (list "cueprint"
|
||||||
(when (> track-count (count-if (lambda (f)
|
"-d" "%N"
|
||||||
|
cue-file)
|
||||||
|
:output :string
|
||||||
|
: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))
|
|
||||||
album-composer)
|
|
||||||
:for track-genre := (or (libcue:ensure-nonempty
|
|
||||||
(libcue:get-cd-text :genre track-cd-text))
|
|
||||||
album-genre)
|
|
||||||
:for origin-file-name := (let* ((raw-file (libcue:track-get-file-name track))
|
|
||||||
(dot (position #\. raw-file :from-end t)))
|
(dot (position #\. raw-file :from-end t)))
|
||||||
(subseq raw-file 0 dot))
|
(subseq raw-file 0 dot))
|
||||||
:for origin-file := (make-pathname :name origin-file-name
|
:for origin-file := (make-pathname :name origin-file-name
|
||||||
:type "flac"
|
:type "flac"
|
||||||
:defaults origin)
|
:defaults origin)
|
||||||
:for metadata := (let ((metadata))
|
:for track-title := (first track-data-raw)
|
||||||
(flet ((add-metadata (field value)
|
:for track-number := (second track-data-raw)
|
||||||
(when value
|
:for data-raw := (append album-data-raw
|
||||||
(push (cons field value) metadata))))
|
(loop :repeat 5
|
||||||
(add-metadata "ALBUM" album-title)
|
:collect (pop track-data-raw)))
|
||||||
(add-metadata "TRACKTOTAL" track-count-str)
|
:for metadata := (mapcar #'cons
|
||||||
(add-metadata "ALBUMARTIST" album-artist)
|
'("ALBUM"
|
||||||
(add-metadata "TITLE" track-title)
|
"TRACKTOTAL"
|
||||||
(add-metadata "TRACKNUMBER" track-number-str)
|
"ALBUMARTIST"
|
||||||
(add-metadata "ARTIST" track-artist)
|
"TITLE"
|
||||||
(add-metadata "COMPOSER" track-composer)
|
"TRACKNUMBER"
|
||||||
(add-metadata "GENRE" track-genre)
|
"ARTIST"
|
||||||
(add-metadata "DATE" track-date))
|
"COMPOSER"
|
||||||
metadata)
|
"GENRE")
|
||||||
:for output-file-name := (format nil
|
data-raw)
|
||||||
|
:for metadata-filtered := (delete-if (compose #'emptyp #'cdr) metadata)
|
||||||
|
:for previous-action := nil :then current-action
|
||||||
|
:for output-file-name := (ppcre:regex-replace-all *invalid-char-scanner*
|
||||||
|
(format nil
|
||||||
"~A - ~A"
|
"~A - ~A"
|
||||||
track-number-str
|
track-number
|
||||||
(ppcre:regex-replace-all
|
track-title)
|
||||||
*invalid-char-scanner*
|
"!!!")
|
||||||
track-title
|
|
||||||
"!!!"))
|
|
||||||
:for current-action := (make-copy-flac-action
|
:for current-action := (make-copy-flac-action
|
||||||
:origin origin-file
|
:origin origin-file
|
||||||
:target (pathname-normalize-unicode
|
:target (pathname-normalize-unicode
|
||||||
(make-pathname :name output-file-name
|
(make-pathname :name output-file-name
|
||||||
:type "opus"
|
:type "opus"
|
||||||
:defaults target))
|
:defaults target))
|
||||||
:start (libcue:ensure-nonempty
|
:metadata metadata-filtered)
|
||||||
(libcue:track-get-start track))
|
|
||||||
:length (libcue:ensure-nonempty
|
|
||||||
(libcue:track-get-length track))
|
|
||||||
:metadata metadata)
|
|
||||||
:do (fset:adjoinf actions current-action)
|
:do (fset:adjoinf actions current-action)
|
||||||
:finally (return actions)))))))
|
:with previous-file
|
||||||
|
:do (cond
|
||||||
|
((equal previous-file origin-file-name)
|
||||||
|
(setf (copy-flac-action-start current-action) (pop break-start-data-raw))
|
||||||
|
(setf (copy-flac-action-end previous-action) (pop break-end-data-raw)))
|
||||||
|
(t
|
||||||
|
(unless (uiop:file-exists-p origin-file)
|
||||||
|
(return))
|
||||||
|
(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.
|
||||||
|
|
|
@ -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)))
|
||||||
|
(seconds (parse-integer (aref all-times 1)))
|
||||||
|
(frames (parse-integer (aref all-times 2)))
|
||||||
(millis (round (* frames 1000) 75)))
|
(millis (round (* frames 1000) 75)))
|
||||||
(handle-overflow (millis 1000) (seconds 60) (minutes 60) (hours))
|
(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