Browse Source

Added spotify.el to emacs.

pull/2/head
Josh Wolfe 6 years ago
parent
commit
c1598659e9
  1. 21
      .emacs.d/init.el
  2. 241
      .emacs.d/spotify.el/README.md
  3. 342
      .emacs.d/spotify.el/oauth2.el
  4. 284
      .emacs.d/spotify.el/spotify-api.el
  5. 89
      .emacs.d/spotify.el/spotify-apple.el
  6. 157
      .emacs.d/spotify.el/spotify-controller.el
  7. 98
      .emacs.d/spotify.el/spotify-dbus.el
  8. 145
      .emacs.d/spotify.el/spotify-playlist-search.el
  9. 50
      .emacs.d/spotify.el/spotify-remote.el
  10. 131
      .emacs.d/spotify.el/spotify-track-search.el
  11. 132
      .emacs.d/spotify.el/spotify.el
  12. 25
      .emacs.d/spotify.el/spotify_oauth2_callback_server.py
  13. 1
      .gitignore
  14. 2
      polybar/config

21
.emacs.d/init.el

@ -271,6 +271,19 @@ is already narrowed." @@ -271,6 +271,19 @@ is already narrowed."
;;
;; G E N E R A L P A C K A G E S
;;
(use-package delight
:config
(delight '((emacs-lisp-mode "ξ" :major)
(lisp-interaction-mode "λ" :major)
(python-mode "π" :major)
(c-mode "𝐂 " :major)
(org-mode "Ø" :major)
(company-mode " α" company)
(ivy-mode " ι" ivy)
(eldoc-mode " ε" eldoc)
(undo-tree-mode "" undo-tree)
(auto-revert-mode "" autorevert))))
(use-package helm
:bind (("M-x" . helm-M-x)
("C-x C-f" . helm-find-files)
@ -320,6 +333,14 @@ is already narrowed." @@ -320,6 +333,14 @@ is already narrowed."
(with-eval-after-load 'flycheck
(setq-default flycheck-disabled-checkers '(emacs-lisp-checkdoc))))
(when (executable-find "spotify")
(add-to-list 'load-path "~/.emacs.d/spotify.el")
(require 'spotify)
(setq spotify-mode-line-refresh-interval 1)
(setq spotify-mode-line-truncate-length 25) ; default: 15
(global-spotify-remote-mode 1))
;;
;; L A N G U A G E S P E C I F I C
;;

241
.emacs.d/spotify.el/README.md

@ -0,0 +1,241 @@ @@ -0,0 +1,241 @@
# Spotify.el
**Control Spotify app from within Emacs.**
![track-search](./img/track-search.png)
Spotify.el is a collection of extensions that allows you to control the Spotify
application from within your favorite text editor.
**Note:** This is _very_ alpha software, and it works only in Mac OS X and Linux.
## Features
* Spotify client integration for GNU/Linux (via D-Bus) and OS X (via AppleScript)
* Communicates with the Spotify API via Oauth2
* Displays the current track in mode line
* Create playlists (public or private)
* Browse the Spotify featured playlists, your own playlists, and their tracks
* Search for tracks and playlists that match the given keywords
* Easily control basic Spotify player features like, play/pause, previous,
next, shuffle, and repeat with the Spotify Remote minor mode
## Installation
First, make sure your system satisfies the given dependencies:
* Emacs 24.4+
* Python 2.7+ (needed for the Oauth2 callback server)
To manually install spotify.el, just clone this project somewhere in your
disk, add that directory in the `load-path`, and require the `spotify` module:
````el
(add-to-list 'load-path "<spotify.el-dir>")
(require 'spotify)
;; Settings
(setq spotify-oauth2-client-secret "<spotify-app-client-secret>")
(setq spotify-oauth2-client-id "<spotify-app-client-id>")
````
Or if you use [el-get](https://github.com/dimitri/el-get):
````el
(add-to-list
'el-get-sources
'(:name spotify.el
:type github
:pkgname "danielfm/spotify.el"
:description "Control the Spotify app from within Emacs"
:url "https://github.com/danielfm/spotify.el"
:after (progn
(setq spotify-oauth2-client-secret "<spotify-app-client-secret>")
(setq spotify-oauth2-client-id "<spotify-app-client-id>"))))
````
In order to get the the client ID and client secret, you need to create
[a Spotify app](https://developer.spotify.com/my-applications), specifying
<http://localhost:8591/> as the redirect URI.
### Creating The Spotify App
Go to [Create an Application](https://developer.spotify.com/my-applications/#!/applications/create)
and give your application a name and a description:
![Creating a Spotify App 1/2](./img/spotify-app-01.png)
At this point, the client ID and the client secret is already available, so set
those values to `spotify-oauth2-client-id` and `spotify-oauth2-client-secret`,
respectively.
Then, scroll down a little bit, type <http://localhost:8591/> as the Redirect
URI for the application, and click **Add**:
![Creating a Spotify App 2/2](./img/spotify-app-02.png)
Finally, scroll to the end of the page and hit **Save**.
## Usage
### Starting A New Session
In order to connect with the Spotify API and refresh the access token,
run <kbd>M-x spotify-connect</kbd>. This will start the Oauth2 authentication
and authorization workflow.
You may be asked to type a password since the tokens are securely stored as an
encrypted file in the local filesystem. After you enter your credentials and
authorizes the app, you should see a greeting message in the echo area.
To disconnect, run <kbd>M-x spotify-disconnect</kbd>.
### Searching For Tracks
To search for tracks, run <kbd>M-x spotify-track-search</kbd> and type in your
query. The results will be displayed in a separate buffer with the following
key bindings:
| Key | Description |
|:-----------------|:-------------------------------------------------------------|
| <kbd>l</kbd> | Loads the next page of results (pagination) |
| <kbd>g</kbd> | Clears the results and reloads the first page of results |
| <kbd>RET</kbd> | Plays the track under the cursor in the context of its album |
The resulting buffer loads the `spotify-remote-mode` by default.
**Tip:** In order to customize the number of items fetched per page, just change
the variable `spotify-api-search-limit`:
````el
;; Do not use values larger than 50 for better compatibility across endpoints
(setq spotify-api-search-limit 50)
````
### Playing a Spotify URI
To ask the Spotify client to play a resource by URI, run
<kbd>M-x spotify-play-uri</kbd> and enter the resource URI.
### Creating Playlists
To create new playlists, run <kbd>M-x spotify-create-playlist</kbd> and follow
the prompts.
Currently it's not possible to add tracks to a playlist you own, or to remove
tracks from them.
### Searching For Playlists
To return the playlists for the current user, run
<kbd>M-x spotify-my-playlists</kbd>, or
<kbd>M-x spotify-user-playlists</kbd> to list the public playlists for some
given user. To search playlists that match the given search criteria, run
<kbd>M-x spotify-playlist-search CRITERIA</kbd>. Also, run
<kbd>M-x spotify-featured-playlists</kbd> in order to browse the featured
playlists from Spotify en_US.
Change the following variables in order to customize the locale and region for
the featuerd playlists endpoint:
````el
;; Spanish (Mexico)
(setq spotify-api-locale "es_MX")
(setq spotify-api-country "MX")
````
All these commands will display results in a separate buffer with the following
key bindings:
| Key | Description |
|:---------------|:-----------------------------------------------------------|
| <kbd>l</kbd> | Loads the next page of results (pagination) |
| <kbd>g</kbd> | Clears the results and reloads the first page of results |
| <kbd>f</kbd> | Follows the playlist under the cursor |
| <kbd>u</kbd> | Unfollows the playlist under the cursor |
| <kbd>t</kbd> | Lists the tracks of the playlist under the cursor |
| <kbd>RET</kbd> | Plays the playlist under the cursor from the beginning (*) |
Once you opened the list of tracks of a playlist, you get the following key
bindings in the resulting buffer:
| Key | Description |
|:-----------------|:--------------------------------------------------------------------|
| <kbd>l</kbd> | Loads the next page of results (pagination) |
| <kbd>g</kbd> | Clears the results and reloads the first page of results |
| <kbd>f</kbd> | Follows the current playlist |
| <kbd>u</kbd> | Unfollows the current playlist |
| <kbd>RET</kbd> | Plays the track under the cursor in the context of the playlist (*) |
| <kbd>M-RET</kbd> | Plays the track under the cursor in the context of its album |
Both buffers load the `spotify-remote-mode` by default.
(*) No proper support for this in Spotify client for GNU/Linux
### Remote Minor Mode
Whenever you enable the `spotify-remote-mode` you get the following key
bindings:
| Key | Function | Description |
|:-------------------|:-------------------------|:-------------------------------|
| <kbd>M-p M-s</kbd> | `spotify-toggle-shuffle` | Turn shuffle on/off (*) |
| <kbd>M-p M-r</kbd> | `spotify-toggle-repeat` | Turn repeat on/off (*) |
| <kbd>M-p M-p</kbd> | `spotify-toggle-play` | Play/pause |
| <kbd>M-p M-f</kbd> | `spotify-next-track` | Next track |
| <kbd>M-p M-b</kbd> | `spotify-previous-track` | Previous track |
This is particularly useful for those using keyboards without media keys.
Also, the current song being played by the Spotify client is displayed at the
mode line along with the player status (playing, paused). The interval in which
the mode line is updated can be configured via the
`spotify-mode-line-refresh-interval` variable:
````el
;; Updates the mode line every second (set to 0 to disable this feature)
(setq spotify-mode-line-refresh-interval 1)
````
(*) No proper support for this in Spotify client for GNU/Linux
#### Customizing The Mode Line
The information displayed in the mode line can be customized by setting the
desired format in `spotify-mode-line-format`. The following placeholders are
supported:
| Symbol | Description | Example |
|:-------|:----------------------------------------|:-------------------------------|
| `%u` | Track URI | `spotify:track:<id>` |
| `%a` | Artist name | `Pink Floyd` |
| `%at` | Artist name (truncated) | `Pink Floyd` |
| `%t` | Track name | `Us and Them` |
| `%tt` | Track name (truncated) | `Us and Them` |
| `%n` | Track # | `7` |
| `%d` | Track disc # | `1` |
| `%s` | Player state (*) | `playing`, `paused`, `stopped` |
| `%l` | Track duration, in minutes | `7:49` |
| `%p` | Current player position, in minutes (*) | `2:23` |
The default format is `"%at - %tt [%l]"`.
The number of characters to be shown in truncated fields can be configured via
the `spotify-mode-line-truncate-length` variable.
````el
(setq spotify-mode-line-truncate-length 10) ; default: 15
````
(*) No proper support for this in Spotify client for GNU/Linux
#### Global Remote Mode
This mode can be enabled globally by running
<kbd>M-x global-spotify-remote-mode</kbd>.
## License
Copyright (C) Daniel Fernandes Martins
Distributed under the New BSD License. See COPYING for further details.

342
.emacs.d/spotify.el/oauth2.el

@ -0,0 +1,342 @@ @@ -0,0 +1,342 @@
;;; oauth2.el --- OAuth 2.0 Authorization Protocol
;; Copyright (C) 2011-2013 Free Software Foundation, Inc
;; Author: Julien Danjou <julien@danjou.info>
;; Version: 0.10
;; Keywords: comm
;; Modified by Daniel Martins <daniel.tritone@gmail.com>
;; The main change is to use a simple HTTP server in order to receive
;; the Oauth2 callback instead of having the user to copy and paste
;; the code by hand.
;; This file is part of GNU Emacs.
;; GNU Emacs 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 of the License, or
;; (at your option) any later version.
;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Implementation of the OAuth 2.0 draft.
;;
;; The main entry point is `oauth2-auth-and-store' which will return a token
;; structure. This token structure can be then used with
;; `oauth2-url-retrieve-synchronously' or `oauth2-url-retrieve' to retrieve
;; any data that need OAuth authentication to be accessed.
;;
;; If the token needs to be refreshed, the code handles it automatically and
;; store the new value of the access token.
;;; Code:
(eval-when-compile (require 'cl))
(require 'plstore)
(require 'json)
(require 'url-http)
(defvar url-http-method nil)
(defvar url-http-data nil)
(defvar url-http-extra-headers nil)
(defvar url-callback-function nil)
(defvar url-callback-arguments nil)
(defun oauth2-request-authorization (auth-url client-id &optional scope state redirect-uri)
"Request OAuth authorization at AUTH-URL by launching `browse-url'.
CLIENT-ID is the client id provided by the provider.
It returns the code provided by the service."
(browse-url (concat auth-url
(if (string-match-p "\?" auth-url) "&" "?")
"client_id=" (url-hexify-string client-id)
"&response_type=code"
"&redirect_uri=" (url-hexify-string (or redirect-uri "urn:ietf:wg:oauth:2.0:oob"))
(if scope (concat "&scope=" (url-hexify-string scope)) "")
(if state (concat "&state=" (url-hexify-string state)) "")))
(first (split-string (shell-command-to-string
(format "python %s"
(locate-library "spotify_oauth2_callback_server.py"))) "\n")))
(defun oauth2-request-access-parse ()
"Parse the result of an OAuth request."
(goto-char (point-min))
(when (search-forward-regexp "^$" nil t)
(json-read)))
(defun oauth2-make-access-request (url data)
"Make an access request to URL using DATA in POST."
(let ((url-request-method "POST")
(url-request-data data)
(url-request-extra-headers
'(("Content-Type" . "application/x-www-form-urlencoded"))))
(with-current-buffer (url-retrieve-synchronously url)
(let ((data (oauth2-request-access-parse)))
(kill-buffer (current-buffer))
data))))
(defstruct oauth2-token
plstore
plstore-id
client-id
client-secret
access-token
refresh-token
token-url
access-response)
(defun oauth2-request-access (token-url client-id client-secret code &optional redirect-uri)
"Request OAuth access at TOKEN-URL.
The CODE should be obtained with `oauth2-request-authorization'.
Return an `oauth2-token' structure."
(when code
(let ((result
(oauth2-make-access-request
token-url
(concat
"client_id=" client-id
"&client_secret=" client-secret
"&code=" code
"&redirect_uri=" (url-hexify-string (or redirect-uri "urn:ietf:wg:oauth:2.0:oob"))
"&grant_type=authorization_code"))))
(make-oauth2-token :client-id client-id
:client-secret client-secret
:access-token (cdr (assoc 'access_token result))
:refresh-token (cdr (assoc 'refresh_token result))
:token-url token-url
:access-response result))))
;;;###autoload
(defun oauth2-refresh-access (token)
"Refresh OAuth access TOKEN.
TOKEN should be obtained with `oauth2-request-access'."
(setf (oauth2-token-access-token token)
(cdr (assoc 'access_token
(oauth2-make-access-request
(oauth2-token-token-url token)
(concat "client_id=" (oauth2-token-client-id token)
"&client_secret=" (oauth2-token-client-secret token)
"&refresh_token=" (oauth2-token-refresh-token token)
"&grant_type=refresh_token")))))
;; If the token has a plstore, update it
(let ((plstore (oauth2-token-plstore token)))
(when plstore
(plstore-put plstore (oauth2-token-plstore-id token)
nil `(:access-token
,(oauth2-token-access-token token)
:refresh-token
,(oauth2-token-refresh-token token)
:access-response
,(oauth2-token-access-response token)
))
(plstore-save plstore)))
token)
;;;###autoload
(defun oauth2-auth (auth-url token-url client-id client-secret &optional scope state redirect-uri)
"Authenticate application via OAuth2."
(oauth2-request-access
token-url
client-id
client-secret
(oauth2-request-authorization
auth-url client-id scope state redirect-uri)
redirect-uri))
(defcustom oauth2-token-file (concat user-emacs-directory "oauth2.plstore")
"File path where store OAuth tokens."
:group 'oauth2
:type 'file)
(defun oauth2-compute-id (auth-url token-url resource-url)
"Compute an unique id based on URLs.
This allows to store the token in an unique way."
(secure-hash 'md5 (concat auth-url token-url resource-url)))
;;;###autoload
(defun oauth2-auth-and-store (auth-url token-url resource-url client-id client-secret &optional redirect-uri)
"Request access to a resource and store it using `plstore'."
;; We store a MD5 sum of all URL
(let* ((plstore (plstore-open oauth2-token-file))
(id (oauth2-compute-id auth-url token-url resource-url))
(plist (cdr (plstore-get plstore id))))
;; Check if we found something matching this access
(if plist
;; We did, return the token object
(make-oauth2-token :plstore plstore
:plstore-id id
:client-id client-id
:client-secret client-secret
:access-token (plist-get plist :access-token)
:refresh-token (plist-get plist :refresh-token)
:token-url token-url
:access-response (plist-get plist :access-response))
(let ((token (oauth2-auth auth-url token-url
client-id client-secret resource-url nil redirect-uri)))
;; Set the plstore
(setf (oauth2-token-plstore token) plstore)
(setf (oauth2-token-plstore-id token) id)
(plstore-put plstore id nil `(:access-token
,(oauth2-token-access-token token)
:refresh-token
,(oauth2-token-refresh-token token)
:access-response
,(oauth2-token-access-response token)))
(plstore-save plstore)
token))))
(defun oauth2-url-append-access-token (token url)
"Append access token to URL."
(concat url
(if (string-match-p "\?" url) "&" "?")
"access_token=" (oauth2-token-access-token token)))
(defvar oauth--url-advice nil)
(defvar oauth--token-data nil)
;; FIXME: We should change URL so that this can be done without an advice.
(defadvice url-http-handle-authentication (around oauth-hack activate)
(if (not oauth--url-advice)
ad-do-it
(let ((url-request-method url-http-method)
(url-request-data url-http-data)
(url-request-extra-headers url-http-extra-headers)))
(url-retrieve-internal (oauth2-url-append-access-token
(oauth2-refresh-access (car oauth--token-data))
(cdr oauth--token-data))
url-callback-function
url-callback-arguments)
;; This is to make `url' think it's done.
(when (boundp 'success) (setq success t)) ;For URL library in Emacs<24.4.
(setq ad-return-value t))) ;For URL library in Emacs≥24.4.
;;;###autoload
(defun oauth2-url-retrieve-synchronously (token url &optional request-method request-data request-extra-headers)
"Retrieve an URL synchronously using TOKEN to access it.
TOKEN can be obtained with `oauth2-auth'."
(let* ((oauth--token-data (cons token url)))
(let ((oauth--url-advice t) ;Activate our advice.
(url-request-method request-method)
(url-request-data request-data)
(url-request-extra-headers request-extra-headers))
(url-retrieve-synchronously
(oauth2-url-append-access-token token url)))))
;;;###autoload
(defun oauth2-url-retrieve (token url callback &optional
cbargs
request-method request-data request-extra-headers)
"Retrieve an URL asynchronously using TOKEN to access it.
TOKEN can be obtained with `oauth2-auth'. CALLBACK gets called with CBARGS
when finished. See `url-retrieve'."
;; TODO add support for SILENT and INHIBIT-COOKIES. How to handle this in `url-http-handle-authentication'.
(let* ((oauth--token-data (cons token url)))
(let ((oauth--url-advice t) ;Activate our advice.
(url-request-method request-method)
(url-request-data request-data)
(url-request-extra-headers request-extra-headers))
(url-retrieve
(oauth2-url-append-access-token token url)
callback cbargs))))
;;;; ChangeLog:
;; 2014-01-28 Rüdiger Sonderfeld <ruediger@c-plusplus.de>
;;
;; oauth2.el: Add support for async retrieve.
;;
;; * packages/oauth2/oauth2.el (oauth--tokens-need-renew): Remove.
;; (oauth--token-data): New variable.
;; (url-http-handle-authentication): Call `url-retrieve-internal'
;; directly instead of depending on `oauth--tokens-need-renew'.
;; (oauth2-url-retrieve-synchronously): Call `url-retrieve' once.
;; (oauth2-url-retrieve): New function.
;;
;; Signed-off-by: Rüdiger Sonderfeld <ruediger@c-plusplus.de>
;; Signed-off-by: Julien Danjou <julien@danjou.info>
;;
;; 2013-07-22 Stefan Monnier <monnier@iro.umontreal.ca>
;;
;; * oauth2.el: Only require CL at compile time and avoid flet.
;; (success): Don't defvar.
;; (oauth--url-advice, oauth--tokens-need-renew): New dynbind variables.
;; (url-http-handle-authentication): Add advice.
;; (oauth2-url-retrieve-synchronously): Use the advice instead of flet.
;;
;; 2013-06-29 Julien Danjou <julien@danjou.info>
;;
;; oauth2: release 0.9, require url-http
;;
;; This is needed so that the `flet' calls doesn't restore the overriden
;; function to an unbound one.
;;
;; Signed-off-by: Julien Danjou <julien@danjou.info>
;;
;; 2012-08-01 Julien Danjou <julien@danjou.info>
;;
;; oauth2: upgrade to 0.8, add missing require on cl
;;
;; 2012-07-03 Julien Danjou <julien@danjou.info>
;;
;; oauth2: store access-reponse, bump versino to 0.7
;;
;; 2012-06-25 Julien Danjou <julien@danjou.info>
;;
;; oauth2: add redirect-uri parameter, update to 0.6
;;
;; 2012-05-29 Julien Danjou <julien@danjou.info>
;;
;; * packages/oauth2/oauth2.el: Revert fix URL double escaping, update to
;; 0.5
;;
;; 2012-05-04 Julien Danjou <julien@danjou.info>
;;
;; * packages/oauth2/oauth2.el: Don't use aget, update to 0.4
;;
;; 2012-04-19 Julien Danjou <julien@danjou.info>
;;
;; * packages/oauth2/oauth2.el: Fix URL double escaping, update to 0.3
;;
;; 2011-12-20 Julien Danjou <julien@danjou.info>
;;
;; oauth2: update version 0.2
;;
;; * oauth2: update version to 0.2
;;
;; 2011-12-20 Julien Danjou <julien@danjou.info>
;;
;; oauth2: allow to use any HTTP request type
;;
;; * oauth2: allow to use any HTTP request type
;;
;; 2011-10-08 Julien Danjou <julien@danjou.info>
;;
;; * oauth2.el: Require json.
;; Fix compilation warning with success variable from url.el.
;;
;; 2011-09-26 Julien Danjou <julien@danjou.info>
;;
;; * packages/oauth2/oauth2.el (oauth2-request-authorization): Add missing
;; calls to url-hexify-string.
;;
;; 2011-09-26 Julien Danjou <julien@danjou.info>
;;
;; * packages/oauth2/oauth2.el: Reformat to avoid long lines.
;;
;; 2011-09-23 Julien Danjou <julien@danjou.info>
;;
;; New package oauth2
;;
(provide 'oauth2)
;;; oauth2.el ends here

284
.emacs.d/spotify.el/spotify-api.el

@ -0,0 +1,284 @@ @@ -0,0 +1,284 @@
;; spotify-api.el --- Spotify.el API integration layer
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;; Code:
(defvar *spotify-oauth2-token*)
(defvar *spotify-user*)
(defconst spotify-api-endpoint "https://api.spotify.com/v1")
(defconst spotify-oauth2-auth-url "https://accounts.spotify.com/authorize")
(defconst spotify-oauth2-token-url "https://accounts.spotify.com/api/token")
(defconst spotify-oauth2-scopes "playlist-read-private playlist-modify-public playlist-modify-private user-read-private")
(defconst spotify-oauth2-callback "http://localhost:8591/")
(defcustom spotify-oauth2-client-id ""
"The unique identifier for your application. More info at
https://developer.spotify.com/web-api/tutorial/."
:type 'string)
(defcustom spotify-oauth2-client-secret ""
"The key that you will need to pass in secure calls to the Spotify Accounts and
Web API services. More info at
https://developer.spotify.com/web-api/tutorial/."
:type 'string)
(defcustom spotify-api-search-limit 50
"Number of items returned when searching for something using the Spotify API."
:type 'integer)
(defcustom spotify-api-locale "en_US"
"Optional. The desired language, consisting of an ISO 639 language code and
an ISO 3166-1 alpha-2 country code, joined by an underscore.
For example: es_MX, meaning Spanish (Mexico). Provide this parameter if you
want the category metadata returned in a particular language."
:type 'string)
(defcustom spotify-api-country "US"
"Optional. A country: an ISO 3166-1 alpha-2 country code. Provide this
parameter if you want to narrow the list of returned categories to those
relevant to a particular country. If omitted, the returned items will be
globally relevant."
:type 'string)
(defun spotify-api-auth ()
"Starts the Spotify Oauth2 authentication and authorization workflow."
(oauth2-auth-and-store spotify-oauth2-auth-url
spotify-oauth2-token-url
spotify-oauth2-scopes
spotify-oauth2-client-id
spotify-oauth2-client-secret
spotify-oauth2-callback))
(defun spotify-api-call (method uri &optional data is-retry)
"Makes a request to the given Spotify service endpoint and returns the parsed
JSON response."
(let ((url (concat spotify-api-endpoint uri))
(headers '(("Content-Type" . "application/json"))))
(with-current-buffer (oauth2-url-retrieve-synchronously *spotify-oauth2-token*
url method data headers)
(toggle-enable-multibyte-characters t)
(goto-char (point-min))
;; If (json-read) signals 'end-of-file, we still kill the temp buffer
;; and re-signal the error
(condition-case err
(when (search-forward-regexp "^$" nil t)
(let* ((json-object-type 'hash-table)
(json-array-type 'list)
(json-key-type 'symbol)
(json (json-read))
(error-json (gethash 'error json)))
(kill-buffer)
;; Retries the request when the token expires and gets refreshed
(if (and (hash-table-p error-json)
(eq 401 (gethash 'status error-json))
(not is-retry))
(spotify-api-call method uri data t)
json)))
(end-of-file
(kill-buffer)
(signal (car err) (cdr err)))))))
(defun spotify-disconnect ()
"Clears the Spotify session currently in use."
(interactive)
(makunbound '*spotify-oauth2-token*)
(makunbound '*spotify-user*)
(stop-mode-line-timer)
(message "Spotify session closed"))
;;;###autoload
(defun spotify-connect ()
"Starts a new Spotify session."
(interactive)
(spotify-disconnect)
(defvar *spotify-oauth2-token* (spotify-api-auth))
(defvar *spotify-user* (spotify-api-call "GET" "/me"))
(when *spotify-user*
(message "Welcome, %s!" (spotify-current-user-name))
(start-mode-line-timer)))
(defun spotify-connected-p ()
"Returns whether there's an established session with Spotify API."
(and (boundp '*spotify-user*) (not (null *spotify-user*))))
(defun spotify-current-user-name ()
"Returns the user's display name of the current Spotify session."
(gethash 'display_name *spotify-user*))
(defun spotify-current-user-id ()
"Returns the user's id of the current Spotify session."
(spotify-get-item-id *spotify-user*))
(defun spotify-get-items (json)
"Returns the list of items from the given json object."
(gethash 'items json))
(defun spotify-get-search-track-items (json)
"Returns track items from the given search results json."
(spotify-get-items (gethash 'tracks json)))
(defun spotify-get-search-playlist-items (json)
"Returns playlist items from the given search results json."
(spotify-get-items (gethash 'playlists json)))
(defun spotify-get-message (json)
"Returns the message from the featured playlists response."
(gethash 'message json))
(defun spotify-get-playlist-tracks (json)
(mapcar #'(lambda (item)
(gethash 'track item))
(spotify-get-items json)))
(defun spotify-get-search-playlist-items (json)
"Returns the playlist items from the given search results json."
(spotify-get-items (gethash 'playlists json)))
(defun spotify-get-track-album (json)
"Returns the simplified album object from the given track object."
(gethash 'album json))
(defun spotify-get-track-number (json)
"Returns the track number from the given track object."
(gethash 'track_number json))
(defun spotify-get-track-duration (json)
"Returns the track duration, in milliseconds, from the given track object."
(gethash 'duration_ms json))
(defun spotify-get-track-duration-formatted (json)
"Returns the formatted track duration from the given track object."
(format-seconds "%m:%02s" (/ (spotify-get-track-duration json) 1000)))
(defun spotify-get-track-album-name (json)
"Returns the album name from the given track object."
(spotify-get-item-name (spotify-get-track-album json)))
(defun spotify-get-track-artist (json)
"Returns the first artist from the given track object."
(spotify-get-item-name (first (gethash 'artists json))))
(defun spotify-get-track-popularity (json)
"Returns the popularity from the given track/album/artist object."
(gethash 'popularity json))
(defun spotify-is-track-playable (json)
"Returns whether the given track is playable by the current user."
(not (eq :json-false (gethash 'is_playable json))))
(defun spotify-get-item-name (json)
"Returns the name from the given track/album/artist object."
(gethash 'name json))
(defun spotify-get-item-id (json)
"Returns the id from the givem object."
(gethash 'id json))
(defun spotify-get-item-uri (json)
"Returns the uri from the given track/album/artist object."
(gethash 'uri json))
(defun spotify-get-playlist-track-count (json)
"Returns the number of tracks of the given playlist object."
(gethash 'total (gethash 'tracks json)))
(defun spotify-get-playlist-owner-id (json)
"Returns the owner id of the given playlist object."
(spotify-get-item-id (gethash 'owner json)))
(defun spotify-api-search (type query page)
"Searches artists, albums, tracks or playlists that match a keyword string,
depending on the `type' argument."
(let ((offset (* spotify-api-search-limit (1- page))))
(spotify-api-call "GET"
(concat "/search?"
(url-build-query-string `((q ,query)
(type ,type)
(limit ,spotify-api-search-limit)
(offset ,offset)
(market from_token))
nil t)))))
(defun spotify-api-featured-playlists (page)
"Returns the given page of Spotify's featured playlists."
(let ((offset (* spotify-api-search-limit (1- page))))
(spotify-api-call
"GET"
(concat "/browse/featured-playlists?"
(url-build-query-string `((locale ,spotify-api-locale)
(country ,spotify-api-country)
(limit ,spotify-api-search-limit)
(offset ,offset))
nil t)))))
(defun spotify-api-user-playlists (user-id page)
"Returns the playlists for the given user."
(let ((offset (* spotify-api-search-limit (1- page))))
(spotify-api-call
"GET"
(concat (format "/users/%s/playlists?" (url-hexify-string user-id))
(url-build-query-string `((limit ,spotify-api-search-limit)
(offset ,offset))
nil t)))))
(defun spotify-api-playlist-create (user-id name is-public)
"Creates a new playlist with the given name for the given user."
(spotify-api-call
"POST"
(format "/users/%s/playlists"
(url-hexify-string user-id))
(format "{\"name\":\"%s\",\"public\":\"%s\"}"
name
(if is-public "true" "false"))))
(defun spotify-api-playlist-follow (playlist)
"Adds the current user as a follower of a playlist."
(condition-case err
(let ((owner (spotify-get-playlist-owner-id playlist))
(id (spotify-get-item-id playlist)))
(spotify-api-call
"PUT"
(format "/users/%s/playlists/%s/followers"
(url-hexify-string owner)
(url-hexify-string id))))
(end-of-file t)))
(defun spotify-api-playlist-unfollow (playlist)
"Removes the current user as a follower of a playlist."
(condition-case err
(let ((owner (spotify-get-playlist-owner-id playlist))
(id (spotify-get-item-id playlist)))
(spotify-api-call
"DELETE"
(format "/users/%s/playlists/%s/followers"
(url-hexify-string owner)
(url-hexify-string id))))
(end-of-file t)))
(defun spotify-api-playlist-tracks (playlist page)
"Returns the tracks of the given user's playlist."
(let ((owner (spotify-get-playlist-owner-id playlist))
(id (spotify-get-item-id playlist))
(offset (* spotify-api-search-limit (1- page))))
(spotify-api-call
"GET"
(concat (format "/users/%s/playlists/%s/tracks?"
(url-hexify-string owner)
(url-hexify-string id) offset)
(url-build-query-string `((limit ,spotify-api-search-limit)
(offset ,offset)
(market from_token))
nil t)))))
(defun spotify-popularity-bar (popularity)
"Returns the popularity indicator bar proportional to the given parameter,
which must be a number between 0 and 100."
(let ((num-bars (truncate (/ popularity 10))))
(concat (make-string num-bars ?\u25cf)
(make-string (- 10 num-bars) ?\u25cb))))
(provide 'spotify-api)

89
.emacs.d/spotify.el/spotify-apple.el

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
;; spotify-apple.el --- Apple-specific code for Spotify.el
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;; Code:
(defcustom spotify-osascript-bin-path "/usr/bin/osascript"
"Path to `osascript' binary."
:type 'string)
;; Do not change this unless you know what you're doing
(defvar spotify-player-status-script "
tell application \"Spotify\"
set playerState to get player state as string
# Empty data in order to avoid returning an error
if (playerState = \"stopped\") then
return \"\n\n\n\n\n\nstopped\n\"
end if
set trackId to id of current track as string
set trackArtist to artist of current track as string
set trackName to name of current track as string
set trackNumber to track number of current track as string
set trackDiscNumber to disc number of current track as string
set trackDuration to duration of current track as string
set playerPosition to get player position as string
return trackId & \"\n\" & trackArtist & \"\n\" & trackName & \"\n\" & trackNumber & \"\n\" & trackDiscNumber & \"\n\" & trackDuration & \"\n\" & playerState & \"\n\" & playerPosition
end tell")
(defun spotify-apple-command-line (cmd)
"Returns a command line prefix for any Spotify command."
(format "%s -e 'tell application \"Spotify\" to %s'" spotify-osascript-bin-path cmd))
(defun spotify-apple-command (cmd)
"Sends the given command to the Spotify client and returns the resulting
status string."
(replace-regexp-in-string
"\n$" ""
(shell-command-to-string (spotify-apple-command-line cmd))))
(defun spotify-apple-player-status-command-line ()
"Returns the current Spotify player status that is set to the mode line."
(format "echo %s | %s"
(shell-quote-argument spotify-player-status-script)
spotify-osascript-bin-path))
(defun spotify-apple-set-mode-line-from-process-output (process output)
"Sets the output of the player status process to the mode line."
(spotify-replace-mode-line-flags output)
(with-current-buffer (process-buffer process)
(delete-region (point-min) (point-max))))
(defun spotify-apple-player-status ()
"Updates the mode line to display the current Spotify player status."
(let* ((process-name "spotify-player-status")
(process-status (process-status process-name)))
(if (and (spotify-connected-p) (not process-status))
(let ((process (start-process-shell-command process-name "*spotify-player-status*" (spotify-apple-player-status-command-line))))
(set-process-filter process 'spotify-apple-set-mode-line-from-process-output))
(spotify-update-mode-line ""))))
(defun spotify-apple-player-state ()
(spotify-apple-command "get player state"))
(defun spotify-apple-player-toggle-play ()
(spotify-apple-command "playpause"))
(defun spotify-apple-player-next-track ()
(spotify-apple-command "next track"))
(defun spotify-apple-player-previous-track ()
(spotify-apple-command "previous track"))
(defun spotify-apple-toggle-repeat ()
(spotify-apple-command "set repeating to not repeating"))
(defun spotify-apple-toggle-shuffle ()
(spotify-apple-command "set shuffling to not shuffling"))
(defun spotify-apple-player-play-track (track-id context-id)
(spotify-apple-command (format "play track \"%s\" in context \"%s\"" track-id context-id)))
(defun spotify-apple-player-pause ()
(spotify-apple-command "pause"))
(provide 'spotify-apple)

157
.emacs.d/spotify.el/spotify-controller.el

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
;; spotify-controller.el --- Generic player controller interface for Spotify.el
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;; Code:
(defmacro if-gnu-linux (then else)
"Evaluates `then' form if Emacs is running in GNU/Linux, otherwise evaluates
`else' form."
`(if (eq system-type 'gnu/linux) ,then ,else))
(defmacro when-gnu-linux (then)
"Evaluates `then' form if Emacs is running in GNU/Linux."
`(if-gnu-linux ,then nil))
(defmacro if-darwin (then else)
"Evaluates `then' form if Emacs is running in OS X, otherwise evaluates
`else' form."
`(if (eq system-type 'darwin) ,then ,else))
(defmacro when-darwin (then)
"Evaluates `then' form if Emacs is running in OS X."
`(if-darwin ,then nil))
(defcustom spotify-transport (if-gnu-linux 'dbus 'apple)
"How the commands should be sent to Spotify process. Defaults for `dbus' for
GNU/Linux, `apple' otherwise."
:type '(choice (symbol :tag "AppleScript" apple)
(symbol :tag "D-Bus" dbus)))
;; TODO: No modeline support for linux just yet
(defcustom spotify-mode-line-refresh-interval (if-gnu-linux 0 1)
"The interval, in seconds, that the mode line must be updated. Set to 0 to
disable this feature."
:type 'integer)
(defcustom spotify-mode-line-truncate-length 15
"The maximum number of characters to truncated fields in
`spotify-mode-line-format'.")
(defcustom spotify-mode-line-format "%at - %t [%l]"
"Format used to display the current Spotify client player status. The
following placeholders are supported:
* %u - Track URI (i.e. spotify:track:<ID>)
* %a - Artist name
* %at - Artist name (truncated)
* %t - Track name
* %tt - Track name (truncated)
* %n - Track #
* %d - Track disc #
* %s - Player state (i.e. playing, paused, stopped)
* %l - Track duration, in minutes (i.e. 01:35)
* %p - Player position in current track, in minutes (i.e. 01:35)
")
(defvar spotify-timer nil)
;; simple facility to emulate multimethods
(defun spotify-apply (suffix &rest args)
(let ((func-name (format "spotify-%s-%s" spotify-transport suffix)))
(apply (intern func-name) args)))
(defun spotify-replace-mode-line-flags (metadata)
""
(let ((mode-line spotify-mode-line-format)
(fields (split-string metadata "\n"))
(duration-format "%m:%02s"))
(if (or (< (length fields) 8)
(string= "stopped" (seventh fields)))
(setq mode-line "")
(progn
(setq mode-line (replace-regexp-in-string "%u" (first fields) mode-line))
(setq mode-line (replace-regexp-in-string "%at" (truncate-string-to-width (second fields) spotify-mode-line-truncate-length 0 nil "...") mode-line))
(setq mode-line (replace-regexp-in-string "%a" (second fields) mode-line))
(setq mode-line (replace-regexp-in-string "%tt" (truncate-string-to-width (third fields) spotify-mode-line-truncate-length 0 nil "...") mode-line))
(setq mode-line (replace-regexp-in-string "%t" (third fields) mode-line))
(setq mode-line (replace-regexp-in-string "%n" (fourth fields) mode-line))
(setq mode-line (replace-regexp-in-string "%d" (fifth fields) mode-line))
(setq mode-line (replace-regexp-in-string "%s" (seventh fields) mode-line))
(setq mode-line (replace-regexp-in-string "%l" (format-seconds duration-format (/ (string-to-number (sixth fields)) 1000)) mode-line))
(setq mode-line (replace-regexp-in-string "%p" (format-seconds duration-format (string-to-number (eighth fields))) mode-line))))
(spotify-update-mode-line mode-line)))
(defun start-mode-line-timer ()
"Starts the timer that updates the mode line according to the Spotify
player status."
(stop-mode-line-timer)
(when (> spotify-mode-line-refresh-interval 0)
(let ((first-run (format "%d sec" spotify-mode-line-refresh-interval))
(interval spotify-mode-line-refresh-interval))
(setq spotify-timer
(run-at-time first-run interval 'spotify-refresh-mode-line)))))
(defun stop-mode-line-timer ()
"Stops the timer that updates the mode line."
(when (and (boundp 'spotify-timer) (timerp spotify-timer))
(cancel-timer spotify-timer)
(spotify-player-status)))
(defun spotify-player-status ()
"Updates the mode line to display the current Spotify player status."
(interactive)
(spotify-apply "player-status"))
(defun spotify-refresh-mode-line (&rest args)
"Starts the player status process in order to update the mode line."
(spotify-apply "player-status"))
(defun spotify-play-uri (uri)
"Sends a `play' command to Spotify process passing the given URI."
(interactive "SSpotify URI: ")
(spotify-apply "player-play-track" uri nil))
(defun spotify-play-track (track &optional context)
"Sends a `play' command to Spotify process passing a context id."
(interactive)
(spotify-apply "player-play-track"
(when track (spotify-get-item-uri track))
(when context (spotify-get-item-uri context))))
(defun spotify-toggle-play ()
"Sends a `playpause' command to Spotify process."
(interactive)
(spotify-apply "player-toggle-play"))
(defun spotify-play ()
"Sends a `play' command to Spotify process."
(interactive)
(spotify-apply "player-play"))
(defun spotify-next-track ()
"Sends a `next track' command to Spotify process."
(interactive)
(spotify-apply "player-next-track"))
(defun spotify-previous-track ()
"Sends a `previous track' command to Spotify process."
(interactive)
(spotify-apply "player-previous-track"))
(defun spotify-pause ()
"Sends a `pause' command to Spotify process."
(interactive)
(spotify-apply "player-pause"))
(defun spotify-toggle-repeat ()
"Sends a command to Spotify process to toggle the repeating flag."
(interactive)
(spotify-apply "toggle-repeat"))
(defun spotify-toggle-shuffle ()
"Sends a command to Spotify process to toggle the shuffling flag."
(interactive)
(spotify-apply "toggle-shuffle"))
(provide 'spotify-controller)

98
.emacs.d/spotify.el/spotify-dbus.el

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
;;; spotify-dbus --- Dbus-specific code for Spotify.el
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;;; Commentary:
;; Somehow shuffeling, setting volume and loop status work not as expected.
;; Querying the attribute does not return the expected value and setting it
;; has no effect.
;; The dbus interface of spotify seems to be broken.
;;; Code:
(require 'dbus)
(defun spotify-dbus-call (method &rest args)
"Call METHOD with optional ARGS via D-Bus on the Spotify service."
(apply 'dbus-call-method-asynchronously
:session
"org.mpris.MediaPlayer2.spotify"
"/org/mpris/MediaPlayer2"
"org.mpris.MediaPlayer2.Player"
method
nil args))
(defun spotify-dbus-get-property (property)
"Get value of PROPERTY via D-Bus on the Spotify service."
(dbus-get-property :session
"org.mpris.MediaPlayer2.spotify"
"/org/mpris/MediaPlayer2"
"org.mpris.MediaPlayer2.Player"
property))
(defun spotify-dbus-set-property (property value)
"Set PROPERTY to VALUE via D-Bus on the Spotify service."
(dbus-set-property :session
"org.mpris.MediaPlayer2.spotify"
"/org/mpris/MediaPlayer2"
"org.mpris.MediaPlayer2.Player"
property
value))
(defun spotify-dbus-player-status ()
"Updates the mode line to display the current Spotify player status."
(let ((metadata (spotify-dbus-get-property "Metadata")))
(if (and (spotify-connected-p) metadata)
(let ((track-id (car (car (cdr (assoc "mpris:trackid" metadata)))))
(artist (car (car (car (cdr (assoc "xesam:artist" metadata))))))
(title (car (car (cdr (assoc "xesam:title" metadata)))))
(track-n (car (car (cdr (assoc "xesam:trackNumber" metadata)))))
(disc-n (car (car (cdr (assoc "xesam:discNumber" metadata)))))
(length (/ (car (car (cdr (assoc "mpris:length" metadata)))) 1000)))
(if (> track-n 0)
(spotify-replace-mode-line-flags
(concat track-id "\n"
artist "\n"
title "\n"
(number-to-string track-n) "\n"
(number-to-string disc-n) "\n"
(number-to-string length) "\n"
"-\n" ;; TODO: unable to get the player state via D-Bus
"-\n" ;; TODO: unable to get the player position via D-Bus
))
(spotify-update-mode-line "")))
(spotify-update-mode-line ""))))
(defun spotify-dbus-player-toggle-play ()
"Toggle Play/Pause."
(spotify-dbus-call "PlayPause"))
(defun spotify-dbus-player-next-track ()
"Play next track."
(spotify-dbus-call "Next"))
(defun spotify-dbus-player-previous-track ()
"Play previous previous."
(spotify-dbus-call "Previous"))
;; TODO: Currently not supported by the Spotify client D-Bus interface
(defun spotify-dbus-toggle-repeat ()
(message "Toggling repeat status not supported by the Spotify client"))
;; TODO: Currently not supported by the Spotify client D-Bus interface
(defun spotify-dbus-toggle-shuffle ()
(message "Toggline shuffle status not supported by the Spotify client"))
;; TODO: Synchronize this to work the same way as the apple version, if possible
(defun spotify-dbus-player-play-track (track-id context-id)
(when track-id (spotify-dbus-call "Pause"))
(run-at-time "1 sec" nil 'spotify-dbus-call "OpenUri" (or track-id context-id)))
(defun spotify-dbus-player-play ()
(spotify-dbus-call "Play"))
(defun spotify-dbus-player-pause ()
(spotify-dbus-call "Pause"))
(provide 'spotify-dbus)

145
.emacs.d/spotify.el/spotify-playlist-search.el

@ -0,0 +1,145 @@ @@ -0,0 +1,145 @@
;; spotify-playlist-search.el --- Spotify.el playlist search major mode
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;; Code:
(require 'spotify-api)
(defvar spotify-playlist-search-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "RET") 'spotify-playlist-select)
(define-key map (kbd "t") 'spotify-playlist-tracks)
(define-key map (kbd "l") 'spotify-playlist-load-more)
(define-key map (kbd "g") 'spotify-playlist-reload)
(define-key map (kbd "f") 'spotify-playlist-follow)
(define-key map (kbd "u") 'spotify-playlist-unfollow)
map)
"Local keymap for `spotify-playlist-search-mode' buffers.")
;; Enables the `spotify-remote-mode' the track search buffer
(add-hook 'spotify-playlist-search-mode-hook 'spotify-remote-mode)
(define-derived-mode spotify-playlist-search-mode tabulated-list-mode "Playlist-Search"
"Major mode for displaying the playlists returned by a Spotify search.")
(defun spotify-playlist-select ()
"Plays the playlist under the cursor."
(interactive)
(let ((selected-playlist (tabulated-list-get-id)))
(spotify-play-track nil selected-playlist)))
(defun spotify-playlist-reload ()
"Reloads the first page of results for the current playlist view."
(interactive)
(let ((page 1))
(cond ((bound-and-true-p spotify-query) (spotify-playlist-search-update page))
((bound-and-true-p spotify-browse-message) (spotify-featured-playlists-update page))
(t (spotify-user-playlists-update spotify-user-id page)))))
(defun spotify-playlist-load-more ()
"Loads the next page of results for the current playlist view."
(interactive)
(let ((next-page (1+ spotify-current-page)))
(cond ((bound-and-true-p spotify-query) (spotify-playlist-search-update next-page))
((bound-and-true-p spotify-browse-message) (spotify-featured-playlists-update next-page))
(t (spotify-user-playlists-update spotify-user-id next-page)))))
(defun spotify-playlist-follow ()
"Adds the current user as the follower of the playlist under the cursor."
(interactive)
(let* ((selected-playlist (tabulated-list-get-id))
(name (spotify-get-item-name selected-playlist)))
(when (and (y-or-n-p (format "Follow playlist '%s'?" name))
(spotify-api-playlist-follow selected-playlist))
(message (format "Followed playlist '%s'" name)))))
(defun spotify-playlist-unfollow ()
"Removes the current user as the follower of the playlist under the cursor."
(interactive)
(let* ((selected-playlist (tabulated-list-get-id))
(name (spotify-get-item-name selected-playlist)))
(when (and (y-or-n-p (format "Unfollow playlist '%s'?" name))
(spotify-api-playlist-unfollow selected-playlist))
(message (format "Unfollow playlist '%s'" name)))))
(defun spotify-playlist-search-update (current-page)
"Fetches the given page of results using the search endpoint."
(let* ((json (spotify-api-search 'playlist spotify-query current-page))
(items (spotify-get-search-playlist-items json)))
(if items
(progn
(spotify-playlist-search-print items current-page)
(message "playlist view updated"))
(message "No more playlists"))))
(defun spotify-user-playlists-update (user-id current-page)
"Fetches the given page of results using the user's playlist endpoint."
(let* ((json (spotify-api-user-playlists user-id current-page))
(items (spotify-get-items json)))
(if items
(progn
(spotify-playlist-search-print items current-page)
(message "Playlist view updated"))
(message "No more playlists"))))
(defun spotify-featured-playlists-update (current-page)
"Fetches the given page of results of Spotify's featured playlists."
(let* ((json (spotify-api-featured-playlists current-page))
(msg (spotify-get-message json))
(items (spotify-get-search-playlist-items json)))
(if items
(progn
(spotify-playlist-search-print items current-page)
(setq-local spotify-browse-message msg)
(message msg))
(message "No more playlists"))))
(defun spotify-playlist-tracks ()
"Displays the tracks that belongs to the playlist under the cursor."
(interactive)
(let* ((selected-playlist (tabulated-list-get-id))
(name (spotify-get-item-name selected-playlist)))
(let ((buffer (get-buffer-create (format "*Playlist Tracks: %s*" name))))
(with-current-buffer buffer
(spotify-track-search-mode)
(spotify-track-search-set-list-format)
(setq-local spotify-selected-playlist selected-playlist)
(spotify-playlist-tracks-update 1)
(pop-to-buffer buffer)
buffer))))
(defun spotify-playlist-set-list-format ()
"Configures the column data for the typical playlist view."
(setq tabulated-list-format
(vector `("Playlist Name" ,(- (window-width) 45) t)
'("Owner Id" 30 t)
'("# Tracks" 8 (lambda (row-1 row-2)
(< (spotify-get-playlist-track-count (first row-1))
(spotify-get-playlist-track-count (first row-2)))) :right-align t))))
(defun spotify-playlist-search-print (playlists current-page)
"Appends the given playlists to the current playlist view."
(let (entries)
(dolist (playlist playlists)
(let ((user-id (spotify-get-playlist-owner-id playlist))
(playlist-name (spotify-get-item-name playlist)))
(push (list playlist
(vector playlist-name
(cons user-id
(list 'face 'link
'follow-link t
'action `(lambda (_) (spotify-user-playlists ,user-id))
'help-echo (format "Show %s's public playlists" user-id)))
(number-to-string (spotify-get-playlist-track-count playlist))))
entries)))
(when (eq 1 current-page)
(setq-local tabulated-list-entries nil))
(setq-local tabulated-list-entries (append tabulated-list-entries (nreverse entries)))
(setq-local spotify-current-page current-page)
(spotify-playlist-set-list-format)
(tabulated-list-init-header)
(tabulated-list-print t)))
(provide 'spotify-playlist-search)

50
.emacs.d/spotify.el/spotify-remote.el

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
;; spotify-remote.el --- Spotify.el remote minor mode
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;; Code:
(defvar spotify-remote-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "M-p M-s") 'spotify-toggle-shuffle)
(define-key map (kbd "M-p M-r") 'spotify-toggle-repeat)
(define-key map (kbd "M-p M-p") 'spotify-toggle-play)
(define-key map (kbd "M-p M-b") 'spotify-previous-track)
(define-key map (kbd "M-p M-f") 'spotify-next-track)
map)
"Local keymap for `spotify-remote-mode' buffers.")
(defvar spotify-mode-line-prefix " \u266b")
(defvar spotify-mode-line spotify-mode-line-prefix)
(define-minor-mode spotify-remote-mode
"Toggles Spotify Remote mode.
A positive prefix argument enables the mode, any other prefix
argument disables it. From Lisp, argument omitted or nil enables
the mode, `toggle' toggles the state.
When Spotify Remote mode is enabled, it's possible to toggle
the repeating and shuffling status of the running Spotify process.
See commands \\[spotify-toggle-repeating] and
\\[spotify-toggle-shuffling]."
:group 'spotify
:init-value nil
:lighter spotify-mode-line)
(defun spotify-update-mode-line (str)
"Sets the given str to the mode line, prefixed with the mode identifier."
(let ((normalized-str (replace-regexp-in-string "\n$" "" str)))
(if (eq "" normalized-str)
(setq spotify-mode-line spotify-mode-line-prefix)
(setq spotify-mode-line (concat spotify-mode-line-prefix " " normalized-str)))
(when (bound-and-true-p spotify-remote-mode)
(force-mode-line-update))))
(defun turn-on-spotify-remote-mode ()
"Turns the `spotify-remote-mode' on in the current buffer."
(spotify-remote-mode 1))
(define-globalized-minor-mode global-spotify-remote-mode
spotify-remote-mode turn-on-spotify-remote-mode)
(provide 'spotify-remote)

131
.emacs.d/spotify.el/spotify-track-search.el

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
;; spotify-track-search.el --- Spotify.el track search major mode
;; Copyright (C) 2014-2016 Daniel Fernandes Martins
;; Code:
(require 'spotify-api)
(defvar spotify-track-search-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "RET") 'spotify-track-select)
(define-key map (kbd "M-RET") 'spotify-track-select-album)
(define-key map (kbd "l") 'spotify-track-load-more)
(define-key map (kbd "g") 'spotify-track-reload)
(define-key map (kbd "f") 'spotify-track-playlist-follow)
(define-key map (kbd "u") 'spotify-track-playlist-unfollow)
map)
"Local keymap for `spotify-track-search-mode' buffers.")
;; Enables the `spotify-remote-mode' the track search buffer
(add-hook 'spotify-track-search-mode-hook 'spotify-remote-mode)
(define-derived-mode spotify-track-search-mode tabulated-list-mode "Track-Search"
"Major mode for displaying the track listing returned by a Spotify search.")
(defun spotify-track-select ()
"Plays the track under the cursor. If the track list represents a playlist,
the given track is played in the context of that playlist; otherwise, it will
be played in the context of its album."
(interactive)
(let ((selected-track (tabulated-list-get-id)))
(if (bound-and-true-p spotify-selected-playlist)
(spotify-play-track selected-track spotify-selected-playlist)
(spotify-play-track selected-track (spotify-get-track-album selected-track)))))
(defun spotify-track-playlist-follow ()
"Adds the current user as the follower of the track's playlist under the cursor."
(interactive)
(if (bound-and-true-p spotify-selected-playlist)
(when (and (y-or-n-p (format "Follow playlist '%s'?" (spotify-get-item-name spotify-selected-playlist)))
(spotify-api-playlist-follow spotify-selected-playlist))
(message (format "Followed playlist '%s'" (spotify-get-item-name spotify-selected-playlist))))
(message "Cannot follow a playlist from here")))
(defun spotify-track-playlist-unfollow ()
"Removes the current user as the follower of the track's playlist under the cursor."
(interactive)
(if (bound-and-true-p spotify-selected-playlist)
(when (and (y-or-n-p (format "Unfollow playlist '%s'?" (spotify-get-item-name spotify-selected-playlist)))
(spotify-api-playlist-unfollow spotify-selected-playlist))
(message (format "Unfollowed playlist '%s'" (spotify-get-item-name spotify-selected-playlist))))
(message "Cannot unfollow a playlist from here")))
(defun spotify-track-select-album ()
"Plays the album of the track under the cursor in the context of its album."
(interactive)
(let ((selected-track (tabulated-list-get-id)))
(spotify-play-track selected-track
(spotify-get-track-album selected-track))))
(defun spotify-track-reload ()
"Reloads the first page of results for the current track view."
(interactive)
(if (bound-and-true-p spotify-query)
(spotify-track-search-update 1)
(spotify-playlist-tracks-update 1)))
(defun spotify-track-load-more ()
"Loads the next page of results for the current track view."
(interactive)
(if (bound-and-true-p spotify-query)
(spotify-track-search-update (1+ spotify-current-page))
(spotify-playlist-tracks-update (1+ spotify-current-page))))
(defun spotify-track-search-update (current-page)
"Fetches the given page of results using the search endpoint."
(let* ((json (spotify-api-search 'track spotify-query current-page))
(items (spotify-get-search-track-items json)))