aboutsummaryrefslogtreecommitdiff
path: root/emms-librefm-scrobbler.el
diff options
context:
space:
mode:
Diffstat (limited to 'emms-librefm-scrobbler.el')
-rw-r--r--emms-librefm-scrobbler.el327
1 files changed, 327 insertions, 0 deletions
diff --git a/emms-librefm-scrobbler.el b/emms-librefm-scrobbler.el
new file mode 100644
index 0000000..a49a458
--- /dev/null
+++ b/emms-librefm-scrobbler.el
@@ -0,0 +1,327 @@
+;;; emms-librefm-scrobbler.el --- Libre.FM Scrobbing API
+
+;; Copyright (C) 2014 Free Software Foundation, Inc.
+
+;; Author: Yoni Rabkin <yrk@gnu.org>
+
+;; Keywords: emms, libre.fm, GNU FM
+
+;; 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:
+
+;; To use libre.fm you need to add username and password to
+;; ~/.authinfo.gpg or an equivalent file understood by auth-source.
+;; To enable scrobbling call (emms-librefm-scrobbler-enable).
+
+;;; Code:
+
+(require 'emms-playing-time)
+(require 'auth-source)
+
+
+(defvar emms-librefm-scrobbler-handshake-url
+ "turtle.libre.fm"
+ "Endpoint for client handshake.")
+
+(defvar emms-librefm-scrobbler-method
+ "https"
+ "Transfer method.")
+
+(defvar emms-librefm-scrobbler-username nil
+ "Libre.fm username.
+
+Note that the preferred way of authenticating is using authinfo
+and only setting `emms-librefm-scrobbler-handshake-url'. See the
+manual for details.")
+
+(defvar emms-librefm-scrobbler-password nil
+ "Libre.fm user password.
+
+Note that the preferred way of authenticating is using authinfo.
+See also `emms-librefm-scrobbler-username'.")
+
+(defvar emms-librefm-scrobbler-debug
+ ""
+ "Debugging variable to store communication.")
+
+(defvar emms-librefm-scrobbler-session-id
+ nil
+ "Session ID for Libre.fm.")
+
+(defvar emms-librefm-scrobbler-now-playing-url
+ ""
+ "URL for getting the track playing.")
+
+(defvar emms-librefm-scrobbler-submission-url
+ ""
+ "URL for submissions.")
+
+(defvar emms-librefm-scrobbler-track-play-start-timestamp
+ nil
+ "Time when a track started playing.")
+
+(defvar emms-librefm-scrobbler-display-submissions
+ t
+ "Whether to display a user message on every submission.")
+
+
+;;; ------------------------------------------------------------------
+;;; authenticate
+;;; ------------------------------------------------------------------
+(defun emms-librefm-scrobbler--get-auth-detail (token)
+ "Return TOKEN from auth-source.
+TOKEN is :user of :secret."
+ ;; TODO: Maybe we should enable :create t here. But it could be
+ ;; kind of annoying as it makes a pop-up when no name is present.
+ (plist-get
+ (car (auth-source-search :host (list emms-librefm-scrobbler-handshake-url "libre.fm")
+ :user (unless (equal emms-librefm-scrobbler-username "")
+ emms-librefm-scrobbler-username)
+ :max 1 :require '(:user :secret)))
+ token))
+
+(defun emms-librefm-scrobbler--username ()
+ "Return username for libre.fm."
+ (or (emms-librefm-scrobbler--get-auth-detail :user)
+ emms-librefm-scrobbler-username))
+
+(defun emms-librefm-scrobbler--password ()
+ "Return password for libre.fm."
+ (let ((token (emms-librefm-scrobbler--get-auth-detail :secret)))
+ (cond ((functionp token) (funcall token))
+ ((characterp token) token)
+ (t emms-librefm-scrobbler-password))))
+
+;;; ------------------------------------------------------------------
+;;; handshake
+;;; ------------------------------------------------------------------
+
+(defun emms-librefm-scrobbler-handshake-string (url username password)
+ "Return the client handshake string."
+ (when (= 0 (length url))
+ (error "bad url"))
+ (when (= 0 (length username))
+ (error "bad username"))
+ (when (= 0 (length password))
+ (error "bad password"))
+ (let ((timestamp (format-time-string "%s")))
+ (concat emms-librefm-scrobbler-method
+ "://"
+ url "/?"
+ "hs=true" "&"
+ "p=1.2" "&"
+ "c=emm" "&"
+ "v=1.0" "&"
+ "u=" (url-encode-url username) "&"
+ "t=" timestamp "&"
+ "a=" (md5 (concat (md5 password) timestamp)))))
+
+(defun emms-librefm-scrobbler-handshake-call (url username password)
+ "Perform client handshake and return a response in a buffer."
+ (let ((url-request-method "POST"))
+ (let ((response
+ (url-retrieve-synchronously
+ (emms-librefm-scrobbler-handshake-string
+ url username password))))
+ (setq emms-librefm-scrobbler-debug
+ (with-current-buffer response
+ (buffer-substring-no-properties (point-min)
+ (point-max))))
+ response)))
+
+(defun emms-librefm-scrobbler-handle-handshake-response (resbuf)
+ "Handle the client handshake server response."
+ (when (not (bufferp resbuf))
+ (error "response not a buffer"))
+ (with-current-buffer resbuf
+ (goto-char (point-min))
+ (when (not (re-search-forward "^.*200 OK$" (point-at-eol) t))
+ (error "bad HTTP server response"))
+ ;; go to the start of the FM response
+ (when (not (re-search-forward "\n\n" (point-max) t))
+ (error "bad FM server response"))
+ (let ((status (buffer-substring (point-at-bol)
+ (point-at-eol))))
+ (when (not (string= status "OK"))
+ (error "FM server returned: %s" status))
+ (let (session-id
+ now-playing-url
+ submission-url)
+ (forward-line 1)
+ (setq session-id (buffer-substring (point-at-bol)
+ (point-at-eol)))
+ (forward-line 1)
+ (setq now-playing-url (buffer-substring (point-at-bol)
+ (point-at-eol)))
+ (forward-line 1)
+ (setq submission-url (buffer-substring (point-at-bol)
+ (point-at-eol)))
+ (when (or (= 0 (length session-id))
+ (= 0 (length now-playing-url))
+ (= 0 (length submission-url)))
+ (error "couldn't parse FM server response"))
+ (setq emms-librefm-scrobbler-session-id session-id
+ emms-librefm-scrobbler-now-playing-url now-playing-url
+ emms-librefm-scrobbler-submission-url submission-url)
+ (message "handshake successful")))))
+
+(defun emms-librefm-scrobbler-handshake ()
+ "Perform client handshake call and handle response."
+ (emms-librefm-scrobbler-handle-handshake-response
+ (emms-librefm-scrobbler-handshake-call
+ emms-librefm-scrobbler-handshake-url
+ (emms-librefm-scrobbler--username)
+ (emms-librefm-scrobbler--password))))
+
+
+;;; ------------------------------------------------------------------
+;;; submission
+;;; ------------------------------------------------------------------
+
+(defun emms-librefm-scrobbler-make-query (track rating)
+ "Format the url parameters for scrobbling."
+ (setq rating
+ (cond ((equal 'love rating) "L")
+ ((equal 'ban rating) "B")
+ ((equal 'skip rating) "S")
+ (t "")))
+ (let ((artist (emms-track-get track 'info-artist))
+ (title (emms-track-get track 'info-title))
+ (album (or (emms-track-get track 'info-album) ""))
+ (track-number (emms-track-get track 'info-tracknumber))
+ (musicbrainz-id "")
+ (track-length (number-to-string
+ (or (emms-track-get track
+ 'info-playing-time)
+ 0))))
+ (if (and artist title)
+ (concat
+ "s=" emms-librefm-scrobbler-session-id
+ "&a[0]=" (url-encode-url artist)
+ "&t[0]=" (url-encode-url title)
+ "&i[0]=" (url-encode-url
+ (or emms-librefm-scrobbler-track-play-start-timestamp
+ (format-time-string "%s")))
+ "&o[0]=" "P"
+ "&r[0]=" (url-encode-url rating)
+ "&l[0]=" track-length
+ "&b[0]=" (url-encode-url album)
+ "&n[0]=" track-number
+ "&m[0]=" musicbrainz-id)
+ (error "Track title and artist must be known."))))
+
+
+;;; ------------------------------------------------------------------
+;;; asynchronous submission
+;;; ------------------------------------------------------------------
+
+(defun emms-librefm-scrobbler-get-response-status ()
+ "Check the HTTP header and return the body."
+ (let ((ok200 "HTTP/1.1 200 OK"))
+ (if (< (point-max) 1)
+ (error "No response from submission server"))
+ (if (not (string= ok200 (buffer-substring-no-properties (point-min) 16)))
+ (error "submission server not responding correctly"))
+ (goto-char (point-min))
+ (re-search-forward "\n\n")
+ (buffer-substring-no-properties
+ (point-at-bol) (point-at-eol))))
+
+(defun emms-librefm-scrobbler-make-async-submission-call (track rating)
+ "Make asynchronous submission call."
+ (let ((flarb (emms-librefm-scrobbler-make-query track rating)))
+ (let* ((url-request-method "POST")
+ (url-request-data flarb)
+ (url-request-extra-headers
+ `(("Content-type" . "application/x-www-form-urlencoded"))))
+ (url-retrieve emms-librefm-scrobbler-submission-url
+ #'emms-librefm-scrobbler-async-submission-callback
+ (list (cons track rating))))))
+
+(defun emms-librefm-scrobbler-async-submission-callback (status &optional cbargs)
+ "Pass response of asynchronous submission call to handler."
+ (let ((response (emms-librefm-scrobbler-get-response-status)))
+ ;; From the API docs: This indicates that the
+ ;; submission request was accepted for processing. It
+ ;; does not mean that the submission was valid, but
+ ;; only that the authentication and the form of the
+ ;; submission was validated.
+ (let ((track (car cbargs)))
+ (cond ((string= response "OK")
+ (when emms-librefm-scrobbler-display-submissions
+ (message "Libre.fm: Submitted %s"
+ (emms-track-get track 'info-title))))
+ ((string= response "BADSESSION")
+ (emms-librefm-scrobbler-handshake)
+ (emms-librefm-scrobbler-make-async-submission-call (car cbargs) (cdr cbargs)))
+ (t
+ (error "unhandled submission failure"))))))
+
+
+;;; ------------------------------------------------------------------
+;;; hooks
+;;; ------------------------------------------------------------------
+
+(defun emms-librefm-scrobbler-start-hook ()
+ (setq emms-librefm-scrobbler-track-play-start-timestamp
+ (format-time-string "%s")))
+
+(defun emms-librefm-scrobbler-stop-hook ()
+ "Submit the track to libre.fm if it has been played for 240
+seconds or half the length of the track."
+ (let ((current-track (emms-playlist-current-selected-track)))
+ (let ((track-length (emms-track-get current-track 'info-playing-time)))
+ (when (and track-length
+ ;; only submit files
+ (eq (emms-track-type current-track) 'file))
+ (when (and
+ ;; track must be longer than 30 secs
+ (> track-length 30)
+ ;; track must be played for more than 240 secs or
+ ;; half the tracks length, whichever comes first.
+ (> emms-playing-time (min 240 (/ track-length 2))))
+ (emms-librefm-scrobbler-make-async-submission-call
+ current-track nil))))))
+
+(defun emms-librefm-scrobbler-enable ()
+ "Enable the scrobbler and submit played tracks."
+ (interactive)
+ (when (not emms-librefm-scrobbler-session-id)
+ (emms-librefm-scrobbler-handshake))
+ (add-hook 'emms-player-started-hook
+ 'emms-librefm-scrobbler-start-hook t)
+ (add-hook 'emms-player-stopped-hook
+ 'emms-librefm-scrobbler-stop-hook)
+ (add-hook 'emms-player-finished-hook
+ 'emms-librefm-scrobbler-stop-hook))
+
+(defun emms-librefm-scrobbler-disable ()
+ "Disable the scrobbler and don't submit played tracks."
+ (interactive)
+ (setq emms-librefm-scrobbler-session-id nil)
+ (remove-hook 'emms-player-started-hook
+ 'emms-librefm-scrobbler-start-hook)
+ (remove-hook 'emms-player-stopped-hook
+ 'emms-librefm-scrobbler-stop-hook)
+ (remove-hook 'emms-player-finished-hook
+ 'emms-librefm-scrobbler-stop-hook))
+
+
+(provide 'emms-librefm-scrobbler)
+
+
+;;; emms-librefm-scrobbler.el ends here