diff --git a/smudge-api.el b/smudge-api.el index 1790fa8..e0616a4 100644 --- a/smudge-api.el +++ b/smudge-api.el @@ -347,6 +347,18 @@ 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 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)))) + +(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." @@ -463,6 +475,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-image.el b/smudge-image.el new file mode 100644 index 0000000..804dc0c --- /dev/null +++ b/smudge-image.el @@ -0,0 +1,78 @@ +;;; 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: + +(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 +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 :width 64))) + ;; 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))) + (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) + (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-playlist.el b/smudge-playlist.el index 96aad8f..584834a 100644 --- a/smudge-playlist.el +++ b/smudge-playlist.el @@ -13,12 +13,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))) @@ -141,11 +144,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." @@ -154,18 +159,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 53de8ba..8eb7517 100644 --- a/smudge-track.el +++ b/smudge-track.el @@ -12,12 +12,16 @@ (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) + (defvar smudge-track-search-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map tabulated-list-mode-map) @@ -53,9 +57,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 () @@ -68,14 +72,14 @@ 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 () "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 () @@ -106,111 +110,122 @@ without a context." "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." (let ((buffer (current-buffer))) (smudge-api-album-tracks - 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")))))) + 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) + ;; 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." (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. -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 ))) - (unless (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)))))) - (unless (bound-and-true-p smudge-selected-album) - (vector '("Popularity" 14 t))))))) +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 ))) + (unless (bound-and-true-p smudge-selected-playlist) + (setq tabulated-list-sort-key `("#" . nil))) + (setq tabulated-list-format + (vconcat (vector + `("" -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))) + (+ (* 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)))))) + (unless (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." @@ -218,30 +233,45 @@ 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))) (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) - (unless (bound-and-true-p smudge-selected-album) - (smudge-api-popularity-bar (smudge-api-get-track-popularity song))))) - entries)))) - (smudge-track-search-set-list-format) + (vector + (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 + (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) + (unless (bound-and-true-p smudge-selected-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 + (+ (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))) (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))) @@ -274,15 +304,15 @@ Default to sortin 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