1
0
Эх сурвалжийг харах

normalize album, get track-count, total-duration and cover

Innocenty Enikeew 7 жил өмнө
parent
commit
e993ad947f
3 өөрчлөгдсөн 139 нэмэгдсэн , 190 устгасан
  1. 2 1
      chad-music.asd
  2. 86 189
      db.lisp
  3. 51 0
      utils.lisp

+ 2 - 1
chad-music.asd

@@ -8,4 +8,5 @@
                #:ironclad
                #:ironclad
                #:split-sequence)
                #:split-sequence)
   :serial t
   :serial t
-  :components ((:file "db")))
+  :components ((:file "utils")
+               (:file "db")))

+ 86 - 189
db.lisp

@@ -1,182 +1,62 @@
 (in-package :cl-user)
 (in-package :cl-user)
 (defpackage chad-music.db
 (defpackage chad-music.db
-  (:use :cl #:audio-streams #:alexandria))
+  (:use :cl #:audio-streams #:alexandria #:chad-music.utils))
 (in-package :chad-music.db)
 (in-package :chad-music.db)
 
 
-(eval-when (:compile-toplevel :load-toplevel :execute)
-  #+dbg
-  (defvar *standard-optimize-settings* '(optimize (debug 3)))
-  #-dbg
-  (defvar *standard-optimize-settings* '(optimize (speed 3) (safety 0) (space 0) (debug 0)))
-  )
-
 (defstruct album
 (defstruct album
-  album-artist year album original-date part-of-set
+  id album-artist year album original-date
   genre album-type album-status mb-id track-count total-duration cover)
   genre album-type album-status mb-id track-count total-duration cover)
 
 
-(defstruct track2
-  album artist track-no title
-  bit-rate is-vbr duration)
-
 (defstruct track
 (defstruct track
-  album-artist artist year album original-date genre album-type album-status mb-id
-  track-no part-of-set title
-  bit-rate is-vbr duration
-  ;; (album-artist nil :type (or string null))
-  ;; (artist nil :type (or null string))
-  ;; (year nil :type (or null fixnum))
-  ;; (album nil :type (or null string))
-  ;; (original-date nil :type (or null string))
-  ;; (genre nil :type (or null string))
-  ;; (album-type nil :type (or null string))
-  ;; (album-status nil :type (or null string))
-  ;; (mb-id nil :type (or null string))
-  ;; (track-no nil :type (or null fixnum list))
-  ;; part-of-set
-  ;; (title nil :type (or null string))
-  ;; (bit-rate nil :type (or null fixnum))
-  ;; (is-vbr nil :type (or null boolean))
-  ;; (duration nil :type (or null fixnum))
-  )
-
-(defgeneric text-tag (stream desc) (:method ((object t) desc) nil))
-(defmethod text-tag ((mp3 id3:mp3-file) desc)
-  (declare #.*standard-optimize-settings*)
-
-  (loop for frame in (id3:get-frames mp3 '("TXXX"))
-     when (string-equal (id3:desc frame) desc)
-     do (return-from text-tag (id3:val frame))))
-
-(defgeneric bit-rate (stream) (:method ((object t)) nil))
-(defgeneric is-vbr (stream) (:method ((object t)) nil))
-(defgeneric duration (stream) (:method ((object t)) nil))
-(defmethod bit-rate ((mp3 id3:mp3-file))
-  (let ((info (id3:audio-info mp3)))
-    (when info
-      (round (mpeg::bit-rate info) 1000))))
-
-(defmethod is-vbr ((mp3 id3:mp3-file))
-  (let ((info (id3:audio-info mp3)))
-    (when info
-      (mpeg::is-vbr info))))
-
-(defmethod duration ((mp3 id3:mp3-file))
-  (let ((info (id3:audio-info mp3)))
-    (when info
-      (mpeg::len info))))
-
-(defmethod bit-rate ((m4a m4a:mp4-file))
-  (let ((info (m4a:audio-info m4a)))
-    (when info
-      (round (m4a::avg-bit-rate info) 1000))))
-
-(defmethod duration ((m4a m4a:mp4-file))
-  (let ((info (m4a:audio-info m4a)))
-    (when info
-      (m4a::seconds info))))
-
-(defun parse-file (file)
-  (declare #.*standard-optimize-settings*)
+  album artist track-no title part-of-set
+  bit-rate is-vbr duration)
 
 
-  (let ((it (open-audio-file file)))
-    (when it
-      (make-track
-       :album-artist (abstract-tag:album-artist it)
-       :artist (abstract-tag:artist it)
-       :year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t))
-       :album (abstract-tag:album it)
-       :original-date (abstract-tag:original-date it)
-       :genre (abstract-tag::genre it)
-       :album-type (string-downcase (text-tag it "MusicBrainz Album Type"))
-       :album-status (string-downcase (text-tag it "MusicBrainz Album Status"))
-       :mb-id (text-tag it "MusicBrainz Album Id")
-       :track-no (abstract-tag::track it)
-       :part-of-set (abstract-tag::disk it)
-       :title (abstract-tag:title it)
-       :bit-rate (bit-rate it)
-       :is-vbr (is-vbr it)
-       :duration (duration it)))))
+(defun get-file-id (file)
+  (crypto:byte-array-to-hex-string
+   (crypto:digest-sequence :md5 (flex:string-to-octets (namestring file) :external-format :utf-8))))
 
 
-(defun album-id (album-artist year album-title)
+(defun get-album-id (album-artist year album-title)
   (crypto:byte-array-to-hex-string
   (crypto:byte-array-to-hex-string
    (crypto:digest-sequence :md5 (flex:string-to-octets
    (crypto:digest-sequence :md5 (flex:string-to-octets
                                  (format nil "~A-~A-~A" album-artist year album-title)
                                  (format nil "~A-~A-~A" album-artist year album-title)
                                  :external-format :utf-8))))
                                  :external-format :utf-8))))
 
 
-(defun parse-file2 (file albums-db)
+(defun parse-file (file albums-db)
   (declare #.*standard-optimize-settings*)
   (declare #.*standard-optimize-settings*)
 
 
   (let ((it (open-audio-file file)))
   (let ((it (open-audio-file file)))
     (when it
     (when it
-      (let* ((album-artist (abstract-tag:album-artist it))
+      (let* ((album-artist (or (abstract-tag:album-artist it)
+                               (abstract-tag:artist it)))
              (year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t)))
              (year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t)))
              (album-title (abstract-tag:album it))
              (album-title (abstract-tag:album it))
-             (album-id (album-id album-artist year album-title)))
+             (album-id (get-album-id album-artist year album-title)))
         (multiple-value-bind (album foundp) (gethash album-id albums-db)
         (multiple-value-bind (album foundp) (gethash album-id albums-db)
           (unless foundp
           (unless foundp
             (setf album (make-album
             (setf album (make-album
+                         :id album-id
                          :album-artist album-artist
                          :album-artist album-artist
                          :year year
                          :year year
                          :album album-title
                          :album album-title
                          :original-date (abstract-tag:original-date it)
                          :original-date (abstract-tag:original-date it)
                          :genre (abstract-tag::genre it)
                          :genre (abstract-tag::genre it)
-                         :part-of-set (abstract-tag::disk it)
-                         :album-type (string-downcase (text-tag it "MusicBrainz Album Type"))
-                         :album-status (string-downcase (text-tag it "MusicBrainz Album Status"))
+                         :album-type (utils:awhen (text-tag it "MusicBrainz Album Type") (string-downcase utils:it))
+                         :album-status (utils:awhen (text-tag it "MusicBrainz Album Status") (string-downcase utils:it))
                          :mb-id (text-tag it "MusicBrainz Album Id"))
                          :mb-id (text-tag it "MusicBrainz Album Id"))
                   (gethash album-id albums-db) album))
                   (gethash album-id albums-db) album))
-          (make-track2
+          (make-track
            :album album
            :album album
            :artist (abstract-tag:artist it)
            :artist (abstract-tag:artist it)
            :track-no (abstract-tag::track it)
            :track-no (abstract-tag::track it)
            :title (abstract-tag:title it)
            :title (abstract-tag:title it)
+           :part-of-set (abstract-tag::disk it)
            :bit-rate (bit-rate it)
            :bit-rate (bit-rate it)
            :is-vbr (is-vbr it)
            :is-vbr (is-vbr it)
-           :duration (duration it)))))))
+           :duration (round (duration it))))))))
 
 
 (defstruct entry path data added modified present)
 (defstruct entry path data added modified present)
 
 
-(defun file-id (file)
-  (crypto:byte-array-to-hex-string
-   (crypto:digest-sequence :md5 (flex:string-to-octets (namestring file) :external-format :utf-8))))
-
-(defun rescan (paths &optional (db (make-hash-table :test 'equal)))
-  (declare #.*standard-optimize-settings*)
-
-  (let ((added 0) (updated 0) (removed 0))
-    (declare (fixnum added updated removed))
-
-    (loop for value being the hash-values in db
-       do (setf (entry-present value) nil))
-
-    (unless (listp paths)
-      (setf paths (list paths)))
-
-    (labels ((scan-file (file)
-               (let ((file-id (file-id file)))
-                 (multiple-value-bind (entry foundp)
-                     (gethash file-id db)
-                   (let ((modified (file-write-date file)))
-                     (unless foundp
-                       (incf added)
-                       (setf entry (make-entry :path file :added (get-universal-time))
-                             (gethash file-id db) entry))
-                     (unless (and foundp (= (the fixnum (entry-modified entry)) modified))
-                       (when foundp (incf updated))
-                       (setf (entry-data entry) (parse-file file)
-                             (entry-modified entry) modified))
-                     (setf (entry-present entry) t))))))
-      (dolist (dir paths)
-        (cl-fad:walk-directory dir #'scan-file :follow-symlinks nil))
-
-      (loop for file being the hash-keys in db using (hash-value entry)
-         unless (entry-present entry)
-         do (incf removed) and
-         do (remhash file db))
-
-      (values db added updated removed))))
-
-(defun rescan2 (paths &optional (dbs (cons (make-hash-table :test 'equal) (make-hash-table :test 'equal))))
+(defun rescan (paths &optional (dbs (cons (make-hash-table :test 'equal) (make-hash-table :test 'equal))))
   (declare #.*standard-optimize-settings*)
   (declare #.*standard-optimize-settings*)
 
 
   (let ((added 0) (updated 0) (removed 0)
   (let ((added 0) (updated 0) (removed 0)
@@ -191,7 +71,7 @@
       (setf paths (list paths)))
       (setf paths (list paths)))
 
 
     (labels ((scan-file (file)
     (labels ((scan-file (file)
-               (let ((file-id (file-id file)))
+               (let ((file-id (get-file-id file)))
                  (multiple-value-bind (entry foundp)
                  (multiple-value-bind (entry foundp)
                      (gethash file-id tracks-db)
                      (gethash file-id tracks-db)
                    (let ((modified (file-write-date file)))
                    (let ((modified (file-write-date file)))
@@ -201,7 +81,7 @@
                              (gethash file-id tracks-db) entry))
                              (gethash file-id tracks-db) entry))
                      (unless (and foundp (= (the fixnum (entry-modified entry)) modified))
                      (unless (and foundp (= (the fixnum (entry-modified entry)) modified))
                        (when foundp (incf updated))
                        (when foundp (incf updated))
-                       (setf (entry-data entry) (parse-file2 file albums-db)
+                       (setf (entry-data entry) (parse-file file albums-db)
                              (entry-modified entry) modified))
                              (entry-modified entry) modified))
                      (setf (entry-present entry) t))))))
                      (setf (entry-present entry) t))))))
       (dolist (dir paths)
       (dolist (dir paths)
@@ -210,32 +90,46 @@
       (let ((album-stats (make-hash-table)))
       (let ((album-stats (make-hash-table)))
         (loop for file being the hash-keys in tracks-db using (hash-value entry)
         (loop for file being the hash-keys in tracks-db using (hash-value entry)
            do (if (entry-present entry)
            do (if (entry-present entry)
-                  (alexandria:when-let (track (entry-data entry))
-                    (incf (car (gethash (track))))
-                    )
+                  (when-let (track (entry-data entry))
+                    (let ((album (track-album track)))
+                      (multiple-value-bind (stats foundp) (gethash album album-stats)
+                        (unless foundp
+                          (setf stats (cons 0 0)
+                                (gethash album album-stats) stats)
+                          (unless (album-cover album)
+                            (setf (album-cover album)
+                                  (probe-file (cl-fad:merge-pathnames-as-file
+                                               (cl-fad:pathname-directory-pathname (entry-path entry))
+                                               "cover.jpg")))))
+                        (incf (the fixnum (car stats))) ;; track-count
+                        (incf (the fixnum (cdr stats))  ;; total-duration
+                              (the fixnum (or (track-duration track) 0))))))
                   (progn
                   (progn
                     (incf removed)
                     (incf removed)
-                    (remhash file tracks-db)))))
+                    (remhash file tracks-db))))
+        (loop for album being the hash-keys in album-stats using (hash-value stats)
+           do (setf (album-track-count album) (car stats)
+                    (album-total-duration album) (cdr stats))))
 
 
       (values (cons tracks-db albums-db) added updated removed))))
       (values (cons tracks-db albums-db) added updated removed))))
 
 
 (defparameter +album-type-order+ '("album" "lp" "ep" "single" "compilation" "live" "soundtrack"
 (defparameter +album-type-order+ '("album" "lp" "ep" "single" "compilation" "live" "soundtrack"
                                    "spokenword" "remix" "mixed" "dj-mix" "mixtape" "broadcast")
                                    "spokenword" "remix" "mixed" "dj-mix" "mixtape" "broadcast")
   "Half-arbitrary album type order")
   "Half-arbitrary album type order")
-(defun track-comparator (slots)
+(defun gen-comparator (slots)
   (labels ((clear-track-no (track-no)
   (labels ((clear-track-no (track-no)
              (when track-no
              (when track-no
                (parse-integer (if (consp track-no)
                (parse-integer (if (consp track-no)
                                   (car track-no)
                                   (car track-no)
                                   track-no)
                                   track-no)
                               :junk-allowed t))))
                               :junk-allowed t))))
-    (alexandria:named-lambda info< (a b)
+    (named-lambda info<> (a b)
       (declare #.*standard-optimize-settings*)
       (declare #.*standard-optimize-settings*)
-      (dolist (slot slots)
+      (dolist (slot slots 0)
         (let ((slot-a (slot-value a slot))
         (let ((slot-a (slot-value a slot))
               (slot-b (slot-value b slot)))
               (slot-b (slot-value b slot)))
           (when (xor (null slot-a) (null slot-b))
           (when (xor (null slot-a) (null slot-b))
-            (return-from info< (null slot-b)))
+            (return-from info<> (if (null slot-b) 1 -1)))
           (case slot
           (case slot
             (album-type
             (album-type
              (setf slot-a (or (position slot-a +album-type-order+ :test 'string-equal) 0)
              (setf slot-a (or (position slot-a +album-type-order+ :test 'string-equal) 0)
@@ -247,9 +141,19 @@
                       (case slot
                       (case slot
                         ((album-type year track-no) (= slot-a slot-b))
                         ((album-type year track-no) (= slot-a slot-b))
                         (t (string-equal slot-a slot-b))))
                         (t (string-equal slot-a slot-b))))
-            (return-from info< (case slot
-                                 ((album-type year track-no) (< slot-a slot-b))
-                                 (t (string< slot-a slot-b))))))))))
+            (return-from info<> (case slot
+                                 ((album-type year track-no) (- slot-a slot-b))
+                                 (t (if (string< slot-a slot-b) -1 1))))))))))
+(defparameter +album<>+ (gen-comparator '(album-artist album-type original-date year album)))
+(defun album< (a b)
+  (< (funcall +album<>+ a b) 0))
+
+(defparameter +track<>+ (gen-comparator '(track-no title)))
+(defun track< (a b)
+  (let ((albs (funcall +album<>+ (track-album a) (track-album b))))
+    (if (zerop albs)
+        (< (funcall +track<>+ a b) 0)
+        albs)))
 
 
 (defun match-filter (data category filter)
 (defun match-filter (data category filter)
   (declare #.*standard-optimize-settings*)
   (declare #.*standard-optimize-settings*)
@@ -258,9 +162,10 @@
       (let ((words (split-sequence:split-sequence #\Space filter)))
       (let ((words (split-sequence:split-sequence #\Space filter)))
         (every #'(lambda (word)
         (every #'(lambda (word)
                    (case category
                    (case category
-                     (album (or (search word (track-album data) :test 'char-equal)
-                                (search word (track-album-artist data) :test 'char-equal)))
-                     (year (search word (princ-to-string (track-year data)) :test 'char-equal))
+                     (album (or (search word (album-album data) :test 'char-equal)
+                                (search word (album-album-artist data) :test 'char-equal)))
+                     (year (or (search word (princ-to-string (album-year data)) :test 'char-equal)
+                               (search word (album-original-date data) :test 'char-equal)))
                      (t (search word (slot-value data category) :test 'char-equal))))
                      (t (search word (slot-value data category) :test 'char-equal))))
                words))))
                words))))
 
 
@@ -271,53 +176,45 @@
              (equal (slot-value data (car r)) (cdr r)))
              (equal (slot-value data (car r)) (cdr r)))
          restrictions))
          restrictions))
 
 
-(defun query-category (db category &key filter restrictions limit (offset 0))
+(defun query-category (albums-db category &key filter restrictions limit (offset 0))
   (declare #.*standard-optimize-settings*
   (declare #.*standard-optimize-settings*
            (type (or null fixnum) limit offset))
            (type (or null fixnum) limit offset))
+o  (let ((results (make-hash-table :test (case category
+                                          (album 'eq)
+                                          (t 'equalp)))))
+    (loop for data being the hash-value of albums-db
+       for result = (case category
+                      (album data)
+                      (t (slot-value data category)))
+       when (and
+             result
+             (match-restrictions data restrictions)
+             (match-filter data category filter))
+       do (setf (gethash result results) t))
+    (let* ((total (hash-table-count results))
+           (start (min total offset))
+           (end (min total (+ offset limit))))
+      (subseq (sort (hash-table-keys results)
+                    (case category
+                      (album #'album<)
+                      (year #'<)
+                      (t #'string<)))
+              start end))))
 
 
-  (labels ((category-data (data category)
-             (case category
-               (album (with-slots (album-artist year album original-date genre album-type album-status mb-id) data
-                        (make-album :album-artist album-artist
-                                    :year year
-                                    :album album
-                                    :original-date original-date
-                                    :genre genre
-                                    :album-type album-type
-                                    :album-status album-status
-                                    :mb-id mb-id)))
-               (t (slot-value data category)))))
-    (let ((results (make-hash-table :test 'equalp)))
-      (loop for entry being the hash-value of db
-         for data = (entry-data entry)
-         when (and data
-                   (match-restrictions data restrictions)
-                   (match-filter data category filter))
-         do (setf (gethash (category-data data category) results) t))
-      (let* ((total (hash-table-count results))
-             (start (min total offset))
-             (end (min total (+ offset limit))))
-        (subseq (sort (hash-table-keys results)
-                      (case category
-                        (album (track-comparator '(album-artist album-type original-date year album)))
-                        (year #'<)
-                        (t #'string<)))
-                start end)))))
-
-(defun query-tracks (db &key filter restrictions limit (offset 0))
+(defun query-tracks (tracks-db &key filter restrictions limit (offset 0))
   (declare #.*standard-optimize-settings*
   (declare #.*standard-optimize-settings*
            (type (or null fixnum) limit offset))
            (type (or null fixnum) limit offset))
   (let (results)
   (let (results)
-    (loop for entry being the hash-value of db
+    (loop for entry being the hash-value of tracks-db
        for data = (entry-data entry)
        for data = (entry-data entry)
        when (and data
        when (and data
-                 (match-restrictions data restrictions)
+                 (match-restrictions (track-album data) restrictions)
                  (match-filter data 'title filter))
                  (match-filter data 'title filter))
        do (push (cons (entry-path entry) data) results))
        do (push (cons (entry-path entry) data) results))
     (let* ((total (length results))
     (let* ((total (length results))
            (start (min total offset))
            (start (min total offset))
            (end (min total (+ offset limit))))
            (end (min total (+ offset limit))))
       (subseq (sort results
       (subseq (sort results
-                    (track-comparator '(album-artist album-type original-date year album track-no title))
+                    #'track<
                     :key 'cdr)
                     :key 'cdr)
               start end))))
               start end))))

+ 51 - 0
utils.lisp

@@ -0,0 +1,51 @@
+(in-package :cl-user)
+(defpackage chad-music.utils
+  (:use :cl #:audio-streams #:alexandria)
+  (:export #:*standard-optimize-settings*
+           #:text-tag
+           #:bit-rate
+           #:is-vbr
+           #:duration))
+(in-package :chad-music.utils)
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  #+dbg
+  (defvar *standard-optimize-settings* '(optimize (debug 3)))
+  #-dbg
+  (defvar *standard-optimize-settings* '(optimize (speed 3) (safety 0) (space 0) (debug 0))))
+
+(defgeneric text-tag (stream desc) (:method ((object t) desc) nil))
+(defmethod text-tag ((mp3 id3:mp3-file) desc)
+  (declare #.*standard-optimize-settings*)
+
+  (loop for frame in (id3:get-frames mp3 '("TXXX"))
+     when (string-equal (id3:desc frame) desc)
+     do (return-from text-tag (id3:val frame))))
+
+(defgeneric bit-rate (stream) (:method ((object t)) nil))
+(defgeneric is-vbr (stream) (:method ((object t)) nil))
+(defgeneric duration (stream) (:method ((object t)) nil))
+(defmethod bit-rate ((mp3 id3:mp3-file))
+  (let ((info (id3:audio-info mp3)))
+    (when info
+      (round (mpeg::bit-rate info) 1000))))
+
+(defmethod is-vbr ((mp3 id3:mp3-file))
+  (let ((info (id3:audio-info mp3)))
+    (when info
+      (mpeg::is-vbr info))))
+
+(defmethod duration ((mp3 id3:mp3-file))
+  (let ((info (id3:audio-info mp3)))
+    (when info
+      (mpeg::len info))))
+
+(defmethod bit-rate ((m4a m4a:mp4-file))
+  (let ((info (m4a:audio-info m4a)))
+    (when info
+      (round (m4a::avg-bit-rate info) 1000))))
+
+(defmethod duration ((m4a m4a:mp4-file))
+  (let ((info (m4a:audio-info m4a)))
+    (when info
+      (m4a::seconds info))))