add emms
[emacs.git] / .emacs.d / elisp / emms / lisp / emms-lastfm-scrobbler.el
1 ;;; emms-lastfm-scrobbler.el --- Last.FM Music API
2
3 ;; Copyright (C) 2009, 2010 Free Software Foundation, Inc.
4
5 ;; Authors: Bram van der Kroef <bram@fortfrances.com>, Yoni Rabkin
6 ;; <yonirabkin@member.fsf.org>
7
8 ;; Keywords: emms, lastfm
9
10 ;; EMMS is free software; you can redistribute it and/or modify it
11 ;; under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 3, or (at your option)
13 ;; any later version.
14 ;;
15 ;; EMMS is distributed in the hope that it will be useful, but WITHOUT
16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
18 ;; License for more details.
19 ;;
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with EMMS; see the file COPYING. If not, write to the Free
22 ;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23 ;; MA 02110-1301, USA.
24
25 ;;; Code:
26
27 ;;; ------------------------------------------------------------------
28 ;;; Submission API [http://www.last.fm/api/submissions]
29 ;;; ------------------------------------------------------------------
30
31 (require 'emms)
32 (require 'emms-playing-time)
33 (require 'emms-lastfm-client)
34
35 ;; Variables referenced from emms-lastfm-client:
36 ;; emms-lastfm-client-username, emms-lastfm-client-api-key,
37 ;; emms-lastfm-client-api-secret-key, emms-lastfm-client-api-session-key,
38 ;; emms-lastfm-client-track
39 ;; Functions referenced:
40 ;; emms-lastfm-client-xspf-get, emms-lastfm-client-xspf-extension,
41 ;; emms-lastfm-client-initialize-session
42
43 (defcustom emms-lastfm-scrobbler-submit-track-types '(file)
44 "Specify what types of tracks to submit to Last.fm.
45 The default is to only submit files.
46
47 To submit every track to Last.fm, set this to t."
48 :type '(choice (const :tag "All" t)
49 (set :tag "Types"
50 (const :tag "Files" file)
51 (const :tag "URLs" url)
52 (const :tag "Playlists" playlist)
53 (const :tag "Streamlists" streamlist)
54 (const :tag "Last.fm streams" lastfm-streaming)))
55 :group 'emms-lastfm)
56
57 (defvar emms-lastfm-scrobbler-submission-protocol-number "1.2.1"
58 "Version of the submissions protocol to which Emms conforms.")
59
60 (defvar emms-lastfm-scrobbler-published-version "1.0"
61 "Version of this package published to the Last.fm service.")
62
63 (defvar emms-lastfm-scrobbler-submission-session-id nil
64 "Scrobble session id, for now-playing and submission requests.")
65
66 (defvar emms-lastfm-scrobbler-submission-now-playing-url nil
67 "URL that should be used for a now-playing request.")
68
69 (defvar emms-lastfm-scrobbler-submission-url nil
70 "URL that should be used for submissions")
71
72 (defvar emms-lastfm-scrobbler-client-identifier "emm"
73 "Client identifier for Emms (Last.fm define this, not us).")
74
75 (defvar emms-lastfm-scrobbler-track-play-start-timestamp nil
76 "UTC timestamp.")
77
78 ;; 1.3 Authentication Token for Web Services Authentication: token =
79 ;; md5(shared_secret + timestamp)
80
81 (defun emms-lastfm-scrobbler-make-token-for-web-services (timestamp)
82 (when (not (and emms-lastfm-client-api-secret-key timestamp))
83 (error "secret and timestamp needed to make an auth token"))
84 (md5 (concat emms-lastfm-client-api-secret-key timestamp)))
85
86 ;; Handshake: The initial negotiation with the submissions server to
87 ;; establish authentication and connection details for the session.
88
89 (defun emms-lastfm-scrobbler-handshake ()
90 "Make handshake call."
91 (let* ((url-request-method "GET"))
92 (let ((response
93 (url-retrieve-synchronously
94 (emms-lastfm-scrobbler-make-handshake-call))))
95 (emms-lastfm-scrobbler-handle-handshake
96 (with-current-buffer response
97 (buffer-substring-no-properties
98 (point-min) (point-max)))))))
99
100 (defun emms-lastfm-scrobbler-make-handshake-call ()
101 "Return a submission protocol handshake string."
102 (when (not (and emms-lastfm-scrobbler-submission-protocol-number
103 emms-lastfm-scrobbler-client-identifier
104 emms-lastfm-scrobbler-published-version
105 emms-lastfm-client-username))
106 (error "missing variables to generate handshake call"))
107 (let ((timestamp (emms-lastfm-scrobbler-timestamp)))
108 (concat
109 "http://post.audioscrobbler.com/?hs=true"
110 "&p=" emms-lastfm-scrobbler-submission-protocol-number
111 "&c=" emms-lastfm-scrobbler-client-identifier
112 "&v=" emms-lastfm-scrobbler-published-version
113 "&u=" emms-lastfm-client-username
114 "&t=" timestamp
115 "&a=" (emms-lastfm-scrobbler-make-token-for-web-services timestamp)
116 "&api_key=" emms-lastfm-client-api-key
117 "&sk=" emms-lastfm-client-api-session-key)))
118
119 (defun emms-lastfm-scrobbler-handle-handshake (response)
120 (let ((ok200 "HTTP/1.1 200 OK"))
121 (when (not (string= ok200 (substring response 0 15)))
122 (error "server not responding correctly"))
123 (with-temp-buffer
124 (insert response)
125 (goto-char (point-min))
126 (re-search-forward "\n\n")
127 (let ((status (buffer-substring-no-properties
128 (point-at-bol) (point-at-eol))))
129 (cond ((string= status "OK")
130 (forward-line)
131 (setq emms-lastfm-scrobbler-submission-session-id
132 (buffer-substring-no-properties
133 (point-at-bol) (point-at-eol)))
134 (forward-line)
135 (setq emms-lastfm-scrobbler-submission-now-playing-url
136 (buffer-substring-no-properties
137 (point-at-bol) (point-at-eol)))
138 (forward-line)
139 (setq emms-lastfm-scrobbler-submission-url
140 (buffer-substring-no-properties
141 (point-at-bol) (point-at-eol))))
142 ((string= status "BANNED")
143 (error "this version of Emms has been BANNED"))
144 ((string= status "BADAUTH")
145 (error "bad authentication paramaters to handshake"))
146 ((string= status "BADTIME")
147 (error "handshake timestamp diverges too much"))
148 (t
149 (error "unhandled handshake failure")))))))
150
151 (defun emms-lastfm-scrobbler-assert-submission-handshake ()
152 (when (not (and emms-lastfm-scrobbler-submission-session-id
153 emms-lastfm-scrobbler-submission-now-playing-url
154 emms-lastfm-scrobbler-submission-url))
155 (error "cannot use submission API before handshake")))
156
157 (defun emms-lastfm-scrobbler-hexify-encode (str)
158 "UTF-8 encode and URL-hexify STR."
159 (url-hexify-string (encode-coding-string str 'utf-8)))
160
161 (defun emms-lastfm-scrobbler-timestamp ()
162 "Return a UNIX UTC timestamp."
163 (format-time-string "%s"))
164
165 (defun emms-lastfm-scrobbler-get-response-status ()
166 "Check the http header and return the body"
167 (let ((ok200 "HTTP/1.1 200 OK"))
168 (if (< (point-max) 1)
169 (error "No response from submission server"))
170 (if (not (string= ok200 (buffer-substring-no-properties (point-min) 16)))
171 (error "submission server not responding correctly"))
172 (goto-char (point-min))
173 (re-search-forward "\n\n")
174 (buffer-substring-no-properties
175 (point-at-bol) (point-at-eol))))
176
177 (defun emms-lastfm-scrobbler-submission-data (track rating)
178 "Format the url parameters containing the track artist, title, rating, time the
179 track was played, etc."
180 ;; (emms-lastfm-scrobbler-assert-submission-handshake)
181 (setq rating
182 (cond ((equal 'love rating) "L")
183 ((equal 'ban rating) "B")
184 ((equal 'skip rating) "S")
185 (t "")))
186 (let ((artist (emms-track-get track 'info-artist))
187 (title (emms-track-get track 'info-title))
188 (album (or (emms-track-get track 'info-album) ""))
189 (track-number (emms-track-get track 'info-tracknumber))
190 (musicbrainz-id "")
191 (track-length (number-to-string
192 (or (emms-track-get track
193 'info-playing-time)
194 0))))
195 (if (and artist title)
196 (concat
197 "s=" (emms-lastfm-scrobbler-hexify-encode
198 emms-lastfm-scrobbler-submission-session-id)
199 "&a[0]=" (emms-lastfm-scrobbler-hexify-encode artist)
200 "&t[0]=" (emms-lastfm-scrobbler-hexify-encode title)
201 "&i[0]=" (emms-lastfm-scrobbler-hexify-encode
202 emms-lastfm-scrobbler-track-play-start-timestamp)
203 "&o[0]=" (if (equal (emms-track-type track)
204 'lastfm-streaming)
205 (concat "L"
206 (emms-lastfm-scrobbler-hexify-encode
207 (emms-lastfm-client-xspf-get
208 'trackauth
209 (emms-lastfm-client-xspf-extension
210 emms-lastfm-client-track))))
211 "P")
212 "&r[0]=" (emms-lastfm-scrobbler-hexify-encode rating)
213 "&l[0]=" track-length
214 "&b[0]=" (emms-lastfm-scrobbler-hexify-encode album)
215 "&n[0]=" track-number
216 "&m[0]=" musicbrainz-id)
217 (error "Track title and artist must be known."))))
218
219 (defun emms-lastfm-scrobbler-nowplaying-data (track)
220 "Format the parameters for the Now playing submission."
221 ;; (emms-lastfm-scrobbler-assert-submission-handshake)
222 (let ((artist (emms-track-get track 'info-artist))
223 (title (emms-track-get track 'info-title))
224 (album (or (emms-track-get track 'info-album) ""))
225 (track-number (emms-track-get track
226 'info-tracknumber))
227 (musicbrainz-id "")
228 (track-length (number-to-string
229 (or (emms-track-get track
230 'info-playing-time)
231 0))))
232 (if (and artist title)
233 (concat
234 "s=" (emms-lastfm-scrobbler-hexify-encode
235 emms-lastfm-scrobbler-submission-session-id)
236 "&a=" (emms-lastfm-scrobbler-hexify-encode artist)
237 "&t=" (emms-lastfm-scrobbler-hexify-encode title)
238 "&b=" (emms-lastfm-scrobbler-hexify-encode album)
239 "&l=" track-length
240 "&n=" track-number
241 "&m=" musicbrainz-id)
242 (error "Track title and artist must be known."))))
243
244 (defun emms-lastfm-scrobbler-allowed-track-type (track)
245 "Check if the track-type is one of the allowed types"
246 (let ((track-type (emms-track-type track)))
247 (or (eq emms-lastfm-scrobbler-submit-track-types t)
248 (and (listp emms-lastfm-scrobbler-submit-track-types)
249 (memq track-type emms-lastfm-scrobbler-submit-track-types)))))
250
251 ;;; ------------------------------------------------------------------
252 ;;; EMMS hooks
253 ;;; ------------------------------------------------------------------
254
255 (defun emms-lastfm-scrobbler-start-hook ()
256 "Update the now playing info displayed on the user's last.fm page. This
257 doesn't affect the user's profile, so it con be done even for tracks that
258 should not be submitted."
259 ;; wait 5 seconds for the stop hook to submit the last track
260 (sit-for 5)
261 (let ((current-track (emms-playlist-current-selected-track)))
262 (setq emms-lastfm-scrobbler-track-play-start-timestamp
263 (emms-lastfm-scrobbler-timestamp))
264 (if (emms-lastfm-scrobbler-allowed-track-type current-track)
265 (emms-lastfm-scrobbler-make-async-nowplaying-call
266 current-track))))
267
268 (defun emms-lastfm-scrobbler-stop-hook ()
269 "Submit the track to last.fm if it has been played for 240
270 seconds or half the length of the track."
271 (let ((current-track (emms-playlist-current-selected-track)))
272 (let ((track-length (emms-track-get current-track 'info-playing-time)))
273 (when (and track-length
274 (emms-lastfm-scrobbler-allowed-track-type current-track))
275 (when (and
276 ;; track must be longer than 30 secs
277 (> track-length 30)
278 ;; track must be played for more than 240 secs or
279 ;; half the tracks length, whichever comes first.
280 (> emms-playing-time (min 240 (/ track-length 2))))
281 (emms-lastfm-scrobbler-make-async-submission-call
282 current-track nil))))))
283
284 (defun emms-lastfm-scrobbler-enable ()
285 "Enable the Last.fm scrobbler and submit the tracks EMMS plays
286 to last.fm"
287 (interactive)
288 (emms-lastfm-client-initialize-session)
289 (if (not emms-lastfm-scrobbler-submission-session-id)
290 (emms-lastfm-scrobbler-handshake))
291 (add-hook 'emms-player-started-hook
292 'emms-lastfm-scrobbler-start-hook t)
293 (add-hook 'emms-player-stopped-hook
294 'emms-lastfm-scrobbler-stop-hook)
295 (add-hook 'emms-player-finished-hook
296 'emms-lastfm-scrobbler-stop-hook))
297
298 (defun emms-lastfm-scrobbler-disable ()
299 "Stop submitting to last.fm"
300 (interactive)
301 (remove-hook 'emms-player-started-hook
302 'emms-lastfm-scrobbler-start-hook)
303 (remove-hook 'emms-player-stopped-hook
304 'emms-lastfm-scrobbler-stop-hook)
305 (remove-hook 'emms-player-finished-hook
306 'emms-lastfm-scrobbler-stop-hook))
307
308 ;;; ------------------------------------------------------------------
309 ;;; Asynchronous Submission
310 ;;; ------------------------------------------------------------------
311
312
313 (defun emms-lastfm-scrobbler-make-async-submission-call (track rating)
314 "Make asynchronous submission call."
315 (let ((flarb (emms-lastfm-scrobbler-submission-data track rating)))
316 (let* ((url-request-method "POST")
317 (url-request-data flarb)
318 (url-request-extra-headers
319 `(("Content-type" . "application/x-www-form-urlencoded"))))
320 (url-retrieve emms-lastfm-scrobbler-submission-url
321 #'emms-lastfm-scrobbler-async-submission-callback
322 (list (cons track rating))))))
323
324 (defun emms-lastfm-scrobbler-async-submission-callback (status &optional cbargs)
325 "Pass response of asynchronous submission call to handler."
326 (emms-lastfm-scrobbler-assert-submission-handshake)
327 (let ((response (emms-lastfm-scrobbler-get-response-status)))
328 ;; From the API docs: This indicates that the
329 ;; submission request was accepted for processing. It
330 ;; does not mean that the submission was valid, but
331 ;; only that the authentication and the form of the
332 ;; submission was validated.
333 (let ((track (car cbargs)))
334 (cond ((string= response "OK")
335 (message "Last.fm: Submitted %s"
336 (emms-track-get track 'info-title)))
337 ((string= response "BADSESSION")
338 (emms-lastfm-scrobbler-handshake)
339 (emms-lastfm-scrobbler-make-async-submission-call (car cbargs) (cdr cbargs)))
340 (t
341 (error "unhandled submission failure"))))))
342
343 (defun emms-lastfm-scrobbler-make-async-nowplaying-call (track)
344 "Make asynchronous now-playing submission call."
345 (emms-lastfm-scrobbler-assert-submission-handshake)
346 (let* ((url-request-method "POST")
347 (url-request-data
348 (emms-lastfm-scrobbler-nowplaying-data track))
349 (url-request-extra-headers
350 `(("Content-type" . "application/x-www-form-urlencoded"))))
351 (url-retrieve emms-lastfm-scrobbler-submission-now-playing-url
352 #'emms-lastfm-scrobbler-async-nowplaying-callback
353 (list (cons track nil)))))
354
355 (defun emms-lastfm-scrobbler-async-nowplaying-callback (status &optional cbargs)
356 "Pass response of asynchronous now-playing submission call to handler."
357 (let ((response (emms-lastfm-scrobbler-get-response-status)))
358 (cond ((string= response "OK") nil)
359 ((string= response "BADSESSION")
360 (emms-lastfm-scrobbler-handshake)
361 (emms-lastfm-scrobbler-make-async-nowplaying-call (car cbargs)))
362 (t
363 (error "unhandled submission failure")))))
364
365 (provide 'emms-lastfm-scrobbler)
366
367 ;;; emms-lastfm-scrobbler.el ends here.