Yoni Rabkin <[email protected]> writes:

> Go ahead and make all the changes you want and send in the revised
> patch. I'll test it for a few days and then push it to the Savannah
> repo. Please note that I don't scrobble local tracks at all so I won't
> be testing your code paths.
>
> If you can add a patch to the Last.fm section of the manual explaining
> how to use your features that would be very much appreciated. Emms is
> one of those free software project which maintains useful and relevant
> documentation (a hallowed subcategory, I know).
>
> If you want to do further work I would recommend getting commit
> access. No need to go through me.
>
> Thanks!

This patch has the changes and a new section in the manual.

>From ed380ba58e4aa07cd1299d59fc9bb590d0c8e191 Mon Sep 17 00:00:00 2001
From: Bram <[email protected]>
Date: Thu, 16 Sep 2010 13:31:10 -0500
Subject: [PATCH]  * emms.texinfo  Added documentation for the emms-lastfm-scrobbler.el

 * emms-lastfm-client.el  Changed api-key and username variables to customizable
   variables.

 * emms-lastfm-scrobbler.el  Hook for emms-player-started-hook is appended to the
   end so it doesn't cause the other hooks to be delayed 5 secs. Added a check to
   -enable that starts the handshake if the -submission-session-id is not set.
---
 doc/emms.texinfo              |   26 +++
 lisp/emms-lastfm-client.el    |  286 ++++++---------------------------
 lisp/emms-lastfm-scrobbler.el |  367 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 440 insertions(+), 239 deletions(-)
 create mode 100644 lisp/emms-lastfm-scrobbler.el

diff --git a/doc/emms.texinfo b/doc/emms.texinfo
index e09e139..7fa626c 100644
--- a/doc/emms.texinfo
+++ b/doc/emms.texinfo
@@ -2251,6 +2251,7 @@ Last.fm's paid subscribers''.
 @menu
 * Last.fm Setup::            Configuring Emms to use Last.fm.
 * Last.fm Radio::            Listening to music through Last.fm
+* Last.fm Audioscrobbler::   Submitting music to Last.fm
 @end menu
 
 @node Last.fm Setup
@@ -2341,6 +2342,31 @@ Library.
 @kbd{M-x emms-lastfm-client-play-user-neighborhood}: A Last.fm user's
 ``neighborhood''.
 
