From ced989d03ecbc74cd8ee5b4ad5bc2a8701b19697 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Mon, 8 Feb 2021 18:03:34 -0500 Subject: [PATCH 01/16] Serialize & Deserialize OAuth token to disk --- spotify-api.el | 76 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/spotify-api.el b/spotify-api.el index d9ebb95..f3b1c01 100644 --- a/spotify-api.el +++ b/spotify-api.el @@ -32,6 +32,8 @@ (defvar *spotify-oauth2-ts* nil "Unix timestamp in which the OAuth2 token was retrieved. This is used to manually refresh the token when it's about to expire.") +(defvar *spotify-oauth2-token-file* "~/.emacs.d/.cache/spotify/token" + "Location where the OAuth2 token is serialized.") (defcustom spotify-oauth2-client-id "" "The unique identifier for your application. @@ -152,32 +154,66 @@ that runs a local httpd for code -> token exchange." client-secret (spotify-oauth2-request-authorization auth-url client-id scope state redirect-uri) - redirect-uri)) + redirect-uri)) + +(defun spotify-serialize-token () + "Save OAuth2 token to file." + (and + (not (null *spotify-oauth2-token-file*)) + (not (null *spotify-oauth2-token*)) + (progn + (delete-file *spotify-oauth2-token-file*) + (make-empty-file *spotify-oauth2-token-file*) + t) + (with-temp-file *spotify-oauth2-token-file* + (prin1 `(,*spotify-oauth2-token* ,*spotify-oauth2-ts*) (current-buffer))))) + +(defun spotify-deserialize-token () + "Read OAuth2 token from file." + (and + (file-exists-p *spotify-oauth2-token-file*) + (with-temp-buffer + (insert-file-contents *spotify-oauth2-token-file*) + (if (= 0 (buffer-size (current-buffer))) + nil + (progn + (goto-char (point-min)) + (pcase-let ((`(,spotify-oauth2-token ,spotify-oauth2-ts) (read (current-buffer)))) + (setq *spotify-oauth2-token* spotify-oauth2-token) + (setq *spotify-oauth2-ts* spotify-oauth2-ts))))))) + +(defun spotify-persist-token (token now) + "Persist TOKEN and current time NOW to disk and set in memory too." + (setq *spotify-oauth2-token* token) + (setq *spotify-oauth2-ts* now) + (spotify-serialize-token)) ;; 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 Spotify API." + "Retrieve the Oauth2 access token used to interact with the Spotify API. +Use the first available token in order of: memory, disk, retrieve from API via +OAuth2 protocol. Refresh if expired." (let ((now (string-to-number (format-time-string "%s")))) - (if (null *spotify-oauth2-token*) - (let ((token (spotify-oauth2-auth spotify-oauth2-auth-url - spotify-oauth2-token-url - spotify-oauth2-client-id - spotify-oauth2-client-secret - spotify-oauth2-scopes - nil - spotify-oauth2-callback))) - (setq *spotify-oauth2-token* token) - (setq *spotify-oauth2-ts* now) + (if (null (or *spotify-oauth2-token* (spotify-deserialize-token))) + (let ((token (spotify-oauth2-auth spotify-oauth2-auth-url + spotify-oauth2-token-url + spotify-oauth2-client-id + spotify-oauth2-client-secret + spotify-oauth2-scopes + nil + spotify-oauth2-callback))) + (spotify-persist-token token now) + (if (null token) + (user-error "OAuth2 authentication failed") + token)) + ;; Spotify tokens appear to expire in 3600 seconds (60 min). We renew + ;; at 3000 (50 min) to play it safe + (if (> now (+ *spotify-oauth2-ts* 3000)) + (let ((token (oauth2-refresh-access *spotify-oauth2-token*))) + (spotify-persist-token token now) (if (null token) - (user-error "OAuth2 authentication failed") + (user-error "Could not refresh OAuth2 token") token)) - (if (> now (+ *spotify-oauth2-ts* 3000)) - (let ((token (oauth2-refresh-access *spotify-oauth2-token*))) - (setq *spotify-oauth2-token* token) - (setq *spotify-oauth2-ts* now) - (if (null token) - (user-error "Could not refresh OAuth2 token") - token)) *spotify-oauth2-token*)))) (defun spotify-api-call-async (method uri &optional data callback is-retry) From eb603a86dd004df525422a1cc2bd0cc6d63ab86d Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Mon, 8 Feb 2021 22:08:28 -0500 Subject: [PATCH 02/16] Use request.el --- spotify-api.el | 49 ++++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/spotify-api.el b/spotify-api.el index f3b1c01..cf98962 100644 --- a/spotify-api.el +++ b/spotify-api.el @@ -10,6 +10,7 @@ ;;; Code: (require 'simple-httpd) +(require 'request) ;; Due to an issue related to compilation and the way oauth2 uses defadvice ;; (including a FIXME as of 0.1.1), this declaration exists to prevent @@ -216,39 +217,25 @@ OAuth2 protocol. Refresh if expired." token)) *spotify-oauth2-token*)))) -(defun spotify-api-call-async (method uri &optional data callback is-retry) +(defun spotify-api-call-async (method uri &optional data callback) "Make a request to the given Spotify service endpoint URI via METHOD. Call CALLBACK with the parsed JSON response." - (oauth2-url-retrieve - (spotify-oauth2-token) - (concat spotify-api-endpoint uri) - (lambda (_) - (toggle-enable-multibyte-characters t) - (goto-char (point-min)) - (condition-case _ - (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-async method uri data callback t) - (when callback (funcall callback json))))) - - ;; Handle empty responses - (end-of-file - (kill-buffer) - (when callback (funcall callback nil))))) - nil - method - (or data "") - '(("Content-Type" . "application/json")))) + (request (concat spotify-api-endpoint uri) + :headers `(("Authorization" . + ,(format "Bearer %s" (oauth2-token-access-token (spotify-oauth2-token)))) + ("Accept" . "application/json") + ("Content-Type" . "application/json")) + :type method + :parser (lambda () + (let ((json-object-type 'hash-table) + (json-array-type 'list) + (json-key-type 'symbol)) + (json-read))) + :encoding 'utf-8 + :data data + :success (cl-function + (lambda (&key response &allow-other-keys) + (when callback (funcall callback (request-response-data response))))))) (defun spotify-current-user (callback) "Call CALLBACK with the currently logged in user." From 71582ae636d7ca9fa6bd813c6a193175aee29dad Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Tue, 9 Feb 2021 09:29:24 -0500 Subject: [PATCH 03/16] Send content-length as required by Spotify API --- spotify-api.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spotify-api.el b/spotify-api.el index cf98962..a60edb8 100644 --- a/spotify-api.el +++ b/spotify-api.el @@ -224,7 +224,8 @@ Call CALLBACK with the parsed JSON response." :headers `(("Authorization" . ,(format "Bearer %s" (oauth2-token-access-token (spotify-oauth2-token)))) ("Accept" . "application/json") - ("Content-Type" . "application/json")) + ("Content-Type" . "application/json") + ("Content-Length" . ,(length data))) :type method :parser (lambda () (let ((json-object-type 'hash-table) From 36e0f1f509ee75ec4e4e4c360b05d85b20ab2500 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Tue, 9 Feb 2021 12:03:31 -0500 Subject: [PATCH 04/16] Handle empty responses --- spotify-api.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spotify-api.el b/spotify-api.el index a60edb8..4aefbcf 100644 --- a/spotify-api.el +++ b/spotify-api.el @@ -231,7 +231,8 @@ Call CALLBACK with the parsed JSON response." (let ((json-object-type 'hash-table) (json-array-type 'list) (json-key-type 'symbol)) - (json-read))) + (when (> (buffer-size) 0) + (json-read)))) :encoding 'utf-8 :data data :success (cl-function From cfcd1bf225b41421dbe761e4194df1189d2e89f8 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Wed, 10 Feb 2021 14:51:19 -0500 Subject: [PATCH 05/16] Player status metadata (for lyrics package support) --- spotify-apple.el | 2 +- spotify-connect.el | 4 ++-- spotify-controller.el | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/spotify-apple.el b/spotify-apple.el index d2f4c75..cba9663 100644 --- a/spotify-apple.el +++ b/spotify-apple.el @@ -65,7 +65,7 @@ Return the resulting status string." (defun spotify-apple-set-player-status-from-process-output (process output) "Set the OUTPUT of the player status PROCESS to the player status." - (spotify-replace-player-status-flags output) + (spotify-update-metadata output) (with-current-buffer (process-buffer process) (delete-region (point-min) (point-max)))) diff --git a/spotify-connect.el b/spotify-connect.el index ff30fe6..833ce88 100644 --- a/spotify-connect.el +++ b/spotify-connect.el @@ -48,8 +48,8 @@ Returns a JSON string in the format: (format "\"player_repeating\":%s" (if (string= (gethash 'repeat_state status) "off") "false" "true")) "}"))) - (spotify-replace-player-status-flags json) - (spotify-replace-player-status-flags nil))))) + (spotify-update-metadata json) + (spotify-update-metadata nil))))) (defmacro spotify-when-device-active (body) "Evaluate BODY when there is an active device, otherwise show an error message." diff --git a/spotify-controller.el b/spotify-controller.el index ba4f840..8c9d1b1 100644 --- a/spotify-controller.el +++ b/spotify-controller.el @@ -101,6 +101,9 @@ The following placeholders are supported: (defvar spotify-player-status "" "The text to be displayed in the global mode line or title bar.") +(defvar spotify-player-metadata nil + "The metadata about the currently playing track.") + (defun spotify-apply (suffix &rest args) "Simple facility to emulate multimethods. Apply SUFFIX to spotify-prefixed functions, applying ARGS." @@ -109,6 +112,28 @@ Apply SUFFIX to spotify-prefixed functions, applying ARGS." (unless (string= suffix "player-status") (spotify-player-status)))) +(defun spotify-update-metadata (metadata) + "Compose the playing status string to be displayed in the mode-line from METADATA." + (let* ((player-status spotify-player-status-format) + (duration-format "%m:%02s") + (json-object-type 'hash-table) + (json-key-type 'symbol) + (json (condition-case nil + (json-read-from-string metadata) + (error (spotify-update-player-status "") + nil)))) + (when json + (progn + (setq player-status (replace-regexp-in-string "%a" (truncate-string-to-width (gethash 'artist json) spotify-player-status-truncate-length 0 nil "...") player-status)) + (setq player-status (replace-regexp-in-string "%t" (truncate-string-to-width (gethash 'name json) spotify-player-status-truncate-length 0 nil "...") player-status)) + (setq player-status (replace-regexp-in-string "%n" (number-to-string (gethash 'track_number json)) player-status)) + (setq player-status (replace-regexp-in-string "%l" (format-seconds duration-format (/ (gethash 'duration json) 1000)) player-status)) + (setq player-status (replace-regexp-in-string "%s" (spotify-player-status-shuffling-indicator (gethash 'player_shuffling json)) player-status)) + (setq player-status (replace-regexp-in-string "%r" (spotify-player-status-repeating-indicator (gethash 'player_repeating json)) player-status)) + (setq player-status (replace-regexp-in-string "%p" (spotify-player-status-playing-indicator (gethash 'player_state json)) player-status)) + (spotify-update-player-status player-status) + (setq spotify-player-metadata json))))) + (defun spotify-update-player-status (str) "Set the given STR to the player status, prefixed with the mode identifier." (when (not (string= str spotify-player-status)) From c82b73b69d319cd8d7c69d4acfdff280495cb676 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Fri, 12 Feb 2021 21:19:13 -0500 Subject: [PATCH 06/16] Consolidate duplicate functions --- spotify-controller.el | 21 --------------------- spotify-dbus.el | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/spotify-controller.el b/spotify-controller.el index 8c9d1b1..5550007 100644 --- a/spotify-controller.el +++ b/spotify-controller.el @@ -160,27 +160,6 @@ This corresponds to the current REPEATING state." spotify-player-status-repeating-text spotify-player-status-not-repeating-text)) -(defun spotify-replace-player-status-flags (metadata) - "Compose the playing status string to be displayed in the player-status from METADATA." - (let* ((player-status spotify-player-status-format) - (duration-format "%m:%02s") - (json-object-type 'hash-table) - (json-key-type 'symbol) - (json (condition-case nil - (json-read-from-string metadata) - (error (spotify-update-player-status "") - nil)))) - (when json - (progn - (setq player-status (replace-regexp-in-string "%a" (truncate-string-to-width (gethash 'artist json) spotify-player-status-truncate-length 0 nil "...") player-status)) - (setq player-status (replace-regexp-in-string "%t" (truncate-string-to-width (gethash 'name json) spotify-player-status-truncate-length 0 nil "...") player-status)) - (setq player-status (replace-regexp-in-string "%n" (number-to-string (gethash 'track_number json)) player-status)) - (setq player-status (replace-regexp-in-string "%l" (format-seconds duration-format (/ (gethash 'duration json) 1000)) player-status)) - (setq player-status (replace-regexp-in-string "%s" (spotify-player-status-shuffling-indicator (gethash 'player_shuffling json)) player-status)) - (setq player-status (replace-regexp-in-string "%r" (spotify-player-status-repeating-indicator (gethash 'player_repeating json)) player-status)) - (setq player-status (replace-regexp-in-string "%p" (spotify-player-status-playing-indicator (gethash 'player_state json)) player-status)) - (spotify-update-player-status player-status))))) - (defun spotify-start-player-status-timer () "Start the timer that will update the mode line according to the Spotify player status." (spotify-stop-player-status-timer) diff --git a/spotify-dbus.el b/spotify-dbus.el index 53b558c..34f6ea8 100644 --- a/spotify-dbus.el +++ b/spotify-dbus.el @@ -54,7 +54,7 @@ (track-number (car (car (cdr (assoc "xesam:trackNumber" metadata))))) (duration (/ (car (car (cdr (assoc "mpris:length" metadata)))) 1000))) (if (> track-number 0) - (spotify-replace-player-status-flags + (spotify-update-metadata (concat "{" " \"artist\": \"" artist "\"" ",\"duration\": " (number-to-string duration) "" From 70546885dc1d35ea6fdda772c66b79283160259c Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Fri, 12 Feb 2021 14:47:57 -0500 Subject: [PATCH 07/16] Show artwork in track search, according to customize --- spotify-track-search.el | 93 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/spotify-track-search.el b/spotify-track-search.el index 9a912dc..6fa629e 100644 --- a/spotify-track-search.el +++ b/spotify-track-search.el @@ -15,6 +15,11 @@ (defvar spotify-query) (defvar spotify-selected-album) (defvar spotify-recently-played) +(defvar spotify-artwork-fetch-target-count 0) +(defvar spotify-artwork-fetch-count 0) + +(defcustom spotify-show-artwork t + "Whether to show album/playlist artwork in search results.") (defvar spotify-track-search-mode-map (let ((map (make-sparse-keymap))) @@ -199,20 +204,75 @@ Default to sortin tracks by number when listing the tracks from an album." (when (not (bound-and-true-p spotify-selected-playlist)) (setq tabulated-list-sort-key `("#" . nil))) (setq tabulated-list-format - (vconcat (vector `("#" 3 ,(lambda (row-1 row-2) - (< (+ (* 100 (spotify-get-disc-number (car row-1))) - (spotify-get-track-number (car row-1))) - (+ (* 100 (spotify-get-disc-number (car row-2))) - (spotify-get-track-number (car row-2))))) :right-align t) - `("Track Name" ,default-width t) - `("Artist" ,default-width t) - `("Album" ,default-width t) - `("Time" 8 (lambda (row-1 row-2) + (vconcat (vector `("" ,(if spotify-show-artwork 4 0)) + `("#" 3 ,(lambda (row-1 row-2) + (< (+ (* 100 (spotify-get-disc-number (car row-1))) + (spotify-get-track-number (car row-1))) + (+ (* 100 (spotify-get-disc-number (car row-2))) + (spotify-get-track-number (car row-2))))) :right-align t) + `("Track Name" ,default-width t) + `("Artist" ,default-width t) + `("Album" ,default-width t) + `("Time" 8 (lambda (row-1 row-2) (< (spotify-get-track-duration (car row-1)) (spotify-get-track-duration (car row-2)))))) (when (not (bound-and-true-p spotify-selected-album)) (vector '("Popularity" 14 t))))))) +(defun spotify-track-tabulated-list-print-entry (id cols) + "Insert a Tabulated List entry at point. +This implementation asynchronously inserts album images in the +table buffer after the rows are printed. It reimplements most of +the `tabulated-list-print-entry' function but depends on a url +being the first column's data. It does not print that url in the +column. ID is a Lisp object identifying the entry to print, and +COLS is a vector of column descriptors." + (let ((beg (point)) + (x (max tabulated-list-padding 0)) + (ncols (length tabulated-list-format)) + (inhibit-read-only t) + (cb (current-buffer))) + (if (> tabulated-list-padding 0) + (insert (make-string x ?\s))) + (insert-image (create-image "~/Downloads/temp.jpg")) + (url-retrieve (aref cols 0) + (lambda (status) + (let ((img (create-image + (progn + (goto-char (point-min)) + (re-search-forward "^$") + (forward-char) + (delete-region (point) (point-min)) + (buffer-substring-no-properties (point-min) (point-max))) + nil t))) + ;; kill the image data buffer. We have the data now + (kill-buffer) + ;; switch to the table buffer + (set-buffer cb) + (let ((inhibit-read-only t)) + (save-excursion + (goto-char beg) + (delete-char 1) + (insert-image img))) + (setq spotify-artwork-fetch-count (1+ spotify-artwork-fetch-count)) + (when (= spotify-artwork-fetch-count spotify-artwork-fetch-target-count) + ;; Undocumented function. Could be dangerous if there's a bug + (setq inhibit-redisplay nil))))) + (insert ?\s) + (let ((tabulated-list--near-rows ; Bind it if not bound yet (Bug#25506). + (or (bound-and-true-p tabulated-list--near-rows) + (list (or (tabulated-list-get-entry (point-at-bol 0)) + cols) + cols)))) + ;; don't print the URL column + (dotimes (n (- ncols 1)) + (setq x (tabulated-list-print-col (+ 1 n) (aref cols (+ 1 n)) x)))) + (insert ?\n) + ;; Ever so slightly faster than calling `put-text-property' twice. + (add-text-properties + beg (point) + `(tabulated-list-id ,id tabulated-list-entry ,cols)))) + (defun spotify-track-search-print (songs page) "Append SONGS to the PAGE of track view." (let (entries) @@ -223,7 +283,8 @@ Default to sortin tracks by number when listing the tracks from an album." (album-name (spotify-get-item-name album)) (album (spotify-get-track-album song))) (push (list song - (vector (number-to-string (spotify-get-track-number song)) + (vector (if spotify-show-artwork (gethash 'url (nth 2 (gethash 'images (gethash 'album song)))) "") + (number-to-string (spotify-get-track-number song)) (spotify-get-item-name song) (cons artist-name (list 'face 'link @@ -240,7 +301,17 @@ Default to sortin tracks by number when listing the tracks from an album." (spotify-get-track-duration-formatted song) (when (not (bound-and-true-p spotify-selected-album)) (spotify-popularity-bar (spotify-get-track-popularity song))))) - entries)))) + entries)))) + (if spotify-show-artwork + (progn + (setq tabulated-list-printer #'spotify-track-tabulated-list-print-entry) + (setq spotify-artwork-fetch-target-count + (+ (length songs) (if (eq 1 page) 0 (count-lines (point-min) (point-max))))) + (setq spotify-artwork-fetch-count 0) + (setq line-spacing 10) + (message "Fetching tracks...") + (setq inhibit-redisplay t)) + (setq tabulated-list-printer #'tabulated-list-print-entry)) (spotify-track-search-set-list-format) (when (eq 1 page) (setq-local tabulated-list-entries nil)) (setq-local tabulated-list-entries (append tabulated-list-entries (nreverse entries))) From b4bdfd899ae84286385ef7fbdfa5c34c5ebfb9ef Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Fri, 12 Feb 2021 14:52:21 -0500 Subject: [PATCH 08/16] Set text properties after artwork is inserted --- spotify-track-search.el | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spotify-track-search.el b/spotify-track-search.el index 6fa629e..803c5e2 100644 --- a/spotify-track-search.el +++ b/spotify-track-search.el @@ -253,7 +253,11 @@ COLS is a vector of column descriptors." (save-excursion (goto-char beg) (delete-char 1) - (insert-image img))) + (insert-image img) + ;; Ever so slightly faster than calling `put-text-property' twice. + (add-text-properties + beg (point) + `(tabulated-list-id ,id tabulated-list-entry ,cols)))) (setq spotify-artwork-fetch-count (1+ spotify-artwork-fetch-count)) (when (= spotify-artwork-fetch-count spotify-artwork-fetch-target-count) ;; Undocumented function. Could be dangerous if there's a bug @@ -267,11 +271,7 @@ COLS is a vector of column descriptors." ;; don't print the URL column (dotimes (n (- ncols 1)) (setq x (tabulated-list-print-col (+ 1 n) (aref cols (+ 1 n)) x)))) - (insert ?\n) - ;; Ever so slightly faster than calling `put-text-property' twice. - (add-text-properties - beg (point) - `(tabulated-list-id ,id tabulated-list-entry ,cols)))) + (insert ?\n))) (defun spotify-track-search-print (songs page) "Append SONGS to the PAGE of track view." From 958dc1e79a54f1769a5cf2866217de8d3f16dd1a Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Fri, 12 Feb 2021 17:46:56 -0500 Subject: [PATCH 09/16] Don't permanently prevent emacs from displaying! --- spotify-track-search.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spotify-track-search.el b/spotify-track-search.el index 803c5e2..9bf88be 100644 --- a/spotify-track-search.el +++ b/spotify-track-search.el @@ -310,6 +310,8 @@ COLS is a vector of column descriptors." (setq spotify-artwork-fetch-count 0) (setq line-spacing 10) (message "Fetching tracks...") + ;; in case the fetch chokes somehow, don't lock up all of emacs forever + (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) (setq inhibit-redisplay t)) (setq tabulated-list-printer #'tabulated-list-print-entry)) (spotify-track-search-set-list-format) From f9a4d60c4f1894666f6894138b4bee969e4ef25a Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Thu, 25 Mar 2021 15:18:12 -0400 Subject: [PATCH 10/16] Smudge merge fixes --- smudge-track.el | 154 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 40 deletions(-) diff --git a/smudge-track.el b/smudge-track.el index 087f325..d0efe7d 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -15,6 +15,11 @@ (defvar smudge-query) (defvar smudge-selected-album) (defvar smudge-recently-played) +(defvar smudge-artwork-fetch-target-count 0) +(defvar smudge-artwork-fetch-count 0) + +(defcustom smudge-show-artwork t + "Whether to show artwork when searching for tracks.") (defvar smudge-track-search-mode-map (let ((map (make-sparse-keymap))) @@ -189,26 +194,82 @@ without a context." (message "No more tracks")))))) (defun smudge-track-search-set-list-format () - "Configure the column data for the typical track view. + "Configure the column data for the typical track view. Default to sortin tracks by number when listing the tracks from an album." - (let* ((base-width (truncate (/ (- (window-width) 30) 3))) - (default-width (if (bound-and-true-p smudge-selected-album) (+ base-width 4) base-width ))) - (when (not (bound-and-true-p smudge-selected-playlist)) - (setq tabulated-list-sort-key `("#" . nil))) - (setq tabulated-list-format - (vconcat (vector `("#" 3 ,(lambda (row-1 row-2) - (< (+ (* 100 (smudge-api-get-disc-number (car row-1))) - (smudge-api-get-track-number (car row-1))) - (+ (* 100 (smudge-api-get-disc-number (car row-2))) - (smudge-api-get-track-number (car row-2))))) :right-align t) - `("Track Name" ,default-width t) - `("Artist" ,default-width t) - `("Album" ,default-width t) - `("Time" 8 (lambda (row-1 row-2) - (< (smudge-get-track-duration (car row-1)) - (smudge-get-track-duration (car row-2)))))) - (when (not (bound-and-true-p smudge-selected-album)) - (vector '("Popularity" 14 t))))))) + (let* ((base-width (truncate (/ (- (window-width) 30) 3))) + (default-width (if (bound-and-true-p smudge-selected-album) (+ base-width 4) base-width ))) + (when (not (bound-and-true-p smudge-selected-playlist)) + (setq tabulated-list-sort-key `("#" . nil))) + (setq tabulated-list-format + (vconcat (vector + `("" ,(if smudge-show-artwork 4 0)) + `("#" 3 ,(lambda (row-1 row-2) + (< (+ (* 100 (smudge-api-get-disc-number (car row-1))) + (smudge-api-get-track-number (car row-1))) + (+ (* 100 (smudge-api-get-disc-number (car row-2))) + (smudge-api-get-track-number (car row-2))))) :right-align t) + `("Track Name" ,default-width t) + `("Artist" ,default-width t) + `("Album" ,default-width t) + `("Time" 8 (lambda (row-1 row-2) + (< (smudge-api-get-track-duration (car row-1)) + (smudge-api-get-track-duration (car row-2)))))) + (when (not (bound-and-true-p smudge-selected-album)) + (vector '("Popularity" 14 t))))))) + +(defun smudge-track-tabulated-list-print-entry (id cols) + "Insert a Tabulated List entry at point. +This implementation asynchronously inserts album images in the +table buffer after the rows are printed. It reimplements most of +the `tabulated-list-print-entry' function but depends on a url +being the first column's data. It does not print that url in the +column. ID is a Lisp object identifying the entry to print, and +COLS is a vector of column descriptors." + (let ((beg (point)) + (x (max tabulated-list-padding 0)) + (ncols (length tabulated-list-format)) + (inhibit-read-only t) + (cb (current-buffer))) + (if (> tabulated-list-padding 0) + (insert (make-string x ?\s))) + (insert-image (create-image "~/Downloads/temp.jpg")) + (url-retrieve (aref cols 0) + (lambda (status) + (let ((img (create-image + (progn + (goto-char (point-min)) + (re-search-forward "^$") + (forward-char) + (delete-region (point) (point-min)) + (buffer-substring-no-properties (point-min) (point-max))) + nil t))) + ;; kill the image data buffer. We have the data now + (kill-buffer) + ;; switch to the table buffer + (set-buffer cb) + (let ((inhibit-read-only t)) + (save-excursion + (goto-char beg) + (delete-char 1) + (insert-image img) + ;; Ever so slightly faster than calling `put-text-property' twice. + (add-text-properties + beg (point) + `(tabulated-list-id ,id tabulated-list-entry ,cols)))) + (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) + (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) + ;; Undocumented function. Could be dangerous if there's a bug + (setq inhibit-redisplay nil))))) + (insert ?\s) + (let ((tabulated-list--near-rows ; Bind it if not bound yet (Bug#25506). + (or (bound-and-true-p tabulated-list--near-rows) + (list (or (tabulated-list-get-entry (point-at-bol 0)) + cols) + cols)))) + ;; don't print the URL column + (dotimes (n (- ncols 1)) + (setq x (tabulated-list-print-col (+ 1 n) (aref cols (+ 1 n)) x)))) + (insert ?\n))) (defun smudge-track-search-print (songs page) "Append SONGS to the PAGE of track view." @@ -216,28 +277,41 @@ Default to sortin tracks by number when listing the tracks from an album." (dolist (song songs) (when (smudge-api-is-track-playable song) (let* ((artist-name (smudge-api-get-track-artist-name song)) - (album (or (smudge-api-get-track-album song) smudge-selected-album)) - (album-name (smudge-api-get-item-name album)) - (album (smudge-api-get-track-album song))) + (album (or (smudge-api-get-track-album song) smudge-selected-album)) + (album-name (smudge-api-get-item-name album)) + (album (smudge-api-get-track-album song))) (push (list song - (vector (number-to-string (smudge-api-get-track-number song)) - (smudge-api-get-item-name song) - (cons artist-name - (list 'face 'link - 'follow-link t - 'action `(lambda (_) (smudge-track-search ,(format "artist:\"%s\"" artist-name))) - 'help-echo (format "Show %s's tracks" artist-name) - 'artist-or-album 'artist)) - (cons album-name - (list 'face 'link - 'follow-link t - 'action `(lambda (_) (smudge-track-album-tracks ,album)) - 'help-echo (format "Show %s's tracks" album-name) - 'artist-or-album 'album)) - (smudge-api-get-track-duration-formatted song) - (when (not (bound-and-true-p smudge-selected-album)) - (smudge-api-popularity-bar (smudge-api-get-track-popularity song))))) - entries)))) + (vector (if smudge-show-artwork (gethash 'url (nth 2 (gethash 'images (gethash 'album song)))) "") + (number-to-string (smudge-api-get-track-number song)) + (smudge-api-get-item-name song) + (cons artist-name + (list 'face 'link + 'follow-link t + 'action `(lambda (_) (smudge-track-search ,(format "artist:\"%s\"" artist-name))) + 'help-echo (format "Show %s's tracks" artist-name) + 'artist-or-album 'artist)) + (cons album-name + (list 'face 'link + 'follow-link t + 'action `(lambda (_) (smudge-track-album-tracks ,album)) + 'help-echo (format "Show %s's tracks" album-name) + 'artist-or-album 'album)) + (smudge-api-get-track-duration-formatted song) + (when (not (bound-and-true-p smudge-selected-album)) + (smudge-api-popularity-bar (smudge-api-get-track-popularity song))))) + entries)))) + (if smudge-show-artwork + (progn + (setq tabulated-list-printer #'smudge-track-tabulated-list-print-entry) + (setq smudge-artwork-fetch-target-count + (+ (length songs) (if (eq 1 page) 0 (count-lines (point-min) (point-max))))) + (setq smudge-artwork-fetch-count 0) + (setq line-spacing 10) + (message "Fetching tracks...") + ;; in case the fetch chokes somehow, don't lock up all of emacs forever + (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) + (setq inhibit-redisplay t)) + (setq tabulated-list-printer #'tabulated-list-print-entry)) (smudge-track-search-set-list-format) (when (eq 1 page) (setq-local tabulated-list-entries nil)) (setq-local tabulated-list-entries (append tabulated-list-entries (nreverse entries))) From 1cf850a653b4d02af9d30288e4afb8f409451648 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Thu, 25 Mar 2021 16:13:10 -0400 Subject: [PATCH 11/16] Put the image in the margin. Column widths work correctly then --- smudge-track.el | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/smudge-track.el b/smudge-track.el index d0efe7d..31e1a87 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -58,7 +58,7 @@ without a context." (let* ((track (tabulated-list-get-id)) (context (cond ((bound-and-true-p smudge-selected-playlist) smudge-selected-playlist) ((bound-and-true-p smudge-selected-album) smudge-selected-album) - (t nil)))) + (t nil)))) (smudge-controller-play-track track context))) (defun smudge-track-selected-button-type () @@ -78,7 +78,7 @@ without a context." "Plays the album of the track under the cursor." (interactive) (let* ((track (tabulated-list-get-id)) - (album (smudge-api-get-track-album track))) + (album (smudge-api-get-track-album track))) (smudge-controller-play-track track album))) (defun smudge-track-playlist-follow () @@ -202,7 +202,7 @@ Default to sortin tracks by number when listing the tracks from an album." (setq tabulated-list-sort-key `("#" . nil))) (setq tabulated-list-format (vconcat (vector - `("" ,(if smudge-show-artwork 4 0)) + `("" -1) `("#" 3 ,(lambda (row-1 row-2) (< (+ (* 100 (smudge-api-get-disc-number (car row-1))) (smudge-api-get-track-number (car row-1))) @@ -232,7 +232,6 @@ COLS is a vector of column descriptors." (cb (current-buffer))) (if (> tabulated-list-padding 0) (insert (make-string x ?\s))) - (insert-image (create-image "~/Downloads/temp.jpg")) (url-retrieve (aref cols 0) (lambda (status) (let ((img (create-image @@ -250,12 +249,7 @@ COLS is a vector of column descriptors." (let ((inhibit-read-only t)) (save-excursion (goto-char beg) - (delete-char 1) - (insert-image img) - ;; Ever so slightly faster than calling `put-text-property' twice. - (add-text-properties - beg (point) - `(tabulated-list-id ,id tabulated-list-entry ,cols)))) + (put-image img (point) "test" 'left-margin))) (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) ;; Undocumented function. Could be dangerous if there's a bug @@ -268,8 +262,12 @@ COLS is a vector of column descriptors." cols)))) ;; don't print the URL column (dotimes (n (- ncols 1)) - (setq x (tabulated-list-print-col (+ 1 n) (aref cols (+ 1 n)) x)))) - (insert ?\n))) + (setq x (tabulated-list-print-col (+ n 1) (aref cols (+ n 1)) x)))) + (insert ?\n) + ;; Ever so slightly faster than calling `put-text-property' twice. + (add-text-properties + beg (point) + `(tabulated-list-id ,id tabulated-list-entry ,cols))))) (defun smudge-track-search-print (songs page) "Append SONGS to the PAGE of track view." @@ -281,7 +279,10 @@ COLS is a vector of column descriptors." (album-name (smudge-api-get-item-name album)) (album (smudge-api-get-track-album song))) (push (list song - (vector (if smudge-show-artwork (gethash 'url (nth 2 (gethash 'images (gethash 'album song)))) "") + (vector + (if smudge-show-artwork + (gethash 'url (nth 2 (gethash 'images (gethash 'album song)))) + "") (number-to-string (smudge-api-get-track-number song)) (smudge-api-get-item-name song) (cons artist-name @@ -310,7 +311,9 @@ COLS is a vector of column descriptors." (message "Fetching tracks...") ;; in case the fetch chokes somehow, don't lock up all of emacs forever (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) - (setq inhibit-redisplay t)) + (setq inhibit-redisplay t) + (setq left-margin-width 6) + (set-window-buffer (selected-window) (current-buffer))) (setq tabulated-list-printer #'tabulated-list-print-entry)) (smudge-track-search-set-list-format) (when (eq 1 page) (setq-local tabulated-list-entries nil)) From 6fa7ec4db4694543b87549b2181a452b0940392d Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Thu, 25 Mar 2021 17:05:12 -0400 Subject: [PATCH 12/16] Handle absence of artwork in album track listing mode for now --- smudge-api.el | 6 +++++ smudge-track.el | 61 +++++++++++++++++++++++++------------------------ 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/smudge-api.el b/smudge-api.el index 14e9dcf..36c01d9 100644 --- a/smudge-api.el +++ b/smudge-api.el @@ -346,6 +346,12 @@ Call CALLBACK with the parsed JSON response." "Return the owner id of the given playlist JSON object." (smudge-api-get-item-id (gethash 'owner json))) +(defun smudge-api-get-song-art-url (song) + "Return the first image url for a SONG." + (let* ((album (gethash 'album song)) + (image (and album (nth 2 (gethash 'images album))))) + (and image (gethash 'url image)))) + (defun smudge-api-search (type query page callback) "Search artists, albums, tracks or playlists. Call CALLBACK with PAGE of items that match QUERY, depending on TYPE." diff --git a/smudge-track.el b/smudge-track.el index 31e1a87..e6be774 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -168,7 +168,7 @@ without a context." (smudge-api-album-tracks album page - (lambda (json) + (lambda (json) (if-let ((items (smudge-api-get-items json))) (with-current-buffer buffer (setq-local smudge-current-page page) @@ -229,31 +229,34 @@ COLS is a vector of column descriptors." (x (max tabulated-list-padding 0)) (ncols (length tabulated-list-format)) (inhibit-read-only t) - (cb (current-buffer))) + (cb (current-buffer)) + (track-image-url (aref cols 0))) (if (> tabulated-list-padding 0) (insert (make-string x ?\s))) - (url-retrieve (aref cols 0) - (lambda (status) - (let ((img (create-image - (progn - (goto-char (point-min)) - (re-search-forward "^$") - (forward-char) - (delete-region (point) (point-min)) - (buffer-substring-no-properties (point-min) (point-max))) - nil t))) - ;; kill the image data buffer. We have the data now - (kill-buffer) - ;; switch to the table buffer - (set-buffer cb) - (let ((inhibit-read-only t)) - (save-excursion - (goto-char beg) - (put-image img (point) "test" 'left-margin))) - (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) - (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) - ;; Undocumented function. Could be dangerous if there's a bug - (setq inhibit-redisplay nil))))) + (if track-image-url + (url-retrieve track-image-url + (lambda (status) + (let ((img (create-image + (progn + (goto-char (point-min)) + (re-search-forward "^$") + (forward-char) + (delete-region (point) (point-min)) + (buffer-substring-no-properties (point-min) (point-max))) + nil t))) + ;; kill the image data buffer. We have the data now + (kill-buffer) + ;; switch to the table buffer + (set-buffer cb) + (let ((inhibit-read-only t)) + (save-excursion + (goto-char beg) + (put-image img (point) "test" 'left-margin))) + (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) + (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) + ;; Undocumented function. Could be dangerous if there's a bug + (setq inhibit-redisplay nil))))) + (setq inhibit-redisplay nil)) (insert ?\s) (let ((tabulated-list--near-rows ; Bind it if not bound yet (Bug#25506). (or (bound-and-true-p tabulated-list--near-rows) @@ -267,7 +270,7 @@ COLS is a vector of column descriptors." ;; Ever so slightly faster than calling `put-text-property' twice. (add-text-properties beg (point) - `(tabulated-list-id ,id tabulated-list-entry ,cols))))) + `(tabulated-list-id ,id tabulated-list-entry ,cols)))) (defun smudge-track-search-print (songs page) "Append SONGS to the PAGE of track view." @@ -276,13 +279,11 @@ COLS is a vector of column descriptors." (when (smudge-api-is-track-playable song) (let* ((artist-name (smudge-api-get-track-artist-name song)) (album (or (smudge-api-get-track-album song) smudge-selected-album)) - (album-name (smudge-api-get-item-name album)) - (album (smudge-api-get-track-album song))) + (album-name (smudge-api-get-item-name album))) + (message "song: %s" (json-encode song)) (push (list song (vector - (if smudge-show-artwork - (gethash 'url (nth 2 (gethash 'images (gethash 'album song)))) - "") + (if smudge-show-artwork (smudge-api-get-song-art-url song) "") (number-to-string (smudge-api-get-track-number song)) (smudge-api-get-item-name song) (cons artist-name From 52dc95468e3a326a61c34e83c449ae423c25ddc8 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Thu, 25 Mar 2021 17:34:19 -0400 Subject: [PATCH 13/16] Artwork for album list --- smudge-api.el | 13 +++++++++++-- smudge-track.el | 30 +++++++++++++++++++----------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/smudge-api.el b/smudge-api.el index 36c01d9..af63843 100644 --- a/smudge-api.el +++ b/smudge-api.el @@ -347,10 +347,10 @@ Call CALLBACK with the parsed JSON response." (smudge-api-get-item-id (gethash 'owner json))) (defun smudge-api-get-song-art-url (song) - "Return the first image url for a SONG." + "Return the medium sized image url for a SONG." (let* ((album (gethash 'album song)) (image (and album (nth 2 (gethash 'images album))))) - (and image (gethash 'url image)))) + (and image (gethash 'url image)))) (defun smudge-api-search (type query page callback) "Search artists, albums, tracks or playlists. @@ -468,6 +468,15 @@ Call CALLBACK with results." nil callback))) +(defun smudge-api-album (album-id callback) + "Call CALLBACK with info for album with ALBUM-ID." + (smudge-api-call-async + "GET" + (format "/albums/%s" + (url-hexify-string album-id)) + nil + callback)) + (defun smudge-api-album-tracks (album page callback) "Call CALLBACK with PAGE of tracks for ALBUM." (let ((album-id (smudge-api-get-item-id album)) diff --git a/smudge-track.el b/smudge-track.el index e6be774..fff7170 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -166,17 +166,26 @@ without a context." "Fetch PAGE of of tracks for ALBUM." (let ((buffer (current-buffer))) (smudge-api-album-tracks - album - page + album + page (lambda (json) - (if-let ((items (smudge-api-get-items json))) - (with-current-buffer buffer - (setq-local smudge-current-page page) - (setq-local smudge-selected-album album) - (pop-to-buffer buffer) - (smudge-track-search-print items page) - (message "Track view updated")) - (message "No more tracks")))))) + (if-let ((items (smudge-api-get-items json))) + (with-current-buffer buffer + (setq-local smudge-current-page page) + (setq-local smudge-selected-album album) + ;; jam the album data into every song so we can extract + ;; the artwork + (smudge-api-album (gethash 'id album) + (lambda (album-json) + (message "album json: %s" album-json) + (pop-to-buffer buffer) + (let ((items-with-image-url + (mapc (lambda (item) + (puthash 'album album-json item)) + items))) + (smudge-track-search-print items page) + (message "Track view updated"))))) + (message "No more tracks")))))) (defun smudge-track-recently-played-tracks-update (page) "Fetch PAGE of results for the recently played tracks." @@ -280,7 +289,6 @@ COLS is a vector of column descriptors." (let* ((artist-name (smudge-api-get-track-artist-name song)) (album (or (smudge-api-get-track-album song) smudge-selected-album)) (album-name (smudge-api-get-item-name album))) - (message "song: %s" (json-encode song)) (push (list song (vector (if smudge-show-artwork (smudge-api-get-song-art-url song) "") From 39e736bc9e908b2b987d165434e8574b7c467a63 Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Thu, 1 Apr 2021 10:44:09 -0400 Subject: [PATCH 14/16] Small tweaks --- smudge-track.el | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/smudge-track.el b/smudge-track.el index fff7170..af4f857 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -203,15 +203,15 @@ without a context." (message "No more tracks")))))) (defun smudge-track-search-set-list-format () - "Configure the column data for the typical track view. -Default to sortin tracks by number when listing the tracks from an album." + "Configure the column data for the typical track view. +Default to sorting tracks by number when listing the tracks from an album." (let* ((base-width (truncate (/ (- (window-width) 30) 3))) (default-width (if (bound-and-true-p smudge-selected-album) (+ base-width 4) base-width ))) (when (not (bound-and-true-p smudge-selected-playlist)) (setq tabulated-list-sort-key `("#" . nil))) (setq tabulated-list-format (vconcat (vector - `("" -1) + `("" -1) ;; image url column - do not display `("#" 3 ,(lambda (row-1 row-2) (< (+ (* 100 (smudge-api-get-disc-number (car row-1))) (smudge-api-get-track-number (car row-1))) @@ -260,10 +260,9 @@ COLS is a vector of column descriptors." (let ((inhibit-read-only t)) (save-excursion (goto-char beg) - (put-image img (point) "test" 'left-margin))) + (put-image img (point) "track image" 'left-margin))) (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) - ;; Undocumented function. Could be dangerous if there's a bug (setq inhibit-redisplay nil))))) (setq inhibit-redisplay nil)) (insert ?\s) @@ -320,6 +319,7 @@ COLS is a vector of column descriptors." (message "Fetching tracks...") ;; in case the fetch chokes somehow, don't lock up all of emacs forever (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) + ;; Undocumented function. Could be dangerous if there's a bug (setq inhibit-redisplay t) (setq left-margin-width 6) (set-window-buffer (selected-window) (current-buffer))) From 3209e9681809e2ab4b13eb150bdffa92a758547a Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Thu, 1 Apr 2021 13:33:35 -0400 Subject: [PATCH 15/16] Begin refactoring image functions --- smudge-image.el | 69 +++++++++++++++++++++++++++++++++++++ smudge-track.el | 90 +++++++++++-------------------------------------- 2 files changed, 88 insertions(+), 71 deletions(-) create mode 100644 smudge-image.el diff --git a/smudge-image.el b/smudge-image.el new file mode 100644 index 0000000..c59005b --- /dev/null +++ b/smudge-image.el @@ -0,0 +1,69 @@ +;;; smudge-image.el --- Smudge image support library -*- lexical-binding: t; -*- + +;; Copyright (C) 2021 Jason Dufair + +;;; Commentary: + +;; This library implements methods that support image display for smudge + +;;; Code: + +(defvar smudge-artwork-fetch-target-count 0) +(defvar smudge-artwork-fetch-count 0) + +(defun smudge-image-tabulated-list-print-entry (id cols) + "Insert a Tabulated List entry at point. +This implementation asynchronously inserts album images in the +table buffer after the rows are printed. It reimplements most of +the `tabulated-list-print-entry' function but depends on a url +being the first column's data. It does not print that url in the +column. ID is a Lisp object identifying the entry to print, and +COLS is a vector of column descriptors." + (let ((beg (point)) + (x (max tabulated-list-padding 0)) + (ncols (length tabulated-list-format)) + (inhibit-read-only t) + (cb (current-buffer)) + (image-url (aref cols 0))) + (if (> tabulated-list-padding 0) + (insert (make-string x ?\s))) + (if image-url + (url-retrieve image-url + (lambda (_) + (let ((img (create-image + (progn + (goto-char (point-min)) + (re-search-forward "^$") + (forward-char) + (delete-region (point) (point-min)) + (buffer-substring-no-properties (point-min) (point-max))) + nil t))) + ;; kill the image data buffer. We have the data now + (kill-buffer) + ;; switch to the table buffer + (set-buffer cb) + (let ((inhibit-read-only t)) + (save-excursion + (goto-char beg) + (put-image img (point) "track image" 'left-margin))) + (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) + (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) + (setq inhibit-redisplay nil))))) + (setq inhibit-redisplay nil)) + (insert ?\s) + (let ((tabulated-list--near-rows ; Bind it if not bound yet (Bug#25506). + (or (bound-and-true-p tabulated-list--near-rows) + (list (or (tabulated-list-get-entry (point-at-bol 0)) + cols) + cols)))) + ;; don't print the URL column + (dotimes (n (- ncols 1)) + (setq x (tabulated-list-print-col (+ n 1) (aref cols (+ n 1)) x)))) + (insert ?\n) + ;; Ever so slightly faster than calling `put-text-property' twice. + (add-text-properties + beg (point) + `(tabulated-list-id ,id tabulated-list-entry ,cols)))) + +(provide 'smudge-image) +;;; smudge-image.el ends here diff --git a/smudge-track.el b/smudge-track.el index af4f857..855e4b2 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -10,17 +10,19 @@ (require 'smudge-api) (require 'smudge-controller) +(require 'smudge-image) (defvar smudge-current-page) (defvar smudge-query) (defvar smudge-selected-album) (defvar smudge-recently-played) -(defvar smudge-artwork-fetch-target-count 0) -(defvar smudge-artwork-fetch-count 0) (defcustom smudge-show-artwork t "Whether to show artwork when searching for tracks.") +(defvar smudge-artwork-fetch-target-count 0) +(defvar smudge-artwork-fetch-count 0) + (defvar smudge-track-search-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map tabulated-list-mode-map) @@ -226,60 +228,6 @@ Default to sorting tracks by number when listing the tracks from an album." (when (not (bound-and-true-p smudge-selected-album)) (vector '("Popularity" 14 t))))))) -(defun smudge-track-tabulated-list-print-entry (id cols) - "Insert a Tabulated List entry at point. -This implementation asynchronously inserts album images in the -table buffer after the rows are printed. It reimplements most of -the `tabulated-list-print-entry' function but depends on a url -being the first column's data. It does not print that url in the -column. ID is a Lisp object identifying the entry to print, and -COLS is a vector of column descriptors." - (let ((beg (point)) - (x (max tabulated-list-padding 0)) - (ncols (length tabulated-list-format)) - (inhibit-read-only t) - (cb (current-buffer)) - (track-image-url (aref cols 0))) - (if (> tabulated-list-padding 0) - (insert (make-string x ?\s))) - (if track-image-url - (url-retrieve track-image-url - (lambda (status) - (let ((img (create-image - (progn - (goto-char (point-min)) - (re-search-forward "^$") - (forward-char) - (delete-region (point) (point-min)) - (buffer-substring-no-properties (point-min) (point-max))) - nil t))) - ;; kill the image data buffer. We have the data now - (kill-buffer) - ;; switch to the table buffer - (set-buffer cb) - (let ((inhibit-read-only t)) - (save-excursion - (goto-char beg) - (put-image img (point) "track image" 'left-margin))) - (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) - (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) - (setq inhibit-redisplay nil))))) - (setq inhibit-redisplay nil)) - (insert ?\s) - (let ((tabulated-list--near-rows ; Bind it if not bound yet (Bug#25506). - (or (bound-and-true-p tabulated-list--near-rows) - (list (or (tabulated-list-get-entry (point-at-bol 0)) - cols) - cols)))) - ;; don't print the URL column - (dotimes (n (- ncols 1)) - (setq x (tabulated-list-print-col (+ n 1) (aref cols (+ n 1)) x)))) - (insert ?\n) - ;; Ever so slightly faster than calling `put-text-property' twice. - (add-text-properties - beg (point) - `(tabulated-list-id ,id tabulated-list-entry ,cols)))) - (defun smudge-track-search-print (songs page) "Append SONGS to the PAGE of track view." (let (entries) @@ -309,21 +257,21 @@ COLS is a vector of column descriptors." (when (not (bound-and-true-p smudge-selected-album)) (smudge-api-popularity-bar (smudge-api-get-track-popularity song))))) entries)))) - (if smudge-show-artwork - (progn - (setq tabulated-list-printer #'smudge-track-tabulated-list-print-entry) - (setq smudge-artwork-fetch-target-count - (+ (length songs) (if (eq 1 page) 0 (count-lines (point-min) (point-max))))) - (setq smudge-artwork-fetch-count 0) - (setq line-spacing 10) - (message "Fetching tracks...") - ;; in case the fetch chokes somehow, don't lock up all of emacs forever - (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) - ;; Undocumented function. Could be dangerous if there's a bug - (setq inhibit-redisplay t) - (setq left-margin-width 6) - (set-window-buffer (selected-window) (current-buffer))) - (setq tabulated-list-printer #'tabulated-list-print-entry)) + (setq tabulated-list-printer #'tabulated-list-print-entry) + + (when smudge-show-artwork + (setq tabulated-list-printer #'smudge-image-tabulated-list-print-entry) + (setq smudge-artwork-fetch-target-count + (+ (length songs) (if (eq 1 page) 0 (count-lines (point-min) (point-max))))) + (setq smudge-artwork-fetch-count 0) + (setq line-spacing 10) + (message "Fetching tracks...") + ;; in case the fetch chokes somehow, don't lock up all of emacs forever + (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) + ;; Undocumented function. Could be dangerous if there's a bug + (setq inhibit-redisplay t) + (setq left-margin-width 6) + (set-window-buffer (selected-window) (current-buffer))) (smudge-track-search-set-list-format) (when (eq 1 page) (setq-local tabulated-list-entries nil)) (setq-local tabulated-list-entries (append tabulated-list-entries (nreverse entries))) From efead7f70887e325e791b5962f5285f8c939d78f Mon Sep 17 00:00:00 2001 From: Jason Dufair Date: Sat, 3 Apr 2021 02:31:28 -0400 Subject: [PATCH 16/16] Use refactored image functions with playlist search --- smudge-api.el | 6 ++ smudge-image.el | 19 +++-- smudge-playlist.el | 56 +++++++++----- smudge-track.el | 180 ++++++++++++++++++++++----------------------- 4 files changed, 147 insertions(+), 114 deletions(-) diff --git a/smudge-api.el b/smudge-api.el index af63843..ae35f7c 100644 --- a/smudge-api.el +++ b/smudge-api.el @@ -352,6 +352,12 @@ Call CALLBACK with the parsed JSON response." (image (and album (nth 2 (gethash 'images album))))) (and image (gethash 'url image)))) +(defun smudge-api-get-playlist-art-url (playlist) + "Return the smallest possible image url for PLAYLIST (we only need 64x64 and it gets scaled)." + (let* ((images (gethash 'images playlist)) + (image (and images (or (nth 3 images) (nth 2 images) (first images))))) + (and image (gethash 'url image)))) + (defun smudge-api-search (type query page callback) "Search artists, albums, tracks or playlists. Call CALLBACK with PAGE of items that match QUERY, depending on TYPE." diff --git a/smudge-image.el b/smudge-image.el index c59005b..804dc0c 100644 --- a/smudge-image.el +++ b/smudge-image.el @@ -8,9 +8,20 @@ ;;; Code: +(defcustom smudge-show-artwork t + "Whether to show artwork when searching for tracks." + :type 'boolean + :group 'smudge) + (defvar smudge-artwork-fetch-target-count 0) (defvar smudge-artwork-fetch-count 0) +(defun smudge-image-increment-count () + "Increment count of fetched (or the absence of) images. Handle redisplay." + (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) + (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) + (setq inhibit-redisplay nil))) + (defun smudge-image-tabulated-list-print-entry (id cols) "Insert a Tabulated List entry at point. This implementation asynchronously inserts album images in the @@ -37,7 +48,7 @@ COLS is a vector of column descriptors." (forward-char) (delete-region (point) (point-min)) (buffer-substring-no-properties (point-min) (point-max))) - nil t))) + nil t :width 64))) ;; kill the image data buffer. We have the data now (kill-buffer) ;; switch to the table buffer @@ -46,10 +57,8 @@ COLS is a vector of column descriptors." (save-excursion (goto-char beg) (put-image img (point) "track image" 'left-margin))) - (setq smudge-artwork-fetch-count (1+ smudge-artwork-fetch-count)) - (when (= smudge-artwork-fetch-count smudge-artwork-fetch-target-count) - (setq inhibit-redisplay nil))))) - (setq inhibit-redisplay nil)) + (smudge-image-increment-count)))) + (smudge-image-increment-count)) (insert ?\s) (let ((tabulated-list--near-rows ; Bind it if not bound yet (Bug#25506). (or (bound-and-true-p tabulated-list--near-rows) diff --git a/smudge-playlist.el b/smudge-playlist.el index 27e4f0d..5f7f9cb 100644 --- a/smudge-playlist.el +++ b/smudge-playlist.el @@ -11,12 +11,15 @@ (require 'smudge-api) (require 'smudge-controller) (require 'smudge-track) +(require 'smudge-image) (defvar smudge-user-id) (defvar smudge-current-page) (defvar smudge-browse-message) (defvar smudge-selected-playlist) (defvar smudge-query) +(defvar smudge-artwork-fetch-target-count 0) +(defvar smudge-artwork-fetch-count 0) (defvar smudge-playlist-search-mode-map (let ((map (make-sparse-keymap))) @@ -139,11 +142,13 @@ (defun smudge-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) - (< (smudge-api-get-playlist-track-count (car row-1)) - (smudge-api-get-playlist-track-count (car row-2)))) :right-align t)))) + (vector + `("" -1) ;; image url column - do not display + `("Playlist Name" ,(- (window-width) 45) t) + '("Owner Id" 30 t) + '("# Tracks" 8 (lambda (row-1 row-2) + (< (smudge-api-get-playlist-track-count (car row-1)) + (smudge-api-get-playlist-track-count (car row-2)))) :right-align t)))) (defun smudge-playlist-search-print (playlists page) "Append PLAYLISTS to PAGE of the current playlist view." @@ -152,18 +157,35 @@ (let ((user-id (smudge-api-get-playlist-owner-id playlist)) (playlist-name (smudge-api-get-item-name playlist))) (push (list playlist - (vector (cons playlist-name - (list 'face 'link - 'follow-link t - 'action `(lambda (_) (smudge-playlist-tracks)) - 'help-echo (format "Show %s's tracks" playlist-name))) - (cons user-id - (list 'face 'link - 'follow-link t - 'action `(lambda (_) (smudge-user-playlists ,user-id)) - 'help-echo (format "Show %s's public playlists" user-id))) - (number-to-string (smudge-api-get-playlist-track-count playlist)))) - entries))) + (vector + (if smudge-show-artwork (smudge-api-get-playlist-art-url playlist) "") + (cons playlist-name + (list 'face 'link + 'follow-link t + 'action `(lambda (_) (smudge-playlist-tracks)) + 'help-echo (format "Show %s's tracks" playlist-name))) + (cons user-id + (list 'face 'link + 'follow-link t + 'action `(lambda (_) (smudge-user-playlists ,user-id)) + 'help-echo (format "Show %s's public playlists" user-id))) + (number-to-string (smudge-api-get-playlist-track-count playlist)))) + entries))) + (setq tabulated-list-printer #'tabulated-list-print-entry) + (when smudge-show-artwork + (setq tabulated-list-printer #'smudge-image-tabulated-list-print-entry) + (setq smudge-artwork-fetch-target-count + (+ (length playlists) (if (eq 1 page) 0 (count-lines (point-min) (point-max))))) + (setq smudge-artwork-fetch-count 0) + (setq line-spacing 10) + (message "Fetching playlists...") + ;; in case the fetch chokes somehow, don't lock up all of emacs forever + (run-at-time "3 sec" nil (lambda () (setq inhibit-redisplay nil))) + ;; Undocumented function. Could be dangerous if there's a bug + (setq inhibit-redisplay t) + (message "inhibit-redisplay: %s" inhibit-redisplay) + (setq left-margin-width 6) + (set-window-buffer (selected-window) (current-buffer))) (when (eq 1 page) (setq-local tabulated-list-entries nil)) (smudge-playlist-set-list-format) (setq-local tabulated-list-entries (append tabulated-list-entries (nreverse entries))) diff --git a/smudge-track.el b/smudge-track.el index 855e4b2..b36c44d 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -17,9 +17,6 @@ (defvar smudge-selected-album) (defvar smudge-recently-played) -(defcustom smudge-show-artwork t - "Whether to show artwork when searching for tracks.") - (defvar smudge-artwork-fetch-target-count 0) (defvar smudge-artwork-fetch-count 0) @@ -45,10 +42,10 @@ If the cursor is on a button representing an artist or album, start playing that (interactive) (let ((button-type (smudge-track-selected-button-type))) (cond ((eq 'artist button-type) - (smudge-track-artist-select)) - ((eq 'album button-type) - (smudge-track-album-select)) - (t (smudge-track-select-default))))) + (smudge-track-artist-select)) + ((eq 'album button-type) + (smudge-track-album-select)) + (t (smudge-track-select-default))))) (defun smudge-track-select-default () "Play the track under the cursor. @@ -58,9 +55,9 @@ track is played in the context of that album. Otherwise, it will be played without a context." (interactive) (let* ((track (tabulated-list-get-id)) - (context (cond ((bound-and-true-p smudge-selected-playlist) smudge-selected-playlist) - ((bound-and-true-p smudge-selected-album) smudge-selected-album) - (t nil)))) + (context (cond ((bound-and-true-p smudge-selected-playlist) smudge-selected-playlist) + ((bound-and-true-p smudge-selected-album) smudge-selected-album) + (t nil)))) (smudge-controller-play-track track context))) (defun smudge-track-selected-button-type () @@ -73,7 +70,7 @@ without a context." "Plays the artist of the track under the cursor." (interactive) (let* ((track (tabulated-list-get-id)) - (artist (smudge-api-get-track-artist track))) + (artist (smudge-api-get-track-artist track))) (smudge-controller-play-track track artist))) (defun smudge-track-album-select () @@ -87,82 +84,82 @@ without a context." "Add the current user as the follower of the selected playlist." (interactive) (if (bound-and-true-p smudge-selected-playlist) - (let ((playlist smudge-selected-playlist)) - (when (y-or-n-p (format "Follow playlist '%s'? " (smudge-api-get-item-name playlist))) - (smudge-api-playlist-follow - playlist - (lambda (_) - (message (format "Followed playlist '%s'" (smudge-api-get-item-name playlist))))))) + (let ((playlist smudge-selected-playlist)) + (when (y-or-n-p (format "Follow playlist '%s'? " (smudge-api-get-item-name playlist))) + (smudge-api-playlist-follow + playlist + (lambda (_) + (message (format "Followed playlist '%s'" (smudge-api-get-item-name playlist))))))) (message "Cannot Follow a playlist from here"))) (defun smudge-track-playlist-unfollow () "Remove the current user as the follower of the selected playlist." (interactive) (if (bound-and-true-p smudge-selected-playlist) - (let ((playlist smudge-selected-playlist)) - (when (y-or-n-p (format "Unfollow playlist '%s'? " (smudge-api-get-item-name playlist))) - (smudge-api-playlist-unfollow - playlist - (lambda (_) - (message (format "Unfollowed playlist '%s'" (smudge-api-get-item-name playlist))))))) + (let ((playlist smudge-selected-playlist)) + (when (y-or-n-p (format "Unfollow playlist '%s'? " (smudge-api-get-item-name playlist))) + (smudge-api-playlist-unfollow + playlist + (lambda (_) + (message (format "Unfollowed playlist '%s'" (smudge-api-get-item-name playlist))))))) (message "Cannot unfollow a playlist from here"))) (defun smudge-track-reload () "Reloads the first page of results for the current track view." (interactive) (cond ((bound-and-true-p smudge-recently-played) - (smudge-track-recently-played-tracks-update 1)) - ((bound-and-true-p smudge-selected-playlist) - (smudge-track-playlist-tracks-update 1)) - ((bound-and-true-p smudge-query) - (smudge-track-search-update smudge-query 1)) - ((bound-and-true-p smudge-selected-album) - (smudge-track-album-tracks-update smudge-selected-album 1)))) + (smudge-track-recently-played-tracks-update 1)) + ((bound-and-true-p smudge-selected-playlist) + (smudge-track-playlist-tracks-update 1)) + ((bound-and-true-p smudge-query) + (smudge-track-search-update smudge-query 1)) + ((bound-and-true-p smudge-selected-album) + (smudge-track-album-tracks-update smudge-selected-album 1)))) (defun smudge-track-load-more () "Load the next page of results for the current track view." (interactive) (cond ((bound-and-true-p smudge-recently-played) - (smudge-track-recently-played-tracks-update (1+ smudge-current-page))) - ((bound-and-true-p smudge-selected-playlist) - (smudge-track-playlist-tracks-update (1+ smudge-current-page))) - ((bound-and-true-p smudge-selected-album) - (smudge-track-album-tracks-update smudge-selected-album (1+ smudge-current-page))) - ((bound-and-true-p smudge-query) - (smudge-track-search-update smudge-query (1+ smudge-current-page))))) + (smudge-track-recently-played-tracks-update (1+ smudge-current-page))) + ((bound-and-true-p smudge-selected-playlist) + (smudge-track-playlist-tracks-update (1+ smudge-current-page))) + ((bound-and-true-p smudge-selected-album) + (smudge-track-album-tracks-update smudge-selected-album (1+ smudge-current-page))) + ((bound-and-true-p smudge-query) + (smudge-track-search-update smudge-query (1+ smudge-current-page))))) (defun smudge-track-search-update (query page) "Fetch the PAGE of results using QUERY at the search endpoint." (let ((buffer (current-buffer))) (smudge-api-search - 'track - query - page - (lambda (json) - (if-let ((items (smudge-api-get-search-track-items json))) - (with-current-buffer buffer - (setq-local smudge-current-page page) - (setq-local smudge-query query) - (pop-to-buffer buffer) - (smudge-track-search-print items page) - (message "Track view updated")) - (message "No more tracks")))))) + 'track + query + page + (lambda (json) + (if-let ((items (smudge-api-get-search-track-items json))) + (with-current-buffer buffer + (setq-local smudge-current-page page) + (setq-local smudge-query query) + (pop-to-buffer buffer) + (smudge-track-search-print items page) + (message "Track view updated")) + (message "No more tracks")))))) (defun smudge-track-playlist-tracks-update (page) "Fetch PAGE of results for the current playlist." (when (bound-and-true-p smudge-selected-playlist) (let ((buffer (current-buffer))) (smudge-api-playlist-tracks - smudge-selected-playlist - page - (lambda (json) - (if-let ((items (smudge-api-get-playlist-tracks json))) - (with-current-buffer buffer - (setq-local smudge-current-page page) - (pop-to-buffer buffer) - (smudge-track-search-print items page) - (message "Track view updated")) - (message "No more tracks"))))))) + smudge-selected-playlist + page + (lambda (json) + (if-let ((items (smudge-api-get-playlist-tracks json))) + (with-current-buffer buffer + (setq-local smudge-current-page page) + (pop-to-buffer buffer) + (smudge-track-search-print items page) + (message "Track view updated")) + (message "No more tracks"))))))) (defun smudge-track-album-tracks-update (album page) "Fetch PAGE of of tracks for ALBUM." @@ -193,16 +190,16 @@ without a context." "Fetch PAGE of results for the recently played tracks." (let ((buffer (current-buffer))) (smudge-api-recently-played - page - (lambda (json) - (if-let ((items (smudge-api-get-playlist-tracks json))) - (with-current-buffer buffer - (setq-local smudge-current-page page) - (setq-local smudge-recently-played t) - (pop-to-buffer buffer) - (smudge-track-search-print items page) - (message "Track view updated")) - (message "No more tracks")))))) + page + (lambda (json) + (if-let ((items (smudge-api-get-playlist-tracks json))) + (with-current-buffer buffer + (setq-local smudge-current-page page) + (setq-local smudge-recently-played t) + (pop-to-buffer buffer) + (smudge-track-search-print items page) + (message "Track view updated")) + (message "No more tracks")))))) (defun smudge-track-search-set-list-format () "Configure the column data for the typical track view. @@ -224,9 +221,9 @@ Default to sorting tracks by number when listing the tracks from an album." `("Album" ,default-width t) `("Time" 8 (lambda (row-1 row-2) (< (smudge-api-get-track-duration (car row-1)) - (smudge-api-get-track-duration (car row-2)))))) - (when (not (bound-and-true-p smudge-selected-album)) - (vector '("Popularity" 14 t))))))) + (smudge-api-get-track-duration (car row-2)))))) + (when (not (bound-and-true-p smudge-selected-album)) + (vector '("Popularity" 14 t))))))) (defun smudge-track-search-print (songs page) "Append SONGS to the PAGE of track view." @@ -258,7 +255,6 @@ Default to sorting tracks by number when listing the tracks from an album." (smudge-api-popularity-bar (smudge-api-get-track-popularity song))))) entries)))) (setq tabulated-list-printer #'tabulated-list-print-entry) - (when smudge-show-artwork (setq tabulated-list-printer #'smudge-image-tabulated-list-print-entry) (setq smudge-artwork-fetch-target-count @@ -272,8 +268,8 @@ Default to sorting tracks by number when listing the tracks from an album." (setq inhibit-redisplay t) (setq left-margin-width 6) (set-window-buffer (selected-window) (current-buffer))) - (smudge-track-search-set-list-format) (when (eq 1 page) (setq-local tabulated-list-entries nil)) + (smudge-track-search-set-list-format) (setq-local tabulated-list-entries (append tabulated-list-entries (nreverse entries))) (tabulated-list-init-header) (tabulated-list-print t))) @@ -289,15 +285,15 @@ Default to sorting tracks by number when listing the tracks from an album." "Call CALLBACK with results of user playlist selection." (interactive) (smudge-api-current-user - (lambda (user) - (smudge-api-user-playlists - (smudge-api-get-item-id user) - 1 - (lambda (json) - (if-let* ((choices (mapcar (lambda (a) - (list (smudge-api-get-item-name a) (smudge-api-get-item-id a))) - (smudge-api-get-items json))) - (selected (completing-read "Select Playlist: " choices))) + (lambda (user) + (smudge-api-user-playlists + (smudge-api-get-item-id user) + 1 + (lambda (json) + (if-let* ((choices (mapcar (lambda (a) + (list (smudge-api-get-item-name a) (smudge-api-get-item-id a))) + (smudge-api-get-items json))) + (selected (completing-read "Select Playlist: " choices))) (when (not (string= "" selected)) (funcall callback (cadr (assoc selected choices)))))))))) @@ -306,15 +302,15 @@ Default to sorting tracks by number when listing the tracks from an album." (interactive) (let ((selected-track (tabulated-list-get-id))) (smudge-track-select-playlist - (lambda (playlist) - (smudge-api-current-user - (lambda (user) - (smudge-api-playlist-add-track - (smudge-api-get-item-id user) - playlist - (smudge-api-get-item-uri selected-track) - (lambda (_) - (message "Song added."))))))))) + (lambda (playlist) + (smudge-api-current-user + (lambda (user) + (smudge-api-playlist-add-track + (smudge-api-get-item-id user) + playlist + (smudge-api-get-item-uri selected-track) + (lambda (_) + (message "Song added."))))))))) (provide 'smudge-track) ;;; smudge-track.el ends here