Skip to content

Commit

Permalink
Switch to a queue-based representation for the scrobble cache and mak…
Browse files Browse the repository at this point in the history
…e ALL scrobbles go through it, network failure or not. Caching should only happen for scrobbles, not other calls so this is a nice simplification. Other assorted cleanups to the API and cl-store usage.
  • Loading branch information
kingcons committed Aug 29, 2011
1 parent d44bf52 commit 1b47785
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 23 deletions.
3 changes: 2 additions & 1 deletion cl-scrobbler.asd
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
:description "A library for scrobbling to last.fm"
:version "0.1"
:author "Brit Butler <redline6561@gmail.com>"
:depends-on (#:md5 #:drakma #:cl-store #:st-json)
:depends-on (#:md5 #:drakma #:st-json #:cl-store)
:components ((:module src
:serial t
:components ((:file "package")
(:file "config")
(:file "queue")
(:file "cache")
(:file "errors")
(:file "util")
Expand Down
6 changes: 4 additions & 2 deletions src/auth.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ a binary file and setting the *SESSION-KEY* global on success."
do (request-user-auth token))
(let ((session (getjso "session" (read-json (get-session token)))))
(with-open-file (out (config-file "session") :direction :output
:if-exists :supersede :if-does-not-exist :create)
:if-exists :supersede :if-does-not-exist :create
:element-type '(unsigned-byte 8))
(cl-store:store (mapcar (lambda (key)
(getjso key session))
'("name" "key" "subscriber")) out))
Expand All @@ -42,6 +43,7 @@ a binary file and setting the *SESSION-KEY* global on success."
"Attempt to retrieve session key from disk. If it is not present, authorize
a new session with last.fm and store the key for future use."
(if (probe-file (config-file "session"))
(with-open-file (in (config-file "session"))
(with-open-file (in (config-file "session")
:element-type '(unsigned-byte 8)))
(setf *session-key* (second (cl-store:restore in))))
(authorize-scrobbling)))
24 changes: 12 additions & 12 deletions src/cache.lisp
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
(in-package :cl-scrobbler)

(defvar *scrobble-cache* (make-array 8 :adjustable t :fill-pointer 0)
"A vector of cached scrobbles to send when last.fm is available.")
(defvar *scrobble-cache* (make-queue)
"A queue of cached scrobbles to send when last.fm is available.")

(defvar *cached-methods* '("track.scrobble")
"A list of Last.fm methods that should be cached for later resubmission in
case initial submission does not succeed.")
(defun cache-contents ()
"Get a copy of the contents of the *SCROBBLE-CACHE*."
(loop for song in (queue-elements *scrobble-cache*) collecting song))

(defun add-to-cache (call params)
"Add the attempted CALL and PARAMS to the cache and serialize it to disk."
(vector-push-extend (cons call params) *scrobble-cache*)
(defun add-to-cache (scrobble)
"Add the attempted SONG to the cache and serialize it to disk."
(enqueue *scrobble-cache* scrobble)
(with-open-file (out (config-file "cache") :direction :output
:if-does-not-exist :create :if-exists :supersede)
:if-does-not-exist :create :if-exists :supersede
:element-type '(unsigned-byte 8))
(cl-store:store *scrobble-cache* out)))

(defun restore-cache ()
"Restore the cache from disk."
(with-open-file (in (config-file "cache"))
(with-open-file (in (config-file "cache") :element-type '(unsigned-byte 8))
(setf *scrobble-cache* (cl-store:restore in))))

(defmacro with-caching ((call params) &body body)
Expand All @@ -25,5 +26,4 @@ a condition is encountered, a cache-it restart is available to invoke
ADD-TO-CACHE with CALL and PARAMS."
`(restart-case (progn ,@body)
(cache-it ()
(when (member ,call *cached-methods* :test #'string=)
(add-to-cache ,call ,params)))))
(add-to-cache ,call ,params))))
6 changes: 2 additions & 4 deletions src/package.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
(:import-from #:st-json #:read-json
#:getjso)
(:export #:*config-dir*
#:*scrobble-cache*
#:cache-contents
#:restore-cache
#:get-session-key
#:valid-scrobble-p
#:update-now-playing
#:scrobble))

#:queue-scrobble))
30 changes: 30 additions & 0 deletions src/queue.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
(in-package :cl-scrobbler)


;;;; This queue code courtesy of Peter Norvig and Richard Water's technical
;;;; paper "Implementing Queues in Lisp" published by the ACM in 1991.
;;;; It is more easily, and freely, available in 'Some Useful Lisp Algorithms'
;;;; which is available online at http://www.merl.com/papers/docs/TR91-04.pdf

;;; This implementation taken from Figure 17 and ever so slightly adapted.
;;; TODO: Read the paper. Then scrap the CDR coding. It's 2011 after all.

(defun make-queue ()
(let ((queue (list nil)))
(cons queue queue)))

(defun queue-elements (queue)
(cdar queue))

(defun empty-queue-p (queue)
(null (queue-elements queue)))

(defun queue-front (queue)
(cadar queue))

(defun dequeue (queue)
(car (setf (car queue) (queue-elements queue))))

(defun enqueue (queue item)
(setf (cdr queue)
(setf (cddr queue) (list item))))
18 changes: 14 additions & 4 deletions src/scrobble.lisp
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
(in-package :cl-scrobbler)


;;;; Finally! The good stuff...

(defun valid-scrobble-p (track-length seconds-played)
"Last.fm defines a valid scrobble as a track that is over 30 seconds long
which has been played for over half its length OR 4 minutes."
Expand All @@ -8,9 +11,16 @@ which has been played for over half its length OR 4 minutes."
(> seconds-played 240))))

(defcall "track.updateNowPlaying" (track artist sk)
(:method :post :docs "Update the Now Playing status on last.fm. If the service
cannot be reached no caching or subsequent attempts are made."))
(:method :post :docs "Update the Now Playing status on last.fm."))

(defcall "track.scrobble" (track timestamp artist sk)
(:method :post :docs "Scrobble the track or put it in the cache if last.fm
could not be reached."))
(:method :post :docs "Scrobble the track!"))

(defun queue-scrobble (track artist timestamp &key now-playing-p)
"Add a song to the queue to be scrobbled. If there is a network failure, the
track will be stored for later scrobbling when a connection is reestablished."
(when now-playing-p
(update-now-playing track artist *session-key*))
(add-log-entry :path "scrobbler-plays.log" "[~d]> Played ~a by ~a"
timestamp track artist)
(add-to-cache 'scrobble (list track timestamp artist *session-key*)))

0 comments on commit 1b47785

Please sign in to comment.