aboutsummaryrefslogtreecommitdiff
path: root/lisp
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
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')
-rw-r--r--lisp/emms-lastfm-client.el743
-rw-r--r--lisp/emms-lastfm.el697
2 files changed, 743 insertions, 697 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
diff --git a/lisp/emms-lastfm.el b/lisp/emms-lastfm.el
deleted file mode 100644
index a58e13a..0000000
--- a/lisp/emms-lastfm.el
+++ /dev/null
@@ -1,697 +0,0 @@
-;;; emms-lastfm.el --- add your listened songs to your profile at last.fm
-
-;; Copyright (C) 2006, 2007, 2008, 2009 Free Software Foundation, Inc.
-
-;; Author: Tassilo Horn <tassilo@member.fsf.org>
-
-;; Keywords: emms, mp3, mpeg, multimedia
-
-;; This file is part of EMMS.
-
-;; 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:
-
-;; This code sends information about what music you are playing to last.fm.
-;; See <URL:http://www.last.fm> and
-;; <URL:http://www.audioscrobbler.net/wiki/Protocol1.1>.
-
-;;; Sample configuration:
-
-;; (setq emms-lastfm-username "my-user-name"
-;; emms-lastfm-password "very-secret!")
-
-;;; Usage:
-
-;; To activate the last.fm emms plugin, run:
-;; `M-x emms-lastfm-enable'
-
-;; Now all music you listen to will be submitted to Last.fm to enhance your
-;; profile.
-
-;; To deactivate the last.fm emms plugin, run:
-;; `M-x emms-lastfm-disable'
-
-;; Beside submitting the tracks you listen to, you can also listen to Last.fm
-;; radio. Simply copy the lastfm:// URL and run & paste:
-;; `M-x emms-lastfm-radio RET lastfm://artist/Britney Spears/fans'
-;; (Of course you don't need to use _this_ URL. :-))
-
-;; You can also insert Last.fm streams into playlists (or use
-;; emms-streams.el to listen to them) by activating the player as
-;; follows.
-;; (add-to-list 'emms-player-list 'emms-player-lastfm-radio)
-;; To insert a Last.fm stream into a playlist, do
-;; (emms-insert-lastfm "lastfm://rest-of-url")
-
-;; There are some functions for conveniently playing the Similar
-;; Artists, Fan Radio, and the Global Tag Radio. Here you only need to
-;; enter the band's name (for the first two) or the tag.
-;; `M-x emms-play-lastfm-similar-artists RET Britney Spears'
-;; `M-x emms-play-lastfm-artist-fan RET Modest Mouse'
-;; `M-x emms-play-lastfm-global-tag RET pop'
-
-;; When you're listening to a Last.fm radio station you have the possibility to
-;; give feedback to them. If you like the current song, type
-;; `M-x emms-lastfm-radio-love'.
-;; If it's not that good, or it just happens to not fit to your actual mood,
-;; type
-;; `M-x emms-lastfm-radio-skip'
-;; and this song will be skipped.
-;; If you really hate that song and you never want to hear it again, ban it by
-;; typing
-;; `M-x emms-lastfm-radio-ban'.
-
-;;; TODO
-;;
-;; - Get the last.fm radio stuff right again. Currently the rating stuff seems
-;; to be broken. There seems to be no official API, so one needs to look
-;; into the sources of the official client which can be found at
-;; http://www.audioscrobbler.net/development/client/.
-
-;; -----------------------------------------------------------------------
-
-(require 'url)
-(require 'emms)
-(require 'emms-mode-line)
-(require 'emms-playing-time)
-(require 'emms-source-file)
-(require 'emms-url)
-
-;;; Variables
-
-(defgroup emms-lastfm nil
- "Interaction with the services offered by http://www.last.fm."
- :prefix "emms-lastfm-"
- :group 'emms)
-
-(defcustom emms-lastfm-username ""
- "Your last.fm username"
- :type 'string
- :group 'emms-lastfm)
-
-(defcustom emms-lastfm-password ""
- "Your last.fm password"
- :type 'string
- :group 'emms-lastfm)
-
-(defcustom emms-lastfm-submission-verbose-p nil
- "If non-nil, display a message every time we submit a track to Last.fm."
- :type 'boolean
- :group 'emms-lastfm)
-
-(defcustom emms-lastfm-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.
-
-Note that it is not very meaningful to submit playlists,
-streamlists, or Last.fm streams to Last.fm."
- :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)))
- :group 'emms-lastfm)
-
-(defconst emms-lastfm-server "http://post.audioscrobbler.com/"
- "The last.fm server responsible for the handshaking
-procedure. Only for internal use.")
-(defconst emms-lastfm-client-id "ems"
- "The client ID of EMMS. Don't change it!")
-(defconst emms-lastfm-client-version 0.2
- "The version registered at last.fm. Don't change it!")
-(defconst emms-lastfm-protocol-version 1.2
- "The version of the supported last.fm protocol. Don't change it.")
-
-;; used internally
-(defvar emms-lastfm-process nil "-- only used internally --")
-(defvar emms-lastfm-session-id nil "-- only used internally --")
-(defvar emms-lastfm-now-playing-url nil "-- only used internally --")
-(defvar emms-lastfm-submit-url nil "-- only used internally --")
-(defvar emms-lastfm-current-track nil "-- only used internally --")
-(defvar emms-lastfm-timer nil "-- only used internally --")
-(defvar emms-lastfm-current-track-starting-time-string nil "-- only used internally --")
-
-;;; Scrobbling
-
-(defun emms-lastfm-new-track-function ()
- "This function should run whenever a new track starts (or a
-paused track resumes) and sets the track submission timer."
- (setq emms-lastfm-current-track
- (emms-playlist-current-selected-track))
- (setq emms-lastfm-current-track-starting-time-string
- (emms-lastfm-current-unix-time-string))
- ;; Tracks should be submitted, if they played 240 secs or half of their
- ;; length, whichever comes first.
- (let ((secs (emms-track-get emms-lastfm-current-track 'info-playing-time))
- (type (emms-track-type emms-lastfm-current-track)))
- (when (and secs
- (or (eq emms-lastfm-submit-track-types t)
- (and (listp emms-lastfm-submit-track-types)
- (memq type emms-lastfm-submit-track-types))))
- (when (> secs 240)
- (setq secs 240))
- (unless (< secs 30) ;; Skip titles shorter than 30 seconds
- (setq secs (- (/ secs 2) emms-playing-time))
- (unless (< secs 0)
- (setq emms-lastfm-timer
- (run-with-timer secs nil 'emms-lastfm-submit-track))))))
- ;; Update the now playing info displayed on the user's last.fm page. This
- ;; doesn't affect the user's profile, so it can be done even for tracks that
- ;; should not be submitted.
- (emms-lastfm-submit-now-playing))
-
-(defun emms-lastfm-http-POST (url string sentinel &optional sentinel-args)
- "Perform a HTTP POST request to URL using STRING as data.
-STRING will be encoded to utf8 before the request. Call SENTINEL
-with the result buffer."
- (let ((url-http-attempt-keepalives nil)
- (url-show-status emms-lastfm-submission-verbose-p)
- (url-request-method "POST")
- (url-request-extra-headers
- '(("Content-type"
- . "application/x-www-form-urlencoded; charset=utf-8")))
- (url-request-data (encode-coding-string string 'utf-8)))
- (url-retrieve url sentinel sentinel-args)))
-
-(defun emms-lastfm-http-GET (url sentinel &optional sentinel-args)
- "Perform a HTTP GET request to URL.
-Call SENTINEL with SENTINEL-ARGS and the result buffer."
- (let ((url-show-status emms-lastfm-submission-verbose-p)
- (url-request-method "GET"))
- (url-retrieve url sentinel sentinel-args)))
-
-(defun emms-lastfm-submit-now-playing ()
- "Submit now-playing infos to last.fm.
-These will be displayed on the user's last.fm page."
- (let* ((artist (emms-track-get emms-lastfm-current-track 'info-artist))
- (title (emms-track-get emms-lastfm-current-track 'info-title))
- (album (emms-track-get emms-lastfm-current-track 'info-album))
- (track-number (emms-track-get emms-lastfm-current-track
- 'info-tracknumber))
- (musicbrainz-id "")
- (track-length (number-to-string
- (or (emms-track-get emms-lastfm-current-track
- 'info-playing-time)
- 0))))
- ;; wait up to 5 seconds to submit np infos in order to finish handshaking.
- (dotimes (i 5)
- (when (not (and emms-lastfm-session-id
- emms-lastfm-now-playing-url))
- (sit-for 1)))
- (when (and emms-lastfm-session-id
- emms-lastfm-now-playing-url)
- (emms-lastfm-http-POST emms-lastfm-now-playing-url
- (concat "&s=" emms-lastfm-session-id
- "&a[0]=" (emms-url-quote artist)
- "&t[0]=" (emms-url-quote title)
- "&b[0]=" (emms-url-quote album)
- "&l[0]=" track-length
- "&n[0]=" track-number
- "&m[0]=" musicbrainz-id)
- 'emms-lastfm-submit-now-playing-sentinel))))
-
-(defun emms-lastfm-submit-now-playing-sentinel (&rest args)
- "Parses the server reponse and inform the user if all worked
-well or if an error occured."
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (goto-char (point-min))
- ;; skip to the first empty line and go one line further. There the last.fm
- ;; response starts.
- (re-search-forward "^$" nil t)
- (forward-line)
- (if (re-search-forward "^OK$" nil t)
- (progn
- (when emms-lastfm-submission-verbose-p
- (message "EMMS: Now playing infos submitted to last.fm"))
- (kill-buffer buffer))
- (message "EMMS: Now playing infos couldn't be submitted to last.fm: %s"
- (emms-read-line)))))
-
-(defun emms-lastfm-cancel-timer ()
- "Cancels `emms-lastfm-timer' if it is running."
- (emms-cancel-timer emms-lastfm-timer)
- (setq emms-lastfm-timer nil))
-
-(defun emms-lastfm-pause ()
- "Handles things to be done when the player is paused or
-resumed."
- (if emms-player-paused-p
- ;; the player paused
- (emms-lastfm-cancel-timer)
- ;; The player resumed
- (emms-lastfm-new-track-function)))
-
-(defun emms-lastfm (&optional ARG)
- "Start submitting the tracks you listened to to
-http://www.last.fm, if ARG is positive. If ARG is negative or
-zero submission of the tracks will be stopped. This applies to
-the current track, too."
- (interactive "p")
- (cond
- ((not (and emms-lastfm-username emms-lastfm-password))
- (message "%s"
- (concat "EMMS: In order to activate the last.fm plugin you "
- "first have to set both `emms-lastfm-username' and "
- "`emms-lastfm-password'")))
- ((not emms-playing-time-p)
- (message "%s"
- (concat "EMMS: The last.fm plugin needs the functionality "
- "provided by `emms-playing-time'. It seems that you "
- "disabled it explicitly in your init file using code "
- "like this: `(emms-playing-time -1)'. Delete that "
- "line and have a look at `emms-playing-time's doc "
- "string")))
- (t
- (if (and ARG (> ARG 0))
- (progn
- ;; Append it. Else the playing time could be started a bit too late.
- (add-hook 'emms-player-started-hook
- 'emms-lastfm-handshake-if-needed t)
- ;; Has to be appended, because it has to run after
- ;; `emms-playing-time-start'
- (add-hook 'emms-player-started-hook
- 'emms-lastfm-new-track-function t)
- (add-hook 'emms-player-stopped-hook
- 'emms-lastfm-cancel-timer)
- (add-hook 'emms-player-paused-hook
- 'emms-lastfm-pause)
- ;; Clean up after EMMS radio
- (remove-hook 'emms-player-started-hook
- 'emms-lastfm-cancel-timer-after-stop)
- (message "EMMS Last.fm plugin activated"))
- (remove-hook 'emms-player-started-hook
- 'emms-lastfm-handshake-if-needed)
- (remove-hook 'emms-player-started-hook
- 'emms-lastfm-new-track-function)
- (remove-hook 'emms-player-stopped-hook
- 'emms-lastfm-cancel-timer)
- (remove-hook 'emms-player-paused-hook
- 'emms-lastfm-pause)
- (when emms-lastfm-timer (emms-cancel-timer emms-lastfm-timer))
- (setq emms-lastfm-session-id nil
- emms-lastfm-submit-url nil
- emms-lastfm-process nil
- emms-lastfm-current-track nil)
- (message "EMMS Last.fm plugin deactivated")))))
-
-(defalias 'emms-lastfm-activate 'emms-lastfm)
-(emms-make-obsolete 'emms-lastfm-activate 'emms-lastfm "EMMS 2.2")
-
-(defun emms-lastfm-enable ()
- "Enable the emms last.fm plugin."
- (interactive)
- (emms-lastfm 1))
-
-(defun emms-lastfm-disable ()
- "Disable the emms last.fm plugin."
- (interactive)
- (emms-lastfm -1))
-
-(defun emms-lastfm-restart ()
- "Disable and reenable the last.fm plugin. This will cause a new
-handshake."
- (emms-lastfm-disable)
- (emms-lastfm-enable))
-
-(defun emms-lastfm-handshake-if-needed ()
- (when (not (and emms-lastfm-session-id
- emms-lastfm-submit-url
- emms-lastfm-now-playing-url))
- (emms-lastfm-handshake)))
-
-(defun emms-lastfm-current-unix-time-string ()
- (replace-regexp-in-string "\\..*" "" (number-to-string (float-time))))
-
-(defun emms-lastfm-handshake ()
- "Handshakes with the last.fm server."
- (let ((timestamp (emms-lastfm-current-unix-time-string)))
- (emms-lastfm-http-GET
- (concat emms-lastfm-server
- "?hs=true"
- "&p=" (number-to-string emms-lastfm-protocol-version)
- "&c=" emms-lastfm-client-id
- "&v=" (number-to-string emms-lastfm-client-version)
- "&u=" (emms-url-quote emms-lastfm-username)
- "&t=" timestamp
- "&a=" (md5 (concat (md5 emms-lastfm-password) timestamp)))
- 'emms-lastfm-handshake-sentinel)))
-
-(defun emms-lastfm-handshake-sentinel (&rest args)
- "Parses the server reponse and inform the user if all worked
-well or if an error occured."
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (goto-char (point-min))
- ;; skip to the first empty line and go one line further. There the last.fm
- ;; response starts.
- (re-search-forward "^$" nil t)
- (forward-line)
- (let ((response (emms-read-line)))
- (if (not (string-match (rx (or "OK")) response))
- (message "EMMS: Handshake failed: %s" response)
- (forward-line)
- (setq emms-lastfm-session-id (emms-read-line))
- (forward-line)
- (setq emms-lastfm-now-playing-url (emms-read-line))
- (forward-line)
- (setq emms-lastfm-submit-url (emms-read-line))
- (message "EMMS: Handshaking with server done")
- (kill-buffer buffer)))))
-
-(defun emms-lastfm-submit-track ()
- "Submits the current track (`emms-lastfm-current-track') to
-last.fm."
- (let* ((artist (emms-track-get emms-lastfm-current-track 'info-artist))
- (title (emms-track-get emms-lastfm-current-track 'info-title))
- (album (emms-track-get emms-lastfm-current-track 'info-album))
- (track-number (emms-track-get emms-lastfm-current-track 'info-tracknumber))
- (musicbrainz-id "")
- (track-length (number-to-string
- (emms-track-get emms-lastfm-current-track
- 'info-playing-time))))
- (emms-lastfm-http-POST
- emms-lastfm-submit-url
- (concat "&s=" emms-lastfm-session-id
- "&a[0]=" (emms-url-quote artist)
- "&t[0]=" (emms-url-quote title)
- "&i[0]=" emms-lastfm-current-track-starting-time-string
- "&o[0]=P" ;; TODO: Maybe support others. See the API.
- "&r[0]=" ;; The rating. Empty if not applicable (for P it's not)
- "&l[0]=" track-length
- "&b[0]=" (emms-url-quote album)
- "&n[0]=" track-number
- "&m[0]=" musicbrainz-id)
- 'emms-lastfm-submission-sentinel)))
-
-(defun emms-lastfm-submission-sentinel (&rest args)
- "Parses the server reponse and inform the user if all worked
-well or if an error occured."
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (goto-char (point-min))
- ;; skip to the first empty line and go one line further. There the last.fm
- ;; response starts.
- (re-search-forward "^$" nil t)
- (forward-line)
- (if (re-search-forward "^OK$" nil t)
- (progn
- (when emms-lastfm-submission-verbose-p
- (message "EMMS: \"%s\" submitted to last.fm"
- (emms-track-description emms-lastfm-current-track)))
- (kill-buffer buffer))
- (message "EMMS: Song couldn't be submitted to last.fm: %s"
- (emms-read-line)))))
-
-;;; Playback of lastfm:// streams
-
-(defgroup emms-player-lastfm-radio nil
- "EMMS player for Last.fm streams."
- :group 'emms-player
- :prefix "emms-player-lastfm-")
-
-(defcustom emms-player-lastfm-radio (emms-player 'emms-lastfm-radio-start
- 'ignore ; no need to stop
- 'emms-lastfm-radio-playable-p)
- "*Parameters for the Last.fm radio player."
- :type '(cons symbol alist)
- :group 'emms-player-lastfm-radio)
-
-(defconst emms-lastfm-radio-base-url "http://ws.audioscrobbler.com/radio/"
- "The base URL for playing lastfm:// stream.
--- only used internally --")
-
-(defvar emms-lastfm-radio-session nil "-- only used internally --")
-(defvar emms-lastfm-radio-stream-url nil "-- only used internally --")
-
-(defun emms-lastfm-radio-get-handshake-url ()
- (concat emms-lastfm-radio-base-url
- "handshake.php?version=" (number-to-string
- emms-lastfm-client-version)
- "&platform=" emms-lastfm-client-id
- "&username=" (emms-url-quote emms-lastfm-username)
- "&passwordmd5=" (md5 emms-lastfm-password)
- "&debug=" (number-to-string 9)))
-
-(defun emms-lastfm-radio-handshake (fn radio-url)
- "Handshakes with the last.fm server.
-Calls FN when done with RADIO-URL as its only argument."
- (emms-lastfm-http-GET (emms-lastfm-radio-get-handshake-url)
- 'emms-lastfm-radio-handshake-sentinel
- (list fn radio-url)))
-
-(defun emms-lastfm-radio-handshake-sentinel (status fn radio-url)
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (setq emms-lastfm-radio-session (emms-key-value "session"))
- (setq emms-lastfm-radio-stream-url (emms-key-value "stream_url"))
- (kill-buffer buffer)
- (if (and emms-lastfm-radio-session emms-lastfm-radio-stream-url)
- (progn
- (message "EMMS: Handshaking for Last.fm playback successful")
- (funcall fn radio-url))
- (message "EMMS: Failed handshaking for Last.fm playback"))))
-
-(defun emms-lastfm-radio-1 (lastfm-url)
- "Internal function used by `emms-lastfm-radio'."
- (if (and emms-lastfm-radio-session
- emms-lastfm-radio-stream-url)
- (progn
- (emms-lastfm-http-GET
- (concat emms-lastfm-radio-base-url
- "adjust.php?"
- "session=" emms-lastfm-radio-session
- "&url=" (emms-url-quote lastfm-url)
- "&debug=" (number-to-string 0))
- 'emms-lastfm-radio-sentinel))
- (message "EMMS: Cannot play Last.fm stream")))
-
-(defun emms-lastfm-radio (lastfm-url)
- "Plays the stream associated with the given Last.fm URL. (A
-Last.fm URL has the form lastfm://foo/bar/baz, e.g.
-
- lastfm://artist/Manowar/similarartists
-
-or
-
- lastfm://globaltags/metal."
- (interactive "sLast.fm URL: ")
- ;; Streamed songs must not be added to the lastfm profile
- (emms-lastfm-disable)
- (if (not (and emms-lastfm-radio-session
- emms-lastfm-radio-stream-url))
- (emms-lastfm-radio-handshake #'emms-lastfm-radio-1 lastfm-url)
- (emms-lastfm-radio-1 lastfm-url)))
-
-(defun emms-lastfm-radio-playable-p (track)
- "Determine whether the Last.fm player can play this track."
- (let ((name (emms-track-get track 'name))
- (type (emms-track-get track 'type)))
- (and (eq type 'lastfm)
- (string-match "^lastfm://" name))))
-
-(defun emms-lastfm-radio-start (track)
- "Start playing TRACK."
- (when (emms-lastfm-radio-playable-p track)
- (let ((name (emms-track-get track 'name)))
- (emms-lastfm-radio name))))
-
-(defcustom emms-lastfm-radio-metadata-period 15
- "When listening to Last.fm Radio every how many seconds should
-emms-lastfm poll for metadata? If set to nil, there won't be any
-polling at all.
-
-The default is 15: That means that the mode line will display the
-wrong (last) track's data for a maximum of 15 seconds. If your
-network connection has a big latency this value may be too
-high. (But then streaming a 128KHz mp3 won't be fun anyway.)"
- :type '(choice integer
- (const :tag "Disable" nil))
- :group 'emms-lastfm)
-
-(defun emms-lastfm-cancel-timer-after-stop ()
- (add-hook 'emms-player-stopped-hook
- 'emms-lastfm-cancel-timer))
-
-(defun emms-lastfm-radio-sentinel (&rest args)
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (if (string= (emms-key-value "response" buffer) "OK")
- (progn
- (kill-buffer buffer)
- (add-hook 'emms-player-started-hook
- 'emms-lastfm-cancel-timer-after-stop)
- (emms-play-url emms-lastfm-radio-stream-url)
- (when emms-lastfm-radio-metadata-period
- (when emms-lastfm-timer
- (emms-lastfm-cancel-timer))
- (setq emms-lastfm-timer
- (run-with-timer 0 emms-lastfm-radio-metadata-period
- 'emms-lastfm-radio-request-metadata)))
- (message "EMMS: Playing Last.fm stream"))
- (kill-buffer buffer)
- (message "EMMS: Bad response from Last.fm"))))
-
-(defun emms-lastfm-np (&optional insertp callback)
- "Show the currently-playing lastfm radio tune.
-
-If INSERTP is non-nil, insert the description into the current
-buffer instead.
-
-If CALLBACK is a function, call it with the current buffer and
-description as arguments instead of displaying the description or
-inserting it."
- (interactive "P")
- (emms-lastfm-radio-request-metadata
- (lambda (status insertp buffer callback)
- (let ((response-buf (current-buffer))
- artist title)
- (emms-http-decode-buffer response-buf)
- (setq artist (emms-key-value "artist" response-buf)
- title (emms-key-value "track" response-buf))
- (kill-buffer response-buf)
- (let ((msg (if (and title artist)
- (format emms-show-format
- (format "%s - %s" artist title))
- "Nothing playing right now")))
- (cond ((functionp callback)
- (when (and title artist)
- (funcall callback buffer msg)))
- ((and insertp title artist)
- (with-current-buffer buffer
- (insert msg)))
- (t (message msg))))))
- (list insertp (current-buffer) callback)))
-
-(defun emms-lastfm-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-play-lastfm-similar-artists (artist)
- "Plays the similar artist radio of ARTIST."
- (interactive (list (emms-lastfm-read-artist)))
- (emms-lastfm-radio (concat "lastfm://artist/"
- artist
- "/similarartists")))
-
-(defun emms-play-lastfm-global-tag (tag)
- "Plays the global tag radio of TAG."
- (interactive "sGlobal Tag: ")
- (emms-lastfm-radio (concat "lastfm://globaltags/" tag)))
-
-(defun emms-play-lastfm-artist-fan (artist)
- "Plays the artist fan radio of ARTIST."
- (interactive (list (emms-lastfm-read-artist)))
- (emms-lastfm-radio (concat "lastfm://artist/" artist "/fans")))
-
-(defun emms-lastfm-radio-love ()
- "Inform Last.fm that you love the currently playing song."
- (interactive)
- (emms-lastfm-radio-rating "love"))
-
-(defun emms-lastfm-radio-skip ()
- "Inform Last.fm that you want to skip the currently playing
-song."
- (interactive)
- (emms-lastfm-radio-rating "skip"))
-
-(defun emms-lastfm-radio-ban ()
- "Inform Last.fm that you want to ban the currently playing
-song."
- (interactive)
- (emms-lastfm-radio-rating "ban"))
-
-(defun emms-lastfm-radio-rating (command)
- (emms-lastfm-http-GET
- (concat emms-lastfm-radio-base-url
- "control.php?"
- "session=" emms-lastfm-radio-session
- "&command=" command
- "&debug=" (number-to-string 0))
- 'emms-lastfm-radio-rating-sentinel))
-
-(defun emms-lastfm-radio-rating-sentinel (&rest args)
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (if (string= (emms-key-value "response" buffer) "OK")
- (message "EMMS: Rated current track")
- (message "EMMS: Rating failed"))
- (kill-buffer buffer)))
-
-(defun emms-lastfm-radio-request-metadata (&optional fn data)
- "Request the metadata of the current song and display it.
-
-If FN is given, call it instead of
-`emms-lastfm-radio-request-metadata-sentinel', with DATA as its
-first parameter.
-
-If DATA is given, it should be a list."
- (interactive)
- (emms-lastfm-http-GET
- (concat emms-lastfm-radio-base-url
- "np.php?"
- "session=" emms-lastfm-radio-session
- "&debug=" (number-to-string 0))
- (or fn 'emms-lastfm-radio-request-metadata-sentinel)
- data))
-
-(defun emms-lastfm-radio-request-metadata-sentinel (&rest args)
- (let ((buffer (current-buffer)))
- (emms-http-decode-buffer buffer)
- (let ((artist (emms-key-value "artist" buffer))
- (title (emms-key-value "track" buffer))
- (track (emms-playlist-current-selected-track)))
- (kill-buffer buffer)
- (emms-track-set track 'info-artist artist)
- (emms-track-set track 'info-title title)
- (emms-track-updated track))))
-
-
-;;; Utility functions
-
-(defun emms-read-line ()
- (buffer-substring-no-properties (line-beginning-position)
- (line-end-position)))
-
-(defun emms-key-value (key &optional buffer)
- "Returns the value of KEY from BUFFER.
-If BUFFER is nil, use the current buffer.
-
-BUFFER has to contain a key-value list like:
-
-foo=bar
-x=17"
- (unless (and buffer (not (buffer-live-p buffer)))
- (with-current-buffer (or buffer (current-buffer))
- (goto-char (point-min))
- (when (re-search-forward (concat "^" key "=") nil t)
- (buffer-substring-no-properties (point) (line-end-position))))))
-
-(provide 'emms-lastfm)
-;;; emms-lastfm.el ends here