|
@@ -2,6 +2,7 @@
|
|
|
(defpackage chad-music.db
|
|
(defpackage chad-music.db
|
|
|
(:use :cl #:audio-streams #:alexandria #:chad-music.utils)
|
|
(:use :cl #:audio-streams #:alexandria #:chad-music.utils)
|
|
|
(:export #:*standard-optimize-settings*
|
|
(:export #:*standard-optimize-settings*
|
|
|
|
|
+ #:*db*
|
|
|
#:album #:id #:artist #:year #:album #:original-date #:publisher #:country #:genre #:type #:status #:mb-id #:track-count #:total-duration #:cover
|
|
#:album #:id #:artist #:year #:album #:original-date #:publisher #:country #:genre #:type #:status #:mb-id #:track-count #:total-duration #:cover
|
|
|
#:make-album #:album-id #:album-artist #:album-year #:album-album #:album-original-date #:album-publisher #:album-country
|
|
#:make-album #:album-id #:album-artist #:album-year #:album-album #:album-original-date #:album-publisher #:album-country
|
|
|
#:album-genre #:album-type #:album-status #:album-mb-id #:album-track-count #:album-total-duration #:album-cover
|
|
#:album-genre #:album-type #:album-status #:album-mb-id #:album-track-count #:album-total-duration #:album-cover
|
|
@@ -12,22 +13,32 @@
|
|
|
#:make-entry #:entry-track #:entry-added #:entry-modified #:entry-present
|
|
#:make-entry #:entry-track #:entry-added #:entry-modified #:entry-present
|
|
|
#:get-album-id #:get-file-id #:rescan
|
|
#:get-album-id #:get-file-id #:rescan
|
|
|
#:clear-track-no
|
|
#:clear-track-no
|
|
|
- #:query-category #:query-tracks))
|
|
|
|
|
|
|
+ #:save-db #:load-db
|
|
|
|
|
+ #:query-category #:query-tracks #:album-tracks))
|
|
|
(in-package :chad-music.db)
|
|
(in-package :chad-music.db)
|
|
|
|
|
|
|
|
|
|
+(defstruct db
|
|
|
|
|
+ (albums (make-hash-table :test 'equal) :type hash-table)
|
|
|
|
|
+ (tracks (make-hash-table :test 'equal) :type hash-table)
|
|
|
|
|
+ (album-tracks (make-hash-table :test 'equal) :type hash-table))
|
|
|
|
|
+
|
|
|
|
|
+(defvar *db* (make-db) "Metadata database")
|
|
|
|
|
+
|
|
|
(defstruct album
|
|
(defstruct album
|
|
|
id artist year album original-date publisher country
|
|
id artist year album original-date publisher country
|
|
|
genre type status mb-id track-count total-duration cover)
|
|
genre type status mb-id track-count total-duration cover)
|
|
|
|
|
|
|
|
(defstruct track
|
|
(defstruct track
|
|
|
- album artist no title part-of-set
|
|
|
|
|
|
|
+ id album-id artist no title part-of-set
|
|
|
bit-rate is-vbr duration path)
|
|
bit-rate is-vbr duration path)
|
|
|
|
|
|
|
|
(defstruct entry track added modified present)
|
|
(defstruct entry track added modified present)
|
|
|
|
|
|
|
|
(defun get-file-id (file)
|
|
(defun get-file-id (file)
|
|
|
(crypto:byte-array-to-hex-string
|
|
(crypto:byte-array-to-hex-string
|
|
|
- (crypto:digest-sequence :md5 (flex:string-to-octets (namestring file) :external-format :utf-8))))
|
|
|
|
|
|
|
+ (crypto:digest-sequence :md5 (flex:string-to-octets
|
|
|
|
|
+ (namestring file)
|
|
|
|
|
+ :external-format :utf-8))))
|
|
|
|
|
|
|
|
(defun get-album-id (artist year title)
|
|
(defun get-album-id (artist year title)
|
|
|
(crypto:byte-array-to-hex-string
|
|
(crypto:byte-array-to-hex-string
|
|
@@ -35,22 +46,26 @@
|
|
|
(format nil "~A-~A-~A" artist year title)
|
|
(format nil "~A-~A-~A" artist year title)
|
|
|
:external-format :utf-8))))
|
|
:external-format :utf-8))))
|
|
|
|
|
|
|
|
-(defun parse-file (file albums-db)
|
|
|
|
|
|
|
+(defun parse-file (file track-id)
|
|
|
(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 (or (abstract-tag:album-artist it)
|
|
|
|
|
|
|
+ (let* ((albums-db (db-albums *db*))
|
|
|
|
|
+ (album-artist (or (abstract-tag:album-artist it)
|
|
|
(abstract-tag:artist it)))
|
|
(abstract-tag:artist it)))
|
|
|
- (year (utils:awhen (abstract-tag::year it) (parse-integer utils:it :junk-allowed t)))
|
|
|
|
|
|
|
+ (album-year (utils:awhen (or (abstract-tag::year it)
|
|
|
|
|
+ (abstract-tag:original-date it))
|
|
|
|
|
+ (parse-integer utils:it :junk-allowed t)))
|
|
|
(album-title (abstract-tag:album it))
|
|
(album-title (abstract-tag:album it))
|
|
|
- (album-id (get-album-id album-artist year album-title)))
|
|
|
|
|
- (multiple-value-bind (album foundp) (gethash album-id albums-db)
|
|
|
|
|
|
|
+ (album-id (get-album-id album-artist album-year album-title)))
|
|
|
|
|
+ (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
|
|
:id album-id
|
|
|
:artist album-artist
|
|
:artist album-artist
|
|
|
- :year year
|
|
|
|
|
|
|
+ :year album-year
|
|
|
:album album-title
|
|
:album album-title
|
|
|
:original-date (abstract-tag:original-date it)
|
|
:original-date (abstract-tag:original-date it)
|
|
|
:publisher (publisher it)
|
|
:publisher (publisher it)
|
|
@@ -61,7 +76,8 @@
|
|
|
: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-track
|
|
(make-track
|
|
|
- :album album
|
|
|
|
|
|
|
+ :id track-id
|
|
|
|
|
+ :album-id album-id
|
|
|
:artist (abstract-tag:artist it)
|
|
:artist (abstract-tag:artist it)
|
|
|
:no (abstract-tag::track it)
|
|
:no (abstract-tag::track it)
|
|
|
:title (abstract-tag:title it)
|
|
:title (abstract-tag:title it)
|
|
@@ -71,12 +87,16 @@
|
|
|
:duration (round (or (duration it) 0))
|
|
:duration (round (or (duration it) 0))
|
|
|
:path file))))))
|
|
:path file))))))
|
|
|
|
|
|
|
|
-(defun rescan (paths &optional (dbs (cons (make-hash-table :test 'equal) (make-hash-table :test 'equal))))
|
|
|
|
|
|
|
+(defun track-album (track)
|
|
|
|
|
+ (gethash (track-album-id track)
|
|
|
|
|
+ (db-albums *db*)))
|
|
|
|
|
+
|
|
|
|
|
+(defun rescan (paths)
|
|
|
(declare #.*standard-optimize-settings*)
|
|
(declare #.*standard-optimize-settings*)
|
|
|
|
|
|
|
|
(let ((added 0) (updated 0) (removed 0)
|
|
(let ((added 0) (updated 0) (removed 0)
|
|
|
- (tracks-db (car dbs))
|
|
|
|
|
- (albums-db (cdr dbs)))
|
|
|
|
|
|
|
+ (albums-db (db-albums *db*))
|
|
|
|
|
+ (tracks-db (db-tracks *db*)))
|
|
|
(declare (fixnum added updated removed))
|
|
(declare (fixnum added updated removed))
|
|
|
|
|
|
|
|
(loop for value being the hash-values in tracks-db
|
|
(loop for value being the hash-values in tracks-db
|
|
@@ -96,7 +116,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-track entry) (parse-file file albums-db)
|
|
|
|
|
|
|
+ (setf (entry-track entry) (parse-file file file-id)
|
|
|
(entry-modified entry) modified))
|
|
(entry-modified entry) modified))
|
|
|
(setf (entry-present entry) t))))))
|
|
(setf (entry-present entry) t))))))
|
|
|
(dolist (dir paths)
|
|
(dolist (dir paths)
|
|
@@ -122,11 +142,28 @@
|
|
|
(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))))
|
|
|
|
|
|
|
+ (loop for album-id being the hash-keys in albums-db using (hash-value album)
|
|
|
|
|
+ for stats = (gethash album album-stats)
|
|
|
|
|
+ do (if stats
|
|
|
|
|
+ (setf (album-track-count album) (car stats)
|
|
|
|
|
+ (album-total-duration album) (cdr stats))
|
|
|
|
|
+ (remhash album-id albums-db))))
|
|
|
|
|
+ (reindex-db)
|
|
|
|
|
+ (values added updated removed))))
|
|
|
|
|
|
|
|
- (values (cons tracks-db albums-db) added updated removed))))
|
|
|
|
|
|
|
+(defun reindex-db ()
|
|
|
|
|
+ (declare #.*standard-optimize-settings*)
|
|
|
|
|
+
|
|
|
|
|
+ (let ((tracks-db (db-tracks *db*))
|
|
|
|
|
+ (album-tracks-db (db-album-tracks *db*)))
|
|
|
|
|
+ (clrhash album-tracks-db)
|
|
|
|
|
+ (loop for entry being the hash-values in tracks-db
|
|
|
|
|
+ for track = (entry-track entry)
|
|
|
|
|
+ when track
|
|
|
|
|
+ do (push track (gethash (track-album-id track) album-tracks-db)))
|
|
|
|
|
+ (loop for album-id being the hash-keys in album-tracks-db using (hash-value tracks)
|
|
|
|
|
+ do (setf (gethash album-id album-tracks-db)
|
|
|
|
|
+ (sort tracks 'track<)))))
|
|
|
|
|
|
|
|
(defun clear-track-no (track-no)
|
|
(defun clear-track-no (track-no)
|
|
|
(when track-no
|
|
(when track-no
|
|
@@ -200,10 +237,11 @@
|
|
|
(equal (slot-value data (car r)) (cdr r)))
|
|
(equal (slot-value data (car r)) (cdr r)))
|
|
|
restrictions))
|
|
restrictions))
|
|
|
|
|
|
|
|
-(defun query-category (albums-db category &key filter restrictions limit (offset 0) count-only)
|
|
|
|
|
|
|
+(defun query-category (category &key filter restrictions limit (offset 0) count-only)
|
|
|
(declare #.*standard-optimize-settings*
|
|
(declare #.*standard-optimize-settings*
|
|
|
(type (or null fixnum) limit offset))
|
|
(type (or null fixnum) limit offset))
|
|
|
- (let ((results (make-hash-table :test (case category
|
|
|
|
|
|
|
+ (let ((albums-db (db-albums *db*))
|
|
|
|
|
+ (results (make-hash-table :test (case category
|
|
|
(album 'eq)
|
|
(album 'eq)
|
|
|
(t 'equal)))))
|
|
(t 'equal)))))
|
|
|
(loop for data being the hash-value of albums-db
|
|
(loop for data being the hash-value of albums-db
|
|
@@ -231,10 +269,10 @@
|
|
|
(t #'car))))
|
|
(t #'car))))
|
|
|
start end)))))
|
|
start end)))))
|
|
|
|
|
|
|
|
-(defun query-tracks (tracks-db &key filter restrictions limit (offset 0))
|
|
|
|
|
|
|
+(defun query-tracks (&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 ((tracks-db (db-tracks *db*)) results)
|
|
|
(loop for entry being the hash-value of tracks-db
|
|
(loop for entry being the hash-value of tracks-db
|
|
|
for track = (entry-track entry)
|
|
for track = (entry-track entry)
|
|
|
when (and track
|
|
when (and track
|
|
@@ -247,3 +285,30 @@
|
|
|
(subseq (sort results
|
|
(subseq (sort results
|
|
|
#'track<)
|
|
#'track<)
|
|
|
start end))))
|
|
start end))))
|
|
|
|
|
+
|
|
|
|
|
+(defun album-tracks (album-id)
|
|
|
|
|
+ (gethash album-id (db-album-tracks *db*)))
|
|
|
|
|
+
|
|
|
|
|
+(defun save-db (file-name)
|
|
|
|
|
+ (declare #.*standard-optimize-settings*)
|
|
|
|
|
+ (with-output-to-file (s file-name :if-exists :supersede)
|
|
|
|
|
+ (labels ((print-values (table)
|
|
|
|
|
+ (loop for value being the hash-value of table
|
|
|
|
|
+ do (print value s))))
|
|
|
|
|
+ (print-values (db-albums *db*))
|
|
|
|
|
+ (print-values (db-tracks *db*)))))
|
|
|
|
|
+
|
|
|
|
|
+(defun load-db (file-name)
|
|
|
|
|
+ (declare #.*standard-optimize-settings*)
|
|
|
|
|
+ (with-input-from-file (s file-name)
|
|
|
|
|
+ (let ((albums (make-hash-table :test 'equal))
|
|
|
|
|
+ (entries (make-hash-table :test 'equal)))
|
|
|
|
|
+ (loop for value = (read s nil)
|
|
|
|
|
+ while value
|
|
|
|
|
+ do (etypecase value
|
|
|
|
|
+ (album (setf (gethash (album-id value) albums) value))
|
|
|
|
|
+ (entry (when (entry-track value)
|
|
|
|
|
+ (setf (gethash (track-id (entry-track value)) entries) value)))))
|
|
|
|
|
+ (let ((*db* (make-db :albums albums :tracks entries)))
|
|
|
|
|
+ (reindex-db)
|
|
|
|
|
+ *db*))))
|