Skip to content

Commit

Permalink
Merge branch 'master' into transient-map
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfm committed Dec 13, 2023
2 parents dcfe644 + 7965cd4 commit 80dbd41
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 102 deletions.
2 changes: 2 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: danielfm
liberapay: danielfm
4 changes: 2 additions & 2 deletions .github/workflows/melpazoid.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.6
- name: Set up Python 3.12
uses: actions/setup-python@v1
with: { python-version: 3.6 }
with: { python-version: 3.12 }
- name: Install
run: |
python -m pip install --upgrade pip
Expand Down
145 changes: 93 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Smudge

[![MELPA](https://melpa.org/packages/smudge-badge.svg)](https://melpa.org/#/smudge)

**Control Spotify app from within Emacs.**

[![asciicast](https://asciinema.org/a/218654.svg)](https://asciinema.org/a/218654)
Expand All @@ -24,52 +26,87 @@ Connect feature.

## Installation

(Requires Emacs 27.1+)
Smudge requires Emacs 27.1+.

### Vanilla Emacs

`package.el` is the built-in package manager in Emacs.

Smudge is available on the two major package.el community maintained repos - MELPA Stable and MELPA.
Smudge is available on the two major package.el community maintained repos MELP AStable and MELPA.

You can install Smudge with the following command:

M-x package-install [RET] smudge [RET]
<kbd>M-x</kbd> package-install <kbd>[RET]</kbd> smudge <kbd>[RET]</kbd>

To manually install Smudge instead, just clone this project somewhere in your
disk, add that directory in the `load-path`, and require the `smudge` module:
Or put the following snippet into your Emacs configuration:

````el
(add-to-list 'load-path "<smudge-dir>")
(require 'smudge)
````
```el
(use-package smudge
:bind-keymap ("C-c ." . smudge-command-map)
:config
(setq smudge-oauth2-client-secret "..."
smudge-oauth2-client-id "..."))
```

### Doom Emacs

Add the following to the `packages.el` file:

```el
;; Fetch from MELPA
(package! smudge)
;; Fetch from GitHub
(package! smudge
:recipe (:host github :repo "danielfm/smudge"))
```

Add the following to the `config.el` file:

``` el
(use-package! smudge
:bind-keymap ("C-c ." . smudge-command-map)
:custom
(smudge-oauth2-client-secret "...")
(smudge-oauth2-client-id "...")
;; optional: enable transient map for frequent commands
(smudge-player-use-transient-map t))
```

## Configuration

````el
;; Settings
```el
(setq smudge-oauth2-client-secret "<spotify-app-client-secret>")
(setq smudge-oauth2-client-id "<spotify-app-client-id>")
(define-key smudge-mode-map (kbd "C-c .") 'smudge-command-map)
````
```

That keymap prefix is just a suggestion, following the conventions suggested for minor modes as
defined in the Emacs manual [Key Binding
Conventions](https://www.gnu.org/software/emacs/manual/html_node/elisp/Key-Binding-Conventions.html#Key-Binding-Conventions). Previous
versions of this package used "M-p"
That <kbd>C-c .</kbd> keymap prefix is just a suggestion, following the
conventions suggested for minor modes as defined in the Emacs manual
[Key Binding Conventions](https://www.gnu.org/software/emacs/manual/html_node/elisp/Key-Binding-Conventions.html#Key-Binding-Conventions).
Previous versions of this package used <kbd>M-p</kbd>.

A transient map can be enabled to allow repeating frequent commands (defined in
`smudge-transient-command-map`) without having to repeat the prefix key for `smudge-command-map`.
````el
A transient map can be enabled to allow repeating frequent commands
(defined in `smudge-transient-command-map`) without having to repeat the
prefix key for `smudge-command-map`.

```el
(setq smudge-player-use-transient-map t)
````
```

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:8080/smudge-api-callback> as the redirect URI (or whichever port you have specified via customize).
The OAuth2 exchange is handled by `simple-httpd`. If you are not already using this package for something else, you should not need to customize this port. Otherwise, you'll want to set it to whatever port you are running on.
In order to get the client ID and client secret, you need to create a
[Spotify app](https://developer.spotify.com/my-applications), specifying
<http://localhost:8080/smudge-api-callback> as the redirect URI (or whichever
port you have specified via customize). The OAuth2 exchange is handled by
`simple-httpd`. If you are not already using this package for something else,
you should not need to customize this port. Otherwise, you'll want to set it
to whatever port you are running on.

To use the "Spotify Connect" transport (vs. controlling only your local instance - though you can
also control your local instance as well), set `smudge-transport` to `'connect` as follows. This
feature requires a Spotify premium subscription.
To use the "Spotify Connect" transport (vs. controlling only your local
instance - though you can also control your local instance as well), set
`smudge-transport` to `'connect` as follows. **This feature requires a Spotify
premium subscription.**

````el
(setq smudge-transport 'connect)
Expand Down Expand Up @@ -100,24 +137,24 @@ At this point, the client ID and the client secret are available, so set those v
Whenever you enable the `global-smudge-remote-mode` minor mode you get the following
key bindings:

| Key | Function | Description |
|:---------------------|:---------------------------------------|:-------------------------------------------|
| <kbd>C-c . M-s</kbd> | `smudge-controller-toggle-shuffle` | Turn shuffle on/off [1] |
| <kbd>C-c . M-r</kbd> | `smudge-controller-toggle-repeat` | Turn repeat on/off [1] |
| <kbd>C-c . M-p</kbd> | `smudge-controller-toggle-play` | Play/pause |
| <kbd>C-c . M-f</kbd> | `smudge-controller-next-track` | Next track |
| <kbd>C-c . M-b</kbd> | `smudge-controller-previous-track` | Previous track |
| <kbd>C-c . p m</kbd> | `smudge-my-playlists` | Show your playlists |
| <kbd>C-c . p f</kbd> | `smudge-featured-playlists` | Show the featured playlists |
| <kbd>C-c . p s</kbd> | `smudge-playlist-search` | Search for playlists |
| <kbd>C-c . p u</kbd> | `smudge-user-playlists` | Show playlists for the given user |
| <kbd>C-c . p c</kbd> | `smudge-create-playlist` | Create a new playlist |
| <kbd>C-c . t r</kbd> | `smudge-recently-played` | List of recently played tracks |
| <kbd>C-c . t s</kbd> | `smudge-track-search` | Search for tracks |
| <kbd>C-c . v u</kbd> | `smudge-controller-volume-up` | Increase the volume [2] |
| <kbd>C-c . v d</kbd> | `smudge-controller-volume-down` | Decrease the volume [2] |
| <kbd>C-c . v m</kbd> | `smudge-controller-volume-mute-unmute` | Alternate the volume between 0 and 100 [2] |
| <kbd>C-c . d</kbd> | `smudge-select-device` | Select a playback device [2] |
| Key | Function | Description |
|:------------------------|:---------------------------------------|:-------------------------------------------|
| <kbd>[prefix] M-s</kbd> | `smudge-controller-toggle-shuffle` | Turn shuffle on/off [1] |
| <kbd>[prefix] M-r</kbd> | `smudge-controller-toggle-repeat` | Turn repeat on/off [1] |
| <kbd>[prefix] M-p</kbd> | `smudge-controller-toggle-play` | Play/pause |
| <kbd>[prefix] M-f</kbd> | `smudge-controller-next-track` | Next track |
| <kbd>[prefix] M-b</kbd> | `smudge-controller-previous-track` | Previous track |
| <kbd>[prefix] p m</kbd> | `smudge-my-playlists` | Show your playlists |
| <kbd>[prefix] p f</kbd> | `smudge-featured-playlists` | Show the featured playlists |
| <kbd>[prefix] p s</kbd> | `smudge-playlist-search` | Search for playlists |
| <kbd>[prefix] p u</kbd> | `smudge-user-playlists` | Show playlists for the given user |
| <kbd>[prefix] p c</kbd> | `smudge-create-playlist` | Create a new playlist |
| <kbd>[prefix] t r</kbd> | `smudge-recently-played` | List of recently played tracks |
| <kbd>[prefix] t s</kbd> | `smudge-track-search` | Search for tracks |
| <kbd>[prefix] v u</kbd> | `smudge-controller-volume-up` | Increase the volume [2] |
| <kbd>[prefix] v d</kbd> | `smudge-controller-volume-down` | Decrease the volume [2] |
| <kbd>[prefix] v m</kbd> | `smudge-controller-volume-mute-unmute` | Alternate the volume between 0 and 100 [2] |
| <kbd>[prefix] d</kbd> | `smudge-select-device` | Select a playback device [2] |

The current song being played by Smudge is displayed in the mode
line along with the player status (playing, paused). The interval in which the
Expand Down Expand Up @@ -222,6 +259,7 @@ key bindings:
| <kbd>a</kbd> | Adds track to a playlist |
| <kbd>l</kbd> | Loads the next page of results (pagination) |
| <kbd>g</kbd> | Clears the results and reloads the first page of results |
| <kbd>k</kbd> | Adds track to the queue |
| <kbd>M-RET</kbd> | Plays the track under the cursor in the context of its album [1] |

[1] D-Bus implementation for GNU/Linux do not support passing the context, so
Expand Down Expand Up @@ -263,11 +301,11 @@ playlists from Spotify en_US.
Change the following variables in order to customize the locale and region for
the featured playlists endpoint:

````el
```el
;; Spanish (Mexico)
(setq smudge-api-locale "es_MX")
(setq smudge-api-country "MX")
````
```

All these commands will display results in a separate buffer with the following
key bindings:
Expand All @@ -287,10 +325,12 @@ bindings in the resulting buffer:
| Key | Description |
|:-----------------|:--------------------------------------------------------------------|
| <kbd>a</kbd> | Adds track to a playlist |
| <kbd>r</kbd> | Removes track from current playlist
| <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>k</kbd> | Adds track to the queue |
| <kbd>M-RET</kbd> | Plays the track under the cursor in the context of the playlist [1] |

Both buffers load the `global-smudge-remote-mode` by default.
Expand All @@ -317,20 +357,21 @@ By default, the player status (playing, paused, track name, time, shuffle, repea
in the modeline. If you want to display the status in the title bar when using a graphical display,
you can set the following:

````el
```el
(setq smudge-status-location 'title-bar)
````
```

Valid values include `'title-bar`, `'modeline` and `nil`, where nil turns off the display of the
player status completely. If the value is set to `title-bar` but you are not using a graphical
display, the player status will be displayed in the mode line instead.

If you want to customize the separator between the existing title bar text and the player status,
you can set the following, i.e.:
````el

```el
(setq smudge-title-bar-separator "----")
````
```

Otherwise, it defaults to 4 spaces.

## License
Expand Down
46 changes: 44 additions & 2 deletions smudge-api.el
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

;;; Commentary:

;; This library is the interface to the Spotify RESTful API. It also does some custom handling of
;; the OAuth code exchange via 'simple-httpd
;; This library is the interface to the Spotify RESTful API. It also does some
;; custom handling of the OAuth code exchange via 'simple-httpd

;;; Code:

(require 'simple-httpd)
(require 'request)
(require 'oauth2)
(require 'browse-url)

(defcustom smudge-oauth2-client-id ""
"The unique identifier for your application.
Expand Down Expand Up @@ -420,6 +421,23 @@ Call CALLBACK with results."
(format "{\"uris\": [ %s ]}" tracks)
callback)))

(defun smudge-api-playlist-remove-track (playlist-id track-id callback)
"Remove TRACK-ID from PLAYLIST-ID.
Removed by USER-ID. Call CALLBACK with results."
(smudge-api-playlist-remove-tracks playlist-id (list track-id) callback))

(defun smudge-api-playlist-remove-tracks (playlist-id track-ids callback)
"Remove TRACK-IDS from PLAYLIST-ID for USER-ID.
Call CALLBACK with results."
(let ((tracks (format "%s" (mapconcat
(lambda (x) (format "{\"uri\": %s}" (smudge-api-format-id "track" x)))
track-ids ","))))
(smudge-api-call-async
"DELETE"
(format "/playlists/%s/tracks" (url-hexify-string playlist-id))
(format "{\"tracks\": [ %s ]}" tracks)
callback)))

(defun smudge-api-playlist-follow (playlist callback)
"Add the current user as a follower of PLAYLIST.
Call CALLBACK with results."
Expand Down Expand Up @@ -596,5 +614,29 @@ Call CALLBACK if provided."
nil
callback))


(defun smudge-api-queue-add-track (track-id &optional callback)
"Add given TRACK-ID to the queue and call CALLBACK afterwards."
(smudge-api-call-async
"POST"
(concat "/me/player/queue?"
(url-build-query-string `((uri ,track-id))
nil t))
nil
callback))

(defun smudge-api-queue-add-tracks (track-ids &optional callback)
"Add given TRACK-IDS to the queue and call CALLBACK afterwards."
;; Spotify's API doesn't provide a endpoint that would enable us to
;; add multiple tracks to the queue at the same time.
;; Thus we have to synchronously add the tracks
;; one by one to the queue.
(if (car track-ids)
(smudge-api-queue-add-track (car track-ids)
(lambda (_)
(smudge-api-queue-add-tracks (cdr track-ids)
nil)))
(funcall callback)))

(provide 'smudge-api)
;;; smudge-api.el ends here
14 changes: 6 additions & 8 deletions smudge-apple.el
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@

;;; Commentary:

;; This library handles controlling Spotify via Applescript commands. It implements a set of
;; multimethod-like functions that are dispatched in smudge-controller.el.
;; This library handles controlling Spotify via Applescript commands. It
;; implements a set of multimethod-like functions that are dispatched in
;; smudge-controller.el.

;;; Code:

(require 'smudge-controller)

(defvar smudge-apple-player-status-script)
(defvar smudge-apple-player-status-script-file)

(defcustom smudge-osascript-bin-path "/usr/bin/osascript"
"Path to `osascript' binary."
:group 'smudge
:type 'string)

;; Do not change this unless you know what you're doing
(setq smudge-apple-player-status-script "
(defconst smudge-apple-player-status-script "
# Source: https://github.com/andrehaveman/smudge-node-applescript
on escape_quotes(string_to_escape)
set AppleScript's text item delimiters to the \"\\\"\"
Expand All @@ -49,8 +47,8 @@ end tell
")

;; Write script to a temp file
(setq smudge-apple-player-status-script-file
(make-temp-file "smudge.el" nil nil smudge-apple-player-status-script))
(defconst smudge-apple-player-status-script-file
(make-temp-file "smudge.el" nil nil smudge-apple-player-status-script))

(defun smudge-apple-command-line (cmd)
"Return a command line prefix for any Spotify command CMD."
Expand Down
14 changes: 7 additions & 7 deletions smudge-connect.el
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

;;; Commentary:

;; This library uses the "connect" APIs to control transport functions of remote and local instances
;; of Spotify clients. It implements a set of multimethod-like functions that are dispatched in
;; smudge-controller.el.
;; This library uses the "connect" APIs to control transport functions of
;; remote and local instances of Spotify clients. It implements a set of
;; multimethod-like functions that are dispatched in smudge-controller.el.

;; smudge-connect.el --- Smudge transport for the Spotify Connect API

Expand Down Expand Up @@ -118,16 +118,16 @@ Returns a JSON string in the format:
(message "Volume decreased to %d%%" new-volume))))))))

(defun smudge-connect-volume-mute-unmute ()
"Mute/unmute the volume on the actively playing device by setting the volume to 0."
"Mute/unmute the actively playing device by setting the volume to 0."
(smudge-connect-when-device-active
(smudge-api-get-player-status
(lambda (status)
(let ((volume (smudge-connect-get-volume status)))
(if (eq volume 0)
(smudge-api-set-volume (smudge-connect-get-device-id status) 100
(lambda (_) (message "Volume unmuted")))
(lambda (_) (message "Volume unmuted")))
(smudge-api-set-volume (smudge-connect-get-device-id status) 0
(lambda (_) (message "Volume muted")))))))))
(lambda (_) (message "Volume muted")))))))))

(defun smudge-connect-toggle-repeat ()
"Toggle repeat for the current track."
Expand Down Expand Up @@ -156,7 +156,7 @@ Returns a JSON string in the format:
(defun smudge-connect--is-shuffling (player-status)
"Business logic for shuffling state of PLAYER-STATUS."
(and player-status
(not (eq (gethash 'shuffle_state player-status) :json-false))))
(not (eq (gethash 'shuffle_state player-status) :json-false))))

(defun smudge-connect--is-repeating (player-status)
"Business logic for repeat state of PLAYER-STATUS."
Expand Down
Loading

0 comments on commit 80dbd41

Please sign in to comment.