Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helm integration #52

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,19 @@ you can set the following, i.e.:
````
Otherwise, it defaults to 4 spaces.

## Helm Integration ##

Helm integration can be enabled for all Spotify features of this package
by adding this to your config

````el
(setq spotify-helm-integration 1)
````

Note: Spotify will use tabulated modes by default if this is turned on without
helm installed. If you do install helm afterwards, please ensure to reload the
package or restart emacs for it to take effect.

## Donate

If this project is useful for you, buy me a beer!
Expand Down
20 changes: 18 additions & 2 deletions spotify-api.el
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ relevant to a particular country. If omitted, the returned items will be
globally relevant."
:type 'string)

(defcustom spotify-helm-integration nil
"Optional. If true, then use the helm front end for all APIs."
:type 'boolean)

;; Do not rely on the auto-refresh logic from oauth2.el, which seems broken for async requests
(defun spotify-oauth2-token ()
"Retrieve the Oauth2 access token that must be used to interact with the
Expand Down Expand Up @@ -198,8 +202,11 @@ the current user."
(gethash 'id json))

(defun spotify-get-item-uri (json)
"Return the uri from the given track/album/artist JSON object."
(gethash 'uri json))
"Return the uri from the given track/album/artist JSON object.
Track link objects are preceded if relinking is applied for the track server side"
(if-let (linked-from-json (gethash 'linked_from json))
(gethash 'uri linked-from-json)
(gethash 'uri json)))

(defun spotify-get-playlist-track-count (json)
"Return the number of tracks of the given playlist JSON object."
Expand Down Expand Up @@ -445,5 +452,14 @@ which must be a number between 0 and 100."
nil
callback))

(defun spotify-api-enqueue (uri &optional callback)
"Add track/episode to playlist queue."
(spotify-api-call-async
"POST"
(concat "/me/player/queue?"
(url-build-query-string `((uri ,uri))
nil t))
nil
callback))

(provide 'spotify-api)
17 changes: 12 additions & 5 deletions spotify-device-select.el
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

(require 'spotify-api)

(when (require 'helm nil 'noerror)
(require 'spotify-helm-integration))

(defcustom spotify-selected-device-id ""
"The id of the device selected for transport."
:type 'string)
Expand All @@ -33,11 +36,15 @@
(if-let ((devices (gethash 'devices json))
(line (string-to-number (format-mode-line "%l"))))
(progn
(pop-to-buffer buffer)
(spotify-devices-print devices)
(goto-char (point-min))
(forward-line (1- line))
(message "Device list updated."))
(if (and spotify-helm-integration (require 'helm nil 'noerror))
(with-current-buffer buffer
(spotify-devices-print devices)
(helm-devices "Spotify Devices"))
(pop-to-buffer buffer)
(spotify-devices-print devices)
(goto-char (point-min))
(forward-line (1- line))
(message "Device list updated.")))
(message "No devices are available."))))))

(defun spotify-devices-print (devices)
Expand Down
249 changes: 249 additions & 0 deletions spotify-helm-integration.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
;;; package --- Summary

;;; Commentary:

;; spotify-helm-integration.el --- Spotify.el helm integration

;; Code:


(defvar helm-playlists-doc-header
" (\\<helm-playlists-map>\\[helm-playlists-load-more-interactive]: Load more playlists)"
"*The doc that is inserted in the Name header of the helm spotify source.")

(defvar helm-tracks-doc-header
" (\\<helm-tracks-map>\\[helm-tracks-load-more-interactive]: Load more tracks)"
"*The doc that is inserted in the Name header of the helm spotify source.")


;; Helm-Keymaps

(defvar helm-playlists-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-map)
(define-key map (kbd "C-l") 'helm-playlists-load-more-interactive)
(define-key map (kbd "C-M-f") 'helm-playlists-follow-interactive)
(define-key map (kbd "C-M-u") 'helm-playlists-unfollow-interactive)
map)
"Local keymap for playlists in helm buffers")

(defvar helm-tracks-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-map)
(define-key map (kbd "C-q") 'helm-tracks-enqueue-interactive)
(define-key map (kbd "C-l") 'helm-tracks-load-more-interactive)
(define-key map (kbd "C-M-a") 'helm-tracks-view-album-interactive)
map)
"Local keymap for tracks in helm buffers")


;;; Helm-playlists
;;
;;

(defcustom helm-playlists-actions (helm-make-actions
"View playlist's tracks `RET'" 'helm-playlists-view-tracks-core
"Load more playlists `C-l'" 'helm-playlists-load-more-core
"Follow playlist `C-M-f'" 'helm-playlists-follow-core
"Unfollow playlist `C-M-u'" 'helm-playlists-unfollow-core)
"Actions for playlists in helm buffers"
:group 'spotify
:type '(alist :key-type string :value-type function))

(defun helm-playlists-view-tracks-core (candidate)
"Helm action to view all tracks of the selected playlist"
(when helm-in-persistent-action
(helm-exit-minibuffer))
(spotify-playlist-tracks candidate))

(defun helm-playlists-load-more-interactive ()
"Helm action wrapper to bind to a key map"
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action 'helm-playlists-load-more-core)))

(defun helm-playlists-load-more-core (_candidate)
"Helm action to load more playlists"
(spotify-playlist-load-more))

(defun helm-playlists-follow-interactive ()
"Helm action wrapper to bind to a key map"
(interactive)
(with-helm-alive-p
(helm-attrset 'follow '(helm-playlists-follow-core . never-split))
(helm-execute-persistent-action 'follow)))

(defun helm-playlists-follow-core (candidate)
"Helm action to follow the selected playlist"
(spotify-playlist-follow candidate))

(defun helm-playlists-unfollow-interactive ()
"Helm action wrapper to bind to a key map"
(interactive)
(with-helm-alive-p
(helm-attrset 'unfollow '(helm-playlists-unfollow-core . never-split))
(helm-execute-persistent-action 'unfollow)))

(defun helm-playlists-unfollow-core (candidate)
"Helm action to unfollow the selected playlist"
(spotify-playlist-unfollow candidate))

(defun helm-playlists (source-name)
"This will use the tab buffer generated from loading playlist items as a source for helm to
operate on"
(lexical-let ((tabulated-list-entries tabulated-list-entries))
(helm :sources (helm-build-in-buffer-source source-name
:header-name (lambda (name)
(concat name (substitute-command-keys helm-playlists-doc-header)))
:data (current-buffer)
:get-line #'buffer-substring
:display-to-real (lambda (_candidate)
(let* ((candidate
(helm-get-selection nil 'withprop))
(tabulated-list-id
(get-text-property 0 'tabulated-list-id candidate)))
tabulated-list-id))
:action helm-playlists-actions
:keymap helm-playlists-map
:fuzzy-match t)
:buffer "*helm spotify*")))


;;; Helm-tracks
;;
;;

(defcustom helm-tracks-actions (helm-make-actions
"Play track `RET'" 'helm-tracks-select-default-core
"Enqueue track `C-q'" 'helm-tracks-enqueue-core
"Load more tracks `C-l'" 'helm-tracks-load-more-core
"View album of track `C-M-a'" 'helm-tracks-view-album-core)
"Actions for tracks in helm buffers"
:group 'spotify
:type '(alist :key-type string :value-type function))

(defun helm-tracks-select-default-core (candidate)
"Helm action to play a selected track & clean up dangling Spotify buffers"
(spotify-track-select-default candidate)
(unless helm-in-persistent-action
(helm-spotify-cleanup-buffers)))

(defun helm-tracks-load-more-interactive ()
"Helm action wrapper to bind to a key map"
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action 'helm-tracks-load-more-core)))

(defun helm-tracks-load-more-core (_candidate)
"Helm action to load more tracks"
(spotify-track-load-more))

(defun helm-tracks-view-album-interactive ()
"Helm action wrapper to bind to a key map"
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action 'helm-tracks-view-album-core)))

(defun helm-tracks-view-album-core (candidate)
"Helm action to view a track's album context"
(let ((album (spotify-get-track-album candidate)))
(spotify-album-tracks album)))

(defun helm-tracks-enqueue-interactive ()
"Helm action wrapper to bind to a key map"
(interactive)
(with-helm-alive-p
(helm-attrset 'enqueue '(helm-tracks-enqueue-core . never-split))
(helm-execute-persistent-action 'enqueue)))

(defun helm-tracks-enqueue-core (candidate)
"Helm action to enqueue a track into the active device's playback"
(hash-table-keys candidate)
(lexical-let ((name (spotify-get-item-name candidate))
(uri (spotify-get-item-uri candidate)))
(spotify-api-enqueue uri (lambda (_)
(message (format "Added '%s' to playback queue" name))))))

(defun helm-tracks (source-name)
"This will use the tab buffer generated from loading track items as a source for helm to
operate on"
(lexical-let ((tabulated-list-entries tabulated-list-entries))
(helm :sources (helm-build-in-buffer-source source-name
:header-name (lambda (name)
(concat name (substitute-command-keys helm-tracks-doc-header)))
:data (current-buffer)
:get-line #'buffer-substring
:display-to-real (lambda (_candidate)
(let* ((candidate
(helm-get-selection nil 'withprop))
(tabulated-list-id
(get-text-property 0 'tabulated-list-id candidate)))
tabulated-list-id))
:action helm-tracks-actions
:keymap helm-tracks-map
:fuzzy-match t)
:buffer "*helm spotify*")))


;;; Helm-devices
;;
;;

(defcustom helm-devices-actions (helm-make-actions
"Select device `RET'" 'helm-devices-select-core)
"Actions for devices in helm buffers"
:group 'spotify
:type '(alist :key-type string :value-type function))

(defun helm-devices-select-core (candidate)
"Helm action to make the selected device active"
(lexical-let ((device-id (spotify-get-device-id candidate))
(name (spotify-get-device-name candidate)))
(spotify-api-transfer-player
device-id
(lambda (json)
(setq spotify-selected-device-id device-id)
(message "Device '%s' selected" name)))
(helm-spotify-cleanup-buffers)))

(defun helm-devices (source-name)
"This will use the tab buffer generated from loading device items as a source for helm to
operate on"
(lexical-let ((tabulated-list-entries tabulated-list-entries))
(helm :sources (helm-build-in-buffer-source source-name
:data (current-buffer)
:get-line #'buffer-substring
:display-to-real (lambda (_candidate)
(let* ((candidate
(helm-get-selection nil 'withprop))
(tabulated-list-id
(get-text-property 0 'tabulated-list-id candidate)))
tabulated-list-id))
:action helm-devices-actions
:fuzzy-match t)
:buffer "*helm spotify*")))


;;; Misc
;;
;;

(defun helm-spotify-cleanup-buffers ()
"Cleanup dangling tabulated-mode buffers from the core search APIs."
(let ((buffer-list (mapcar (lambda (buffer) (buffer-name buffer)) (buffer-list)))
(spotify-buffer-candidates '("*Devices*"
"*Featured Playlists*"
"*Recently Played*"
"\*Playlists: .*\*"
"\*Playlist Search: .*\*"
"\*Track Search: .*\*"
"\*Playlist Tracks: .*\*"
"\*Album: .*\*")))
(mapc (lambda (spotify-buffer) (kill-buffer spotify-buffer))
(seq-filter (lambda (buffer)
(when (some (lambda (candidate) (string-match-p candidate buffer))
spotify-buffer-candidates)
buffer))
buffer-list))))

(provide 'spotify-helm-integration)
Loading