diff options
author | Yoni Rabkin <yoni@rabkins.net> | 2010-09-16 21:43:49 -0400 |
---|---|---|
committer | Yoni Rabkin <yoni@rabkins.net> | 2010-09-16 21:43:49 -0400 |
commit | de3c04a2ee4916f171ee32a91fa302a0eaed427d (patch) | |
tree | 4a7baf3b6d9875ee2d2a2858ab9821d066ced31e | |
parent | 8272fecd7becc4bc9d557282842e722a4e950940 (diff) |
Add scrobbling of local tracks to Last.fm support.
From: Bram van der Kroef <bram@fortfrances.com>
-rw-r--r-- | doc/emms.texinfo | 27 | ||||
-rw-r--r-- | lisp/emms-lastfm-client.el | 286 | ||||
-rw-r--r-- | lisp/emms-lastfm-scrobbler.el | 367 |
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. |