aboutsummaryrefslogtreecommitdiff
path: root/lisp/emms-lastfm-client.el
diff options
context:
space:
mode:
authorYoni Rabkin <yoni@rabkins.net>2009-12-24 21:30:32 +0200
committerYoni Rabkin <yoni@rabkins.net>2009-12-24 21:30:32 +0200
commitea7d3100b660201ff5e24d3c53cb403f23b09786 (patch)
tree6e7748c2f7137fe846f6e8ae33b9df46420f6dce /lisp/emms-lastfm-client.el
parent731ac90ef70b6e9874a396f0c69dbf8be2fff83f (diff)
Replace lisp/emms-lastfm.el with lisp/emms-lastfm-client.el.
The original emms-lastfm.el doesn't work at all with the current Last.fm API.
Diffstat (limited to 'lisp/emms-lastfm-client.el')
-rw-r--r--lisp/emms-lastfm-client.el743
1 files changed, 743 insertions, 0 deletions
diff --git a/lisp/emms-lastfm-client.el b/lisp/emms-lastfm-client.el
new file mode 100644
index 0000000..20ec018
--- /dev/null
+++ b/lisp/emms-lastfm-client.el
@@ -0,0 +1,743 @@
+;;; -*- show-trailing-whitespace: t -*-
+;;; emms-lastfm-client.el --- Last.FM Music API
+
+;; Copyright (C) 2009 Free Software Foundation, Inc.
+
+;; Author: 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.
+
+;;; Commentary:
+;;
+;; There are restrictions on the use of this service. Quoting from
+;; [http://www.last.fm/api/radio]: "Who can I stream radio to? Any API
+;; account can only stream radio to Last.fm's paid subscribers".
+;;
+;; We've spoken to representatives from Last.fm and arrived at the
+;; following agreement: In order to be able to use the service while
+;; preserving the essential freedoms of the GPL each client must apply
+;; for their own API key from Last.fm.
+
+;;; Installation:
+;;
+;; Here is how to get authorization from Last.fm to stream
+;; music. Thankfully this only needs to be done _once_:
+;;
+;; 1. Complete steps 1 and 2 from
+;; [http://www.last.fm/api/authentication] to get an API key and a
+;; secret key. Set `emms-lastfm-client-api-key' and
+;; `emms-lastfm-client-api-secret-key' accordingly.
+;;
+;; 2. M-x emms-lastfm-client-user-authorization
+;;
+;; If this completes successfully a browser window will open asking
+;; for confirmation to allow this application to access the Last.fm
+;; account. Confirm and close the browser.
+;;
+;; 3. M-x emms-lastfm-client-get-session
+;;
+;; After this last step the permanent session key will be stored in
+;; `emms-lastfm-client-session-key-file'. As long as this key value
+;; is accessible the authentication process need never be repeated.
+
+;;; Use:
+;;
+;; M-x emms-lastfm-client-play-similar-artists
+;;
+;; Call the ...-play- family of interactive functions, such the above,
+;; to start streaming music.
+;;
+;; To show the currently playing track: M-x emms-lastfm-client-show
+;;
+;; To skip to the next track: M-x emms-lastfm-client-track-advance
+;;
+;; ...more Last.fm functionality to come, so stay tuned.
+
+;;; Code:
+
+(require 'md5)
+(require 'xml)
+
+(defvar emms-lastfm-client-api-key nil
+ "Key for the Last.fm API.")
+
+(defvar emms-lastfm-client-api-secret-key nil
+ "Secret key for the Last.fm API.")
+
+(defvar emms-lastfm-client-api-session-key nil
+ "Session key for the Last.fm API.")
+
+(defvar emms-lastfm-client-token nil
+ "Authorization token for API.")
+
+(defvar emms-lastfm-client-api-base-url
+ "http://ws.audioscrobbler.com/2.0/"
+ "URL for API calls.")
+
+(defvar emms-lastfm-client-session-key-file
+ (concat (file-name-as-directory emms-directory)
+ "emms-lastfm-client-sessionkey")
+ "File for storing the Last.fm API session key.")
+
+(defvar emms-lastfm-client-playlist-valid nil
+ "True if the playlist hasn't expired.")
+
+(defvar emms-lastfm-client-playlist-timer nil
+ "Playlist timer object.")
+
+(defvar emms-lastfm-client-playlist nil
+ "Latest Last.fm playlist.")
+
+(defvar emms-lastfm-client-track nil
+ "Latest Last.fm track.")
+
+(defvar emms-lastfm-client-original-next-function nil
+ "Original `-next-function' to be restored.")
+
+(defvar emms-lastfm-client-playlist-buffer-name "*Emms Last.fm*"
+ "Name for non-interactive Emms Last.fm buffer.")
+
+(defvar emms-lastfm-client-playlist-buffer nil
+ "Non-interactive Emms Last.fm buffer.")
+
+(defvar emms-lastfm-client-api-method-dict
+ '((auth-get-token . ("auth.gettoken"
+ emms-lastfm-client-auth-get-token-ok
+ emms-lastfm-client-auth-get-token-failed))
+ (auth-get-session . ("auth.getsession"
+ emms-lastfm-client-auth-get-session-ok
+ emms-lastfm-client-auth-get-session-failed))
+ (radio-tune . ("radio.tune"
+ emms-lastfm-client-radio-tune-ok
+ emms-lastfm-client-radio-tune-failed))
+ (radio-getplaylist . ("radio.getplaylist"
+ emms-lastfm-client-radio-getplaylist-ok
+ emms-lastfm-client-radio-getplaylist-failed)))
+ "Mapping symbols to method calls. This is a list of cons pairs
+ where the CAR is the symbol name of the method and the CDR is a
+ list whose CAR is the method call string, CADR is the function
+ to call on a success and CADDR is the function to call on
+ failure.")
+
+;;; ------------------------------------------------------------------
+;;; API method call
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-get-method (method)
+ "Return the associated method cons for the symbol METHOD."
+ (let ((m (cdr (assoc method emms-lastfm-client-api-method-dict))))
+ (if (not m)
+ (error "method not in dictionary: %s" method)
+ m)))
+
+(defun emms-lastfm-client-get-method-name (method)
+ "Return the associated method string for the symbol METHOD."
+ (let ((this (nth 0 (emms-lastfm-client-get-method method))))
+ (if (not this)
+ (error "no name string registered for method: %s" method)
+ this)))
+
+(defun emms-lastfm-client-get-method-ok (method)
+ "Return the associated OK function for METHOD.
+
+This function is called when the method call returns
+successfully."
+ (let ((this (nth 1 (emms-lastfm-client-get-method method))))
+ (if (not this)
+ (error "no OK function registered for method: %s" method)
+ this)))
+
+(defun emms-lastfm-client-get-method-fail (method)
+ "Return the associated fail function for METHOD.
+
+This function is called when the method call returns a failure
+status message."
+ (let ((this (nth 2 (emms-lastfm-client-get-method method))))
+ (if (not this)
+ (error "no fail function registered for method: %s" method)
+ this)))
+
+(defun emms-lastfm-client-encode-arguments (arguments)
+ "Encode ARGUMENTS in UTF-8 for the Last.fm API."
+ (let ((result nil))
+ (while arguments
+ (setq result
+ (append result
+ (list
+ (cons
+ (encode-coding-string (caar arguments) 'utf-8)
+ (encode-coding-string (cdar arguments) 'utf-8)))))
+ (setq arguments (cdr arguments)))
+ result))
+
+(defun emms-lastfm-client-construct-arguments (str arguments)
+ "Return a concatenation of arguments for the URL."
+ (cond ((not arguments) str)
+ (t (emms-lastfm-client-construct-arguments
+ (concat str "&" (caar arguments) "=" (url-hexify-string (cdar arguments)))
+ (cdr arguments)))))
+
+(defun emms-lastfm-client-construct-method-call (method arguments)
+ "Return a complete URL method call for METHOD with ARGUMENTS.
+
+This function includes the cryptographic signature."
+ (concat emms-lastfm-client-api-base-url "?"
+ "method=" (emms-lastfm-client-get-method-name method)
+ (emms-lastfm-client-construct-arguments
+ "" arguments)
+ "&api_sig="
+ (emms-lastfm-client-construct-signature method arguments)))
+
+(defun emms-lastfm-client-construct-write-method-call (method arguments)
+ "Return a complete POST body method call for METHOD with ARGUMENTS.
+
+This function includes the cryptographic signature."
+ (concat "method=" (emms-lastfm-client-get-method-name method)
+ (emms-lastfm-client-construct-arguments
+ "" arguments)
+ "&api_sig="
+ (emms-lastfm-client-construct-signature method arguments)))
+
+;;; ------------------------------------------------------------------
+;;; Response handler
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-handle-response (method xml-response)
+ "Dispatch the handler functions of METHOD for XML-RESPONSE."
+ (let ((status (cdr (assoc 'status (nth 1 (car xml-response)))))
+ (data (cddar xml-response)))
+ (when (not status)
+ (error "error parsing status from: %s" xml-response))
+ (cond ((string= status "failed")
+ (funcall (emms-lastfm-client-get-method-fail method) data))
+ ((string= status "ok")
+ (funcall (emms-lastfm-client-get-method-ok method) data))
+ (t (error "unknown response status %s" status)))))
+
+;;; ------------------------------------------------------------------
+;;; Unathorized request token for an API account
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-construct-urt ()
+ "Return a request for an Unauthorized Request Token."
+ (let ((arguments
+ (emms-lastfm-client-encode-arguments
+ `(("api_key" . ,emms-lastfm-client-api-key)))))
+ (emms-lastfm-client-construct-method-call
+ 'auth-get-token arguments)))
+
+(defun emms-lastfm-client-make-call-urt ()
+ "Make method call for Unauthorized Request Token."
+ (let* ((url-request-method "POST"))
+ (let ((response
+ (url-retrieve-synchronously
+ (emms-lastfm-client-construct-urt))))
+ (emms-lastfm-client-handle-response
+ 'auth-get-token
+ (with-current-buffer response
+ (xml-parse-region (point-min) (point-max)))))))
+
+;; example response: ((lfm ((status . \"ok\")) \"\" (token nil
+;; \"31cab3398a9b46cf7231ef84d73169cf\")))
+
+;;; ------------------------------------------------------------------
+;;; Signatures
+;;; ------------------------------------------------------------------
+;;
+;; From [http://www.last.fm/api/desktopauth]:
+;;
+;; Construct your api method signatures by first ordering all the
+;; parameters sent in your call alphabetically by parameter name and
+;; concatenating them into one string using a <name><value>
+;; scheme. So for a call to auth.getSession you may have:
+;;
+;; api_keyxxxxxxxxmethodauth.getSessiontokenxxxxxxx
+;;
+;; Ensure your parameters are utf8 encoded. Now append your secret
+;; to this string. Finally, generate an md5 hash of the resulting
+;; string. For example, for an account with a secret equal to
+;; 'mysecret', your api signature will be:
+;;
+;; api signature = md5("api_keyxxxxxxxxmethodauth.getSessiontokenxxxxxxxmysecret")
+;;
+;; Where md5() is an md5 hashing operation and its argument is the
+;; string to be hashed. The hashing operation should return a
+;; 32-character hexadecimal md5 hash.
+
+(defun emms-lastfm-client-construct-lexi (arguments)
+ "Return ARGUMENTS sorted in lexicographic order."
+ (let ((lexi (sort arguments
+ '(lambda (a b) (string< (car a) (car b)))))
+ (out ""))
+ (while lexi
+ (setq out (concat out (caar lexi) (cdar lexi)))
+ (setq lexi (cdr lexi)))
+ out))
+
+(defun emms-lastfm-client-construct-signature (method arguments)
+ "Return request signature for METHOD and ARGUMENTS."
+ (let ((complete-arguments
+ (append arguments
+ `(("method" .
+ ,(emms-lastfm-client-get-method-name method))))))
+ (md5
+ (concat (emms-lastfm-client-construct-lexi complete-arguments)
+ emms-lastfm-client-api-secret-key))))
+
+;;; ------------------------------------------------------------------
+;;; General error handling
+;;; ------------------------------------------------------------------
+
+;; Each method call provides its own error codes, but if we don't want
+;; to code a handler for a method we call this instead:
+(defun emms-lastfm-client-default-error-handler (data)
+ "Default method failure handler."
+ (let ((errorcode (cdr (assoc 'code (nth 1 (cadr data)))))
+ (message (nth 2 (cadr data))))
+ (when (not (and errorcode message))
+ (error "failed to read errorcode or message: %s %s"
+ errorcode message))
+ (error "method call failed with code %s: %s"
+ errorcode message)))
+
+;;; ------------------------------------------------------------------
+;;; Request authorization from the user
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-ask-for-auth ()
+ "Open a Web browser for authorizing the application."
+ (when (not (and emms-lastfm-client-api-key
+ emms-lastfm-client-token))
+ (error "API key and authorization token needed."))
+ (browse-url
+ (format "http://www.last.fm/api/auth/?api_key=%s&token=%s"
+ emms-lastfm-client-api-key
+ emms-lastfm-client-token)))
+
+;;; ------------------------------------------------------------------
+;;; Parse XSPF
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-xspf-header (data)
+ "Return an alist representing the XSPF header of DATA."
+ (let (out
+ (orig data))
+ (setq data (cadr data))
+ (while data
+ (when (and (car data)
+ (listp (car data))
+ (= (length (car data)) 3))
+ (setq out (append out (list (cons (nth 0 (car data))
+ (nth 2 (car data)))))))
+ (setq data (cdr data)))
+ (if (not out)
+ (error "failed to parse XSPF header from: %s" orig)
+ out)))
+
+(defun emms-lastfm-client-xspf-tracklist (data)
+ "Return the start of the track-list in DATE."
+ (nthcdr 3 (nth 11 (cadr data))))
+
+(defun emms-lastfm-client-xspf-header-date (header-alist)
+ "Return the date parameter from HEADER-ALIST."
+ (let ((out (cdr (assoc 'date header-alist))))
+ (if (not out)
+ (error "could not read date from header alist: %s"
+ header-alist)
+ out)))
+
+(defun emms-lastfm-client-xspf-header-expiry (header-alist)
+ "Return the expiry parameter from HEADER-ALIST."
+ (let ((out (cdr (assoc 'link header-alist))))
+ (if (not out)
+ (error "could not read expiry from header alist: %s"
+ header-alist)
+ out)))
+
+(defun emms-lastfm-client-xspf-header-creator (header-alist)
+ "Return the creator parameter from HEADER-ALIST."
+ (let ((out (cdr (assoc 'creator header-alist))))
+ (if (not out)
+ (error "could not read creator from header alist: %s"
+ header-alist)
+ out)))
+
+(defun emms-lastfm-client-xspf-playlist (data)
+ "Return the playlist from the XSPF DATA."
+ (let ((playlist (car (nthcdr 11 data))))
+ (if (not playlist)
+ (error "could not read playlist from: %s" data)
+ playlist)))
+
+(defun emms-lastfm-client-xspf-get (node track)
+ "Return data associated with NODE in TRACK."
+ (let ((result nil))
+ (while track
+ (when (consp track)
+ (let ((this (car track)))
+ (when (and (consp this)
+ (= (length this) 3)
+ (symbolp (nth 0 this))
+ (stringp (nth 2 this))
+ (equal (nth 0 this) node))
+ (setq result (nth 2 this)))))
+ (setq track (cdr track)))
+ (if (not result)
+ nil
+ result)))
+
+;;; ------------------------------------------------------------------
+;;; Timers
+;;; ------------------------------------------------------------------
+
+;; timed playlist invalidation is a part of the Last.fm API
+(defun emms-lastfm-client-set-timer (header)
+ "Start timer countdown to playlist invalidation"
+ (when (not header)
+ (error "can't set timer with no header data"))
+ (let ((expiry (parse-integer
+ (emms-lastfm-client-xspf-header-expiry header))))
+ (setq emms-lastfm-client-playlist-valid t)
+ (setq emms-lastfm-client-playlist-timer
+ (run-at-time
+ expiry nil
+ '(lambda () (setq emms-lastfm-client-playlist-valid
+ nil))))))
+
+;;; ------------------------------------------------------------------
+;;; Player
+;;; ------------------------------------------------------------------
+
+;; this should return `nil' to the track-manager when the playlist has
+;; been exhausted
+(defun emms-lastfm-client-consume-next-track ()
+ "Pop and return the next track from the playlist or nil."
+ (when emms-lastfm-client-playlist
+ (if emms-lastfm-client-playlist-valid
+ (let ((track (car emms-lastfm-client-playlist)))
+ ;; we can only request each track once so we pop it off the
+ ;; playlist
+ (setq emms-lastfm-client-playlist
+ (if (stringp (cdr emms-lastfm-client-playlist))
+ (cddr emms-lastfm-client-playlist)
+ (cdr emms-lastfm-client-playlist)))
+ track)
+ (error "playlist invalid"))))
+
+(defun emms-lastfm-client-set-lastfm-playlist-buffer ()
+ (when (not (buffer-live-p emms-lastfm-client-playlist-buffer))
+ (setq emms-lastfm-client-playlist-buffer
+ (emms-playlist-new
+ emms-lastfm-client-playlist-buffer-name))
+ (setq emms-playlist-buffer emms-lastfm-client-playlist-buffer)))
+
+(defun emms-lastfm-client-load-next-track ()
+ (with-current-buffer emms-lastfm-client-playlist-buffer
+ (emms-playlist-clear)
+ (if emms-lastfm-client-playlist
+ (let ((track (emms-lastfm-client-consume-next-track)))
+ (setq emms-lastfm-client-track track)
+ (when emms-player-playing-p
+ (emms-stop))
+ (emms-play-url
+ (emms-lastfm-client-xspf-get 'location track)))
+ (emms-lastfm-client-make-call-radio-getplaylist)
+ (emms-lastfm-client-load-next-track))))
+
+;; call this `-track-advance' to avoid confusion with Emms'
+;; `-next-track-' mechanism
+(defun emms-lastfm-client-track-advance ()
+ (interactive)
+ (when (equal emms-playlist-buffer
+ emms-lastfm-client-playlist-buffer)
+ (emms-lastfm-client-load-next-track)))
+
+(defun emms-lastfm-client-play-playlist ()
+ "Entry point to play tracks from Last.fm."
+ (emms-lastfm-client-set-lastfm-playlist-buffer)
+ (add-hook 'emms-player-finished-hook
+ 'emms-lastfm-client-track-advance)
+ (emms-lastfm-client-track-advance))
+
+;; stolen from Tassilo Horn's original emms-lastfm.el
+(defun emms-lastfm-client-read-artist ()
+ "Read an artist name from the user."
+ (let ((artists nil))
+ (when (boundp 'emms-cache-db)
+ (maphash
+ #'(lambda (file track)
+ (let ((artist (emms-track-get track 'info-artist)))
+ (when artist
+ (add-to-list 'artists artist))))
+ emms-cache-db))
+ (if artists
+ (emms-completing-read "Artist: " artists)
+ (read-string "Artist: "))))
+
+(defun emms-lastfm-client-play-similar-artists (artist)
+ "Play a Last.fm station with music similar to ARTIST."
+ (interactive (list (emms-lastfm-client-read-artist)))
+ (when (not (stringp artist))
+ (error "not a string: %s" artist))
+ (emms-lastfm-client-check-session-key)
+ (emms-lastfm-client-make-call-radio-tune
+ (format "lastfm://artist/%s/similarartists" artist))
+ (emms-lastfm-client-make-call-radio-getplaylist)
+ (emms-lastfm-client-play-playlist))
+
+;;; ------------------------------------------------------------------
+;;; Information
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-convert-track (track)
+ "Convert a Last.fm track to an Emms track."
+ (let ((emms-track (emms-dictionary '*track*)))
+ (emms-track-set emms-track 'name
+ (emms-lastfm-client-xspf-get 'location track))
+ (emms-track-set emms-track 'info-artist
+ (emms-lastfm-client-xspf-get 'creator track))
+ (emms-track-set emms-track 'info-title
+ (emms-lastfm-client-xspf-get 'title track))
+ (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))
+ emms-track))
+
+(defun emms-lastfm-client-show-track (track)
+ "Return description of TRACK."
+ (decode-coding-string
+ (format emms-show-format
+ (emms-track-description
+ (emms-lastfm-client-convert-track track)))
+ 'utf-8))
+
+(defun emms-lastfm-client-show ()
+ "Display a description of the current track."
+ (interactive)
+ (if emms-player-playing-p
+ (message
+ (emms-lastfm-client-show-track emms-lastfm-client-track))
+ nil))
+
+;;; ------------------------------------------------------------------
+;;; Desktop application authorization [http://www.last.fm/api/desktopauth]
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-user-authorization ()
+ "Ask user to authorize the application."
+ (interactive)
+ (emms-lastfm-client-make-call-urt)
+ (emms-lastfm-client-ask-for-auth))
+
+(defun emms-lastfm-client-get-session ()
+ "Retrieve and store session key."
+ (interactive)
+ (emms-lastfm-client-make-call-get-session)
+ (emms-lastfm-client-save-session-key
+ emms-lastfm-client-api-session-key))
+
+;;; ------------------------------------------------------------------
+;;; method: auth.getToken [http://www.last.fm/api/show?service=265]
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-auth-get-token-ok (data)
+ "Function called when auth.getToken succeeds."
+ (setq emms-lastfm-client-token
+ (nth 2 (cadr data)))
+ (if (or (not emms-lastfm-client-token)
+ (not (= (length emms-lastfm-client-token) 32)))
+ (error "could not read token from response %s" data)
+ (message "Emms Last.FM auth.getToken method call success.")))
+
+(defun emms-lastfm-client-auth-get-token-failed (data)
+ "Function called when auth.getToken fails."
+ (emms-lastfm-client-default-error-handler data))
+
+;;; ------------------------------------------------------------------
+;;; method: auth.getSession [http://www.last.fm/api/show?service=125]
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-construct-get-session ()
+ "Return an auth.getSession request string."
+ (let ((arguments
+ (emms-lastfm-client-encode-arguments
+ `(("token" . ,emms-lastfm-client-token)
+ ("api_key" . ,emms-lastfm-client-api-key)))))
+ (emms-lastfm-client-construct-method-call
+ 'auth-get-session arguments)))
+
+(defun emms-lastfm-client-make-call-get-session ()
+ "Make auth.getSession call."
+ (let* ((url-request-method "POST"))
+ (let ((response
+ (url-retrieve-synchronously
+ (emms-lastfm-client-construct-get-session))))
+ (emms-lastfm-client-handle-response
+ 'auth-get-session
+ (with-current-buffer response
+ (xml-parse-region (point-min) (point-max)))))))
+
+(defun emms-lastfm-client-save-session-key (key)
+ "Store KEY."
+ (let ((buffer (find-file-noselect
+ emms-lastfm-client-session-key-file)))
+ (set-buffer buffer)
+ (erase-buffer)
+ (insert key)
+ (save-buffer)
+ (kill-buffer buffer)))
+
+(defun emms-lastfm-client-load-session-key ()
+ "Return stored session key."
+ (let ((file (expand-file-name emms-lastfm-client-session-key-file)))
+ (setq emms-lastfm-client-api-session-key
+ (if (file-readable-p file)
+ (with-temp-buffer
+ (emms-insert-file-contents file)
+ (goto-char (point-min))
+ (buffer-substring-no-properties
+ (point) (point-at-eol)))
+ nil))))
+
+(defun emms-lastfm-client-check-session-key ()
+ "Signal an error condition if there is no session key."
+ (if emms-lastfm-client-api-session-key
+ emms-lastfm-client-api-session-key
+ (if (emms-lastfm-client-load-session-key)
+ emms-lastfm-client-api-session-key
+ (error "no session key for API access"))))
+
+(defun emms-lastfm-client-auth-get-session-ok (data)
+ "Function called on DATA if auth.getSession succeeds."
+ (let ((session-key (nth 2 (nth 5 (cadr data)))))
+ (cond (session-key
+ (setq emms-lastfm-client-api-session-key
+ session-key)
+ (message "Emms Last.fm session key retrieval successful"
+ session-key))
+ (t (error "failed to parse session key data %s" data)))))
+
+(defun emms-lastfm-client-auth-get-session-failed (data)
+ "Function called on DATA if auth.getSession fails."
+ (emms-lastfm-client-default-error-handler data))
+
+;;; ------------------------------------------------------------------
+;;; method: radio.tune [http://www.last.fm/api/show?service=160]
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-construct-radio-tune (station)
+ "Return a request to tune to STATION."
+ (let ((arguments
+ (emms-lastfm-client-encode-arguments
+ `(("sk" . ,emms-lastfm-client-api-session-key)
+ ("station" . ,station)
+ ("api_key" . ,emms-lastfm-client-api-key)))))
+ (emms-lastfm-client-construct-write-method-call
+ 'radio-tune arguments)))
+
+(defun emms-lastfm-client-make-call-radio-tune (station)
+ "Make call to tune to STATION."
+ (let ((url-request-method "POST")
+ (url-request-extra-headers
+ `(("Content-type" . "application/x-www-form-urlencoded")))
+ (url-request-data
+ (emms-lastfm-client-construct-radio-tune station)))
+ (let ((response
+ (url-retrieve-synchronously
+ emms-lastfm-client-api-base-url)))
+ (emms-lastfm-client-handle-response
+ 'radio-tune
+ (with-current-buffer response
+ (xml-parse-region (point-min) (point-max)))))))
+
+(defun emms-lastfm-client-radio-tune-failed (data)
+ "Function called on DATA when tuning fails."
+ (emms-lastfm-client-default-error-handler data))
+
+(defun emms-lastfm-client-radio-tune-ok (data)
+ "Set the current radio station according to DATA."
+ (let ((response (cdr (cadr data)))
+ data)
+ (while response
+ (when (and (listp (car response))
+ (car response)
+ (= (length (car response)) 3))
+ (add-to-list 'data (cons (caar response)
+ (caddr (car response)))))
+ (setq response (cdr response)))
+ (when (not data)
+ (error "could not parse station information %s" data))
+ (setq emms-lastfm-client-tuned-station-alist data)))
+
+;;; ------------------------------------------------------------------
+;;; method: radio.getPlaylist [http://www.last.fm/api/show?service=256]
+;;; ------------------------------------------------------------------
+
+(defun emms-lastfm-client-construct-radio-getplaylist ()
+ "Return a request for a playlist from the tuned station."
+ (let ((arguments
+ (emms-lastfm-client-encode-arguments
+ `(("sk" . ,emms-lastfm-client-api-session-key)
+ ("api_key" . ,emms-lastfm-client-api-key)))))
+ (emms-lastfm-client-construct-write-method-call
+ 'radio-getplaylist arguments)))
+
+(defun emms-lastfm-client-make-call-radio-getplaylist ()
+ "Make call for playlist from the tuned station."
+ (let ((url-request-method "POST")
+ (url-request-extra-headers
+ `(("Content-type" . "application/x-www-form-urlencoded")))
+ (url-request-data
+ (emms-lastfm-client-construct-radio-getplaylist)))
+ (let ((response
+ (url-retrieve-synchronously
+ emms-lastfm-client-api-base-url)))
+ (emms-lastfm-client-handle-response
+ 'radio-getplaylist
+ (with-current-buffer response
+ (xml-parse-region (point-min) (point-max)))))))
+
+(defun emms-lastfm-client-radio-getplaylist-failed (data)
+ "Function called on DATA when retrieving a playlist fails."
+ 'stub-needs-to-handle-playlist-issues
+ (emms-lastfm-client-default-error-handler data))
+
+(defun emms-lastfm-client-list-filter (l)
+ "Remove strings from the roots of list L."
+ (let (acc)
+ (while l
+ (when (listp (car l))
+ (push (car l) acc))
+ (setq l (cdr l)))
+ (reverse acc)))
+
+(defun emms-lastfm-client-radio-getplaylist-ok (data)
+ "Function called on DATA when retrieving a playlist succeeds."
+ (let ((header (emms-lastfm-client-xspf-header data))
+ (tracklist (emms-lastfm-client-xspf-tracklist data)))
+ (emms-lastfm-client-set-timer header)
+ (setq emms-lastfm-client-playlist
+ (emms-lastfm-client-list-filter tracklist))))
+
+(provide 'emms-lastfm-client)
+
+;;; emms-lastfm-client.el ends here