aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYoni Rabkin <yoni@rabkins.net>2010-09-16 21:43:49 -0400
committerYoni Rabkin <yoni@rabkins.net>2010-09-16 21:43:49 -0400
commitde3c04a2ee4916f171ee32a91fa302a0eaed427d (patch)
tree4a7baf3b6d9875ee2d2a2858ab9821d066ced31e
parent8272fecd7becc4bc9d557282842e722a4e950940 (diff)
Add scrobbling of local tracks to Last.fm support.
From: Bram van der Kroef <bram@fortfrances.com>
-rw-r--r--doc/emms.texinfo27
-rw-r--r--lisp/emms-lastfm-client.el286
-rw-r--r--lisp/emms-lastfm-scrobbler.el367
3 files changed, 441 insertions, 239 deletions
diff --git a/doc/emms.texinfo b/doc/emms.texinfo
index e09e139..8f4f459 100644
--- a/doc/emms.texinfo
+++ b/doc/emms.texinfo
@@ -118,6 +118,7 @@ Track Information
Last.fm
* 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
Extending Emms
* New Player:: How to define a new player.
@@ -2251,6 +2252,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 +2343,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..a2171b8
--- /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 <bram@fortfrances.com>, Yoni Rabkin
+;; <yonirabkin@member.fsf.org>
+
+;; 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.