;; 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 . ;; musicbrainz supported requests ;; lookup: //?inc= ;; browse: /?=&limit=&offset=&inc= ;; search: /?query=&limit=&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))