aboutsummaryrefslogtreecommitdiff
path: root/autotag.lisp
diff options
context:
space:
mode:
authorThomas Albers <thomas@thomaslabs.org>2022-07-24 12:21:29 +0200
committerThomas Albers <thomas@thomaslabs.org>2022-07-24 12:21:29 +0200
commitbef3dad92922fbd261267221df349f3d2569b720 (patch)
treeac16bd3629a39a55eeb300b0f2991a240b093aca /autotag.lisp
Initial commitHEADmaster
Diffstat (limited to 'autotag.lisp')
-rwxr-xr-xautotag.lisp187
1 files changed, 187 insertions, 0 deletions
diff --git a/autotag.lisp b/autotag.lisp
new file mode 100755
index 0000000..2199203
--- /dev/null
+++ b/autotag.lisp
@@ -0,0 +1,187 @@
+;; autotag
+;; Copyright (C) 2020 Thomas Albers Raviola
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;; musicbrainz supported requests
+;; lookup: /<ENTITY_TYPE>/<MBID>?inc=<INC>
+;; browse: /<RESULT_ENTITY_TYPE>?<BROWSING_ENTITY_TYPE>=<MBID>&limit=<LIMIT>&offset=<OFFSET>&inc=<INC>
+;; search: /<ENTITY_TYPE>?query=<QUERY>&limit=<LIMIT>&offset=<OFFSET>
+
+(in-package #:autotag)
+
+(defvar *path-replacements*
+ '(" " "_"
+ "/" "_"))
+
+(defvar *title-replacements*
+ '("…" "..."
+ "å" "a"
+ "’" "'"
+ "ö" "o"
+ "Ö" "O"
+ "ä" "a"
+ "Ä" "A"
+ "ü" "u"
+ "Ü" "U"))
+
+(defun release< (release1 release2)
+ (let ((t1 (release-title release1))
+ (t2 (release-title release2)))
+ (if (string-not-equal t1 t2)
+ (or (not (string-equal t1 *album-name*))
+ (string-equal t2 *album-name*)
+ (string> t1 t2))
+ (string< (release-date release1) (release-date release2)))))
+
+(defun select-release (release-list)
+ (flet ((printer (release)
+ (format nil "artist: ~A~30Trelease: ~A~70Ttrack count: ~A~76Tdate: ~A"
+ (release-artist release)
+ (release-title release)
+ (release-track-count release)
+ (release-date release))))
+ (setf release-list (sort (copy-seq release-list) #'release<))
+ (dmenu release-list
+ :lines (length release-list)
+ :printer #'printer
+ :prompt (format nil "~A (~A files):" *album-name* *npathnames*))))
+
+(defun tags-from-release (release)
+ (let ((global-tag `(:album ,(release-title release)
+ :date ,(release-date release)
+ :artist ,(release-artist release))))
+ (flet ((tag-from-track (tr)
+ (apply #'make-tag
+ `(:title ,(track-title tr)
+ :track ,(track-position tr)
+ :keywords ,(track-keywords tr)
+ ,@global-tag))))
+ (mapcar #'tag-from-track (plump:get-elements-by-tag-name release "track")))))
+
+(defun choose-tag (pathname tags)
+ "Choose a tag for a given file, ask user if needed"
+ (let ((k1 (keywords (pathname-name pathname))))
+ (flet ((tag-printer (tag)
+ (princ-to-string (tag-title tag)))
+ (tag-key (tg)
+ (let ((k2 (tag-keywords tg)))
+ (/ (length (set-difference k2 k1 :test #'string-equal))
+ (length k2)))))
+ (or (find (pathname-name pathname) tags
+ :key #'tag-title
+ :test #'string-equal)
+ (progn (setf tags (sort (copy-seq tags) #'< :key #'tag-key))
+ (dmenu tags
+ :printer #'tag-printer
+ :lines (length tags)
+ :prompt (pathname-name pathname)))))))
+
+(defun rename-file2 (pathname new-pathname)
+ (info "~&Renaming ~A to ~A~%" pathname new-pathname)
+ (rename-file pathname (concatenate 'string (pathname-name new-pathname)
+ "." (pathname-type new-pathname))))
+
+(defun sanitize-pathname (pathname)
+ (str:replace-using
+ (append *path-replacements*
+ *title-replacements*)
+ (remove-if #'(lambda (x)
+ (member x '(#\? #\* #\{ #\} #\. #\?)))
+ pathname)))
+
+(defun apply-tag (pathname tag)
+ (unless tag
+ (info "~&No tag could be found for ~A, skipping ...~%" pathname)
+ (return-from apply-tag nil))
+ (when *rename-p*
+ (let ((new-pathname (make-pathname :directory (pathname-directory pathname)
+ :name (sanitize-pathname (tag-title tag))
+ :type (pathname-type pathname))))
+ (if (probe-file new-pathname)
+ (info "~&File already exists, not renaming: ~A~%" new-pathname)
+ (setf pathname (rename-file2 pathname new-pathname)))))
+ (info "~&Writing tags to: ~A~%" pathname)
+ (write-ogg-tags pathname tag))
+
+(defun apply-tags (pathnames tags)
+ (dolist (pathname pathnames)
+ (apply-tag pathname (choose-tag pathname tags))))
+
+(defun apply-tags-in-order (pathnames tags)
+ (flet ((tag-position-number (tr)
+ (parse-integer (track-position tr))))
+ (setf tags (sort tags #'< :key #'tag-position-number))
+ (loop :for pathname :in pathnames
+ :for tag :in tags
+ :do
+ (apply-tag pathname tag))))
+
+(defun get-tags (title artist)
+ (let* ((release (select-release (search-release title artist))))
+ (cond (release
+ (tags-from-release
+ (lookup-release
+ (plump:get-attribute release "id"))))
+ (t
+ (info "~&No release for ~A selected, skipping ...~%"
+ *album-name*)
+ nil))))
+
+(defun expand-directories (pathname)
+ (if (eq (osicat:file-kind pathname) :directory)
+ (osicat:list-directory pathname)
+ (list pathname)))
+
+(defun autotag (argv)
+ (multiple-value-bind (options files)
+ (getopt argv *valid-cmd-line-args*)
+ (when (assoc :help options)
+ (help))
+ (when (assoc :quiet options)
+ (setf *quiet-p* t))
+ (when (assoc :order options)
+ (setf *order-p* t))
+ (when (assoc :dont-rename options)
+ (setf *rename-p* nil))
+ (when (assoc :album options)
+ (setf *album-name* (assoc-value options :album)))
+ (when (assoc :artist options)
+ (setf *album-artist* (assoc-value options :artist)))
+ (when (assoc :id options)
+ (let ((release (select-release (search-release *album-artist* *album-name*))))
+ (princ (and release (plump:get-attribute release "id")))
+ (terpri)))
+ (let* ((pathnames (mappend #'expand-directories
+ (if (null files)
+ (list *album-name*)
+ files)))
+ (*npathnames* (length pathnames))
+ (tags (if (assoc :use-id options)
+ (tags-from-release
+ (lookup-release (assoc-value options :use-id)))
+ (get-tags *album-name* *album-artist*))))
+ (when tags
+ (cond (*order-p*
+ (apply-tags-in-order pathnames tags))
+ (*print-tags-p*
+ (mapc #'print-tag tags))
+ (t
+ (apply-tags pathnames tags)))))))
+
+(defun main ()
+ (handler-case
+ (autotag (uiop:command-line-arguments))
+ (sb-sys:interactive-interrupt () (uiop:quit)))
+ (uiop:quit))