add emms
[emacs.git] / .emacs.d / elisp / emms / lisp / emms-lastfm-client.el
1 ;;; emms-lastfm-client.el --- Last.FM Music API
2
3 ;; Copyright (C) 2009, 2010, 2011 Free Software Foundation, Inc.
4
5 ;; Author: Yoni Rabkin <yonirabkin@member.fsf.org>
6
7 ;; Keywords: emms, lastfm
8
9 ;; EMMS is free software; you can redistribute it and/or modify it
10 ;; under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 3, or (at your option)
12 ;; any later version.
13 ;;
14 ;; EMMS is distributed in the hope that it will be useful, but WITHOUT
15 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17 ;; License for more details.
18 ;;
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with EMMS; see the file COPYING. If not, write to the Free
21 ;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 ;; MA 02110-1301, USA.
23
24 ;;; Commentary:
25 ;;
26 ;; Definitive information on how to setup and use this package is
27 ;; provided in the wonderful Emms manual, in the /doc directory of the
28 ;; Emms distribution.
29
30 ;;; Code:
31
32 (require 'md5)
33 (require 'parse-time)
34 (require 'emms)
35 (require 'emms-source-file)
36 (require 'xml)
37
38 (defcustom emms-lastfm-client-username nil
39 "Valid Last.fm account username."
40 :group 'emms-lastfm
41 :type 'string)
42
43 (defcustom emms-lastfm-client-api-key nil
44 "Key for the Last.fm API."
45 :group 'emms-lastfm
46 :type 'string)
47
48 (defcustom emms-lastfm-client-api-secret-key nil
49 "Secret key for the Last.fm API."
50 :group 'emms-lastfm
51 :type 'string)
52
53 (defvar emms-lastfm-client-api-session-key nil
54 "Session key for the Last.fm API.")
55
56 (defvar emms-lastfm-client-track nil
57 "Latest Last.fm track.")
58
59 (defvar emms-lastfm-client-submission-api t
60 "Use the Last.fm submission API if true, otherwise don't.")
61
62 (defvar emms-lastfm-client-token nil
63 "Authorization token for API.")
64
65 (defvar emms-lastfm-client-api-base-url
66 "http://ws.audioscrobbler.com/2.0/"
67 "URL for API calls.")
68
69 (defvar emms-lastfm-client-session-key-file
70 (concat (file-name-as-directory emms-directory)
71 "emms-lastfm-client-sessionkey")
72 "File for storing the Last.fm API session key.")
73
74 (defvar emms-lastfm-client-cache-directory
75 (file-name-as-directory
76 (concat (file-name-as-directory emms-directory)
77 "emms-lastfm-client-cache"))
78 "File for storing Last.fm cache data.")
79
80 (defvar emms-lastfm-client-playlist-valid nil
81 "True if the playlist hasn't expired.")
82
83 (defvar emms-lastfm-client-playlist-timer nil
84 "Playlist timer object.")
85
86 (defvar emms-lastfm-client-playlist nil
87 "Latest Last.fm playlist.")
88
89 (defvar emms-lastfm-client-track nil
90 "Latest Last.fm track.")
91
92 (defvar emms-lastfm-client-original-next-function nil
93 "Original `-next-function'.")
94
95 (defvar emms-lastfm-client-playlist-buffer-name
96 "*Emms Last.fm*"
97 "Name for non-interactive Emms Last.fm buffer.")
98
99 (defvar emms-lastfm-client-playlist-buffer nil
100 "Non-interactive Emms Last.fm buffer.")
101
102 (defvar emms-lastfm-client-inhibit-cleanup nil
103 "If true, do not perform clean-up after `emms-stop'.")
104
105 (defvar emms-lastfm-client-image-size "mega"
106 "Default size for artist information images.")
107
108 (defvar emms-lastfm-client-artist-info-buffer-name
109 "*Emms Last.fm Artist Info*"
110 "Name for displaying artist information.")
111
112 (defvar emms-lastfm-client-api-method-dict
113 '((auth-get-token . ("auth.gettoken"
114 emms-lastfm-client-auth-get-token-ok
115 emms-lastfm-client-auth-get-token-failed))
116 (auth-get-session . ("auth.getsession"
117 emms-lastfm-client-auth-get-session-ok
118 emms-lastfm-client-auth-get-session-failed))
119 (radio-tune . ("radio.tune"
120 emms-lastfm-client-radio-tune-ok
121 emms-lastfm-client-radio-tune-failed))
122 (radio-getplaylist . ("radio.getplaylist"
123 emms-lastfm-client-radio-getplaylist-ok
124 emms-lastfm-client-radio-getplaylist-failed))
125 (track-love . ("track.love"
126 emms-lastfm-client-track-love-ok
127 emms-lastfm-client-track-love-failed))
128 (track-ban . ("track.ban"
129 emms-lastfm-client-track-ban-ok
130 emms-lastfm-client-track-ban-failed))
131 (artist-getinfo . ("artist.getinfo"
132 emms-lastfm-client-artist-getinfo-ok
133 emms-lastfm-client-artist-getinfo-failed)))
134 "Mapping symbols to method calls. This is a list of cons pairs
135 where the CAR is the symbol name of the method and the CDR is a
136 list whose CAR is the method call string, CADR is the function
137 to call on a success and CADDR is the function to call on
138 failure.")
139
140 ;;; ------------------------------------------------------------------
141 ;;; API method call
142 ;;; ------------------------------------------------------------------
143
144 (defun emms-lastfm-client-get-method (method)
145 "Return the associated method cons for the symbol METHOD."
146 (let ((m (cdr (assoc method emms-lastfm-client-api-method-dict))))
147 (if (not m)
148 (error "method not in dictionary: %s" method)
149 m)))
150
151 (defun emms-lastfm-client-get-method-name (method)
152 "Return the associated method string for the symbol METHOD."
153 (let ((this (nth 0 (emms-lastfm-client-get-method method))))
154 (if (not this)
155 (error "no name string registered for method: %s" method)
156 this)))
157
158 (defun emms-lastfm-client-get-method-ok (method)
159 "Return the associated OK function for METHOD.
160
161 This function is called when the method call returns
162 successfully."
163 (let ((this (nth 1 (emms-lastfm-client-get-method method))))
164 (if (not this)
165 (error "no OK function registered for method: %s" method)
166 this)))
167
168 (defun emms-lastfm-client-get-method-fail (method)
169 "Return the associated fail function for METHOD.
170
171 This function is called when the method call returns a failure
172 status message."
173 (let ((this (nth 2 (emms-lastfm-client-get-method method))))
174 (if (not this)
175 (error "no fail function registered for method: %s" method)
176 this)))
177
178 (defun emms-lastfm-client-encode-arguments (arguments)
179 "Encode ARGUMENTS in UTF-8 for the Last.fm API."
180 (let ((result nil))
181 (while arguments
182 (setq result
183 (append result
184 (list
185 (cons
186 (encode-coding-string (caar arguments) 'utf-8)
187 (encode-coding-string (cdar arguments) 'utf-8)))))
188 (setq arguments (cdr arguments)))
189 result))
190
191 (defun emms-lastfm-client-construct-arguments (str arguments)
192 "Return a concatenation of arguments for the URL."
193 (cond ((not arguments) str)
194 (t (emms-lastfm-client-construct-arguments
195 (concat str "&" (caar arguments) "=" (url-hexify-string (cdar arguments)))
196 (cdr arguments)))))
197
198 (defun emms-lastfm-client-construct-method-call (method arguments)
199 "Return a complete URL method call for METHOD with ARGUMENTS.
200
201 This function includes the cryptographic signature."
202 (concat emms-lastfm-client-api-base-url "?"
203 "method=" (emms-lastfm-client-get-method-name method)
204 (emms-lastfm-client-construct-arguments
205 "" arguments)
206 "&api_sig="
207 (emms-lastfm-client-construct-signature method arguments)))
208
209 (defun emms-lastfm-client-construct-write-method-call (method arguments)
210 "Return a complete POST body method call for METHOD with ARGUMENTS.
211
212 This function includes the cryptographic signature."
213 (concat "method=" (emms-lastfm-client-get-method-name method)
214 (emms-lastfm-client-construct-arguments
215 "" arguments)
216 "&api_sig="
217 (emms-lastfm-client-construct-signature method arguments)))
218
219 ;;; ------------------------------------------------------------------
220 ;;; Response handler
221 ;;; ------------------------------------------------------------------
222
223 (defun emms-lastfm-client-handle-response (method xml-response)
224 "Dispatch the handler functions of METHOD for XML-RESPONSE."
225 (let ((status (cdr (assoc 'status (nth 1 (car xml-response)))))
226 (data (cdr (cdr (car xml-response)))))
227 (when (not status)
228 (error "error parsing status from: %s" xml-response))
229 (cond ((string= status "failed")
230 (funcall (emms-lastfm-client-get-method-fail method) data))
231 ((string= status "ok")
232 (funcall (emms-lastfm-client-get-method-ok method) data))
233 (t (error "unknown response status %s" status)))))
234
235 ;;; ------------------------------------------------------------------
236 ;;; Unathorized request token for an API account
237 ;;; ------------------------------------------------------------------
238
239 (defun emms-lastfm-client-construct-urt ()
240 "Return a request for an Unauthorized Request Token."
241 (let ((arguments
242 (emms-lastfm-client-encode-arguments
243 `(("api_key" . ,emms-lastfm-client-api-key)))))
244 (emms-lastfm-client-construct-method-call
245 'auth-get-token arguments)))
246
247 (defun emms-lastfm-client-make-call-urt ()
248 "Make method call for Unauthorized Request Token."
249 (let* ((url-request-method "POST"))
250 (let ((response
251 (url-retrieve-synchronously
252 (emms-lastfm-client-construct-urt))))
253 (emms-lastfm-client-handle-response
254 'auth-get-token
255 (with-current-buffer response
256 (xml-parse-region (point-min) (point-max)))))))
257
258 ;; example response: ((lfm ((status . \"ok\")) \"\" (token nil
259 ;; \"31cab3398a9b46cf7231ef84d73169cf\")))
260
261 ;;; ------------------------------------------------------------------
262 ;;; Signatures
263 ;;; ------------------------------------------------------------------
264 ;;
265 ;; From [http://www.last.fm/api/desktopauth]:
266 ;;
267 ;; Construct your api method signatures by first ordering all the
268 ;; parameters sent in your call alphabetically by parameter name and
269 ;; concatenating them into one string using a <name><value>
270 ;; scheme. So for a call to auth.getSession you may have:
271 ;;
272 ;; api_keyxxxxxxxxmethodauth.getSessiontokenxxxxxxx
273 ;;
274 ;; Ensure your parameters are utf8 encoded. Now append your secret
275 ;; to this string. Finally, generate an md5 hash of the resulting
276 ;; string. For example, for an account with a secret equal to
277 ;; 'mysecret', your api signature will be:
278 ;;
279 ;; api signature = md5("api_keyxxxxxxxxmethodauth.getSessiontokenxxxxxxxmysecret")
280 ;;
281 ;; Where md5() is an md5 hashing operation and its argument is the
282 ;; string to be hashed. The hashing operation should return a
283 ;; 32-character hexadecimal md5 hash.
284
285 (defun emms-lastfm-client-construct-lexi (arguments)
286 "Return ARGUMENTS sorted in lexicographic order."
287 (let ((lexi (sort arguments
288 '(lambda (a b) (string< (car a) (car b)))))
289 (out ""))
290 (while lexi
291 (setq out (concat out (caar lexi) (cdar lexi)))
292 (setq lexi (cdr lexi)))
293 out))
294
295 (defun emms-lastfm-client-construct-signature (method arguments)
296 "Return request signature for METHOD and ARGUMENTS."
297 (let ((complete-arguments
298 (append arguments
299 `(("method" .
300 ,(emms-lastfm-client-get-method-name method))))))
301 (md5
302 (concat (emms-lastfm-client-construct-lexi complete-arguments)
303 emms-lastfm-client-api-secret-key))))
304
305 ;;; ------------------------------------------------------------------
306 ;;; General error handling
307 ;;; ------------------------------------------------------------------
308
309 ;; Each method call provides its own error codes, but if we don't want
310 ;; to code a handler for a method we call this instead:
311 (defun emms-lastfm-client-default-error-handler (data)
312 "Default method failure handler."
313 (let ((errorcode (cdr (assoc 'code (nth 1 (cadr data)))))
314 (message (nth 2 (cadr data))))
315 (when (not (and errorcode message))
316 (error "failed to read errorcode or message: %s %s"
317 errorcode message))
318 (error "method call failed with code %s: %s"
319 errorcode message)))
320
321 ;;; ------------------------------------------------------------------
322 ;;; Request authorization from the user
323 ;;; ------------------------------------------------------------------
324
325 (defun emms-lastfm-client-ask-for-auth ()
326 "Open a Web browser for authorizing the application."
327 (when (not (and emms-lastfm-client-api-key
328 emms-lastfm-client-token))
329 (error "API key and authorization token needed."))
330 (browse-url
331 (format "http://www.last.fm/api/auth/?api_key=%s&token=%s"
332 emms-lastfm-client-api-key
333 emms-lastfm-client-token)))
334
335 ;;; ------------------------------------------------------------------
336 ;;; Parse XSPF
337 ;;; ------------------------------------------------------------------
338
339 (defun emms-lastfm-client-xspf-header (data)
340 "Return an alist representing the XSPF header of DATA."
341 (let (out
342 (orig data))
343 (setq data (cadr data))
344 (while data
345 (when (and (car data)
346 (listp (car data))
347 (= (length (car data)) 3))
348 (setq out (append out (list (cons (nth 0 (car data))
349 (nth 2 (car data)))))))
350 (setq data (cdr data)))
351 (if (not out)
352 (error "failed to parse XSPF header from: %s" orig)
353 out)))
354
355 (defun emms-lastfm-client-xspf-tracklist (data)
356 "Return the start of the track-list in DATE."
357 (nthcdr 3 (nth 11 (cadr data))))
358
359 (defun emms-lastfm-client-xspf-header-date (header-alist)
360 "Return the date parameter from HEADER-ALIST."
361 (let ((out (cdr (assoc 'date header-alist))))
362 (if (not out)
363 (error "could not read date from header alist: %s"
364 header-alist)
365 out)))
366
367 (defun emms-lastfm-client-xspf-header-expiry (header-alist)
368 "Return the expiry parameter from HEADER-ALIST."
369 (let ((out (cdr (assoc 'link header-alist))))
370 (if (not out)
371 (error "could not read expiry from header alist: %s"
372 header-alist)
373 out)))
374
375 (defun emms-lastfm-client-xspf-header-creator (header-alist)
376 "Return the creator parameter from HEADER-ALIST."
377 (let ((out (cdr (assoc 'creator header-alist))))
378 (if (not out)
379 (error "could not read creator from header alist: %s"
380 header-alist)
381 out)))
382
383 (defun emms-lastfm-client-xspf-playlist (data)
384 "Return the playlist from the XSPF DATA."
385 (let ((playlist (car (nthcdr 11 data))))
386 (if (not playlist)
387 (error "could not read playlist from: %s" data)
388 playlist)))
389
390 ;; note: the result of this function can be used with
391 ;; `emms-lastfm-client-xspf-get' as well
392 (defun emms-lastfm-client-xspf-extension (track)
393 "Return the Extension portion of TRACK."
394 (let ((this (copy-sequence track))
395 (cont t))
396 (while (and cont this)
397 (when (consp this)
398 (let ((head (car this)))
399 (when (consp head)
400 (when (equal 'extension (car head))
401 (setq cont nil)))))
402 (when cont
403 (setq this (cdr this))))
404 (if this
405 (car this)
406 (error "could not find track extension data"))))
407
408 (defun emms-lastfm-client-xspf-get (node track)
409 "Return data associated with NODE in TRACK."
410 (let ((result nil))
411 (while track
412 (when (consp track)
413 (let ((this (car track)))
414 (when (and (consp this)
415 (= (length this) 3)
416 (symbolp (nth 0 this))
417 (stringp (nth 2 this))
418 (equal (nth 0 this) node))
419 (setq result (nth 2 this)))))
420 (setq track (cdr track)))
421 (if (not result)
422 nil
423 result)))
424
425 ;;; ------------------------------------------------------------------
426 ;;; Timers
427 ;;; ------------------------------------------------------------------
428
429 ;; timed playlist invalidation is a part of the Last.fm API
430 (defun emms-lastfm-client-set-timer (header)
431 "Start timer countdown to playlist invalidation"
432 (when (not header)
433 (error "can't set timer with no header data"))
434 (let ((expiry (parse-integer
435 (emms-lastfm-client-xspf-header-expiry header))))
436 (setq emms-lastfm-client-playlist-valid t)
437 (when emms-lastfm-client-playlist-timer
438 (cancel-timer emms-lastfm-client-playlist-timer))
439 (setq emms-lastfm-client-playlist-timer
440 (run-at-time
441 expiry nil
442 '(lambda ()
443 (cancel-timer emms-lastfm-client-playlist-timer)
444 (setq emms-lastfm-client-playlist-valid nil))))))
445
446 ;;; ------------------------------------------------------------------
447 ;;; Player
448 ;;; ------------------------------------------------------------------
449
450 ;; this should return `nil' to the track-manager when the playlist has
451 ;; been exhausted
452 (defun emms-lastfm-client-consume-next-track ()
453 "Pop and return the next track from the playlist or nil."
454 (when emms-lastfm-client-playlist
455 (if emms-lastfm-client-playlist-valid
456 (let ((track (car emms-lastfm-client-playlist)))
457 ;; we can only request each track once so we pop it off the
458 ;; playlist
459 (setq emms-lastfm-client-playlist
460 (if (stringp (cdr emms-lastfm-client-playlist))
461 (cddr emms-lastfm-client-playlist)
462 (cdr emms-lastfm-client-playlist)))
463 track)
464 (error "playlist invalid"))))
465
466 (defun emms-lastfm-client-set-lastfm-playlist-buffer ()
467 "Set `emms-playlist-buffer' to a be an Emms lastfm buffer."
468 (when (buffer-live-p emms-lastfm-client-playlist-buffer)
469 (kill-buffer emms-lastfm-client-playlist-buffer))
470 (setq emms-lastfm-client-playlist-buffer
471 (emms-playlist-new
472 emms-lastfm-client-playlist-buffer-name))
473 (setq emms-playlist-buffer emms-lastfm-client-playlist-buffer))
474
475
476 (defun emms-lastfm-client-load-next-track ()
477 "Queue the next track from Last.fm."
478 (with-current-buffer emms-lastfm-client-playlist-buffer
479 (let ((inhibit-read-only t))
480 (widen)
481 (delete-region (point-min)
482 (point-max)))
483 (if emms-lastfm-client-playlist
484 (let ((track (emms-lastfm-client-consume-next-track)))
485 (setq emms-lastfm-client-track track)
486 (setq emms-lastfm-scrobbler-track-play-start-timestamp
487 (emms-lastfm-scrobbler-timestamp))
488 (let ((emms-lastfm-client-inhibit-cleanup t))
489 (emms-play-url
490 (emms-lastfm-client-xspf-get 'location track))))
491 (emms-lastfm-client-make-call-radio-getplaylist)
492 (emms-lastfm-client-load-next-track))))
493
494 (defun emms-lastfm-client-love-track ()
495 "Submit the currently playing track with a `love' rating."
496 (interactive)
497 (when emms-lastfm-client-track
498 (emms-lastfm-scrobbler-make-async-submission-call
499 (emms-lastfm-client-convert-track
500 emms-lastfm-client-track) 'love)
501 ;; the following submission API call looks redundant but
502 ;; isn't; indeed, it might be done away with in a future
503 ;; version of the Last.fm API (see API docs)
504 (emms-lastfm-client-make-call-track-love)))
505
506 (defun emms-lastfm-client-ban-track ()
507 "Submit currently playing track with a `ban' rating and skip."
508 (interactive)
509 (when emms-lastfm-client-track
510 (emms-lastfm-scrobbler-make-async-submission-call
511 (emms-lastfm-client-convert-track
512 emms-lastfm-client-track) 'ban)
513 ;; the following submission API call looks redundant but
514 ;; isn't; see `...-love-track'
515 (emms-lastfm-client-make-call-track-ban)
516 (emms-lastfm-client-load-next-track)))
517
518 ;; call this `-track-advance' to avoid confusion with Emms'
519 ;; `-next-track-' mechanism
520 (defun emms-lastfm-client-track-advance (&optional first)
521 "Move to the next track in the playlist."
522 (interactive)
523 (when (equal emms-playlist-buffer
524 emms-lastfm-client-playlist-buffer)
525 (when (and emms-lastfm-client-submission-api
526 (not first))
527 (let ((result (emms-lastfm-scrobbler-make-async-submission-call
528 (emms-lastfm-client-convert-track
529 emms-lastfm-client-track) nil)))))
530 (emms-lastfm-client-load-next-track)))
531
532 (defun emms-lastfm-client-next-function ()
533 "Replacement function for `emms-next-noerror'."
534 (if (equal emms-playlist-buffer
535 emms-lastfm-client-playlist-buffer)
536 (emms-lastfm-client-track-advance)
537 (funcall emms-lastfm-client-original-next-function)))
538
539 (defun emms-lastfm-client-clean-after-stop ()
540 "Kill the emms-lastfm buffer."
541 (when (and (equal emms-playlist-buffer
542 emms-lastfm-client-playlist-buffer)
543 (not emms-lastfm-client-inhibit-cleanup))
544 (kill-buffer emms-lastfm-client-playlist-buffer)
545 (setq emms-lastfm-client-playlist-buffer nil)))
546
547 (defun emms-lastfm-client-play-playlist ()
548 "Entry point to play tracks from Last.fm."
549 (emms-lastfm-client-set-lastfm-playlist-buffer)
550 (when (not (equal emms-player-next-function
551 'emms-lastfm-client-next-function))
552 (add-to-list 'emms-player-stopped-hook
553 'emms-lastfm-client-clean-after-stop)
554 (setq emms-lastfm-client-original-next-function
555 emms-player-next-function)
556 (setq emms-player-next-function
557 'emms-lastfm-client-next-function))
558 (emms-lastfm-client-track-advance t))
559
560 ;; stolen from Tassilo Horn's original emms-lastfm.el
561 (defun emms-lastfm-client-read-artist ()
562 "Read an artist name from the user."
563 (let ((artists nil))
564 (when (boundp 'emms-cache-db)
565 (maphash
566 #'(lambda (file track)
567 (let ((artist (emms-track-get track 'info-artist)))
568 (when artist
569 (add-to-list 'artists artist))))
570 emms-cache-db))
571 (if artists
572 (emms-completing-read "Artist: " artists)
573 (read-string "Artist: "))))
574
575 (defun emms-lastfm-client-initialize-session ()
576 "Run per-session functions."
577 (emms-lastfm-client-check-session-key))
578
579 (defun emms-lastfm-client-info ()
580 "Display information about the latest track."
581 (interactive)
582 (emms-lastfm-client-make-call-artist-getinfo))
583
584 ;;; ------------------------------------------------------------------
585 ;;; Stations
586 ;;; ------------------------------------------------------------------
587
588 (defun emms-lastfm-client-play-user-station (username url)
589 "Play URL for USERNAME."
590 (when (not (and username url))
591 (error "username and url must be set"))
592 (emms-lastfm-client-initialize-session)
593 (emms-lastfm-client-make-call-radio-tune
594 (format url username))
595 (emms-lastfm-client-make-call-radio-getplaylist)
596 (emms-lastfm-scrobbler-handshake)
597 (emms-lastfm-client-play-playlist))
598
599 (defun emms-lastfm-client-play-similar-artists (artist)
600 "Play a Last.fm station with music similar to ARTIST."
601 (interactive (list (emms-lastfm-client-read-artist)))
602 (when (not (stringp artist))
603 (error "not a string: %s" artist))
604 (emms-lastfm-client-initialize-session)
605 (emms-lastfm-client-make-call-radio-tune
606 (format "lastfm://artist/%s/similarartists" artist))
607 (emms-lastfm-client-make-call-radio-getplaylist)
608 (emms-lastfm-scrobbler-handshake)
609 (emms-lastfm-client-play-playlist))
610
611 (defun emms-lastfm-client-play-recommended ()
612 "Play a Last.fm station with \"recommended\" tracks."
613 (interactive)
614 (emms-lastfm-client-play-user-station
615 emms-lastfm-client-username
616 "lastfm://user/%s/recommended"))
617
618 (defun emms-lastfm-client-play-loved ()
619 "Play a Last.fm station with \"loved\" tracks."
620 (interactive)
621 (emms-lastfm-client-play-user-station
622 emms-lastfm-client-username
623 "lastfm://user/%s/loved"))
624
625 (defun emms-lastfm-client-play-mix ()
626 "Play the \"Mix\" station the current user."
627 (interactive)
628 (emms-lastfm-client-play-user-station
629 emms-lastfm-client-username
630 "lastfm://user/%s/mix"))
631
632 (defun emms-lastfm-client-play-neighborhood ()
633 "Play a Last.fm station with \"neighborhood\" tracks."
634 (interactive)
635 (emms-lastfm-client-play-user-station
636 emms-lastfm-client-username
637 "lastfm://user/%s/neighbours"))
638
639 (defun emms-lastfm-client-play-library ()
640 "Play a Last.fm station with \"library\" tracks."
641 (interactive)
642 (emms-lastfm-client-play-user-station
643 emms-lastfm-client-username
644 "lastfm://user/%s/personal"))
645
646 (defun emms-lastfm-client-play-user-loved (user)
647 (interactive "sLast.fm username: ")
648 (emms-lastfm-client-play-user-station
649 user
650 "lastfm://user/%s/loved"))
651
652 (defun emms-lastfm-client-play-user-neighborhood (user)
653 (interactive "sLast.fm username: ")
654 (emms-lastfm-client-play-user-station
655 user
656 "lastfm://user/%s/neighbours"))
657
658 (defun emms-lastfm-client-play-user-library (user)
659 (interactive "sLast.fm username: ")
660 (emms-lastfm-client-play-user-station
661 user
662 "lastfm://user/%s/personal"))
663
664 ;;; ------------------------------------------------------------------
665 ;;; Information
666 ;;; ------------------------------------------------------------------
667
668 (defun emms-lastfm-client-convert-track (track)
669 "Convert a Last.fm track to an Emms track."
670 (let ((emms-track (emms-dictionary '*track*)))
671 (emms-track-set emms-track 'name
672 (emms-lastfm-client-xspf-get 'location track))
673 (emms-track-set emms-track 'info-artist
674 (emms-lastfm-client-xspf-get 'creator track))
675 (emms-track-set emms-track 'info-title
676 (emms-lastfm-client-xspf-get 'title track))
677 (emms-track-set emms-track 'info-album
678 (emms-lastfm-client-xspf-get 'album track))
679 (emms-track-set emms-track 'info-playing-time
680 (/ (parse-integer
681 (emms-lastfm-client-xspf-get 'duration
682 track))
683 1000))
684 (emms-track-set emms-track 'type 'lastfm-streaming)
685 emms-track))
686
687 (defun emms-lastfm-client-show-track (track)
688 "Return description of TRACK."
689 (decode-coding-string
690 (format emms-show-format
691 (emms-track-description
692 (emms-lastfm-client-convert-track track)))
693 'utf-8))
694
695 (defun emms-lastfm-client-show ()
696 "Display a description of the current track."
697 (interactive)
698 (if emms-player-playing-p
699 (message
700 (emms-lastfm-client-show-track emms-lastfm-client-track))
701 nil))
702
703 ;;; ------------------------------------------------------------------
704 ;;; Desktop application authorization [http://www.last.fm/api/desktopauth]
705 ;;; ------------------------------------------------------------------
706
707 (defun emms-lastfm-client-user-authorization ()
708 "Ask user to authorize the application."
709 (interactive)
710 (emms-lastfm-client-make-call-urt)
711 (emms-lastfm-client-ask-for-auth))
712
713 (defun emms-lastfm-client-get-session ()
714 "Retrieve and store session key."
715 (interactive)
716 (emms-lastfm-client-make-call-get-session)
717 (emms-lastfm-client-save-session-key
718 emms-lastfm-client-api-session-key))
719
720 ;;; ------------------------------------------------------------------
721 ;;; method: auth.getToken [http://www.last.fm/api/show?service=265]
722 ;;; ------------------------------------------------------------------
723
724 (defun emms-lastfm-client-auth-get-token-ok (data)
725 "Function called when auth.getToken succeeds."
726 (setq emms-lastfm-client-token
727 (nth 2 (cadr data)))
728 (if (or (not emms-lastfm-client-token)
729 (not (= (length emms-lastfm-client-token) 32)))
730 (error "could not read token from response %s" data)
731 (message "Emms Last.FM auth.getToken method call success.")))
732
733 (defun emms-lastfm-client-auth-get-token-failed (data)
734 "Function called when auth.getToken fails."
735 (emms-lastfm-client-default-error-handler data))
736
737 ;;; ------------------------------------------------------------------
738 ;;; method: auth.getSession [http://www.last.fm/api/show?service=125]
739 ;;; ------------------------------------------------------------------
740
741 (defun emms-lastfm-client-construct-get-session ()
742 "Return an auth.getSession request string."
743 (let ((arguments
744 (emms-lastfm-client-encode-arguments
745 `(("token" . ,emms-lastfm-client-token)
746 ("api_key" . ,emms-lastfm-client-api-key)))))
747 (emms-lastfm-client-construct-method-call
748 'auth-get-session arguments)))
749
750 (defun emms-lastfm-client-make-call-get-session ()
751 "Make auth.getSession call."
752 (let* ((url-request-method "POST"))
753 (let ((response
754 (url-retrieve-synchronously
755 (emms-lastfm-client-construct-get-session))))
756 (emms-lastfm-client-handle-response
757 'auth-get-session
758 (with-current-buffer response
759 (xml-parse-region (point-min) (point-max)))))))
760
761 (defun emms-lastfm-client-save-session-key (key)
762 "Store KEY."
763 (let ((buffer (find-file-noselect
764 emms-lastfm-client-session-key-file)))
765 (set-buffer buffer)
766 (erase-buffer)
767 (insert key)
768 (save-buffer)
769 (kill-buffer buffer)))
770
771 (defun emms-lastfm-client-load-session-key ()
772 "Return stored session key."
773 (let ((file (expand-file-name emms-lastfm-client-session-key-file)))
774 (setq emms-lastfm-client-api-session-key
775 (if (file-readable-p file)
776 (with-temp-buffer
777 (emms-insert-file-contents file)
778 (goto-char (point-min))
779 (buffer-substring-no-properties
780 (point) (point-at-eol)))
781 nil))))
782
783 (defun emms-lastfm-client-check-session-key ()
784 "Signal an error condition if there is no session key."
785 (if emms-lastfm-client-api-session-key
786 emms-lastfm-client-api-session-key
787 (if (emms-lastfm-client-load-session-key)
788 emms-lastfm-client-api-session-key
789 (error "no session key for API access"))))
790
791 (defun emms-lastfm-client-auth-get-session-ok (data)
792 "Function called on DATA if auth.getSession succeeds."
793 (let ((session-key (nth 2 (nth 5 (cadr data)))))
794 (cond (session-key
795 (setq emms-lastfm-client-api-session-key session-key)
796 (message "Emms Last.fm session key retrieval successful"))
797 (t (error "failed to parse session key data %s" data)))))
798
799 (defun emms-lastfm-client-auth-get-session-failed (data)
800 "Function called on DATA if auth.getSession fails."
801 (emms-lastfm-client-default-error-handler data))
802
803 ;;; ------------------------------------------------------------------
804 ;;; method: radio.tune [http://www.last.fm/api/show?service=160]
805 ;;; ------------------------------------------------------------------
806
807 (defun emms-lastfm-client-construct-radio-tune (station)
808 "Return a request to tune to STATION."
809 (let ((arguments
810 (emms-lastfm-client-encode-arguments
811 `(("sk" . ,emms-lastfm-client-api-session-key)
812 ("station" . ,station)
813 ("api_key" . ,emms-lastfm-client-api-key)))))
814 (emms-lastfm-client-construct-write-method-call
815 'radio-tune arguments)))
816
817 (defun emms-lastfm-client-make-call-radio-tune (station)
818 "Make call to tune to STATION."
819 (let ((url-request-method "POST")
820 (url-request-extra-headers
821 `(("Content-type" . "application/x-www-form-urlencoded")))
822 (url-request-data
823 (emms-lastfm-client-construct-radio-tune station)))
824 (let ((response
825 (url-retrieve-synchronously
826 emms-lastfm-client-api-base-url)))
827 (emms-lastfm-client-handle-response
828 'radio-tune
829 (with-current-buffer response
830 (xml-parse-region (point-min) (point-max)))))))
831
832 (defun emms-lastfm-client-radio-tune-failed (data)
833 "Function called on DATA when tuning fails."
834 (emms-lastfm-client-default-error-handler data))
835
836 (defun emms-lastfm-client-radio-tune-ok (data)
837 "Set the current radio station according to DATA."
838 (let ((response (cdr (cadr data)))
839 data)
840 (while response
841 (when (and (listp (car response))
842 (car response)
843 (= (length (car response)) 3))
844 (add-to-list 'data (cons (caar response)
845 (car (cdr (cdr (car response)))))))
846 (setq response (cdr response)))
847 (when (not data)
848 (error "could not parse station information %s" data))
849 data))
850
851 ;;; ------------------------------------------------------------------
852 ;;; method: radio.getPlaylist [http://www.last.fm/api/show?service=256]
853 ;;; ------------------------------------------------------------------
854
855 (defun emms-lastfm-client-construct-radio-getplaylist ()
856 "Return a request for a playlist from the tuned station."
857 (let ((arguments
858 (emms-lastfm-client-encode-arguments
859 `(("sk" . ,emms-lastfm-client-api-session-key)
860 ("api_key" . ,emms-lastfm-client-api-key)))))
861 (emms-lastfm-client-construct-write-method-call
862 'radio-getplaylist arguments)))
863
864 (defun emms-lastfm-client-make-call-radio-getplaylist ()
865 "Make call for playlist from the tuned station."
866 (let ((url-request-method "POST")
867 (url-request-extra-headers
868 `(("Content-type" . "application/x-www-form-urlencoded")))
869 (url-request-data
870 (emms-lastfm-client-construct-radio-getplaylist)))
871 (let ((response
872 (url-retrieve-synchronously
873 emms-lastfm-client-api-base-url)))
874 (emms-lastfm-client-handle-response
875 'radio-getplaylist
876 (with-current-buffer response
877 (xml-parse-region (point-min) (point-max)))))))
878
879 (defun emms-lastfm-client-radio-getplaylist-failed (data)
880 "Function called on DATA when retrieving a playlist fails."
881 'stub-needs-to-handle-playlist-issues
882 (emms-lastfm-client-default-error-handler data))
883
884 (defun emms-lastfm-client-list-filter (l)
885 "Remove strings from the roots of list L."
886 (let (acc)
887 (while l
888 (when (listp (car l))
889 (push (car l) acc))
890 (setq l (cdr l)))
891 (reverse acc)))
892
893 (defun emms-lastfm-client-radio-getplaylist-ok (data)
894 "Function called on DATA when retrieving a playlist succeeds."
895 (let ((header (emms-lastfm-client-xspf-header data))
896 (tracklist (emms-lastfm-client-xspf-tracklist data)))
897 (emms-lastfm-client-set-timer header)
898 (setq emms-lastfm-client-playlist
899 (emms-lastfm-client-list-filter tracklist))))
900
901 ;;; ------------------------------------------------------------------
902 ;;; method: track.love [http://www.last.fm/api/show?service=260]
903 ;;; ------------------------------------------------------------------
904
905 (defun emms-lastfm-client-construct-track-love ()
906 "Return a request for setting current track rating to `love'."
907 (let ((arguments
908 (emms-lastfm-client-encode-arguments
909 `(("sk" . ,emms-lastfm-client-api-session-key)
910 ("api_key" . ,emms-lastfm-client-api-key)
911 ("track" . ,(emms-lastfm-client-xspf-get
912 'title emms-lastfm-client-track))
913 ("artist" . ,(emms-lastfm-client-xspf-get
914 'creator emms-lastfm-client-track))))))
915 (emms-lastfm-client-construct-write-method-call
916 'track-love arguments)))
917
918 (defun emms-lastfm-client-make-call-track-love ()
919 "Make call for setting track rating to `love'."
920 (let ((url-request-method "POST")
921 (url-request-extra-headers
922 `(("Content-type" . "application/x-www-form-urlencoded")))
923 (url-request-data
924 (emms-lastfm-client-construct-track-love)))
925 (let ((response
926 (url-retrieve-synchronously
927 emms-lastfm-client-api-base-url)))
928 (emms-lastfm-client-handle-response
929 'track-love
930 (with-current-buffer response
931 (xml-parse-region (point-min) (point-max)))))))
932
933 (defun emms-lastfm-client-track-love-failed (data)
934 "Function called with DATA when setting `love' rating fails."
935 'stub-needs-to-handle-track-love-issues
936 (emms-lastfm-client-default-error-handler data))
937
938 (defun emms-lastfm-client-track-love-ok (data)
939 "Function called with DATA after `love' rating succeeds."
940 'track-love-succeed)
941
942 ;;; ------------------------------------------------------------------
943 ;;; method: artist.getInfo [http://www.last.fm/api/show?service=267]
944 ;;; ------------------------------------------------------------------
945
946 (defun emms-lastfm-client-cache-file (url)
947 "Download a file from URL and return a pathname."
948 (make-directory emms-lastfm-client-cache-directory t)
949 (let ((files (directory-files emms-lastfm-client-cache-directory
950 t)))
951 (dolist (file files)
952 (when (file-regular-p file)
953 (delete-file file)))
954 (call-process "wget" nil nil nil url "-P"
955 (expand-file-name
956 emms-lastfm-client-cache-directory))
957 (car (directory-files emms-lastfm-client-cache-directory
958 t ".\\(jpg\\|png\\)"))))
959
960 (defun emms-lastfm-client-display-artist-getinfo (artist-name
961 lastfm-url
962 artist-image
963 stats-listeners
964 stats-playcount
965 bio-summary
966 bio-complete)
967 "Display a buffer with the artist information."
968 (let ((buf (get-buffer-create
969 emms-lastfm-client-artist-info-buffer-name)))
970 (with-current-buffer buf
971 (let ((inhibit-read-only t))
972 (delete-region (point-min) (point-max))
973 (insert-image
974 (create-image (emms-lastfm-client-cache-file artist-image)))
975 (insert (format "\n\n%s\n\n"
976 (decode-coding-string artist-name 'utf-8)))
977 (insert (format "Last.fm page: %s\n\n" lastfm-url))
978 (insert (format "Listeners: %s\n" stats-listeners))
979 (insert (format "Plays: %s\n\n" stats-playcount))
980 (let ((p (point)))
981 (insert (format "<p>%s</p>" bio-complete))))
982 (setq buffer-read-only t)
983 (text-mode)
984 (goto-char (point-min)))
985 (switch-to-buffer buf)))
986
987 (defun emms-lastfm-client-parse-artist-getinfo (data)
988 "Parse the artist information."
989 (when (or (not data)
990 (not (listp data)))
991 (error "no artist info to parse"))
992 (let ((c (copy-seq (nth 1 data)))
993 artist-name lastfm-url artist-image
994 stats-listeners stats-playcount
995 bio-summary bio-complete)
996 (while c
997 (let ((entry (car c)))
998 (when (listp entry)
999 (let ((name (nth 0 entry))
1000 (value (nth 2 entry)))
1001 (cond ((equal name 'name) (setq artist-name value))
1002 ((equal name 'url) (setq lastfm-url value))
1003 ((equal name 'image)
1004 (let ((size (cdar (nth 1 entry))))
1005 (when (string-equal emms-lastfm-client-image-size
1006 size)
1007 (setq artist-image value))))
1008 ((equal name 'stats)
1009 (setq stats-listeners (nth 2 (nth 3 entry))
1010 stats-playcount (nth 2 (nth 5 entry))))
1011 ((equal name 'bio)
1012 (setq bio-summary (nth 2 (nth 5 entry))
1013 bio-complete (nth 2 (nth 7 entry))))))))
1014 (setq c (cdr c)))
1015 (emms-lastfm-client-display-artist-getinfo
1016 artist-name lastfm-url artist-image
1017 stats-listeners stats-playcount
1018 bio-summary bio-complete)))
1019
1020 (defun emms-lastfm-client-construct-artist-getinfo ()
1021 "Return a request for getting info about an artist."
1022 (let ((arguments
1023 (emms-lastfm-client-encode-arguments
1024 `(("sk" . ,emms-lastfm-client-api-session-key)
1025 ("api_key" . ,emms-lastfm-client-api-key)
1026 ("autocorrect" . "1")
1027 ("artist" . ,(emms-lastfm-client-xspf-get
1028 'creator emms-lastfm-client-track))))))
1029 (emms-lastfm-client-construct-write-method-call
1030 'artist-getinfo arguments)))
1031
1032 (defun emms-lastfm-client-make-call-artist-getinfo ()
1033 "Make a call for artist info."
1034 (let ((url-request-method "POST")
1035 (url-request-extra-headers
1036 `(("Content-type" . "application/x-www-form-urlencoded")))
1037 (url-request-data
1038 (emms-lastfm-client-construct-artist-getinfo)))
1039 (let ((response
1040 (url-retrieve-synchronously
1041 emms-lastfm-client-api-base-url)))
1042 (emms-lastfm-client-handle-response
1043 'artist-getinfo
1044 (with-current-buffer response
1045 (xml-parse-region (point-min) (point-max)))))))
1046
1047 (defun emms-lastfm-client-artist-getinfo-failed (data)
1048 "Function called with DATA when setting `ban' rating fails."
1049 'stub-needs-to-handle-artist-getinfo-issues
1050 (emms-lastfm-client-default-error-handler data))
1051
1052 (defun emms-lastfm-client-artist-getinfo-ok (data)
1053 "Function called with DATA after `ban' rating succeeds."
1054 (emms-lastfm-client-parse-artist-getinfo data))
1055
1056 ;;; ------------------------------------------------------------------
1057 ;;; method: track.ban [http://www.last.fm/api/show?service=261]
1058 ;;; ------------------------------------------------------------------
1059
1060 (defun emms-lastfm-client-construct-track-ban ()
1061 "Return a request for setting current track rating to `ban'."
1062 (let ((arguments
1063 (emms-lastfm-client-encode-arguments
1064 `(("sk" . ,emms-lastfm-client-api-session-key)
1065 ("api_key" . ,emms-lastfm-client-api-key)
1066 ("track" . ,(emms-lastfm-client-xspf-get
1067 'title emms-lastfm-client-track))
1068 ("artist" . ,(emms-lastfm-client-xspf-get
1069 'creator emms-lastfm-client-track))))))
1070 (emms-lastfm-client-construct-write-method-call
1071 'track-ban arguments)))
1072
1073 (defun emms-lastfm-client-make-call-track-ban ()
1074 "Make call for setting track rating to `ban'."
1075 (let ((url-request-method "POST")
1076 (url-request-extra-headers
1077 `(("Content-type" . "application/x-www-form-urlencoded")))
1078 (url-request-data
1079 (emms-lastfm-client-construct-track-ban)))
1080 (let ((response
1081 (url-retrieve-synchronously
1082 emms-lastfm-client-api-base-url)))
1083 (emms-lastfm-client-handle-response
1084 'track-ban
1085 (with-current-buffer response
1086 (xml-parse-region (point-min) (point-max)))))))
1087
1088 (defun emms-lastfm-client-track-ban-failed (data)
1089 "Function called with DATA when setting `ban' rating fails."
1090 'stub-needs-to-handle-track-ban-issues
1091 (emms-lastfm-client-default-error-handler data))
1092
1093 (defun emms-lastfm-client-track-ban-ok (data)
1094 "Function called with DATA after `ban' rating succeeds."
1095 'track-ban-succeed)
1096
1097 (provide 'emms-lastfm-client)
1098
1099 ;;; emms-lastfm-client.el ends here