1
0

db.lisp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. (in-package :cl-user)
  2. (defpackage chad-music.db
  3. (:use :cl #:audio-streams #:alexandria))
  4. (in-package :chad-music.db)
  5. (eval-when (:compile-toplevel :load-toplevel :execute)
  6. #+dbg
  7. (defvar *standard-optimize-settings* '(optimize (debug 3)))
  8. #-dbg
  9. (defvar *standard-optimize-settings* '(optimize (speed 3) (safety 0) (space 0) (debug 0)))
  10. )
  11. (defstruct album
  12. album-artist year album original-date part-of-set
  13. genre album-type album-status mb-id track-count total-duration cover)
  14. (defstruct track2
  15. album artist track-no title
  16. bit-rate is-vbr duration)
  17. (defstruct track
  18. album-artist artist year album original-date genre album-type album-status mb-id
  19. track-no part-of-set title
  20. bit-rate is-vbr duration
  21. ;; (album-artist nil :type (or string null))
  22. ;; (artist nil :type (or null string))
  23. ;; (year nil :type (or null fixnum))
  24. ;; (album nil :type (or null string))
  25. ;; (original-date nil :type (or null string))
  26. ;; (genre nil :type (or null string))
  27. ;; (album-type nil :type (or null string))
  28. ;; (album-status nil :type (or null string))
  29. ;; (mb-id nil :type (or null string))
  30. ;; (track-no nil :type (or null fixnum list))
  31. ;; part-of-set
  32. ;; (title nil :type (or null string))
  33. ;; (bit-rate nil :type (or null fixnum))
  34. ;; (is-vbr nil :type (or null boolean))
  35. ;; (duration nil :type (or null fixnum))
  36. )
  37. (defgeneric text-tag (stream desc) (:method ((object t) desc) nil))
  38. (defmethod text-tag ((mp3 id3:mp3-file) desc)
  39. (declare #.*standard-optimize-settings*)
  40. (loop for frame in (id3:get-frames mp3 '("TXXX"))
  41. when (string-equal (id3:desc frame) desc)
  42. do (return-from text-tag (id3:val frame))))
  43. (defgeneric bit-rate (stream) (:method ((object t)) nil))
  44. (defgeneric is-vbr (stream) (:method ((object t)) nil))
  45. (defgeneric duration (stream) (:method ((object t)) nil))
  46. (defmethod bit-rate ((mp3 id3:mp3-file))
  47. (let ((info (id3:audio-info mp3)))
  48. (when info
  49. (round (mpeg::bit-rate info) 1000))))
  50. (defmethod is-vbr ((mp3 id3:mp3-file))
  51. (let ((info (id3:audio-info mp3)))
  52. (when info
  53. (mpeg::is-vbr info))))
  54. (defmethod duration ((mp3 id3:mp3-file))
  55. (let ((info (id3:audio-info mp3)))
  56. (when info
  57. (mpeg::len info))))
  58. (defmethod bit-rate ((m4a m4a:mp4-file))
  59. (let ((info (m4a:audio-info m4a)))
  60. (when info
  61. (round (m4a::avg-bit-rate info) 1000))))
  62. (defmethod duration ((m4a m4a:mp4-file))
  63. (let ((info (m4a:audio-info m4a)))
  64. (when info
  65. (m4a::seconds info))))
  66. (defun parse-file (file)
  67. (declare #.*standard-optimize-settings*)
  68. (let ((it (open-audio-file file)))
  69. (when it
  70. (make-track
  71. :album-artist (abstract-tag:album-artist it)
  72. :artist (abstract-tag:artist it)
  73. :year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t))
  74. :album (abstract-tag:album it)
  75. :original-date (abstract-tag:original-date it)
  76. :genre (abstract-tag::genre it)
  77. :album-type (string-downcase (text-tag it "MusicBrainz Album Type"))
  78. :album-status (string-downcase (text-tag it "MusicBrainz Album Status"))
  79. :mb-id (text-tag it "MusicBrainz Album Id")
  80. :track-no (abstract-tag::track it)
  81. :part-of-set (abstract-tag::disk it)
  82. :title (abstract-tag:title it)
  83. :bit-rate (bit-rate it)
  84. :is-vbr (is-vbr it)
  85. :duration (duration it)))))
  86. (defun album-id (album-artist year album-title)
  87. (crypto:byte-array-to-hex-string
  88. (crypto:digest-sequence :md5 (flex:string-to-octets
  89. (format nil "~A-~A-~A" album-artist year album-title)
  90. :external-format :utf-8))))
  91. (defun parse-file2 (file albums-db)
  92. (declare #.*standard-optimize-settings*)
  93. (let ((it (open-audio-file file)))
  94. (when it
  95. (let* ((album-artist (abstract-tag:album-artist it))
  96. (year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t)))
  97. (album-title (abstract-tag:album it))
  98. (album-id (album-id album-artist year album-title)))
  99. (multiple-value-bind (album foundp) (gethash album-id albums-db)
  100. (unless foundp
  101. (setf album (make-album
  102. :album-artist album-artist
  103. :year year
  104. :album album-title
  105. :original-date (abstract-tag:original-date it)
  106. :genre (abstract-tag::genre it)
  107. :part-of-set (abstract-tag::disk it)
  108. :album-type (string-downcase (text-tag it "MusicBrainz Album Type"))
  109. :album-status (string-downcase (text-tag it "MusicBrainz Album Status"))
  110. :mb-id (text-tag it "MusicBrainz Album Id"))
  111. (gethash album-id albums-db) album))
  112. (make-track2
  113. :album album
  114. :artist (abstract-tag:artist it)
  115. :track-no (abstract-tag::track it)
  116. :title (abstract-tag:title it)
  117. :bit-rate (bit-rate it)
  118. :is-vbr (is-vbr it)
  119. :duration (duration it)))))))
  120. (defstruct entry path data added modified present)
  121. (defun file-id (file)
  122. (crypto:byte-array-to-hex-string
  123. (crypto:digest-sequence :md5 (flex:string-to-octets (namestring file) :external-format :utf-8))))
  124. (defun rescan (paths &optional (db (make-hash-table :test 'equal)))
  125. (declare #.*standard-optimize-settings*)
  126. (let ((added 0) (updated 0) (removed 0))
  127. (declare (fixnum added updated removed))
  128. (loop for value being the hash-values in db
  129. do (setf (entry-present value) nil))
  130. (unless (listp paths)
  131. (setf paths (list paths)))
  132. (labels ((scan-file (file)
  133. (let ((file-id (file-id file)))
  134. (multiple-value-bind (entry foundp)
  135. (gethash file-id db)
  136. (let ((modified (file-write-date file)))
  137. (unless foundp
  138. (incf added)
  139. (setf entry (make-entry :path file :added (get-universal-time))
  140. (gethash file-id db) entry))
  141. (unless (and foundp (= (the fixnum (entry-modified entry)) modified))
  142. (when foundp (incf updated))
  143. (setf (entry-data entry) (parse-file file)
  144. (entry-modified entry) modified))
  145. (setf (entry-present entry) t))))))
  146. (dolist (dir paths)
  147. (cl-fad:walk-directory dir #'scan-file :follow-symlinks nil))
  148. (loop for file being the hash-keys in db using (hash-value entry)
  149. unless (entry-present entry)
  150. do (incf removed) and
  151. do (remhash file db))
  152. (values db added updated removed))))
  153. (defun rescan2 (paths &optional (dbs (cons (make-hash-table :test 'equal) (make-hash-table :test 'equal))))
  154. (declare #.*standard-optimize-settings*)
  155. (let ((added 0) (updated 0) (removed 0)
  156. (tracks-db (car dbs))
  157. (albums-db (cdr dbs)))
  158. (declare (fixnum added updated removed))
  159. (loop for value being the hash-values in tracks-db
  160. do (setf (entry-present value) nil))
  161. (unless (listp paths)
  162. (setf paths (list paths)))
  163. (labels ((scan-file (file)
  164. (let ((file-id (file-id file)))
  165. (multiple-value-bind (entry foundp)
  166. (gethash file-id tracks-db)
  167. (let ((modified (file-write-date file)))
  168. (unless foundp
  169. (incf added)
  170. (setf entry (make-entry :path file :added (get-universal-time))
  171. (gethash file-id tracks-db) entry))
  172. (unless (and foundp (= (the fixnum (entry-modified entry)) modified))
  173. (when foundp (incf updated))
  174. (setf (entry-data entry) (parse-file2 file albums-db)
  175. (entry-modified entry) modified))
  176. (setf (entry-present entry) t))))))
  177. (dolist (dir paths)
  178. (cl-fad:walk-directory dir #'scan-file :follow-symlinks nil))
  179. (let ((album-stats (make-hash-table)))
  180. (loop for file being the hash-keys in tracks-db using (hash-value entry)
  181. do (if (entry-present entry)
  182. (alexandria:when-let (track (entry-data entry))
  183. (incf (car (gethash (track))))
  184. )
  185. (progn
  186. (incf removed)
  187. (remhash file tracks-db)))))
  188. (values (cons tracks-db albums-db) added updated removed))))
  189. (defparameter +album-type-order+ '("album" "lp" "ep" "single" "compilation" "live" "soundtrack"
  190. "spokenword" "remix" "mixed" "dj-mix" "mixtape" "broadcast")
  191. "Half-arbitrary album type order")
  192. (defun track-comparator (slots)
  193. (labels ((clear-track-no (track-no)
  194. (when track-no
  195. (parse-integer (if (consp track-no)
  196. (car track-no)
  197. track-no)
  198. :junk-allowed t))))
  199. (alexandria:named-lambda info< (a b)
  200. (declare #.*standard-optimize-settings*)
  201. (dolist (slot slots)
  202. (let ((slot-a (slot-value a slot))
  203. (slot-b (slot-value b slot)))
  204. (when (xor (null slot-a) (null slot-b))
  205. (return-from info< (null slot-b)))
  206. (case slot
  207. (album-type
  208. (setf slot-a (or (position slot-a +album-type-order+ :test 'string-equal) 0)
  209. slot-b (or (position slot-b +album-type-order+ :test 'string-equal) 0)))
  210. (track-no
  211. (setf slot-a (clear-track-no slot-a)
  212. slot-b (clear-track-no slot-b))))
  213. (unless (or (and (null slot-a) (null slot-b))
  214. (case slot
  215. ((album-type year track-no) (= slot-a slot-b))
  216. (t (string-equal slot-a slot-b))))
  217. (return-from info< (case slot
  218. ((album-type year track-no) (< slot-a slot-b))
  219. (t (string< slot-a slot-b))))))))))
  220. (defun match-filter (data category filter)
  221. (declare #.*standard-optimize-settings*)
  222. (or (null filter)
  223. (let ((words (split-sequence:split-sequence #\Space filter)))
  224. (every #'(lambda (word)
  225. (case category
  226. (album (or (search word (track-album data) :test 'char-equal)
  227. (search word (track-album-artist data) :test 'char-equal)))
  228. (year (search word (princ-to-string (track-year data)) :test 'char-equal))
  229. (t (search word (slot-value data category) :test 'char-equal))))
  230. words))))
  231. (defun match-restrictions (data restrictions)
  232. (declare #.*standard-optimize-settings*)
  233. (every #'(lambda (r)
  234. (equal (slot-value data (car r)) (cdr r)))
  235. restrictions))
  236. (defun query-category (db category &key filter restrictions limit (offset 0))
  237. (declare #.*standard-optimize-settings*
  238. (type (or null fixnum) limit offset))
  239. (labels ((category-data (data category)
  240. (case category
  241. (album (with-slots (album-artist year album original-date genre album-type album-status mb-id) data
  242. (make-album :album-artist album-artist
  243. :year year
  244. :album album
  245. :original-date original-date
  246. :genre genre
  247. :album-type album-type
  248. :album-status album-status
  249. :mb-id mb-id)))
  250. (t (slot-value data category)))))
  251. (let ((results (make-hash-table :test 'equalp)))
  252. (loop for entry being the hash-value of db
  253. for data = (entry-data entry)
  254. when (and data
  255. (match-restrictions data restrictions)
  256. (match-filter data category filter))
  257. do (setf (gethash (category-data data category) results) t))
  258. (let* ((total (hash-table-count results))
  259. (start (min total offset))
  260. (end (min total (+ offset limit))))
  261. (subseq (sort (hash-table-keys results)
  262. (case category
  263. (album (track-comparator '(album-artist album-type original-date year album)))
  264. (year #'<)
  265. (t #'string<)))
  266. start end)))))
  267. (defun query-tracks (db &key filter restrictions limit (offset 0))
  268. (declare #.*standard-optimize-settings*
  269. (type (or null fixnum) limit offset))
  270. (let (results)
  271. (loop for entry being the hash-value of db
  272. for data = (entry-data entry)
  273. when (and data
  274. (match-restrictions data restrictions)
  275. (match-filter data 'title filter))
  276. do (push (cons (entry-path entry) data) results))
  277. (let* ((total (length results))
  278. (start (min total offset))
  279. (end (min total (+ offset limit))))
  280. (subseq (sort results
  281. (track-comparator '(album-artist album-type original-date year album track-no title))
  282. :key 'cdr)
  283. start end))))