Forráskód Böngészése

[back] Rescan on demand, via POST /api/rescan

Innokentiy Enikeev 4 éve
szülő
commit
e92a96c1ea
4 módosított fájl, 86 hozzáadás és 3 törlés
  1. 1 0
      back/chad-music.asd
  2. 18 2
      back/db.lisp
  3. 47 0
      back/server.lisp
  4. 20 1
      back/utils.lisp

+ 1 - 0
back/chad-music.asd

@@ -3,6 +3,7 @@
   :author "Innokentiy Enikeev <me@enikesha.net>"
   :license "MIT"
   :depends-on (#:alexandria
+               #:bordeaux-threads
                #:cl-cookie
                #:cl-fad
                #:cl-ppcre

+ 18 - 2
back/db.lisp

@@ -14,6 +14,7 @@
            #:make-entry #:entry-track #:entry-added #:entry-modified #:entry-present
            #:get-album-id #:get-file-id #:rescan
            #:clear-track-no
+           #:db-stats
            #:save-db #:load-db
            #:query-category #:query-tracks #:album-tracks))
 (in-package :chad-music.db)
@@ -96,7 +97,7 @@
   (gethash (track-album-id track)
            (db-albums *db*)))
 
-(defun rescan (paths)
+(defun rescan (&optional (paths (mapcar 'car chad-music.server::*path-url-mappings*)))
   (declare #.*standard-optimize-settings*)
 
   (let ((added 0) (updated 0) (removed 0)
@@ -168,7 +169,7 @@
        for track = (entry-track entry)
        when track
        do (setf (gethash (track-album-id track) album-added-db) (entry-added entry))
-       do (push track (gethash (track-album-id track) album-tracks-db)))
+       and 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<)))))
@@ -179,6 +180,7 @@
                      (car track-no)
                      track-no)))
       (etypecase track
+        (null nil)
         (integer track)
         (string (parse-integer track :junk-allowed t))))))
 
@@ -310,6 +312,20 @@
 (defun album-tracks (album-id)
   (gethash album-id (db-album-tracks *db*)))
 
+(defun db-stats ()
+  (declare #.*standard-optimize-settings*)
+  (let ((albums (db-albums *db*))
+	(artists (make-hash-table :test 'equal)))
+    (loop for entry being the hash-values in albums
+	  summing (album-total-duration entry) into total-duration
+	  summing (album-track-count entry) into total-tracks
+	  counting t into total-albums
+	  do (setf (gethash (album-artist entry) artists) t)
+	  finally (return (list :|artists| (hash-table-count artists)
+				:|albums| total-albums
+				:|tracks| total-tracks
+				:|duration| total-duration)))))
+
 (defun save-db (&optional (file-name *db-path*))
   (declare #.*standard-optimize-settings*)
   (with-output-to-file (s file-name :if-exists :supersede)

+ 47 - 0
back/server.lisp

@@ -151,11 +151,55 @@
                              root (car (getf params :splat))))))
       (if file (list 200 nil file) +404+))))
 
+;; Admin tools
+(defvar *rescans* nil)
+(defun update-db ()
+  (let (added updated removed)
+    (sb-impl::call-with-timing #'(lambda (&rest args)
+				   (push (append args
+						 (list :timestamp (get-universal-time)
+						       :added added
+						       :updated updated
+						       :removed removed))
+					 *rescans*))
+			       #'(lambda ()
+				   (multiple-value-bind (a u r) (rescan)
+				     (save-db)
+				     (setf added a updated u removed r))))))
+
+(defvar *rescan-lock* (bt:make-lock "Rescan lock"))
+(defvar *rescan-cond* (bt:make-condition-variable :name "Rescan requested"))
+(defvar *rescan-thread* nil)
+(defvar *rescan-active* nil)
+
+(defun rescanner ()
+  (loop
+    (bt:with-lock-held (*rescan-lock*)
+      (bt:condition-wait *rescan-cond* *rescan-lock*)
+      (setf *rescan-active* t)
+      (handler-case (update-db)
+	(error (e) (format t "Error updating db: ~a" e)))
+      (setf *rescan-active* nil))))
+
+(defun request-rescan (params)
+  (declare #.*standard-optimize-settings* (ignorable params))
+  (bt:condition-notify *rescan-cond*)
+  +200-empty+)
+
+(defun stats (params)
+  (declare #.*standard-optimize-settings* (ignorable params))
+  (let ((stats (db-stats)))
+    (setf (getf stats :|duration|) (format-interval (getf stats :|duration|)))
+    (200-json (append stats
+		      (list :|rescans| (subseq *rescans* 0 (min (length *rescans*) 10)))))))
+
 (defvar *mapper* (myway:make-mapper))
 (myway:connect *mapper* "/api/cat/:category/size" 'get-category-size)
 (myway:connect *mapper* "/api/cat/:category" 'get-category)
 (myway:connect *mapper* "/api/cat" 'get-category-list)
 (myway:connect *mapper* "/api/album/:id/tracks" 'get-album-tracks)
+(myway:connect *mapper* "/api/stats" 'stats)
+(myway:connect *mapper* "/api/rescan" 'request-rescan :method :POST)
 
 (defun main (&rest args &key (port 5000) (debug nil) (use-thread t) (serve-files nil) &allow-other-keys)
   ;; Load config file
@@ -176,6 +220,9 @@
        do (myway:connect *mapper* (concatenate 'string url-prefix "*")
                          (file-server path-prefix))))
 
+  ;; Start rescan processor
+  (setf *rescan-thread* (bt:make-thread 'rescanner :name "DB rescanner"))
+
   ;; Start application
   (apply #'clack:clackup
          (myway:to-app *mapper*)

+ 20 - 1
back/utils.lisp

@@ -13,7 +13,9 @@
            #:http-request
            #:xml-request
            #:select-text
-           #:json-request))
+           #:json-request
+	   #:smart-f
+	   #:format-interval))
 (in-package :chad-music.utils)
 
 (eval-when (:compile-toplevel :load-toplevel :execute)
@@ -192,3 +194,20 @@
     (unless (stringp body)
       (setf body (trivial-utf-8:utf-8-bytes-to-string body)))
     (values (jojo:parse body :as as) status headers uri)))
+
+(defun smart-f (arg &optional digits)
+  (with-output-to-string (s)
+    (prin1 (cond ((= (round arg) arg) (round arg))
+                 (digits (float (/ (round (* arg (expt 10 digits)))
+                                   (expt 10 digits))))
+                 (t arg))
+           s)))
+
+(defun format-interval (seconds)
+  (cond
+    ((< seconds 60) (format nil "~A sec" seconds))
+    ((< seconds (* 60 60)) (format nil "~A mins" (smart-f (/ seconds 60) 1)))
+    ((< seconds (* 60 60 24)) (format nil "~A hours" (smart-f (/ seconds (* 60 60)) 1)))
+    ((< seconds (* 60 60 24 7)) (format nil "~A days" (smart-f (/ seconds (* 60 60 24)) 1)))
+    ((< seconds (* 60 60 24 7 54)) (format nil "~A weeks" (smart-f (/ seconds (* 60 60 24 7)) 1)))
+    (:otherwise (format nil "~A years" (smart-f (/ seconds (* 60 60 24 365.25)) 1)))))