;; elfm.el - a rudimentary tuner for last.fm radio ;; - written by cms@last.fm for the last.fm radio hackday (require 'cl) (require 'url) (require 'url-http) (require 'http-post-simple) (require 'json) ;; nasty nasty global variables ;; here's some configuration (defvar elfm-lastfm-api-key "30cea194341f735c7b05c81d9eab2c92" "api key for elfm.el, cheekily registered as emacs") (defvar elfm-lastfm-api-secret "e792580541a7bc3a3233c10b2c8f5a8d" "api secret used to sign elfm methods" ) (defvar elfm-lastfm-user-name nil "name of the user's lastfm account" ) (defvar elfm-lastfm-is-subscriber 0 "1 or 0, indicating subscriber status of signed in user") ;; here's some state (defvar elfm-lastfm-auth-token nil "temporary auth token granted us during application / user authentication stage") (defvar elfm-lastfm-session-key "reusable session key identifying our radio stations") (defvar *handle-event*) ; store callbacks in here, for post processing async requests (defvar elfm-lastfm-request-mode 'sync) ; set this to sync or async for blocking or non-blocking network requests (defun elfm-get-http-headers(buf) "extract the http headers from the supplied buffer, which is assumed to have a http response in it" (with-current-buffer (get-buffer buf) (buffer-substring (point-min) (end-of-header-position)))) (defun elfm-get-http-content(buf) "extract the document from the supplied buffer, which is assumed to have a http response in it" (with-current-buffer (get-buffer buf) (buffer-substring (+ 1 (end-of-header-position)) (point-max)))) (defun elfm-async-req-handler (status &optional args) "this is the callback routine for handling asynchronous http requests" ;; FIXME - async doesn't really work any more, but there's no reason why it can't given time and attention ;; sync was just far easier to debug while I struggled to remember how elisp variable scoping works (with-current-buffer (clear-buffer (get-buffer-create "*lastfm-request-status*")) (insert (format "\n%S" status))) (let ((headers (elfm-get-http-headers (current-buffer))) (content (elfm-get-http-content (current-buffer)))) (with-current-buffer (clear-buffer (get-buffer-create "*lastfm-request-headers*")) (insert headers)) (with-current-buffer (clear-buffer (get-buffer-create "*lastfm-request-content*")) (insert content))) (if (eq 'async elfm-lastfm-request-mode) (funcall *handle-event* ()))) (defun elfm-http-get-request(url) "dispatch GET url to either sync or async requester, depending on configuration " (cond ((eq 'sync elfm-lastfm-request-mode) (elfm-http-sync-get url)) ((or (eq 'async elfm-lastfm-request-mode) t) (elfm-http-async-get url)))) (defun elfm-http-async-get(url) (url-retrieve url 'elfm-async-req-handler)) (defun elfm-http-sync-get (url) (with-current-buffer (url-retrieve-synchronously url) (elfm-async-req-handler '(nil)))) (defun elfm-alist-to-flat-sorted-list (params) (elfm-flatten-list (elfm-sort-alist-keys params))) ; fished this out of my .elisp directory, but it's too clever by half for me ; I think I probably got it from a textbook (defun elfm-flatten-list (x) (labels ((rec (x acc) (cond ((null x) acc) ((atom x) (cons x acc)) (t (rec (car x) (rec (cdr x) acc)))))) (rec x nil))) (defun elfm-lastfm-api-signature (params) "calculates the md5 signature for an alist of lastfm API parameters" (md5 (elfm-join-param-list (append (elfm-alist-to-flat-sorted-list params) (list elfm-lastfm-api-secret)) ""))) (defun elfm-keypair-alist(alist) "supplied an alist, convert it into a list of strings of the form (\"k1=v1\" \"k2=v2\"" (mapcar (lambda (x) (mapconcat 'identity x "=")) (mapcar (lambda (pair) (list (symbol-name (car pair)) (cdr pair))) alist))) (defun elfm-join-param-list (params glue) "take a flat ordered list of symbols or strings and concatenate them into a string, converting symbol names" (mapconcat (lambda (x) (cond ( (symbolp x) (symbol-name x)) (t x ))) params glue)) (defun elfm-signed-params-alist (params) "given a list of params, make an alist , append an api signature" (setq params (elfm-list-to-alist params)) (acons 'api_sig (elfm-lastfm-api-signature params) params)) (defun elfm-lastfm-auth-token-url () (let ((params (elfm-signed-params-alist `(api_key ,elfm-lastfm-api-key method "auth.getToken")))) (elfm-api-request-url (elfm-join-param-list (elfm-keypair-alist params) "&")))) (defun elfm-lastfm-user-session-url () (let ((params (elfm-signed-params-alist `(api_key ,elfm-lastfm-api-key token ,elfm-lastfm-auth-token method "auth.getSession")))) (elfm-api-request-url (elfm-join-param-list (elfm-keypair-alist params) "&")))) (defun elfm-lastfm-getplaylist-url () (let ((params (acons 'format "json" (elfm-signed-params-alist `(api_key ,elfm-lastfm-api-key sk ,elfm-lastfm-session-key method "radio.getPlaylist" ))))) (elfm-api-request-url (elfm-join-param-list (elfm-keypair-alist params) "&")))) (defun elfm-lastfm-auth-url () (let ((params (elfm-list-to-alist `(api_key ,elfm-lastfm-api-key token ,elfm-lastfm-auth-token)))) (format "http://last.fm/api/auth/?%s" (elfm-join-param-list (elfm-keypair-alist params) "&")))) ; debugging with a local webserver if you're on the train (defun elfm-api-request-url-remote (qstring) (format "http://ws.audioscrobbler.com/2.0/?%s" qstring)) (defun elfm-api-request-url-local (qstring) (format "http://localhost/~cms/token.xml?%s" qstring)) (defun elfm-api-request-url (qstring) (elfm-api-request-url-remote qstring)) ; only idiots parse XML with regular expressions (defun elfm-response-get-tag-content(tag) (goto-char (point-min)) (let ((start (re-search-forward (format "<%s>" tag))) (end (and (re-search-forward (format "" tag)) (re-search-backward "<")))) (buffer-substring-no-properties start end))) (defun elfm-response-get-tag-attribute(tag attr) (goto-char (point-min)) (re-search-forward (format "<%s %s=\"\\(.+?\\)\">" tag attr)) (match-string-no-properties 1)) (defun elfm-make-auth-token-request() (elfm-http-get-request (elfm-lastfm-auth-token-url))) (defun elfm-lastfm-authenticate () (elfm-make-auth-token-request) (with-current-buffer "*lastfm-request-content*" (setq elfm-lastfm-auth-token (elfm-lastfm-response-get-auth-token))) (elfm-open-url-in-browser (elfm-lastfm-auth-url))) ; one day I might add error checking (defun elfm-response-ok-p () (with-current-buffer (get-buffer-create "*lastfm-request-content*") (let ((status (elfm-response-get-tag-attribute "lfm" "status"))) (equal status "ok")))) (defun elfm-lastfm-response-get-auth-token() (with-current-buffer (get-buffer-create "*lastfm-request-content*") (elfm-response-get-tag-content "token"))) (defun elfm-request-user-session() (elfm-http-get-request (elfm-lastfm-user-session-url))) (defun elfm-open-url-in-browser (url) (start-process "browser-process" nil "open" url)) (defun elfm-new-lastfm-user-session () (elfm-request-user-session) (with-current-buffer "*lastfm-request-content*" (setq elfm-lastfm-user-name (elfm-response-get-tag-content "name")) (setq elfm-lastfm-session-key (elfm-response-get-tag-content "key")) (setq elfm-lastfm-is-subscriber (elfm-response-get-tag-content "subscriber")))) (defun elfm-tune-lastfm-radio (lfmurl) (let ((params (elfm-signed-params-alist `(api_key ,elfm-lastfm-api-key sk ,elfm-lastfm-session-key method "radio.tune" station ,lfmurl)))) (http-post-simple "http://ws.audioscrobbler.com/2.0/" params))) ; hack hack hack hack hack (defun elfm-reduce-json-playlist () (let* ((json-blob (json-read-from-string (with-current-buffer "*lastfm-request-content*" (buffer-string)))) (track_v (cdr (assoc 'track (assoc 'trackList (assoc 'playlist json-blob))))) (trackprops (mapcar (lambda (a_list) (cdr a_list)) track_v))) (mapcar (lambda(t_alist) (map 'list (lambda(kk) (assoc kk t_alist)) '(location title creator))) trackprops))) ; I ran out of time, panicked and used eval (defun elfm-play-playlist-mpg123 (playlist) (let ((trackurls (mapcar(lambda (x) (cdr (assoc 'location x))) playlist))) (eval (append '(start-process "mpgplayer" (get-buffer-create "*lastfm-radio*") "/opt/local/bin/mpg123") trackurls nil)) (switch-to-buffer (clear-buffer (get-buffer-create "*lastfm-radio-playlist*"))) (mapc (lambda(elt) (insert (format "%s : %s \n" (assoc 'title elt) (assoc 'creator elt) ))) playlist))) (defun elfm-list-to-alist (lst) "destructively convert an even sized list to an list of dot pairs" (and (evenp (length lst)) (cdr lst) (setf (car lst) (cons (car lst) (cadr lst)) (cdr lst) (cddr lst)) (elfm-list-to-alist (cdr lst))) lst) (defun elfm-sort-alist-keys (lst) "sort a supplied list by the symbol names of it's elements cars" (sort lst (lambda (a b) (string< (symbol-name (car a)) (symbol-name (car b))))))