+...@node Last.fm Audioscrobbler
+...@section Last.fm Audioscrobbler
+
+Emms can submit the tracks you play to your last.fm profile. Assuming
+you have obtained a last.fm api key, as explained in the chapter
+...@xref{last.fm Setup}, all the audioscrobbler needs is your username in
+...@var{emms-lastfm-client-username}. You can enter it with @kbd{M-x
+customize-group RET emms-lastfm}.
+
+...@kbd{m-x emms-lastfm-scrobbler-enable} turns on audioscrobbling.
+
+To turn it off use @kbd{M-x emms-lastfm-scrobbler-disable}.
+
+To turn on Emms' audioscrobber in your .emacs file add:
+...@lisp
+(require 'emms-lastfm-client)
+
+(setq emms-lastfm-client-username "your-lastfm-username")
+(setq emms-lastfm-client-api-key "your-lastfm-api-key")
+(setq emms-lastfm-client-api-secret-key "your-lastfm-api-secret-key")
+
+(emms-lastfm-scrobbler-enable)
+...@end lisp
+
+
 @node Streaming Audio
 @chapter Streaming Audio
 
diff --git a/lisp/emms-lastfm-client.el b/lisp/emms-lastfm-client.el
index 7456bc9..9f66877 100644
--- a/lisp/emms-lastfm-client.el
+++ b/lisp/emms-lastfm-client.el
@@ -34,19 +34,32 @@
 (require 'emms)
 (require 'emms-source-file)
 (require 'xml)
+(require 'emms-lastfm-scrobbler)
 
-(defvar emms-lastfm-client-username nil
-  "Valid Last.fm account username.")
+(defcustom emms-lastfm-client-username nil
+  "Valid Last.fm account username."
+  :group 'emms-lastfm
+  :type 'string)
 
-(defvar emms-lastfm-client-api-key nil
-  "Key for the Last.fm API.")
+(defcustom emms-lastfm-client-api-key nil
+  "Key for the Last.fm API."
+  :group 'emms-lastfm
+  :type 'string)
 
-(defvar emms-lastfm-client-api-secret-key nil
-  "Secret key for the Last.fm API.")
+(defcustom emms-lastfm-client-api-secret-key nil
+  "Secret key for the Last.fm API."
+  :group 'emms-lastfm
+  :type 'string)
 
 (defvar emms-lastfm-client-api-session-key nil
   "Session key for the Last.fm API.")
 
+(defvar emms-lastfm-client-track nil
+  "Latest Last.fm track.")
+
+(defvar emms-lastfm-client-submission-api t
+  "Use the Last.fm submission API if true, otherwise don't.")
+
 (defvar emms-lastfm-client-token nil
   "Authorization token for API.")
 
@@ -80,30 +93,6 @@
 (defvar emms-lastfm-client-playlist-buffer nil
   "Non-interactive Emms Last.fm buffer.")
 
-(defvar emms-lastfm-client-client-identifier "emm"
-  "Client identifier for Emms (Last.fm define this, not us).")
-
-(defvar emms-lastfm-client-submission-protocol-number "1.2.1"
-  "Version of the submissions protocol to which Emms conforms.")
-
-(defvar emms-lastfm-client-published-version "1.0"
-  "Version of this package published to the Last.fm service.")
-
-(defvar emms-lastfm-client-submission-session-id nil
-  "Scrobble session id, for now-playing and submission requests.")
-
-(defvar emms-lastfm-client-submission-now-playing-url nil
-  "URL that should be used for a now-playing request.")
-
-(defvar emms-lastfm-client-submission-url nil
-  "URL that should be used for submissions")
-
-(defvar emms-lastfm-client-track-play-start-timestamp nil
-  "UTC timestamp.")
-
-(defvar emms-lastfm-client-submission-api t
-  "Use the Last.fm submission API if true, otherwise don't.")
-
 (defvar emms-lastfm-client-inhibit-cleanup nil
   "If true, do not perform clean-up after `emms-stop'.")
 
@@ -467,9 +456,6 @@ This function includes the cryptographic signature."
 	 emms-lastfm-client-playlist-buffer-name))
   (setq emms-playlist-buffer emms-lastfm-client-playlist-buffer))
 
-(defun emms-lastfm-client-timestamp ()
-  "Return a UNIX UTC timestamp."
-  (format-time-string "%s" (current-time) t))
 
 (defun emms-lastfm-client-load-next-track ()
   "Queue the next track from Last.fm."
@@ -481,8 +467,8 @@ This function includes the cryptographic signature."
     (if emms-lastfm-client-playlist
 	(let ((track (emms-lastfm-client-consume-next-track)))
 	  (setq emms-lastfm-client-track track)
-	  (setq emms-lastfm-client-track-play-start-timestamp
-		(emms-lastfm-client-timestamp))
+	  (setq emms-lastfm-scrobbler-track-play-start-timestamp
+		(emms-lastfm-scrobbler-timestamp))
 	  (let ((emms-lastfm-client-inhibit-cleanup t))
 	    (emms-play-url
 	     (emms-lastfm-client-xspf-get 'location track))))
@@ -492,28 +478,26 @@ This function includes the cryptographic signature."
 (defun emms-lastfm-client-love-track ()
   "Submit the currently playing track with a `love' rating."
   (interactive)
-  (if emms-lastfm-client-track
-      (let ((result (emms-lastfm-client-make-async-submission-call
-		     emms-lastfm-client-track 'love)))
-	;; the following submission API call looks redundant but
-	;; isn't; indeed, it might be done away with in a future
-	;; version of the Last.fm API (see API docs)
-	(emms-lastfm-client-make-call-track-love)
-	(when (equal result 'track-successfully-submitted)
-	  (message "track sucessfully submitted with a `love' rating")))
-    (error "no current track")))
+  (when emms-lastfm-client-track
+    (emms-lastfm-scrobbler-make-async-submission-call
+     (emms-lastfm-client-convert-track
+      emms-lastfm-client-track) 'love)
+    ;; the following submission API call looks redundant but
+    ;; isn't; indeed, it might be done away with in a future
+    ;; version of the Last.fm API (see API docs)
+    (emms-lastfm-client-make-call-track-love)))
 
 (defun emms-lastfm-client-ban-track ()
   "Submit currently playing track with a `ban' rating and skip."
   (interactive)
-  (if emms-lastfm-client-track
-      (let ((result (emms-lastfm-client-make-async-submission-call
-		     emms-lastfm-client-track 'ban)))
-	(emms-lastfm-client-make-call-track-ban)
-	(when (equal result 'track-successfully-submitted)
-	  (message "track sucessfully submitted with a `ban' rating"))
-	(emms-lastfm-client-load-next-track))
-    (error "no current track")))
+  (when emms-lastfm-client-track
+    (emms-lastfm-scrobbler-make-async-submission-call
+     (emms-lastfm-client-convert-track
+      emms-lastfm-client-track) 'ban)
+    ;; the following submission API call looks redundant but
+    ;; isn't; see `...-love-track'
+    (emms-lastfm-client-make-call-track-ban)
+    (emms-lastfm-client-load-next-track)))
 
 ;; call this `-track-advance' to avoid confusion with Emms'
 ;; `-next-track-' mechanism
@@ -524,10 +508,9 @@ This function includes the cryptographic signature."
 	       emms-lastfm-client-playlist-buffer)
     (when (and emms-lastfm-client-submission-api
 	       (not first))
-      (let ((result (emms-lastfm-client-make-async-submission-call
-		     emms-lastfm-client-track nil)))
-	(when (equal result 'track-successfully-submitted)
-	  (message "track sucessfully submitted"))))
+      (let ((result (emms-lastfm-scrobbler-make-async-submission-call
+		     (emms-lastfm-client-convert-track
+		      emms-lastfm-client-track) nil)))))
     (emms-lastfm-client-load-next-track)))
 
 (defun emms-lastfm-client-next-function ()
@@ -589,7 +572,7 @@ This function includes the cryptographic signature."
   (emms-lastfm-client-make-call-radio-tune
    (format url username))
   (emms-lastfm-client-make-call-radio-getplaylist)
-  (emms-lastfm-client-handshake)
+  (emms-lastfm-scrobbler-handshake)
   (emms-lastfm-client-play-playlist))
 
 (defun emms-lastfm-client-play-similar-artists (artist)
@@ -601,7 +584,7 @@ This function includes the cryptographic signature."
   (emms-lastfm-client-make-call-radio-tune
    (format "lastfm://artist/%s/similarartists" artist))
   (emms-lastfm-client-make-call-radio-getplaylist)
-  (emms-lastfm-client-handshake)
+  (emms-lastfm-scrobbler-handshake)
   (emms-lastfm-client-play-playlist))
 
 (defun emms-lastfm-client-play-loved ()
@@ -659,10 +642,11 @@ This function includes the cryptographic signature."
     (emms-track-set emms-track 'info-album
 		    (emms-lastfm-client-xspf-get 'album track))
     (emms-track-set emms-track 'info-playing-time
-		    (/
-		     (parse-integer
-		      (emms-lastfm-client-xspf-get 'duration track))
-		     1000))
+		    (/ (parse-integer
+			(emms-lastfm-client-xspf-get 'duration
+						     track))
+		       1000))
+    (emms-track-set emms-track 'type 'lastfm-streaming)
     emms-track))
 
 (defun emms-lastfm-client-show-track (track)
@@ -961,182 +945,6 @@ This function includes the cryptographic signature."
   "Function called with DATA after `ban' rating succeeds."
   'track-ban-succeed)
 
-;;; ------------------------------------------------------------------
-;;; Submission API [http://www.last.fm/api/submissions]
-;;; ------------------------------------------------------------------
-
-;; 1.3 Authentication Token for Web Services Authentication: token =
-;; md5(shared_secret + timestamp)
-
-(defun emms-lastfm-client-make-token-for-web-services (timestamp)
-  (when (not (and emms-lastfm-client-api-secret-key timestamp))
-    (error "secret and timestamp needed to make an auth token"))
-  (md5 (concat emms-lastfm-client-api-secret-key timestamp)))
-
-;; Handshake: The initial negotiation with the submissions server to
-;; establish authentication and connection details for the session.
-
-(defun emms-lastfm-client-make-handshake-call ()
-  "Return a submission protocol handshake string."
-  (when (not (and emms-lastfm-client-submission-protocol-number
-		  emms-lastfm-client-client-identifier
-		  emms-lastfm-client-published-version
-		  emms-lastfm-client-username))
-    (error "missing variables to generate handshake call"))
-  (let ((timestamp (format-time-string "%s")))
-    (concat
-     "http://post.audioscrobbler.com/?hs=true";
-     "&p=" emms-lastfm-client-submission-protocol-number
-     "&c=" emms-lastfm-client-client-identifier
-     "&v=" emms-lastfm-client-published-version
-     "&u=" emms-lastfm-client-username
-     "&t=" timestamp
-     "&a=" (emms-lastfm-client-make-token-for-web-services timestamp)
-     "&api_key=" emms-lastfm-client-api-key
-     "&sk=" emms-lastfm-client-api-session-key)))
-
-(defun emms-lastfm-client-handshake ()
-  "Make handshake call."
-  (if emms-lastfm-client-playlist-valid
-      (let* ((url-request-method "GET"))
-	(let ((response
-	       (url-retrieve-synchronously
-		(emms-lastfm-client-make-handshake-call))))
-	  (emms-lastfm-client-handle-handshake
-	   (with-current-buffer response
-	     (buffer-substring-no-properties
-	      (point-min) (point-max))))))
-    (error "cannot handshake without initializing the client")))
-
-(defun emms-lastfm-client-handle-handshake (response)
-  (let ((ok200 "HTTP/1.1 200 OK"))
-    (when (not (string= ok200 (substring response 0 15)))
-      (error "server not responding correctly"))
-    (with-temp-buffer
-      (insert response)
-      (goto-char (point-min))
-      (re-search-forward "\n\n")
-      (let ((status (buffer-substring-no-properties
-		     (point-at-bol) (point-at-eol))))
-	(cond ((string= status "OK")
-	       (forward-line)
-	       (setq emms-lastfm-client-submission-session-id
-		     (buffer-substring-no-properties
-		      (point-at-bol) (point-at-eol)))
-	       (forward-line)
-	       (setq emms-lastfm-client-submission-now-playing-url
-		     (buffer-substring-no-properties
-		      (point-at-bol) (point-at-eol)))
-	       (forward-line)
-	       (setq emms-lastfm-client-submission-url
-		     (buffer-substring-no-properties
-		      (point-at-bol) (point-at-eol))))
-	      ((string= status "BANNED")
-	       (error "this version of Emms has been BANNED"))
-	      ((string= status "BADAUTH")
-	       (error "bad authentication paramaters to handshake"))
-	      ((string= status "BADTIME")
-	       (error "handshake timestamp diverges too much"))
-	      (t
-	       (error "unhandled handshake failure")))))))
-
-(defun emms-lastfm-client-assert-submission-handshake ()
-  (when (not (and emms-lastfm-client-submission-session-id
-		  emms-lastfm-client-submission-now-playing-url
-		  emms-lastfm-client-submission-url))
-    (error "cannot use submission API before handshake")))
-
-(defun emms-lastfm-client-hexify-encode (str)
-  "UTF-8 encode and URL-hexify STR."
-  (url-hexify-string (encode-coding-string str 'utf-8)))
-
-(defun emms-lastfm-client-submission-data (track rating)
-  (emms-lastfm-client-assert-submission-handshake)
-  (setq rating
-	(cond ((equal 'love rating) "L")
-	      ((equal 'ban rating) "B")
-	      ((equal 'skip rating) "S")
-	      (t "")))
-  (concat
-   "s=" (emms-lastfm-client-hexify-encode
-	 emms-lastfm-client-submission-session-id)
-   "&a[0]=" (emms-lastfm-client-hexify-encode
-	     (emms-lastfm-client-xspf-get 'creator track))
-   "&t[0]=" (emms-lastfm-client-hexify-encode
-	     (emms-lastfm-client-xspf-get 'title track))
-   ;; warning: won't extend to submitting multiple tracks
-   "&i[0]=" (emms-lastfm-client-hexify-encode
-	     emms-lastfm-client-track-play-start-timestamp)
-   "&o[0]=L" (emms-lastfm-client-hexify-encode
-	      (emms-lastfm-client-xspf-get
-	       'trackauth
-	       (emms-lastfm-client-xspf-extension track)))
-   "&r[0]=" (emms-lastfm-client-hexify-encode rating)
-   "&l[0]=" "" ; empty string to be explicit
-   "&b[0]=" "" ; empty string to be explicit
-   "&n[0]=" "" ; empty string to be explicit
-   "&m[0]=" "" ; empty string to be explicit
-   ))
-
-(defun emms-lastfm-client-handle-submission-response (response track rating)
-  (let ((ok200 "HTTP/1.1 200 OK"))
-    (when (not (string= ok200 (substring response 0 15)))
-      (error "submission server not responding correctly"))
-    (with-temp-buffer
-      (insert response)
-      (goto-char (point-min))
-      (re-search-forward "\n\n")
-      (let ((status (buffer-substring-no-properties
-		     (point-at-bol) (point-at-eol))))
-	(cond ((string= status "OK")
-	       ;; From the API docs: This indicates that the
-	       ;; submission request was accepted for processing. It
-	       ;; does not mean that the submission was valid, but
-	       ;; only that the authentication and the form of the
-	       ;; submission was validated.
-	       (message "successfully submitted %s"
-			(emms-lastfm-client-xspf-get 'title track)))
-	      ((string= status "BADSESSION")
-	       (emms-lastfm-client-handshake)
-	       (emms-lastfm-client-make-async-submission-call track rating))
-	      (t
-	       (error "unhandled submission failure")))))))
-
-(defun emms-lastfm-client-submit ()
-  "Submit the current track as having been played."
-  (if emms-lastfm-client-track
-      (emms-lastfm-client-make-async-submission-call
-       emms-lastfm-client-track nil)
-    (error "no current track")))
-
-;;; ------------------------------------------------------------------
-;;; Asynchronous Submission
-;;; ------------------------------------------------------------------
-
-(defun emms-lastfm-client-async-submission-callback (status &optional cbargs)
-  "Pass response of asynchronous submission call to handler."
-  (let ((response (copy-sequence
-		   (buffer-substring-no-properties
-		    (point-min) (point-max)))))
-    (emms-lastfm-client-handle-submission-response
-     response
-     (car cbargs) ; track
-     (cdr cbargs) ; rating
-     )))
-
-(defun emms-lastfm-client-make-async-submission-call (track rating)
-  "Make asynchronous submission call."
-  (if emms-lastfm-client-playlist-valid
-      (let* ((url-request-method "POST")
-	     (url-request-data
-	      (emms-lastfm-client-submission-data track rating))
-	     (url-request-extra-headers
-	      `(("Content-type" . "application/x-www-form-urlencoded"))))
-	(url-retrieve emms-lastfm-client-submission-url
-		      #'emms-lastfm-client-async-submission-callback
-		      (list (cons track rating))))
-    (error "cannot make submission call without initializing the client")))
-
 (provide 'emms-lastfm-client)
 
 ;;; emms-lastfm-client.el ends here
diff --git a/lisp/emms-lastfm-scrobbler.el b/lisp/emms-lastfm-scrobbler.el
new file mode 100644
index 0000000..21c253e
--- /dev/null
+++ b/lisp/emms-lastfm-scrobbler.el
@@ -0,0 +1,367 @@
+;;; emms-lastfm-scrobbler.el --- Last.FM Music API
+
+;; Copyright (C) 2009, 2010  Free Software Foundation, Inc.
+
+;; Authors: Bram van der Kroef <[email protected]>, Yoni Rabkin
+;; <[email protected]>
+
+;; Keywords: emms, lastfm
+
+;; EMMS is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+;;
+;; EMMS is distributed in the hope that it will be useful, but WITHOUT
+;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
+;; License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING.  If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Code:
+
+;;; ------------------------------------------------------------------
+;;; Submission API [http://www.last.fm/api/submissions]
+;;; ------------------------------------------------------------------
+
+(require 'emms)
+(require 'emms-playing-time)
+
+;; Variables referenced from emms-lastfm-client:
+;;  emms-lastfm-client-username, emms-lastfm-client-api-key,
+;;  emms-lastfm-client-api-secret-key, emms-lastfm-client-api-session-key,
+;;   emms-lastfm-client-track
+;; Functions referenced:
+;;  emms-lastfm-client-xspf-get, emms-lastfm-client-xspf-extension,
+;;  emms-lastfm-client-initialize-session
+
+(defcustom emms-lastfm-scrobbler-submit-track-types '(file)
+  "Specify what types of tracks to submit to Last.fm.
+The default is to only submit files.
+
+To submit every track to Last.fm, set this to t."
+  :type '(choice (const :tag "All" t)
+                 (set :tag "Types"
+                      (const :tag "Files" file)
+                      (const :tag "URLs" url)
+                      (const :tag "Playlists" playlist)
+                      (const :tag "Streamlists" streamlist)
+                      (const :tag "Last.fm streams" lastfm-streaming)))
+  :group 'emms-lastfm)
+
+(defvar emms-lastfm-scrobbler-submission-protocol-number "1.2.1"
+  "Version of the submissions protocol to which Emms conforms.")
+
+(defvar emms-lastfm-scrobbler-published-version "1.0"
+  "Version of this package published to the Last.fm service.")
+
+(defvar emms-lastfm-scrobbler-submission-session-id nil
+  "Scrobble session id, for now-playing and submission requests.")
+
+(defvar emms-lastfm-scrobbler-submission-now-playing-url nil
+  "URL that should be used for a now-playing request.")
+
+(defvar emms-lastfm-scrobbler-submission-url nil
+  "URL that should be used for submissions")
+
+(defvar emms-lastfm-scrobbler-client-identifier "emm"
+  "Client identifier for Emms (Last.fm define this, not us).")
+
+(defvar emms-lastfm-scrobbler-track-play-start-timestamp nil
+  "UTC timestamp.")
+
+;; 1.3 Authentication Token for Web Services Authentication: token =
+;; md5(shared_secret + timestamp)
+
+(defun emms-lastfm-scrobbler-make-token-for-web-services (timestamp)
+  (when (not (and emms-lastfm-client-api-secret-key timestamp))
+    (error "secret and timestamp needed to make an auth token"))
+  (md5 (concat emms-lastfm-client-api-secret-key timestamp)))
+
+;; Handshake: The initial negotiation with the submissions server to
+;; establish authentication and connection details for the session.
+
+(defun emms-lastfm-scrobbler-handshake ()
+  "Make handshake call."
+  (let* ((url-request-method "GET"))
+    (let ((response
+           (url-retrieve-synchronously
+            (emms-lastfm-scrobbler-make-handshake-call))))
+      (emms-lastfm-scrobbler-handle-handshake
+       (with-current-buffer response
+         (buffer-substring-no-properties
+          (point-min) (point-max)))))))
+
+(defun emms-lastfm-scrobbler-make-handshake-call ()
+  "Return a submission protocol handshake string."
+  (when (not (and emms-lastfm-scrobbler-submission-protocol-number
+		  emms-lastfm-scrobbler-client-identifier
+		  emms-lastfm-scrobbler-published-version
+		  emms-lastfm-client-username))
+    (error "missing variables to generate handshake call"))
+  (let ((timestamp (emms-lastfm-scrobbler-timestamp)))
+    (concat
+     "http://post.audioscrobbler.com/?hs=true";
+     "&p=" emms-lastfm-scrobbler-submission-protocol-number
+     "&c=" emms-lastfm-scrobbler-client-identifier
+     "&v=" emms-lastfm-scrobbler-published-version
+     "&u=" emms-lastfm-client-username
+     "&t=" timestamp
+     "&a=" (emms-lastfm-scrobbler-make-token-for-web-services timestamp)
+     "&api_key=" emms-lastfm-client-api-key
+     "&sk=" emms-lastfm-client-api-session-key)))
+
+(defun emms-lastfm-scrobbler-handle-handshake (response)
+  (let ((ok200 "HTTP/1.1 200 OK"))
+    (when (not (string= ok200 (substring response 0 15)))
+      (error "server not responding correctly"))
+    (with-temp-buffer
+      (insert response)
+      (goto-char (point-min))
+      (re-search-forward "\n\n")
+      (let ((status (buffer-substring-no-properties
+		     (point-at-bol) (point-at-eol))))
+	(cond ((string= status "OK")
+	       (forward-line)
+	       (setq emms-lastfm-scrobbler-submission-session-id
+		     (buffer-substring-no-properties
+		      (point-at-bol) (point-at-eol)))
+	       (forward-line)
+	       (setq emms-lastfm-scrobbler-submission-now-playing-url
+		     (buffer-substring-no-properties
+		      (point-at-bol) (point-at-eol)))
+	       (forward-line)
+	       (setq emms-lastfm-scrobbler-submission-url
+		     (buffer-substring-no-properties
+		      (point-at-bol) (point-at-eol))))
+	      ((string= status "BANNED")
+	       (error "this version of Emms has been BANNED"))
+	      ((string= status "BADAUTH")
+	       (error "bad authentication paramaters to handshake"))
+	      ((string= status "BADTIME")
+	       (error "handshake timestamp diverges too much"))
+	      (t
+	       (error "unhandled handshake failure")))))))
+
+(defun emms-lastfm-scrobbler-assert-submission-handshake ()
+  (when (not (and emms-lastfm-scrobbler-submission-session-id
+		  emms-lastfm-scrobbler-submission-now-playing-url
+		  emms-lastfm-scrobbler-submission-url))
+    (error "cannot use submission API before handshake")))
+
+(defun emms-lastfm-scrobbler-hexify-encode (str)
+  "UTF-8 encode and URL-hexify STR."
+  (url-hexify-string (encode-coding-string str 'utf-8)))
+
+(defun emms-lastfm-scrobbler-timestamp ()
+  "Return a UNIX UTC timestamp."
+  (format-time-string "%s"))
+
+(defun emms-lastfm-scrobbler-get-response-status ()
+  "Check the http header and return the body"
+  (let ((ok200 "HTTP/1.1 200 OK"))
+    (if (< (point-max) 1)
+        (error "No response from submission server"))
+    (if (not (string= ok200 (buffer-substring-no-properties (point-min) 16)))
+        (error "submission server not responding correctly"))
+    (goto-char (point-min))
+    (re-search-forward "\n\n")
+    (buffer-substring-no-properties
+     (point-at-bol) (point-at-eol))))
+
+(defun emms-lastfm-scrobbler-submission-data (track rating)
+  "Format the url parameters containing the track artist, title, rating, time the
+  track was played, etc."
+;;  (emms-lastfm-scrobbler-assert-submission-handshake)
+  (setq rating
+	(cond ((equal 'love rating) "L")
+	      ((equal 'ban rating)  "B")
+	      ((equal 'skip rating) "S")
+	      (t "")))
+  (let ((artist (emms-track-get track 'info-artist))
+        (title  (emms-track-get track 'info-title))
+        (album  (or (emms-track-get track 'info-album) ""))
+        (track-number (emms-track-get track 'info-tracknumber))
+        (musicbrainz-id "")
+        (track-length (number-to-string
+                       (or (emms-track-get track
+                                           'info-playing-time)
+                           0))))
+    (if (and artist title)
+        (concat
+         "s=" (emms-lastfm-scrobbler-hexify-encode
+               emms-lastfm-scrobbler-submission-session-id)
+         "&a[0]=" (emms-lastfm-scrobbler-hexify-encode artist)
+         "&t[0]=" (emms-lastfm-scrobbler-hexify-encode title)
+         "&i[0]=" (emms-lastfm-scrobbler-hexify-encode
+                   emms-lastfm-scrobbler-track-play-start-timestamp)
+         "&o[0]=" (if (equal (emms-track-type track) 
+			     'lastfm-streaming)
+		      (concat "L"
+			      (emms-lastfm-scrobbler-hexify-encode
+			       (emms-lastfm-client-xspf-get
+				'trackauth
+				(emms-lastfm-client-xspf-extension
+				 emms-lastfm-client-track))))
+                    "P")
+         "&r[0]=" (emms-lastfm-scrobbler-hexify-encode rating)
+         "&l[0]=" track-length
+         "&b[0]=" (emms-lastfm-scrobbler-hexify-encode album)
+         "&n[0]=" track-number
+         "&m[0]=" musicbrainz-id)
+      (error "Track title and artist must be known."))))
+
+(defun emms-lastfm-scrobbler-nowplaying-data (track)
+  "Format the parameters for the Now playing submission."
+;;  (emms-lastfm-scrobbler-assert-submission-handshake)
+  (let ((artist (emms-track-get track 'info-artist))
+        (title  (emms-track-get track 'info-title))
+        (album  (or (emms-track-get track 'info-album) ""))
+        (track-number (emms-track-get track
+                                      'info-tracknumber))
+        (musicbrainz-id "")
+        (track-length (number-to-string
+                       (or (emms-track-get track
+                                           'info-playing-time)
+                           0))))
+    (if (and artist title)
+        (concat
+         "s=" (emms-lastfm-scrobbler-hexify-encode
+               emms-lastfm-scrobbler-submission-session-id)
+         "&a=" (emms-lastfm-scrobbler-hexify-encode artist)
+         "&t=" (emms-lastfm-scrobbler-hexify-encode title)
+         "&b=" (emms-lastfm-scrobbler-hexify-encode album)
+         "&l=" track-length
+         "&n=" track-number
+         "&m=" musicbrainz-id)
+      (error "Track title and artist must be known."))))
+
+(defun emms-lastfm-scrobbler-allowed-track-type (track)
+  "Check if the track-type is one of the allowed types"
+  (let ((track-type (emms-track-type track)))
+    (or (eq emms-lastfm-scrobbler-submit-track-types t)
+        (and (listp emms-lastfm-scrobbler-submit-track-types)
+             (memq track-type emms-lastfm-scrobbler-submit-track-types)))))
+
+;;; ------------------------------------------------------------------
+;;; EMMS hooks
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-scrobbler-start-hook ()
+  "Update the now playing info displayed on the user's last.fm page.  This
+  doesn't affect the user's profile, so it con be done even for tracks that
+  should not be submitted."
+  ;; wait 5 seconds for the stop hook to submit the last track
+  (sit-for 5)
+  (let ((current-track (emms-playlist-current-selected-track)))
+    (setq emms-lastfm-scrobbler-track-play-start-timestamp
+          (emms-lastfm-scrobbler-timestamp))
+    (if (emms-lastfm-scrobbler-allowed-track-type current-track)
+        (emms-lastfm-scrobbler-make-async-nowplaying-call
+         current-track))))
+
+(defun emms-lastfm-scrobbler-stop-hook ()
+  "Submit the track to last.fm if it has been played for 240
+seconds or half the length of the track."
+  (let ((current-track (emms-playlist-current-selected-track)))
+    (let ((track-length (emms-track-get current-track 'info-playing-time)))
+      (when (and track-length
+		 (emms-lastfm-scrobbler-allowed-track-type current-track))
+	(when (and
+	       ;; track must be longer than 30 secs
+	       (> track-length 30)
+	       ;; track must be played for more than 240 secs or
+	       ;;   half the tracks length, whichever comes first.
+	       (> emms-playing-time (min 240 (/ track-length 2))))
+	  (emms-lastfm-scrobbler-make-async-submission-call
+	   current-track nil))))))
+
+(defun emms-lastfm-scrobbler-enable ()
+  "Enable the Last.fm scrobbler and submit the tracks EMMS plays
+to last.fm"
+  (interactive)
+  (emms-lastfm-client-initialize-session)
+  (if (not emms-lastfm-scrobbler-submission-session-id)
+      (emms-lastfm-scrobbler-handshake))
+  (add-hook 'emms-player-started-hook
+	    'emms-lastfm-scrobbler-start-hook t)
+  (add-hook 'emms-player-stopped-hook
+	    'emms-lastfm-scrobbler-stop-hook)
+  (add-hook 'emms-player-finished-hook
+	    'emms-lastfm-scrobbler-stop-hook))
+
+(defun emms-lastfm-scrobbler-disable ()
+  "Stop submitting to last.fm"
+  (interactive)
+  (remove-hook 'emms-player-started-hook
+	       'emms-lastfm-scrobbler-start-hook)
+  (remove-hook 'emms-player-stopped-hook
+	       'emms-lastfm-scrobbler-stop-hook)
+  (remove-hook 'emms-player-finished-hook
+	       'emms-lastfm-scrobbler-stop-hook))
+
+;;; ------------------------------------------------------------------
+;;; Asynchronous Submission
+;;; ------------------------------------------------------------------
+
+
+(defun emms-lastfm-scrobbler-make-async-submission-call (track rating)
+  "Make asynchronous submission call."
+  (let ((flarb (emms-lastfm-scrobbler-submission-data track rating)))
+    (setq flooz flarb)
+    (let* ((url-request-method "POST")
+	   (url-request-data flarb)
+	   (url-request-extra-headers
+	    `(("Content-type" . "application/x-www-form-urlencoded"))))
+      (url-retrieve emms-lastfm-scrobbler-submission-url
+		    #'emms-lastfm-scrobbler-async-submission-callback
+		    (list (cons track rating))))))
+
+(defun emms-lastfm-scrobbler-async-submission-callback (status &optional cbargs)
+  "Pass response of asynchronous submission call to handler."
+  (emms-lastfm-scrobbler-assert-submission-handshake)
+  (let ((response (emms-lastfm-scrobbler-get-response-status)))
+    ;; From the API docs: This indicates that the
+    ;; submission request was accepted for processing. It
+    ;; does not mean that the submission was valid, but
+    ;; only that the authentication and the form of the
+    ;; submission was validated.
+    (let ((track (car cbargs)))
+      (cond ((string= response "OK")
+	     (message "Last.fm: Submitted %s"
+		      (emms-track-get track 'info-title)))
+	    ((string= response "BADSESSION")
+	     (emms-lastfm-scrobbler-handshake)
+	     (emms-lastfm-scrobbler-make-async-submission-call (car cbargs) (cdr cbargs)))
+	    (t
+	     (error "unhandled submission failure"))))))
+
+(defun emms-lastfm-scrobbler-make-async-nowplaying-call (track)
+  "Make asynchronous now-playing submission call."
+  (emms-lastfm-scrobbler-assert-submission-handshake)
+  (let* ((url-request-method "POST")
+	 (url-request-data
+	  (emms-lastfm-scrobbler-nowplaying-data track))
+	 (url-request-extra-headers
+	  `(("Content-type" . "application/x-www-form-urlencoded"))))
+    (url-retrieve emms-lastfm-scrobbler-submission-now-playing-url
+		  #'emms-lastfm-scrobbler-async-nowplaying-callback
+		  (list (cons track nil)))))
+
+(defun emms-lastfm-scrobbler-async-nowplaying-callback (status &optional cbargs)
+  "Pass response of asynchronous now-playing submission call to handler."
+  (let ((response (emms-lastfm-scrobbler-get-response-status)))
+    (cond ((string= response "OK") nil)
+	  ((string= response "BADSESSION")
+	   (emms-lastfm-scrobbler-handshake)
+	   (emms-lastfm-scrobbler-make-async-nowplaying-call (car cbargs)))
+	  (t
+	   (error "unhandled submission failure")))))
+
+(provide 'emms-lastfm-scrobbler)
+
+;;; emms-lastfm-scrobbler.el ends here.
-- 
1.7.0

_______________________________________________
Emms-patches mailing list
[email protected]
http://lists.gnu.org/mailman/listinfo/emms-patches

Reply via email to