db.lisp 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. (in-package :cl-user)
  2. (defpackage chad-music.db
  3. (:use :cl #:audio-streams #:alexandria #:chad-music.utils))
  4. (in-package :chad-music.db)
  5. (defstruct album
  6. id album-artist year album original-date
  7. genre album-type album-status mb-id track-count total-duration cover)
  8. (defstruct track
  9. album artist track-no title part-of-set
  10. bit-rate is-vbr duration)
  11. (defun get-file-id (file)
  12. (crypto:byte-array-to-hex-string
  13. (crypto:digest-sequence :md5 (flex:string-to-octets (namestring file) :external-format :utf-8))))
  14. (defun get-album-id (album-artist year album-title)
  15. (crypto:byte-array-to-hex-string
  16. (crypto:digest-sequence :md5 (flex:string-to-octets
  17. (format nil "~A-~A-~A" album-artist year album-title)
  18. :external-format :utf-8))))
  19. (defun parse-file (file albums-db)
  20. (declare #.*standard-optimize-settings*)
  21. (let ((it (open-audio-file file)))
  22. (when it
  23. (let* ((album-artist (or (abstract-tag:album-artist it)
  24. (abstract-tag:artist it)))
  25. (year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t)))
  26. (album-title (abstract-tag:album it))
  27. (album-id (get-album-id album-artist year album-title)))
  28. (multiple-value-bind (album foundp) (gethash album-id albums-db)
  29. (unless foundp
  30. (setf album (make-album
  31. :id album-id
  32. :album-artist album-artist
  33. :year year
  34. :album album-title
  35. :original-date (abstract-tag:original-date it)
  36. :genre (abstract-tag::genre it)
  37. :album-type (utils:awhen (text-tag it "MusicBrainz Album Type") (string-downcase utils:it))
  38. :album-status (utils:awhen (text-tag it "MusicBrainz Album Status") (string-downcase utils:it))
  39. :mb-id (text-tag it "MusicBrainz Album Id"))
  40. (gethash album-id albums-db) album))
  41. (make-track
  42. :album album
  43. :artist (abstract-tag:artist it)
  44. :track-no (abstract-tag::track it)
  45. :title (abstract-tag:title it)
  46. :part-of-set (abstract-tag::disk it)
  47. :bit-rate (bit-rate it)
  48. :is-vbr (is-vbr it)
  49. :duration (round (duration it))))))))
  50. (defstruct entry path data added modified present)
  51. (defun rescan (paths &optional (dbs (cons (make-hash-table :test 'equal) (make-hash-table :test 'equal))))
  52. (declare #.*standard-optimize-settings*)
  53. (let ((added 0) (updated 0) (removed 0)
  54. (tracks-db (car dbs))
  55. (albums-db (cdr dbs)))
  56. (declare (fixnum added updated removed))
  57. (loop for value being the hash-values in tracks-db
  58. do (setf (entry-present value) nil))
  59. (unless (listp paths)
  60. (setf paths (list paths)))
  61. (labels ((scan-file (file)
  62. (let ((file-id (get-file-id file)))
  63. (multiple-value-bind (entry foundp)
  64. (gethash file-id tracks-db)
  65. (let ((modified (file-write-date file)))
  66. (unless foundp
  67. (incf added)
  68. (setf entry (make-entry :path file :added (get-universal-time))
  69. (gethash file-id tracks-db) entry))
  70. (unless (and foundp (= (the fixnum (entry-modified entry)) modified))
  71. (when foundp (incf updated))
  72. (setf (entry-data entry) (parse-file file albums-db)
  73. (entry-modified entry) modified))
  74. (setf (entry-present entry) t))))))
  75. (dolist (dir paths)
  76. (cl-fad:walk-directory dir #'scan-file :follow-symlinks nil))
  77. (let ((album-stats (make-hash-table)))
  78. (loop for file being the hash-keys in tracks-db using (hash-value entry)
  79. do (if (entry-present entry)
  80. (when-let (track (entry-data entry))
  81. (let ((album (track-album track)))
  82. (multiple-value-bind (stats foundp) (gethash album album-stats)
  83. (unless foundp
  84. (setf stats (cons 0 0)
  85. (gethash album album-stats) stats)
  86. (unless (album-cover album)
  87. (setf (album-cover album)
  88. (probe-file (cl-fad:merge-pathnames-as-file
  89. (cl-fad:pathname-directory-pathname (entry-path entry))
  90. "cover.jpg")))))
  91. (incf (the fixnum (car stats))) ;; track-count
  92. (incf (the fixnum (cdr stats)) ;; total-duration
  93. (the fixnum (or (track-duration track) 0))))))
  94. (progn
  95. (incf removed)
  96. (remhash file tracks-db))))
  97. (loop for album being the hash-keys in album-stats using (hash-value stats)
  98. do (setf (album-track-count album) (car stats)
  99. (album-total-duration album) (cdr stats))))
  100. (values (cons tracks-db albums-db) added updated removed))))
  101. (defparameter +album-type-order+ '("album" "lp" "ep" "single" "compilation" "live" "soundtrack"
  102. "spokenword" "remix" "mixed" "dj-mix" "mixtape" "broadcast")
  103. "Half-arbitrary album type order")
  104. (defun gen-comparator (slots)
  105. (labels ((clear-track-no (track-no)
  106. (when track-no
  107. (parse-integer (if (consp track-no)
  108. (car track-no)
  109. track-no)
  110. :junk-allowed t))))
  111. (named-lambda info<> (a b)
  112. (declare #.*standard-optimize-settings*)
  113. (dolist (slot slots 0)
  114. (let ((slot-a (slot-value a slot))
  115. (slot-b (slot-value b slot)))
  116. (when (xor (null slot-a) (null slot-b))
  117. (return-from info<> (if (null slot-b) 1 -1)))
  118. (case slot
  119. (album-type
  120. (setf slot-a (or (position slot-a +album-type-order+ :test 'string-equal) 0)
  121. slot-b (or (position slot-b +album-type-order+ :test 'string-equal) 0)))
  122. (track-no
  123. (setf slot-a (clear-track-no slot-a)
  124. slot-b (clear-track-no slot-b))))
  125. (unless (or (and (null slot-a) (null slot-b))
  126. (case slot
  127. ((album-type year track-no) (= slot-a slot-b))
  128. (t (string-equal slot-a slot-b))))
  129. (return-from info<> (case slot
  130. ((album-type year track-no) (- slot-a slot-b))
  131. (t (if (string< slot-a slot-b) -1 1))))))))))
  132. (defparameter +album<>+ (gen-comparator '(album-artist album-type original-date year album)))
  133. (defun album< (a b)
  134. (< (funcall +album<>+ a b) 0))
  135. (defparameter +track<>+ (gen-comparator '(track-no title)))
  136. (defun track< (a b)
  137. (let ((albs (funcall +album<>+ (track-album a) (track-album b))))
  138. (if (zerop albs)
  139. (< (funcall +track<>+ a b) 0)
  140. albs)))
  141. (defun match-filter (data category filter)
  142. (declare #.*standard-optimize-settings*)
  143. (or (null filter)
  144. (let ((words (split-sequence:split-sequence #\Space filter)))
  145. (every #'(lambda (word)
  146. (case category
  147. (album (or (search word (album-album data) :test 'char-equal)
  148. (search word (album-album-artist data) :test 'char-equal)))
  149. (year (or (search word (princ-to-string (album-year data)) :test 'char-equal)
  150. (search word (album-original-date data) :test 'char-equal)))
  151. (t (search word (slot-value data category) :test 'char-equal))))
  152. words))))
  153. (defun match-restrictions (data restrictions)
  154. (declare #.*standard-optimize-settings*)
  155. (every #'(lambda (r)
  156. (equal (slot-value data (car r)) (cdr r)))
  157. restrictions))
  158. (defun query-category (albums-db category &key filter restrictions limit (offset 0))
  159. (declare #.*standard-optimize-settings*
  160. (type (or null fixnum) limit offset))
  161. o (let ((results (make-hash-table :test (case category
  162. (album 'eq)
  163. (t 'equalp)))))
  164. (loop for data being the hash-value of albums-db
  165. for result = (case category
  166. (album data)
  167. (t (slot-value data category)))
  168. when (and
  169. result
  170. (match-restrictions data restrictions)
  171. (match-filter data category filter))
  172. do (setf (gethash result results) t))
  173. (let* ((total (hash-table-count results))
  174. (start (min total offset))
  175. (end (min total (+ offset limit))))
  176. (subseq (sort (hash-table-keys results)
  177. (case category
  178. (album #'album<)
  179. (year #'<)
  180. (t #'string<)))
  181. start end))))
  182. (defun query-tracks (tracks-db &key filter restrictions limit (offset 0))
  183. (declare #.*standard-optimize-settings*
  184. (type (or null fixnum) limit offset))
  185. (let (results)
  186. (loop for entry being the hash-value of tracks-db
  187. for data = (entry-data entry)
  188. when (and data
  189. (match-restrictions (track-album data) restrictions)
  190. (match-filter data 'title filter))
  191. do (push (cons (entry-path entry) data) results))
  192. (let* ((total (length results))
  193. (start (min total offset))
  194. (end (min total (+ offset limit))))
  195. (subseq (sort results
  196. #'track<
  197. :key 'cdr)
  198. start end))))