From 0a924627e83335695725b3797c74fa82fccba3f9 Mon Sep 17 00:00:00 2001 From: David Moc Date: Sun, 5 Apr 2026 20:33:57 +0200 Subject: Changed the volume, adding coverart, added starring and radio. Signed-off-by: David Moc --- naviel.el | 1237 ++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 932 insertions(+), 305 deletions(-) (limited to 'naviel.el') diff --git a/naviel.el b/naviel.el index 473e023..5def92e 100755 --- a/naviel.el +++ b/naviel.el @@ -1,8 +1,8 @@ ;;; naviel.el --- Navidrome music player client for Emacs -*- lexical-binding: t -*- ;; Author: CdatGoose -;; Version: 0.2 -;; Package-Requires: ((emacs "28.1")) +;; Version: 0.3 +;; Package-Requires: ((emacs "29.1") (transient "0.5.0")) ;; Keywords: multimedia, music ;;; Commentary: @@ -13,7 +13,7 @@ ;; 1. ~/.authinfo: machine your-host login USER password PASS ;; 2. (setq naviel-url "http://host:4533") ;; 3. M-x naviel -;; Code: +;;; Code: (require 'url) (require 'url-util) @@ -22,12 +22,12 @@ (require 'cl-lib) (require 'json) (require 'seq) +(require 'transient) ;;; Customisation -(defgroup naviel nil - "Naviel music client." - :group 'multimedia - :prefix "naviel-") + +(defgroup naviel nil "Naviel music client." :group 'multimedia :prefix "naviel-") + (defcustom naviel-url "http://localhost:4533" "Base URL of the Navidrome server (no trailing slash)." :type 'string :group 'naviel) @@ -61,68 +61,109 @@ (defcustom naviel-genre-song-limit 200 "Maximum songs fetched when browsing a genre." :type 'integer :group 'naviel) +(defcustom naviel-cover-art-width 300 + "Pixel width requested when fetching cover art." + :type 'integer :group 'naviel) +(defcustom naviel-cover-art-display t + "Non-nil to attempt inline cover art display when available." + :type 'boolean :group 'naviel) ;;; Faces -(defface naviel-header-face '((t :inherit bold :height 1.15)) "Main header.") -(defface naviel-separator-face '((t :inherit shadow)) "Separator lines.") -(defface naviel-artist-face '((t :inherit font-lock-function-name-face - :weight semi-bold)) "Artist names.") -(defface naviel-album-face '((t :inherit font-lock-string-face)) "Album names.") -(defface naviel-song-title-face '((t :inherit default)) "Song titles.") -(defface naviel-song-meta-face '((t :inherit shadow)) "Song metadata.") -(defface naviel-track-num-face '((t :inherit shadow)) "Track numbers.") -(defface naviel-year-face '((t :inherit shadow)) "Album years.") -(defface naviel-now-playing-face '((t :inherit success :weight bold)) "Now-playing text.") -(defface naviel-paused-face '((t :inherit warning :weight bold)) "Paused indicator.") -(defface naviel-footer-face '((t :inherit shadow :height 0.85)) "Footer hints.") -(defface naviel-count-face '((t :inherit shadow)) "Item counts.") -(defface naviel-breadcrumb-face '((t :inherit font-lock-comment-face)) "Breadcrumb trail.") -(defface naviel-repeat-face '((t :inherit font-lock-keyword-face)) "Repeat mode.") -(defface naviel-error-face '((t :inherit error)) "In-buffer errors.") -(defface naviel-position-face '((t :inherit shadow)) "Playback position.") + +(defface naviel-header-face + '((t :inherit bold :height 1.1)) "Main header.") +(defface naviel-separator-face + '((t :inherit shadow)) "Separator lines.") +(defface naviel-artist-face + '((t :inherit font-lock-function-name-face :weight semi-bold)) "Artist names.") +(defface naviel-album-face + '((t :inherit font-lock-string-face)) "Album names.") +(defface naviel-song-title-face + '((t :inherit default)) "Song titles.") +(defface naviel-song-meta-face + '((t :inherit shadow)) "Song metadata.") +(defface naviel-track-num-face + '((t :inherit shadow)) "Track numbers.") +(defface naviel-year-face + '((t :inherit shadow)) "Album years.") +(defface naviel-now-playing-face + '((t :inherit success :weight bold)) "Now-playing text.") +(defface naviel-paused-face + '((t :inherit warning :weight bold)) "Paused indicator.") +(defface naviel-footer-face + '((t :inherit shadow :height 0.85)) "Footer hints.") +(defface naviel-count-face + '((t :inherit shadow)) "Item counts.") +(defface naviel-breadcrumb-face + '((t :inherit font-lock-comment-face)) "Breadcrumb trail.") +(defface naviel-repeat-face + '((t :inherit font-lock-keyword-face)) "Repeat mode.") +(defface naviel-error-face + '((t :inherit error)) "In-buffer errors.") +(defface naviel-position-face + '((t :inherit shadow)) "Playback position.") +(defface naviel-progress-fill-face + '((t :inherit success)) "Progress bar fill.") +(defface naviel-progress-empty-face + '((t :inherit shadow)) "Progress bar empty.") +(defface naviel-starred-face + '((t :inherit font-lock-warning-face)) "Starred indicator.") +(defface naviel-radio-face + '((t :inherit font-lock-constant-face)) "Internet radio stations.") ;;; Repeat mode + (defvar naviel-repeat-mode 'off - "Playback repeat mode: \\='off, \\='one, or \\='album.") + "Playback repeat mode: `off', `one', or `album'.") + (defun naviel-toggle-repeat () - "Cycle repeat mode: off -> one -> album -> off." + "Cycle repeat mode: off → one → album → off." (interactive) (setq naviel-repeat-mode (pcase naviel-repeat-mode ('off 'one) ('one 'album) (_ 'off))) (naviel--refresh-footer) (message "Repeat: %s" (naviel--repeat-label))) + (defun naviel--repeat-label () (pcase naviel-repeat-mode ('one "one") ('album "all") (_ "off"))) ;;; Internal state -(defvar naviel--process nil) -(defvar naviel--ipc-process nil) -(defvar naviel--ipc-buffer "") -(defvar naviel--ipc-request-id 0) -(defvar naviel--ipc-callbacks nil) -(defvar naviel--position-timer nil) -(defvar naviel--elapsed nil) -(defvar naviel--duration nil) -(defvar naviel--queue '()) -(defvar naviel--queue-index 0) -(defvar naviel--volume 80) -(defvar naviel--paused nil) -(defvar naviel--current-song nil) + +(defvar naviel--process nil) +(defvar naviel--ipc-process nil) +(defvar naviel--ipc-buffer "") +(defvar naviel--ipc-request-id 0) +(defvar naviel--ipc-callbacks nil) +(defvar naviel--position-timer nil) +(defvar naviel--elapsed nil) +(defvar naviel--duration nil) +(defvar naviel--queue '()) +(defvar naviel--queue-index 0) +(defvar naviel--volume 80) +(defvar naviel--paused nil) +(defvar naviel--current-song nil) (defvar naviel--browser-stack '()) -(defvar naviel--current-view nil) +(defvar naviel--current-view nil) (defvar naviel--mode-line-timer nil) +;; Star cache: hash of id -> t (starred) +(defvar naviel--starred-ids (make-hash-table :test 'equal)) + +(defun naviel--browser-buffer () + "Return the main Naviel browser buffer, creating it if necessary." + (get-buffer-create "*Naviel*")) ;;; Authentication + (defun naviel--credentials () (let* ((host (url-host (url-generic-parse-url naviel-url))) (user (or naviel-username (plist-get (car (auth-source-search :host host :require '(:user))) :user))) (pass (or naviel-password (let* ((entry (car (auth-source-search :host host :require '(:secret)))) - (s (plist-get entry :secret))) + (s (plist-get entry :secret))) (when s (if (functionp s) (funcall s) s)))))) (unless (and user pass) - (error "naviel: no credentials for %s - set naviel-username/naviel-password or use ~/.authinfo" host)) + (error "naviel: no credentials for %s – set naviel-username/naviel-password or ~/.authinfo" host)) (cons user pass))) (defun naviel--auth-params () @@ -138,6 +179,7 @@ (setq r (concat r (string (aref chars (random (length chars))))))))) ;;; Async HTTP / API + (defun naviel--build-url (endpoint params) (let* ((all (append (naviel--auth-params) params)) (q (mapconcat @@ -151,10 +193,9 @@ url (lambda (status) (if (plist-get status :error) - (progn - (ignore-errors (kill-buffer (current-buffer))) - (naviel--show-error (format "Could not reach %s - is the server running?" naviel-url)) - (funcall callback nil)) + (progn (ignore-errors (kill-buffer (current-buffer))) + (naviel--show-error (format "Could not reach %s – is the server running?" naviel-url)) + (funcall callback nil)) (condition-case err (progn (goto-char (point-min)) @@ -187,22 +228,85 @@ (defun naviel--attr (node attr) (xml-get-attribute node attr)) ;;; Error display + (defun naviel--show-error (msg) (with-current-buffer (naviel--browser-buffer) (naviel-browser-mode) (let ((inhibit-read-only t)) (erase-buffer) - (insert (propertize "\n X naviel error\n\n" 'face 'naviel-error-face)) - (insert (propertize (format " %s\n\n" msg) 'face 'naviel-error-face)) - (insert (propertize " Press g to retry, q to quit.\n" 'face 'naviel-footer-face)) + (insert (propertize "\n ✗ naviel error\n\n" 'face 'naviel-error-face)) + (insert (propertize (format " %s\n\n" msg) 'face 'naviel-error-face)) + (insert (propertize " g retry q quit\n" 'face 'naviel-footer-face)) (goto-char (point-min)))) (pop-to-buffer (naviel--browser-buffer))) -;;; Stream URL +;;; Stream / cover-art URLs + (defun naviel--stream-url (song-id) (naviel--build-url "stream" `(("id" . ,song-id)))) +(defun naviel--cover-art-url (cover-id &optional size) + (naviel--build-url "getCoverArt" + `(("id" . ,cover-id) + ("size" . ,(number-to-string (or size naviel-cover-art-width)))))) + +;;; Cover art display + +(defun naviel--display-cover-art (cover-id buffer-name &optional size) + "Fetch cover art for COVER-ID and display it in BUFFER-NAME. +Uses icat (kitty/WezTerm), viu, or sixel if available; else skips." + (when (and naviel-cover-art-display cover-id (not (string= cover-id ""))) + (let* ((url (naviel--cover-art-url cover-id (or size 400))) + (tmp (make-temp-file "naviel-art" nil ".jpg"))) + ;; Fetch asynchronously, display when done + (url-retrieve + url + (lambda (status) + (unless (plist-get status :error) + (goto-char (point-min)) + (re-search-forward "\r?\n\r?\n" nil t) + (let ((data (buffer-substring-no-properties (point) (point-max)))) + (ignore-errors (kill-buffer (current-buffer))) + (with-temp-file tmp (set-buffer-multibyte nil) (insert data)) + (naviel--insert-art-image tmp buffer-name))) + (ignore-errors (kill-buffer (current-buffer)))) + nil t)))) + +(defun naviel--insert-art-image (path buffer-name) + "Insert image at PATH into BUFFER-NAME using the best available method." + (cond + ;; Emacs can display images natively in GUI frames + ((and (display-graphic-p) (image-type-available-p 'jpeg)) + (when (get-buffer buffer-name) + (with-current-buffer buffer-name + (let ((inhibit-read-only t)) + (goto-char (point-min)) + ;; Insert after the header separator line + (when (re-search-forward "^─+$" nil t) + (forward-line 1) + (let ((img (create-image path 'jpeg nil :width 200))) + (insert " ") + (insert-image img) + (insert "\n\n"))))))) + ;; Terminal: try icat (kitty), viu, or chafa as fallback + ((executable-find "icat") + (call-process "icat" nil nil nil "--align" "left" "--width" "30" path)) + ((executable-find "viu") + (call-process "viu" nil nil nil "-w" "30" path)) + ((executable-find "chafa") + (let ((output (with-output-to-string + (call-process "chafa" nil standard-output nil + "--size" "30x15" "--format" "symbols" path)))) + (when (get-buffer buffer-name) + (with-current-buffer buffer-name + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (when (re-search-forward "^─+$" nil t) + (forward-line 1) + (insert output "\n"))))))))) + ;;; mpv IPC + (defun naviel--ipc-socket-path () (expand-file-name "naviel-mpv.sock" temporary-file-directory)) @@ -215,14 +319,11 @@ (condition-case err (setq naviel--ipc-process (make-network-process - :name "naviel-ipc" - :family 'local - :service sock - :filter #'naviel--ipc-filter - :sentinel #'naviel--ipc-sentinel - :coding 'utf-8-unix) + :name "naviel-ipc" :family 'local :service sock + :filter #'naviel--ipc-filter :sentinel #'naviel--ipc-sentinel + :coding 'utf-8-unix) naviel--ipc-buffer "") - (error (message "naviel: IPC connect failed - %s" (error-message-string err))))))) + (error (message "naviel: IPC connect failed – %s" (error-message-string err))))))) (defun naviel--ipc-filter (_proc string) (setq naviel--ipc-buffer (concat naviel--ipc-buffer string)) @@ -258,15 +359,15 @@ (defun naviel--ipc-handle-event (event) (cond ((string= event "end-file") (naviel--next-in-queue)) - ((string= event "pause") (setq naviel--paused t) (naviel--refresh-footer)) - ((string= event "unpause") (setq naviel--paused nil) (naviel--refresh-footer)))) + ((string= event "pause") (setq naviel--paused t) (naviel--refresh-footer)) + ((string= event "unpause") (setq naviel--paused nil) (naviel--refresh-footer)))) (defun naviel--ipc-send (command &optional callback) (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) (cl-incf naviel--ipc-request-id) - (let* ((id naviel--ipc-request-id) - (msg (append `(("command" . ,(vconcat command))) - (when callback `(("request_id" . ,id))))) + (let* ((id naviel--ipc-request-id) + (msg (append `(("command" . ,(vconcat command))) + (when callback `(("request_id" . ,id))))) (json (concat (json-encode msg) "\n"))) (when callback (push (cons id callback) naviel--ipc-callbacks)) (condition-case err @@ -274,10 +375,13 @@ (error (message "naviel IPC send: %s" (error-message-string err))))))) ;;; Playback position polling + (defun naviel--start-position-timer () (naviel--stop-position-timer) (setq naviel--position-timer - (run-with-timer naviel-position-poll-interval naviel-position-poll-interval #'naviel--poll-position))) + (run-with-timer naviel-position-poll-interval + naviel-position-poll-interval + #'naviel--poll-position))) (defun naviel--stop-position-timer () (when naviel--position-timer @@ -287,36 +391,46 @@ (defun naviel--poll-position () (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) (naviel--ipc-send '("get_property" "time-pos") - (lambda (v) (setq naviel--elapsed (and (numberp v) v)) - (naviel--refresh-footer))) + (lambda (v) + (setq naviel--elapsed (and (numberp v) v)) + (naviel--refresh-footer))) (naviel--ipc-send '("get_property" "duration") - (lambda (v) (setq naviel--duration (and (numberp v) v)))))) + (lambda (v) + (setq naviel--duration (and (numberp v) v)))))) ;;; mpv process management + (defun naviel--stop () (naviel--stop-position-timer) - (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) (delete-process naviel--ipc-process)) + (when (and naviel--ipc-process (process-live-p naviel--ipc-process)) + (delete-process naviel--ipc-process)) (setq naviel--ipc-process nil naviel--ipc-buffer "") - (when (and naviel--process (process-live-p naviel--process)) (delete-process naviel--process)) + (when (and naviel--process (process-live-p naviel--process)) + (delete-process naviel--process)) (let ((sock (naviel--ipc-socket-path))) (when (file-exists-p sock) (ignore-errors (delete-file sock)))) - (setq naviel--process nil naviel--paused nil naviel--current-song nil naviel--elapsed nil naviel--duration nil) + (setq naviel--process nil naviel--paused nil naviel--current-song nil + naviel--elapsed nil naviel--duration nil) (naviel--refresh-footer) (naviel--queue-render-if-live)) (defun naviel--play-url (url song-info) (naviel--stop) - (setq naviel--current-song song-info naviel--paused nil naviel--elapsed nil naviel--duration nil) + (setq naviel--current-song song-info naviel--paused nil + naviel--elapsed nil naviel--duration nil) (let* ((sock (naviel--ipc-socket-path)) (proc (start-process "naviel-mpv" nil naviel-mpv-executable "--no-video" "--quiet" (format "--volume=%d" naviel--volume) - (format "--input-ipc-server=%s" sock) url))) + (format "--input-ipc-server=%s" sock) + url))) (setq naviel--process proc)) (run-with-timer 0.3 nil #'naviel--ipc-connect) (run-with-timer 0.6 nil #'naviel--start-position-timer) (naviel--refresh-footer) - (message "Playing: %s - %s" (plist-get song-info :title) (plist-get song-info :artist))) + (message "▶ %s – %s" + (plist-get song-info :title) + (plist-get song-info :artist))) (defun naviel--toggle-pause () (interactive) @@ -329,10 +443,11 @@ (naviel--ipc-send `("seek" ,seconds "relative")) (message "naviel: nothing is playing"))) -(defun naviel-seek-forward () (interactive) (naviel--seek naviel-seek-step)) +(defun naviel-seek-forward () (interactive) (naviel--seek naviel-seek-step)) (defun naviel-seek-backward () (interactive) (naviel--seek (- naviel-seek-step))) ;;; Queue management + (defun naviel--play-queue-index (idx) (setq naviel--queue-index idx) (let* ((song (nth idx naviel--queue)) @@ -345,12 +460,13 @@ (interactive) (pcase naviel-repeat-mode ('one (naviel--play-queue-index naviel--queue-index)) - ('album (naviel--play-queue-index (mod (1+ naviel--queue-index) (max 1 (length naviel--queue))))) - (_ (let ((next (1+ naviel--queue-index))) - (if (< next (length naviel--queue)) - (naviel--play-queue-index next) - (naviel--stop) - (message "naviel: end of queue")))))) + ('album (naviel--play-queue-index + (mod (1+ naviel--queue-index) (max 1 (length naviel--queue))))) + (_ (let ((next (1+ naviel--queue-index))) + (if (< next (length naviel--queue)) + (naviel--play-queue-index next) + (naviel--stop) + (message "naviel: end of queue")))))) (defun naviel--prev-in-queue () (interactive) @@ -378,7 +494,8 @@ (when was-empty (setq naviel--queue-index 0)) (naviel--rerender-current-view) (naviel--queue-render-if-live) - (message "naviel: appended %d track%s from \"%s\"" (length songs) (if (= 1 (length songs)) "" "s") label))) + (message "naviel: +%d from \"%s\" (queue: %d)" + (length songs) label (length naviel--queue)))) (defun naviel-queue-shuffle () (interactive) @@ -390,65 +507,107 @@ (setq naviel--queue (append vec nil))) (when naviel--current-song (let* ((cur-id (plist-get naviel--current-song :id)) - (pos (cl-position-if (lambda (s) (equal (plist-get s :id) cur-id)) naviel--queue))) + (pos (cl-position-if (lambda (s) (equal (plist-get s :id) cur-id)) + naviel--queue))) (when pos (let ((cur (nth pos naviel--queue))) - (setq naviel--queue (cons cur (append (seq-take naviel--queue pos) (seq-drop naviel--queue (1+ pos))))) + (setq naviel--queue + (cons cur (append (seq-take naviel--queue pos) + (seq-drop naviel--queue (1+ pos))))) (setq naviel--queue-index 0))))) (naviel--rerender-current-view) (naviel--queue-render-if-live) - (message "naviel: queue shuffled (%d tracks)" (length naviel--queue))) + (message "naviel: shuffled (%d tracks)" (length naviel--queue))) ;;; API parse helpers + (defun naviel--parse-artists (response) (let (result) (dolist (index (xml-get-children (naviel--get-child response 'artists) 'index) result) (dolist (a (xml-get-children index 'artist)) - (push `(:id ,(naviel--attr a 'id) :name ,(naviel--attr a 'name) :albums ,(naviel--attr a 'albumCount)) result))) + (push `(:id ,(naviel--attr a 'id) + :name ,(naviel--attr a 'name) + :albums ,(naviel--attr a 'albumCount) + :starred ,(naviel--attr a 'starred)) + result))) (nreverse result))) (defun naviel--parse-artist-albums (response) (let (result) (dolist (al (xml-get-children (naviel--get-child response 'artist) 'album) result) - (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) :year ,(naviel--attr al 'year)) result)) + (push `(:id ,(naviel--attr al 'id) + :name ,(naviel--attr al 'name) + :year ,(naviel--attr al 'year) + :coverArt ,(naviel--attr al 'coverArt) + :starred ,(naviel--attr al 'starred)) + result)) (nreverse result))) (defun naviel--parse-album-songs (response) - (let (result) - (dolist (s (xml-get-children (naviel--get-child response 'album) 'song) result) - (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) - :album ,(naviel--attr s 'album) :track ,(naviel--attr s 'track) :duration ,(naviel--attr s 'duration)) result)) + (let* ((album-node (naviel--get-child response 'album)) + result) + (dolist (s (xml-get-children album-node 'song) result) + (push `(:id ,(naviel--attr s 'id) + :title ,(naviel--attr s 'title) + :artist ,(naviel--attr s 'artist) + :album ,(naviel--attr s 'album) + :track ,(naviel--attr s 'track) + :duration ,(naviel--attr s 'duration) + :coverArt ,(naviel--attr s 'coverArt) + :starred ,(naviel--attr s 'starred)) + result)) (nreverse result))) (defun naviel--parse-search (response) - (let* ((r3 (naviel--get-child response 'searchResult3)) artists albums songs) - (dolist (a (xml-get-children r3 'artist)) (push `(:id ,(naviel--attr a 'id) :name ,(naviel--attr a 'name)) artists)) - (dolist (al (xml-get-children r3 'album)) (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) :artist ,(naviel--attr al 'artist)) albums)) - (dolist (s (xml-get-children r3 'song)) (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :duration ,(naviel--attr s 'duration)) songs)) + (let* ((r3 (naviel--get-child response 'searchResult3)) + artists albums songs) + (dolist (a (xml-get-children r3 'artist)) + (push `(:id ,(naviel--attr a 'id) :name ,(naviel--attr a 'name) + :starred ,(naviel--attr a 'starred)) artists)) + (dolist (al (xml-get-children r3 'album)) + (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) + :artist ,(naviel--attr al 'artist) :starred ,(naviel--attr al 'starred)) albums)) + (dolist (s (xml-get-children r3 'song)) + (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) + :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) + :duration ,(naviel--attr s 'duration) :starred ,(naviel--attr s 'starred)) songs)) `(:artists ,(nreverse artists) :albums ,(nreverse albums) :songs ,(nreverse songs)))) (defun naviel--parse-random-songs (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'randomSongs) 'song) result) - (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :duration ,(naviel--attr s 'duration)) result)) + (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) + :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) + :duration ,(naviel--attr s 'duration) :coverArt ,(naviel--attr s 'coverArt) + :starred ,(naviel--attr s 'starred)) + result)) (nreverse result))) (defun naviel--parse-playlists (response) (let (result) (dolist (pl (xml-get-children (naviel--get-child response 'playlists) 'playlist) result) - (push `(:id ,(naviel--attr pl 'id) :name ,(naviel--attr pl 'name) :count ,(naviel--attr pl 'songCount) :duration ,(naviel--attr pl 'duration) :owner ,(naviel--attr pl 'owner)) result)) + (push `(:id ,(naviel--attr pl 'id) :name ,(naviel--attr pl 'name) + :count ,(naviel--attr pl 'songCount) :owner ,(naviel--attr pl 'owner)) + result)) (nreverse result))) (defun naviel--parse-playlist-songs (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'playlist) 'entry) result) - (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :track ,(naviel--attr s 'track) :duration ,(naviel--attr s 'duration)) result)) + (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) + :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) + :track ,(naviel--attr s 'track) :duration ,(naviel--attr s 'duration) + :coverArt ,(naviel--attr s 'coverArt) :starred ,(naviel--attr s 'starred)) + result)) (nreverse result))) (defun naviel--parse-album-list2 (response) (let (result) (dolist (al (xml-get-children (naviel--get-child response 'albumList2) 'album) result) - (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) :artist ,(naviel--attr al 'artist) :year ,(naviel--attr al 'year)) result)) + (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) + :artist ,(naviel--attr al 'artist) :year ,(naviel--attr al 'year) + :coverArt ,(naviel--attr al 'coverArt) :starred ,(naviel--attr al 'starred)) + result)) (nreverse result))) (defun naviel--parse-genres (response) @@ -459,37 +618,139 @@ (name (when text-node (string-trim text-node))) (songs (naviel--attr g 'songCount)) (albums (naviel--attr g 'albumCount))) - (when (and name (not (string= name ""))) (push `(:name ,name :songs ,songs :albums ,albums) result)))) + (when (and name (not (string= name ""))) + (push `(:name ,name :songs ,songs :albums ,albums) result)))) (nreverse result))) (defun naviel--parse-songs-by-genre (response) (let (result) (dolist (s (xml-get-children (naviel--get-child response 'songsByGenre) 'song) result) - (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) :duration ,(naviel--attr s 'duration)) result)) + (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) + :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) + :duration ,(naviel--attr s 'duration) :coverArt ,(naviel--attr s 'coverArt) + :starred ,(naviel--attr s 'starred)) + result)) (nreverse result))) (defun naviel--parse-lyrics (response) - (let* ((lyrics-node (naviel--get-child response 'lyrics))) - (if lyrics-node - (let* ((children (xml-node-children lyrics-node)) - (text-node (seq-find #'stringp children))) - (if text-node (string-trim text-node) "No lyrics found.")) - "No lyrics found."))) + (let* ((node (naviel--get-child response 'lyrics)) + (text (when node (seq-find #'stringp (xml-node-children node))))) + (if (and text (not (string= (string-trim text) ""))) + (string-trim text) + nil))) + +(defun naviel--parse-starred2 (response) + "Return plist with :artists :albums :songs from getStarred2." + (let* ((starred (naviel--get-child response 'starred2)) + artists albums songs) + (dolist (a (xml-get-children starred 'artist)) + (push `(:id ,(naviel--attr a 'id) :name ,(naviel--attr a 'name) :starred t) artists)) + (dolist (al (xml-get-children starred 'album)) + (push `(:id ,(naviel--attr al 'id) :name ,(naviel--attr al 'name) + :artist ,(naviel--attr al 'artist) :year ,(naviel--attr al 'year) + :coverArt ,(naviel--attr al 'coverArt) :starred t) albums)) + (dolist (s (xml-get-children starred 'song)) + (push `(:id ,(naviel--attr s 'id) :title ,(naviel--attr s 'title) + :artist ,(naviel--attr s 'artist) :album ,(naviel--attr s 'album) + :duration ,(naviel--attr s 'duration) :coverArt ,(naviel--attr s 'coverArt) + :starred t) songs)) + `(:artists ,(nreverse artists) :albums ,(nreverse albums) :songs ,(nreverse songs)))) + +(defun naviel--parse-radio-stations (response) + (let (result) + (dolist (s (xml-get-children (naviel--get-child response 'internetRadioStations) + 'internetRadioStation) + result) + (push `(:id ,(naviel--attr s 'id) + :name ,(naviel--attr s 'name) + :stream-url ,(naviel--attr s 'streamUrl) + :home-url ,(naviel--attr s 'homePageUrl)) + result)) + (nreverse result))) + +(defun naviel--parse-void (_response) t) ;;; Async API entry points -(defun naviel--get-artists (cb) (naviel--request-async "getArtists" '() #'naviel--parse-artists cb)) -(defun naviel--get-artist (id cb) (naviel--request-async "getArtist" `(("id" . ,id)) #'naviel--parse-artist-albums cb)) -(defun naviel--get-album (id cb) (naviel--request-async "getAlbum" `(("id" . ,id)) #'naviel--parse-album-songs cb)) -(defun naviel--get-playlists (cb) (naviel--request-async "getPlaylists" '() #'naviel--parse-playlists cb)) -(defun naviel--get-playlist (id cb) (naviel--request-async "getPlaylist" `(("id" . ,id)) #'naviel--parse-playlist-songs cb)) -(defun naviel--get-album-list2 (type size cb) (naviel--request-async "getAlbumList2" `(("type" . ,type) ("size" . ,(number-to-string size))) #'naviel--parse-album-list2 cb)) -(defun naviel--get-genres (cb) (naviel--request-async "getGenres" '() #'naviel--parse-genres cb)) -(defun naviel--get-songs-by-genre (genre count offset cb) (naviel--request-async "getSongsByGenre" `(("genre" . ,genre) ("count" . ,(number-to-string count)) ("offset" . ,(number-to-string offset))) #'naviel--parse-songs-by-genre cb)) -(defun naviel--search-async (q cb) (naviel--request-async "search3" `(("query" . ,q) ("artistCount" . "10") ("albumCount" . "10") ("songCount" . "20")) #'naviel--parse-search cb)) -(defun naviel--get-random-songs (n cb) (naviel--request-async "getRandomSongs" `(("size" . ,(number-to-string n))) #'naviel--parse-random-songs cb)) -(defun naviel--get-lyrics (artist title cb) (naviel--request-async "getLyrics" `(("artist" . ,artist) ("title" . ,title)) #'naviel--parse-lyrics cb)) + +(defun naviel--get-artists (cb) + (naviel--request-async "getArtists" '() #'naviel--parse-artists cb)) +(defun naviel--get-artist (id cb) + (naviel--request-async "getArtist" `(("id" . ,id)) #'naviel--parse-artist-albums cb)) +(defun naviel--get-album (id cb) + (naviel--request-async "getAlbum" `(("id" . ,id)) #'naviel--parse-album-songs cb)) +(defun naviel--get-playlists (cb) + (naviel--request-async "getPlaylists" '() #'naviel--parse-playlists cb)) +(defun naviel--get-playlist (id cb) + (naviel--request-async "getPlaylist" `(("id" . ,id)) #'naviel--parse-playlist-songs cb)) +(defun naviel--get-album-list2 (type size cb) + (naviel--request-async "getAlbumList2" + `(("type" . ,type) ("size" . ,(number-to-string size))) + #'naviel--parse-album-list2 cb)) +(defun naviel--get-genres (cb) + (naviel--request-async "getGenres" '() #'naviel--parse-genres cb)) +(defun naviel--get-songs-by-genre (genre count offset cb) + (naviel--request-async "getSongsByGenre" + `(("genre" . ,genre) + ("count" . ,(number-to-string count)) + ("offset" . ,(number-to-string offset))) + #'naviel--parse-songs-by-genre cb)) +(defun naviel--search-async (q cb) + (naviel--request-async "search3" + `(("query" . ,q) ("artistCount" . "10") + ("albumCount" . "10") ("songCount" . "20")) + #'naviel--parse-search cb)) +(defun naviel--get-random-songs (n cb) + (naviel--request-async "getRandomSongs" `(("size" . ,(number-to-string n))) + #'naviel--parse-random-songs cb)) +(defun naviel--get-lyrics (artist title cb) + (naviel--request-async "getLyrics" + `(("artist" . ,artist) ("title" . ,title)) + #'naviel--parse-lyrics cb)) +(defun naviel--get-starred2 (cb) + (naviel--request-async "getStarred2" '() #'naviel--parse-starred2 cb)) +(defun naviel--get-radio-stations (cb) + (naviel--request-async "getInternetRadioStations" '() + #'naviel--parse-radio-stations cb)) + +(defun naviel--api-star (id type cb) + "Star item ID of TYPE (song/album/artist) by calling the star endpoint." + (let ((param (pcase type + ('song "id") + ('album "albumId") + ('artist "artistId") + (_ "id")))) + (naviel--request-async "star" `((,param . ,id)) #'naviel--parse-void cb))) + +(defun naviel--api-unstar (id type cb) + "Unstar item ID of TYPE." + (let ((param (pcase type + ('song "id") + ('album "albumId") + ('artist "artistId") + (_ "id")))) + (naviel--request-async "unstar" `((,param . ,id)) #'naviel--parse-void cb))) + +;;; Star cache helpers + +(defun naviel--starred-p (id) + "Return non-nil if ID is in the local star cache." + (gethash id naviel--starred-ids)) + +(defun naviel--star-cache-set (id val) + (if val + (puthash id t naviel--starred-ids) + (remhash id naviel--starred-ids))) + +(defun naviel--prime-star-cache (items id-key starred-key) + "Populate star cache from a list of ITEMS using ID-KEY and STARRED-KEY plists." + (dolist (item items) + (let ((id (plist-get item id-key)) + (starred (plist-get item starred-key))) + (when (and id starred (not (string= starred ""))) + (puthash id t naviel--starred-ids))))) ;;; Duration / string helpers + (defun naviel--format-secs (secs) (if (and secs (numberp secs) (> secs 0)) (let* ((s (round secs)) (m (/ s 60)) (ss (% s 60))) @@ -501,65 +762,127 @@ (concat (substring s 0 (- max 1)) "…") (or s ""))) -;;; Browser buffer -(defun naviel--browser-buffer () (get-buffer-create "*Naviel*")) +(defun naviel--star-glyph (id) + "Return a propertized star string for ID, space if unstarred." + (if (naviel--starred-p id) + (propertize "★" 'face 'naviel-starred-face) + (propertize "☆" 'face 'naviel-separator-face))) -(defun naviel--make-breadcrumbs () - (if (null naviel--browser-stack) "" - (propertize - (concat " " (mapconcat (lambda (v) (plist-get v :label)) - (reverse naviel--browser-stack) " ❯ ") "\n") - 'face 'naviel-breadcrumb-face))) +;;; Progress bar + +(defun naviel--progress-bar (elapsed duration width) + "Render a text progress bar of WIDTH chars." + (if (and elapsed duration (> duration 0)) + (let* ((pct (min 1.0 (/ elapsed duration))) + (fill (round (* width pct))) + (empty (- width fill))) + (concat + (propertize (make-string fill ?━) 'face 'naviel-progress-fill-face) + (propertize (make-string empty ?─) 'face 'naviel-progress-empty-face))) + (propertize (make-string width ?─) 'face 'naviel-separator-face))) + +;;; Volume bar (defun naviel--volume-bar (vol) - (let* ((w 10) (f (round (* w (/ vol 100.0))))) - (concat "[" (make-string f ?■) (make-string (- w f) ?-) (format "] %3d%%" vol)))) + (let* ((w 8) (f (round (* w (/ vol 100.0))))) + (concat + (propertize (make-string f ?▪) 'face 'naviel-now-playing-face) + (propertize (make-string (- w f) ?▪) 'face 'naviel-separator-face) + (propertize (format " %3d%%" vol) 'face 'naviel-song-meta-face)))) + +;;; Now-playing bar (defun naviel--now-playing-bar () (let* ((song naviel--current-song) - (elap (naviel--format-secs naviel--elapsed)) - (dur (naviel--format-secs - (or naviel--duration - (when song (let ((d (plist-get song :duration))) (when (and d (not (string= d ""))) (string-to-number d))))))) - (qlen (length naviel--queue))) + (elap naviel--elapsed) + (dur (or naviel--duration + (when song + (let ((d (plist-get song :duration))) + (when (and d (not (string= d ""))) (string-to-number d)))))) + (w (max 50 (- (window-width) 4))) + (pbar (naviel--progress-bar elap dur (min 50 (- w 20))))) (concat - "\n" (propertize (make-string (max 50 (- (window-width) 2)) ?─) 'face 'naviel-separator-face) "\n" + "\n" + (propertize (make-string (max 50 (- (window-width) 2)) ?─) 'face 'naviel-separator-face) + "\n" (if song - (format " %s %s\n %s\n %s\n" - (if naviel--paused (propertize "⏸ " 'face 'naviel-paused-face) (propertize "▶ " 'face 'naviel-now-playing-face)) - (propertize (naviel--trunc (plist-get song :title) 50) 'face 'naviel-now-playing-face) - (propertize (concat (naviel--trunc (plist-get song :artist) 30) " • " (naviel--trunc (or (plist-get song :album) "") 30)) 'face 'naviel-song-meta-face) - (propertize (format "%s / %s [Queue: %d/%d]" elap dur (1+ naviel--queue-index) qlen) 'face 'naviel-position-face)) - (propertize " ■ Nothing playing\n" 'face 'shadow)) - (format " %s repeat: %s\n" - (propertize (format "vol %s" (naviel--volume-bar naviel--volume)) 'face 'naviel-song-meta-face) + (let* ((title (naviel--trunc (plist-get song :title) (min 52 (- w 4)))) + (artist (naviel--trunc (or (plist-get song :artist) "") 30)) + (album (naviel--trunc (or (plist-get song :album) "") 30)) + (e-str (naviel--format-secs elap)) + (d-str (naviel--format-secs dur)) + (qidx (format "[%d/%d]" (1+ naviel--queue-index) (length naviel--queue))) + (state (if naviel--paused + (propertize "⏸" 'face 'naviel-paused-face) + (propertize "▶" 'face 'naviel-now-playing-face)))) + (concat + (format " %s %s\n" + state + (propertize title 'face 'naviel-now-playing-face)) + (format " %s %s %s\n" + (propertize artist 'face 'naviel-artist-face) + (propertize "·" 'face 'naviel-separator-face) + (propertize album 'face 'naviel-album-face)) + (format " %s %s %s %s\n" + pbar + (propertize e-str 'face 'naviel-position-face) + (propertize "/" 'face 'naviel-separator-face) + (propertize (concat d-str " " qidx) 'face 'naviel-position-face)))) + (propertize " · Nothing playing\n" 'face 'shadow)) + (format " vol %s repeat: %s\n" + (naviel--volume-bar naviel--volume) (propertize (naviel--repeat-label) 'face 'naviel-repeat-face)) - (propertize " RET play a append SPC pause n/p skip [ ] seek +/- vol s search l lyrics ? help" 'face 'naviel-footer-face) "\n"))) + (propertize + " RET play a append SPC pause n/p skip [ ] seek +/- vol * star l lyrics ? menu" + 'face 'naviel-footer-face) + "\n"))) (defconst naviel--footer-sep "\0naviel-footer\0") +;;; Breadcrumbs + +(defun naviel--make-breadcrumbs () + (if (null naviel--browser-stack) "" + (propertize + (concat " " (mapconcat (lambda (v) (plist-get v :label)) + (reverse naviel--browser-stack) " ❯ ") + "\n") + 'face 'naviel-breadcrumb-face))) + ;;; Core render + (defun naviel--browser-render (heading items format-fn type &optional section-label) - (setq naviel--current-view `(:type ,type :items ,items :heading ,heading :format-fn ,format-fn :section-label ,section-label)) + (setq naviel--current-view + `(:type ,type :items ,items :heading ,heading + :format-fn ,format-fn :section-label ,section-label)) (naviel--do-render heading items format-fn type section-label)) (defun naviel--do-render (heading items format-fn type section-label) (with-current-buffer (naviel--browser-buffer) (naviel-browser-mode) - (let ((inhibit-read-only t) (saved-line (line-number-at-pos))) + (let ((inhibit-read-only t) + (saved-line (line-number-at-pos))) (erase-buffer) (insert (naviel--make-breadcrumbs)) (insert (propertize (format " %s\n" heading) 'face 'naviel-header-face)) - (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) + (insert (propertize (make-string (min 80 (window-width)) ?─) + 'face 'naviel-separator-face)) (insert "\n") - (when section-label (insert (propertize (format "\n %s\n\n" section-label) 'face 'naviel-breadcrumb-face))) + (when section-label + (insert (propertize (format "\n %s\n\n" section-label) 'face 'naviel-breadcrumb-face))) (if (null items) (insert (propertize " (no results)\n" 'face 'shadow)) (let ((idx 0)) (dolist (item items) - (let* ((is-playing (and naviel--current-song (eq type 'song) (= idx naviel--queue-index) (equal (plist-get item :id) (plist-get naviel--current-song :id)))) + (let* ((is-playing (and naviel--current-song + (eq type 'song) + (= idx naviel--queue-index) + (equal (plist-get item :id) + (plist-get naviel--current-song :id)))) (line (funcall format-fn item is-playing))) - (insert (propertize line 'naviel-item item 'naviel-type type 'naviel-idx idx 'face (when is-playing 'naviel-now-playing-face))) + (insert (propertize line + 'naviel-item item 'naviel-type type 'naviel-idx idx + 'face (when is-playing 'naviel-now-playing-face))) (insert "\n")) (cl-incf idx)))) (insert (propertize naviel--footer-sep 'invisible t)) @@ -569,37 +892,67 @@ (defun naviel--rerender-current-view () (when (and naviel--current-view (get-buffer "*Naviel*")) - (naviel--do-render (plist-get naviel--current-view :heading) (plist-get naviel--current-view :items) (plist-get naviel--current-view :format-fn) (plist-get naviel--current-view :type) (plist-get naviel--current-view :section-label)))) + (naviel--do-render + (plist-get naviel--current-view :heading) + (plist-get naviel--current-view :items) + (plist-get naviel--current-view :format-fn) + (plist-get naviel--current-view :type) + (plist-get naviel--current-view :section-label)))) (defun naviel--refresh-footer () (naviel--rerender-current-view) (naviel--update-mode-line)) ;;; Format callbacks + (defun naviel--format-artist (artist _p) - (format " %-40s %s" - (propertize (naviel--trunc (plist-get artist :name) 40) 'face 'naviel-artist-face) - (let ((n (plist-get artist :albums))) (if (and n (not (string= n ""))) (propertize (format " (%s)" n) 'face 'naviel-count-face) "")))) + (let* ((id (plist-get artist :id)) + (name (naviel--trunc (or (plist-get artist :name) "?") 44)) + (n (plist-get artist :albums))) + (format " %s %-44s %s" + (naviel--star-glyph id) + (propertize name 'face 'naviel-artist-face) + (if (and n (not (string= n ""))) + (propertize (format "(%s albums)" n) 'face 'naviel-count-face) + "")))) (defun naviel--format-album (album _p) - (format " %-40s %s" - (propertize (naviel--trunc (plist-get album :name) 40) 'face 'naviel-album-face) - (let ((y (plist-get album :year))) (if (and y (not (string= y ""))) (propertize y 'face 'naviel-year-face) "")))) + (let* ((id (plist-get album :id)) + (name (naviel--trunc (or (plist-get album :name) "?") 44)) + (y (plist-get album :year))) + (format " %s %-44s %s" + (naviel--star-glyph id) + (propertize name 'face 'naviel-album-face) + (if (and y (not (string= y ""))) + (propertize y 'face 'naviel-year-face) + "")))) (defun naviel--format-album-with-artist (album _p) - (format " %-38s │ %-28s %s" - (propertize (naviel--trunc (or (plist-get album :name) "?") 38) 'face 'naviel-album-face) - (let ((a (plist-get album :artist))) (if (and a (not (string= a ""))) (propertize (naviel--trunc a 28) 'face 'naviel-artist-face) "")) - (let ((y (plist-get album :year))) (if (and y (not (string= y ""))) (propertize (format " │ %s" y) 'face 'naviel-year-face) "")))) + (let* ((id (plist-get album :id)) + (name (naviel--trunc (or (plist-get album :name) "?") 36)) + (artist (naviel--trunc (or (plist-get album :artist) "") 26)) + (y (plist-get album :year))) + (format " %s %-36s %-26s %s" + (naviel--star-glyph id) + (propertize name 'face 'naviel-album-face) + (propertize artist 'face 'naviel-artist-face) + (if (and y (not (string= y ""))) + (propertize y 'face 'naviel-year-face) + "")))) (defun naviel--format-song (song playing) - (let* ((track (or (plist-get song :track) "")) - (title (naviel--trunc (or (plist-get song :title) "?") 38)) - (artist (naviel--trunc (or (plist-get song :artist) "") 26)) - (dur (naviel--format-secs (let ((d (plist-get song :duration))) (when (and d (not (string= d ""))) (string-to-number d)))))) - (format "%s %3s │ %-38s │ %-26s │ %s" - (if playing (propertize "▶" 'face 'naviel-now-playing-face) " ") - (propertize track 'face 'naviel-track-num-face) + (let* ((id (plist-get song :id)) + (track (naviel--trunc (or (plist-get song :track) "") 3)) + (title (naviel--trunc (or (plist-get song :title) "?") 36)) + (artist (naviel--trunc (or (plist-get song :artist) "") 24)) + (dur (naviel--format-secs + (let ((d (plist-get song :duration))) + (when (and d (not (string= d ""))) (string-to-number d)))))) + (format " %s %s %s %-36s %-24s %s" + (if playing (propertize "▶" 'face 'naviel-now-playing-face) + (propertize " " 'face 'naviel-separator-face)) + (naviel--star-glyph id) + (propertize (format "%3s" track) 'face 'naviel-track-num-face) (propertize title 'face (if playing 'naviel-now-playing-face 'naviel-song-title-face)) (propertize artist 'face 'naviel-song-meta-face) (propertize dur 'face 'naviel-song-meta-face)))) @@ -608,25 +961,53 @@ (let* ((name (naviel--trunc (or (plist-get pl :name) "?") 40)) (count (plist-get pl :count)) (owner (plist-get pl :owner))) - (format " %-40s %s %s" + (format " %-41s %-16s %s" (propertize name 'face 'naviel-album-face) - (if (and count (not (string= count ""))) (propertize (format " │ %s tracks" count) 'face 'naviel-count-face) "") - (if (and owner (not (string= owner ""))) (propertize (format " │ %s" owner) 'face 'naviel-song-meta-face) "")))) + (if (and count (not (string= count ""))) + (propertize (format "%s tracks" count) 'face 'naviel-count-face) + "") + (if (and owner (not (string= owner ""))) + (propertize owner 'face 'naviel-song-meta-face) + "")))) (defun naviel--format-genre (genre _p) - (let* ((name (naviel--trunc (or (plist-get genre :name) "?") 30)) - (songs (plist-get genre :songs)) - (albums (plist-get genre :albums))) - (format " %-30s │ %s" + (let* ((name (naviel--trunc (or (plist-get genre :name) "?") 32)) + (songs (or (plist-get genre :songs) "")) + (albums (or (plist-get genre :albums) ""))) + (format " %-32s %s" (propertize name 'face 'naviel-artist-face) - (if (and songs (not (string= songs ""))) (propertize (format "%s songs • %s albums" songs (or albums "?")) 'face 'naviel-count-face) "")))) + (if (not (string= songs "")) + (propertize (format "%s songs · %s albums" songs albums) 'face 'naviel-count-face) + "")))) + +(defun naviel--format-radio (station _p) + (let* ((name (naviel--trunc (or (plist-get station :name) "?") 44)) + (url (naviel--trunc (or (plist-get station :stream-url) "") 40))) + (format " %-44s %s" + (propertize name 'face 'naviel-radio-face) + (propertize url 'face 'naviel-song-meta-face)))) ;;; Render entry points -(defun naviel--render-artists (artists) (naviel--browser-render (format "Artists (%d)" (length artists)) artists #'naviel--format-artist 'artist)) -(defun naviel--render-albums (albums name) (naviel--browser-render name albums #'naviel--format-album 'album (format "%d album%s" (length albums) (if (= 1 (length albums)) "" "s")))) -(defun naviel--render-songs (songs context &optional section) (naviel--browser-render context songs #'naviel--format-song 'song (or section (format "%d track%s" (length songs) (if (= 1 (length songs)) "" "s"))))) + +(defun naviel--render-artists (artists) + (naviel--prime-star-cache artists :id :starred) + (naviel--browser-render (format "Artists (%d)" (length artists)) + artists #'naviel--format-artist 'artist)) + +(defun naviel--render-albums (albums name) + (naviel--prime-star-cache albums :id :starred) + (naviel--browser-render name albums #'naviel--format-album 'album + (format "%d album%s" (length albums) + (if (= 1 (length albums)) "" "s")))) + +(defun naviel--render-songs (songs context &optional section) + (naviel--prime-star-cache songs :id :starred) + (naviel--browser-render context songs #'naviel--format-song 'song + (or section (format "%d track%s" (length songs) + (if (= 1 (length songs)) "" "s"))))) ;;; Queue buffer + (defvar naviel-queue-mode-map (let ((m (make-sparse-keymap))) (define-key m (kbd "RET") #'naviel-queue-jump) @@ -638,6 +1019,7 @@ (define-key m (kbd "SPC") #'naviel--toggle-pause) (define-key m (kbd "s") #'naviel-queue-shuffle) (define-key m (kbd "l") #'naviel-lyrics-show) + (define-key m (kbd "*") #'naviel-toggle-star) (define-key m (kbd "q") #'quit-window) m)) @@ -647,30 +1029,50 @@ (hl-line-mode 1)) (defun naviel--queue-buffer () (get-buffer-create "*Naviel Queue*")) -(defun naviel--queue-render-if-live () (when (get-buffer "*Naviel Queue*") (naviel--queue-render))) + +(defun naviel--queue-render-if-live () + (when (get-buffer "*Naviel Queue*") (naviel--queue-render))) (defun naviel--queue-render () (with-current-buffer (naviel--queue-buffer) (naviel-queue-mode) (let ((inhibit-read-only t) (saved-line (line-number-at-pos))) (erase-buffer) - (insert (propertize (format " Queue (%d track%s)\n" (length naviel--queue) (if (= 1 (length naviel--queue)) "" "s")) 'face 'naviel-header-face)) - (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) + (insert (propertize + (format " Queue (%d track%s)\n" + (length naviel--queue) + (if (= 1 (length naviel--queue)) "" "s")) + 'face 'naviel-header-face)) + (insert (propertize (make-string (min 80 (window-width)) ?─) + 'face 'naviel-separator-face)) (insert "\n\n") (if (null naviel--queue) (insert (propertize " (queue is empty)\n" 'face 'shadow)) (let ((idx 0)) (dolist (song naviel--queue) - (let* ((playing (= idx naviel--queue-index)) - (marker (if playing (propertize "▶ " 'face 'naviel-now-playing-face) " ")) - (title (naviel--trunc (or (plist-get song :title) "?") 38)) - (artist (naviel--trunc (or (plist-get song :artist) "") 24)) - (dur (naviel--format-secs (let ((d (plist-get song :duration))) (when (and d (not (string= d ""))) (string-to-number d))))) - (line (format "%s%3d. │ %-38s │ %-24s │ %s\n" marker (1+ idx) title artist dur))) - (insert (propertize line 'face (when playing 'naviel-now-playing-face) 'naviel-queue-idx idx))) + (let* ((id (plist-get song :id)) + (playing (= idx naviel--queue-index)) + (state (if playing + (propertize "▶" 'face 'naviel-now-playing-face) + " ")) + (title (naviel--trunc (or (plist-get song :title) "?") 36)) + (artist (naviel--trunc (or (plist-get song :artist) "") 24)) + (dur (naviel--format-secs + (let ((d (plist-get song :duration))) + (when (and d (not (string= d ""))) (string-to-number d))))) + (line (format " %s %s %3d %-36s %-24s %s\n" + state (naviel--star-glyph id) + (1+ idx) title artist dur))) + (insert (propertize line + 'face (when playing 'naviel-now-playing-face) + 'naviel-queue-idx idx + 'naviel-item song + 'naviel-type 'song))) (cl-incf idx)))) (insert "\n") - (insert (propertize " RET jump d remove D clear u move-up s shuffle l lyrics q quit" 'face 'naviel-footer-face)) + (insert (propertize + " RET jump d remove D clear u move-up s shuffle * star l lyrics q quit" + 'face 'naviel-footer-face)) (insert "\n") (goto-char (point-min)) (forward-line (max 0 (- saved-line 1)))))) @@ -689,9 +1091,12 @@ (interactive) (let ((idx (get-text-property (point) 'naviel-queue-idx))) (unless idx (user-error "No track at point")) - (setq naviel--queue (append (seq-take naviel--queue idx) (seq-drop naviel--queue (1+ idx)))) - (when (and (> naviel--queue-index idx) (> naviel--queue-index 0)) (cl-decf naviel--queue-index)) - (when (>= naviel--queue-index (length naviel--queue)) (setq naviel--queue-index (max 0 (1- (length naviel--queue))))) + (setq naviel--queue (append (seq-take naviel--queue idx) + (seq-drop naviel--queue (1+ idx)))) + (when (and (> naviel--queue-index idx) (> naviel--queue-index 0)) + (cl-decf naviel--queue-index)) + (when (>= naviel--queue-index (length naviel--queue)) + (setq naviel--queue-index (max 0 (1- (length naviel--queue))))) (naviel--queue-render) (naviel--rerender-current-view))) @@ -715,11 +1120,44 @@ (naviel--queue-render) (forward-line -1))) +;;; Star / unstar + +(defun naviel-toggle-star () + "Toggle the star on the item at point (or currently playing song)." + (interactive) + (let* ((entry (cond + ((eq major-mode 'naviel-queue-mode) + (let ((idx (get-text-property (point) 'naviel-queue-idx))) + (when idx (cons 'song (nth idx naviel--queue))))) + (t (naviel--item-at-point)))) + (type (car entry)) + (item (cdr entry)) + (id (or (plist-get item :id) (plist-get item :name)))) + (unless (and id type (memq type '(song album artist))) + (user-error "naviel: no starrable item at point")) + (if (naviel--starred-p id) + (naviel--api-unstar id type + (lambda (ok) + (when ok + (naviel--star-cache-set id nil) + (naviel--rerender-current-view) + (naviel--queue-render-if-live) + (message "naviel: unstarred")))) + (naviel--api-star id type + (lambda (ok) + (when ok + (naviel--star-cache-set id t) + (naviel--rerender-current-view) + (naviel--queue-render-if-live) + (message "naviel: starred ★"))))))) + ;;; Browser mode + (defvar naviel-browser-mode-map (let ((m (make-sparse-keymap))) (define-key m (kbd "RET") #'naviel--browser-enter) (define-key m (kbd "a") #'naviel--browser-append) + (define-key m (kbd "*") #'naviel-toggle-star) (define-key m (kbd "SPC") #'naviel--toggle-pause) (define-key m (kbd "n") #'naviel--next-in-queue) (define-key m (kbd "p") #'naviel--prev-in-queue) @@ -734,7 +1172,7 @@ (define-key m (kbd "g") #'naviel--browser-refresh) (define-key m (kbd "l") #'naviel-lyrics-show) (define-key m (kbd "q") #'naviel--browser-quit) - (define-key m (kbd "?") #'naviel-help) + (define-key m (kbd "?") #'naviel-dispatch) (define-key m (kbd "R") #'naviel-toggle-repeat) (define-key m (kbd "Q") #'naviel-queue-show) (define-key m (kbd "S") #'naviel-queue-shuffle) @@ -742,6 +1180,8 @@ (define-key m (kbd "N") #'naviel-recently-added) (define-key m (kbd "H") #'naviel-recently-played) (define-key m (kbd "G") #'naviel-genres) + (define-key m (kbd "F") #'naviel-starred-view) + (define-key m (kbd "I") #'naviel-radio) m)) (define-derived-mode naviel-browser-mode special-mode "Naviel" @@ -764,25 +1204,55 @@ ('artist (let ((id (plist-get item :id)) (name (plist-get item :name))) (push `(:type artist :data ,id :label ,name) naviel--browser-stack) - (message "naviel: loading albums...") - (naviel--get-artist id (lambda (albums) (if albums (naviel--render-albums albums name) (pop naviel--browser-stack)))))) + (message "naviel: loading albums…") + (naviel--get-artist + id (lambda (albums) + (if albums (naviel--render-albums albums name) + (pop naviel--browser-stack)))))) ('album (let ((id (plist-get item :id)) (name (plist-get item :name))) (push `(:type album :data ,id :label ,name) naviel--browser-stack) - (message "naviel: loading tracks...") - (naviel--get-album id (lambda (songs) (if songs (progn (setq naviel--queue songs naviel--queue-index 0) (naviel--render-songs songs name)) (pop naviel--browser-stack)))))) + (message "naviel: loading tracks…") + (naviel--get-album + id (lambda (songs) + (if songs + (progn (setq naviel--queue songs naviel--queue-index 0) + (naviel--render-songs songs name)) + (pop naviel--browser-stack)))))) ('playlist (let ((id (plist-get item :id)) (name (plist-get item :name))) (push `(:type playlist :data ,id :label ,name) naviel--browser-stack) - (message "naviel: loading playlist...") - (naviel--get-playlist id (lambda (songs) (if songs (progn (setq naviel--queue songs naviel--queue-index 0) (naviel--render-songs songs name)) (pop naviel--browser-stack)))))) + (message "naviel: loading playlist…") + (naviel--get-playlist + id (lambda (songs) + (if songs + (progn (setq naviel--queue songs naviel--queue-index 0) + (naviel--render-songs songs name)) + (pop naviel--browser-stack)))))) ('genre (let* ((name (plist-get item :name)) - (count (min naviel-genre-song-limit (string-to-number (or (plist-get item :songs) "50"))))) + (count (min naviel-genre-song-limit + (string-to-number (or (plist-get item :songs) "50"))))) (push `(:type genre :data ,name :label ,name) naviel--browser-stack) - (message "naviel: loading songs for genre \"%s\"..." name) - (naviel--get-songs-by-genre name count 0 (lambda (songs) (if songs (progn (setq naviel--queue songs naviel--queue-index 0) (naviel--render-songs songs (format "Genre: %s" name) (format "%d tracks" (length songs)))) (pop naviel--browser-stack) (message "naviel: no songs found for genre \"%s\"" name)))))) - ('song (naviel--play-queue-index (get-text-property (point) 'naviel-idx))) + (message "naviel: loading genre "%s"…" name) + (naviel--get-songs-by-genre + name count 0 + (lambda (songs) + (if songs + (progn (setq naviel--queue songs naviel--queue-index 0) + (naviel--render-songs songs (format "Genre: %s" name) + (format "%d tracks" (length songs)))) + (pop naviel--browser-stack) + (message "naviel: no songs for "%s"" name)))))) + ('radio + ;; Play radio station directly + (let* ((name (plist-get item :name)) + (url (plist-get item :stream-url)) + (pseudo-song `(:id ,url :title ,name :artist "Internet Radio" + :album "" :duration ""))) + (naviel--play-url url pseudo-song))) + ('song + (naviel--play-queue-index (get-text-property (point) 'naviel-idx))) (_ (message "naviel: unknown type: %s" type)))))) (defun naviel--browser-append () @@ -791,122 +1261,280 @@ (unless entry (user-error "No item at point")) (let ((type (car entry)) (item (cdr entry))) (pcase type - ('album (naviel--get-album (plist-get item :id) (lambda (songs) (when songs (naviel--append-songs-to-queue songs (plist-get item :name)))))) - ('playlist (naviel--get-playlist (plist-get item :id) (lambda (songs) (when songs (naviel--append-songs-to-queue songs (plist-get item :name)))))) - ('song (naviel--append-songs-to-queue (list item) (or (plist-get item :title) "?"))) + ('album + (naviel--get-album + (plist-get item :id) + (lambda (songs) (when songs (naviel--append-songs-to-queue songs (plist-get item :name)))))) + ('playlist + (naviel--get-playlist + (plist-get item :id) + (lambda (songs) (when songs (naviel--append-songs-to-queue songs (plist-get item :name)))))) + ('song + (naviel--append-songs-to-queue (list item) (or (plist-get item :title) "?"))) (_ (message "naviel: cannot append type: %s" type)))))) (defun naviel--browser-back () (interactive) (cond - ((null naviel--browser-stack) (message "naviel: already at top level")) - ((= 1 (length naviel--browser-stack)) (setq naviel--browser-stack '()) (naviel--show-artists)) + ((null naviel--browser-stack) + (message "naviel: already at top level")) + ((= 1 (length naviel--browser-stack)) + (setq naviel--browser-stack '()) + (naviel--show-artists)) (t (pop naviel--browser-stack) - (let* ((frame (car naviel--browser-stack)) (type (plist-get frame :type)) (id (plist-get frame :data)) (name (plist-get frame :label))) + (let* ((frame (car naviel--browser-stack)) + (type (plist-get frame :type)) + (id (plist-get frame :data)) + (name (plist-get frame :label))) (pcase type - ('artist (naviel--get-artist id (lambda (a) (when a (naviel--render-albums a name))))) - ('album (naviel--get-album id (lambda (s) (when s (naviel--render-songs s name))))) + ('artist (naviel--get-artist id (lambda (a) (when a (naviel--render-albums a name))))) + ('album (naviel--get-album id (lambda (s) (when s (naviel--render-songs s name))))) ('playlist (naviel--get-playlist id (lambda (s) (when s (naviel--render-songs s name))))) - ('genre (naviel--get-songs-by-genre id naviel-genre-song-limit 0 (lambda (s) (when s (naviel--render-songs s (format "Genre: %s" id)))))) + ('genre (naviel--get-songs-by-genre + id naviel-genre-song-limit 0 + (lambda (s) (when s (naviel--render-songs s (format "Genre: %s" id)))))) (_ (naviel--show-artists))))))) (defun naviel--browser-refresh (&rest _) (interactive) (if (null naviel--browser-stack) (naviel--show-artists) - (let* ((frame (car naviel--browser-stack)) (type (plist-get frame :type)) (id (plist-get frame :data)) (name (plist-get frame :label))) + (let* ((frame (car naviel--browser-stack)) + (type (plist-get frame :type)) + (id (plist-get frame :data)) + (name (plist-get frame :label))) (pcase type ('artist (naviel--get-artist id (lambda (a) (when a (naviel--render-albums a name))))) ('album (naviel--get-album id (lambda (s) (when s (naviel--render-songs s name))))) ('playlist-list (naviel-playlists)) ('playlist (naviel--get-playlist id (lambda (s) (when s (naviel--render-songs s name))))) ('genre-list (naviel-genres)) - ('genre (naviel--get-songs-by-genre id naviel-genre-song-limit 0 (lambda (s) (when s (naviel--render-songs s (format "Genre: %s" id)))))) - ('album-list2 (naviel--get-album-list2 id naviel-album-list-size (lambda (als) (when als (naviel--browser-render name als #'naviel--format-album-with-artist 'album))))) + ('genre (naviel--get-songs-by-genre + id naviel-genre-song-limit 0 + (lambda (s) (when s (naviel--render-songs s (format "Genre: %s" id)))))) + ('album-list2 (naviel--get-album-list2 + id naviel-album-list-size + (lambda (als) (when als (naviel--browser-render + name als + #'naviel--format-album-with-artist 'album))))) + ('starred (naviel-starred-view)) + ('radio (naviel-radio)) (_ (naviel--show-artists)))))) (defun naviel--browser-quit () (interactive) (quit-window)) (defun naviel--show-artists () - (message "naviel: loading library...") + (message "naviel: loading library…") (naviel--get-artists (lambda (artists) - (when artists (setq naviel--browser-stack '()) (naviel--render-artists artists))))) + (when artists + (setq naviel--browser-stack '()) + (naviel--render-artists artists))))) ;;; Mode line + (defun naviel--mode-line-string () (if naviel--current-song - (format " [%s] %s%s" (if naviel--paused "paused" "playing") (naviel--trunc (plist-get naviel--current-song :title) 28) (if (and naviel--elapsed naviel--duration) (format " %s/%s" (naviel--format-secs naviel--elapsed) (naviel--format-secs naviel--duration)) "")) "")) + (format " %s %s%s" + (if naviel--paused "⏸" "▶") + (naviel--trunc (plist-get naviel--current-song :title) 28) + (if (and naviel--elapsed naviel--duration) + (format " %s/%s" + (naviel--format-secs naviel--elapsed) + (naviel--format-secs naviel--duration)) + "")) + "")) (defun naviel--update-mode-line () (force-mode-line-update t)) -;;; Public commands +;;; Transient menu + +(transient-define-prefix naviel-dispatch () + "Naviel command menu." + [:class transient-columns + ["Playback" + ("SPC" "Pause / resume" naviel--toggle-pause) + ("n" "Next track" naviel--next-in-queue) + ("p" "Previous track" naviel--prev-in-queue) + ("[" "Seek back" naviel-seek-backward) + ("]" "Seek forward" naviel-seek-forward) + ("+" "Volume up" naviel--volume-up) + ("-" "Volume down" naviel--volume-down) + ("R" "Cycle repeat" naviel-toggle-repeat)] + ["Library" + ("s" "Search" naviel-search) + ("r" "Random" naviel-play-random) + ("P" "Playlists" naviel-playlists) + ("N" "Recently added" naviel-recently-added) + ("H" "Recently played" naviel-recently-played) + ("G" "Genres" naviel-genres) + ("F" "Starred" naviel-starred-view) + ("I" "Internet radio" naviel-radio)] + ["Item / Queue" + ("RET" "Play / expand" naviel--browser-enter) + ("a" "Append to queue" naviel--browser-append) + ("*" "Toggle star" naviel-toggle-star) + ("l" "Show lyrics" naviel-lyrics-show) + ("Q" "Queue window" naviel-queue-show) + ("S" "Shuffle queue" naviel-queue-shuffle) + ("b" "Back" naviel--browser-back) + ("g" "Refresh" naviel--browser-refresh)]]) + +;;; Public commands + ;;;###autoload (defun naviel () + "Open the Naviel music browser." (interactive) - (unless (executable-find naviel-mpv-executable) (error "naviel: mpv not found on PATH")) + (unless (executable-find naviel-mpv-executable) + (error "naviel: mpv not found on PATH")) (pop-to-buffer (naviel--browser-buffer)) (naviel--show-artists)) ;;;###autoload (defun naviel-lyrics-show () - "Fetch and display lyrics for the song at point, or the currently playing song." + "Fetch and display lyrics for the song at point or currently playing." (interactive) - (let* ((entry (if (eq major-mode 'naviel-queue-mode) - (let ((idx (get-text-property (point) 'naviel-queue-idx))) - (if idx (cons 'song (nth idx naviel--queue)) nil)) - (naviel--item-at-point))) - (type (and entry (car entry))) - (item (and entry (cdr entry))) - (song (if (eq type 'song) item naviel--current-song))) - (if (not song) - (user-error "naviel: No song playing or at point") - (let ((artist (plist-get song :artist)) - (title (plist-get song :title))) - (message "naviel: fetching lyrics for \"%s\"..." title) - (naviel--get-lyrics - artist title - (lambda (lyrics) - (with-current-buffer (get-buffer-create "*Naviel Lyrics*") - (let ((inhibit-read-only t)) - (erase-buffer) - (special-mode) - (local-set-key (kbd "q") #'quit-window) - (insert (propertize (format " %s • %s\n" title artist) 'face 'naviel-header-face)) - (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) - (insert "\n\n ") - (insert (replace-regexp-in-string "\n" "\n " (or lyrics "No lyrics found for this track."))) - (insert "\n\n") - (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) - (insert "\n") - (insert (propertize " Press 'q' to close." 'face 'naviel-footer-face)) - (goto-char (point-min)))) - (pop-to-buffer "*Naviel Lyrics*"))))))) + (let* ((entry (cond + ((eq major-mode 'naviel-queue-mode) + (let ((idx (get-text-property (point) 'naviel-queue-idx))) + (when idx (cons 'song (nth idx naviel--queue))))) + (t (naviel--item-at-point)))) + (type (car entry)) + (item (cdr entry)) + (song (if (eq type 'song) item naviel--current-song))) + (unless song (user-error "naviel: no song at point or currently playing")) + (let ((artist (or (plist-get song :artist) "")) + (title (or (plist-get song :title) ""))) + ;; Fixed: Added backslashes to escape quotes around %s + (message "naviel: fetching lyrics for \"%s\"…" title) + (naviel--get-lyrics + artist title + (lambda (lyrics) + (with-current-buffer (get-buffer-create "*Naviel Lyrics*") + (let ((inhibit-read-only t)) + (erase-buffer) + (special-mode) + (local-set-key (kbd "q") #'quit-window) + (insert (propertize (format " %s\n" title) 'face 'naviel-header-face)) + (insert (propertize (format " %s\n" artist) 'face 'naviel-artist-face)) + (insert (propertize (make-string (min 80 (window-width)) ?─) + 'face 'naviel-separator-face)) + (insert "\n\n") + (if lyrics + (progn + (insert (propertize " " 'face 'default)) + (insert (replace-regexp-in-string "\n" "\n " lyrics))) + (insert (propertize " No lyrics found for this track." 'face 'shadow))) + (insert "\n\n") + (insert (propertize " q close" 'face 'naviel-footer-face)) + (insert "\n") + (goto-char (point-min)))) + (pop-to-buffer "*Naviel Lyrics*")))))) +;;;###autoload +(defun naviel-starred-view () + "Show all starred artists, albums, and songs." + (interactive) + (message "naviel: loading starred items…") + (naviel--get-starred2 + (lambda (results) + (if (null results) + (message "naviel: nothing starred yet") + (let ((artists (plist-get results :artists)) + (albums (plist-get results :albums)) + (songs (plist-get results :songs))) + ;; Pre-populate star cache + (dolist (a artists) (naviel--star-cache-set (plist-get a :id) t)) + (dolist (al albums) (naviel--star-cache-set (plist-get al :id) t)) + (dolist (s songs) (naviel--star-cache-set (plist-get s :id) t)) + (push `(:type starred :data nil :label "Starred") naviel--browser-stack) + (pop-to-buffer (naviel--browser-buffer)) + (naviel-browser-mode) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (naviel--make-breadcrumbs)) + (insert (propertize " ★ Starred\n" 'face 'naviel-header-face)) + (insert (propertize (make-string (min 80 (window-width)) ?─) + 'face 'naviel-separator-face)) + (insert "\n") + (cl-flet ((section (label items type fmt-fn) + (when items + (insert (propertize (format "\n %s\n" label) + 'face 'naviel-breadcrumb-face)) + (let ((idx 0)) + (dolist (item items) + (insert (propertize (funcall fmt-fn item nil) + 'naviel-item item + 'naviel-type type + 'naviel-idx idx)) + (insert "\n") + (cl-incf idx)))))) + (section "Artists" artists 'artist #'naviel--format-artist) + (section "Albums" albums 'album #'naviel--format-album-with-artist) + (when songs + (insert (propertize "\n Songs\n" 'face 'naviel-breadcrumb-face)) + (setq naviel--queue songs naviel--queue-index 0) + (let ((idx 0)) + (dolist (s songs) + (insert (propertize (naviel--format-song s nil) + 'naviel-item s 'naviel-type 'song 'naviel-idx idx)) + (insert "\n") + (cl-incf idx)))) + (when (and (null artists) (null albums) (null songs)) + (insert (propertize " Nothing starred yet.\n" 'face 'shadow)))) + (insert (propertize naviel--footer-sep 'invisible t)) + (insert (naviel--now-playing-bar)) + (goto-char (point-min)) + (forward-line 3))))))) + +;;;###autoload +(defun naviel-radio () + "Browse and play internet radio stations." + (interactive) + (message "naviel: loading radio stations…") + (naviel--get-radio-stations + (lambda (stations) + (if (null stations) + (message "naviel: no internet radio stations configured on this server") + (push `(:type radio :data nil :label "Internet Radio") naviel--browser-stack) + (naviel--browser-render + (format "Internet Radio (%d stations)" (length stations)) + stations #'naviel--format-radio 'radio))))) ;;;###autoload (defun naviel-search (query) (interactive "sSearch Naviel: ") (when (string= query "") (user-error "Search query cannot be empty")) - (message "naviel: searching for \"%s\"..." query) + (message "naviel: searching…") (naviel--search-async query (lambda (results) (when results - (let ((artists (plist-get results :artists)) (albums (plist-get results :albums)) (songs (plist-get results :songs))) + (let ((artists (plist-get results :artists)) + (albums (plist-get results :albums)) + (songs (plist-get results :songs))) + ;; Prime star cache from results + (naviel--prime-star-cache artists :id :starred) + (naviel--prime-star-cache albums :id :starred) + (naviel--prime-star-cache songs :id :starred) (pop-to-buffer (naviel--browser-buffer)) (naviel-browser-mode) (let ((inhibit-read-only t)) (erase-buffer) (insert (propertize (format " Search: %s\n" query) 'face 'naviel-header-face)) - (insert (propertize (make-string (min 80 (window-width)) ?─) 'face 'naviel-separator-face)) + (insert (propertize (make-string (min 80 (window-width)) ?─) + 'face 'naviel-separator-face)) (insert "\n") (cl-flet ((section (label items type fmt-fn) (when items - (insert (propertize (format "\n %s\n" label) 'face 'naviel-breadcrumb-face)) + (insert (propertize (format "\n %s\n" label) + 'face 'naviel-breadcrumb-face)) (let ((idx 0)) (dolist (item items) - (insert (propertize (funcall fmt-fn item nil) 'naviel-item item 'naviel-type type 'naviel-idx idx)) + (insert (propertize (funcall fmt-fn item nil) + 'naviel-item item 'naviel-type type + 'naviel-idx idx)) (insert "\n") (cl-incf idx)))))) (section "Artists" artists 'artist #'naviel--format-artist) (section "Albums" albums 'album #'naviel--format-album) @@ -915,9 +1543,11 @@ (setq naviel--queue songs naviel--queue-index 0) (let ((idx 0)) (dolist (s songs) - (insert (propertize (naviel--format-song s (and naviel--current-song (equal (plist-get s :id) (plist-get naviel--current-song :id)))) 'naviel-item s 'naviel-type 'song 'naviel-idx idx)) + (insert (propertize (naviel--format-song s nil) + 'naviel-item s 'naviel-type 'song 'naviel-idx idx)) (insert "\n") (cl-incf idx))) - (message "naviel: %d song%s in queue" (length songs) (if (= 1 (length songs)) "" "s"))) + (message "naviel: %d song%s in queue" + (length songs) (if (= 1 (length songs)) "" "s"))) (when (and (null artists) (null albums) (null songs)) (insert (propertize " No results found.\n" 'face 'shadow)))) (insert (propertize naviel--footer-sep 'invisible t)) @@ -929,10 +1559,14 @@ (defun naviel-play-random (&optional count) (interactive "P") (let ((n (if count (prefix-numeric-value count) 20))) - (message "naviel: fetching %d random tracks..." n) + (message "naviel: fetching %d random tracks…" n) (naviel--get-random-songs n (lambda (songs) - (when songs (setq naviel--queue songs naviel--queue-index 0) (pop-to-buffer (naviel--browser-buffer)) (naviel--render-songs songs (format "Random - %d tracks" n)) (naviel--play-queue-index 0)))))) + (when songs + (setq naviel--queue songs naviel--queue-index 0) + (pop-to-buffer (naviel--browser-buffer)) + (naviel--render-songs songs (format "Random – %d tracks" n)) + (naviel--play-queue-index 0)))))) ;;;###autoload (defun naviel-stop () @@ -943,77 +1577,70 @@ ;;;###autoload (defun naviel-playlists () (interactive) - (message "naviel: loading playlists...") + (message "naviel: loading playlists…") (naviel--get-playlists (lambda (pls) (if (null pls) (message "naviel: no playlists found") (push `(:type playlist-list :data nil :label "Playlists") naviel--browser-stack) - (naviel--browser-render (format "Playlists (%d)" (length pls)) pls #'naviel--format-playlist 'playlist))))) + (naviel--browser-render (format "Playlists (%d)" (length pls)) + pls #'naviel--format-playlist 'playlist))))) ;;;###autoload (defun naviel-recently-added (&optional size) (interactive "P") (let ((n (if size (prefix-numeric-value size) naviel-album-list-size))) - (message "naviel: fetching recently added albums...") - (naviel--get-album-list2 "newest" n (lambda (albums) (if (null albums) (message "naviel: no results") (push `(:type album-list2 :data "newest" :label "Recently Added") naviel--browser-stack) (naviel--browser-render (format "Recently Added (%d)" (length albums)) albums #'naviel--format-album-with-artist 'album)))))) + (message "naviel: fetching recently added…") + (naviel--get-album-list2 + "newest" n + (lambda (albums) + (if (null albums) (message "naviel: no results") + (push `(:type album-list2 :data "newest" :label "Recently Added") naviel--browser-stack) + (naviel--browser-render (format "Recently Added (%d)" (length albums)) + albums #'naviel--format-album-with-artist 'album)))))) ;;;###autoload (defun naviel-recently-played (&optional size) (interactive "P") (let ((n (if size (prefix-numeric-value size) naviel-album-list-size))) - (message "naviel: fetching recently played albums...") - (naviel--get-album-list2 "recent" n (lambda (albums) (if (null albums) (message "naviel: no results") (push `(:type album-list2 :data "recent" :label "Recently Played") naviel--browser-stack) (naviel--browser-render (format "Recently Played (%d)" (length albums)) albums #'naviel--format-album-with-artist 'album)))))) + (message "naviel: fetching recently played…") + (naviel--get-album-list2 + "recent" n + (lambda (albums) + (if (null albums) (message "naviel: no results") + (push `(:type album-list2 :data "recent" :label "Recently Played") naviel--browser-stack) + (naviel--browser-render (format "Recently Played (%d)" (length albums)) + albums #'naviel--format-album-with-artist 'album)))))) + +;;;###autoload +(defun naviel-starred-albums (&optional size) + "Show up to SIZE starred albums via getAlbumList2." + (interactive "P") + (let ((n (if size (prefix-numeric-value size) naviel-album-list-size))) + (naviel--get-album-list2 + "starred" n + (lambda (albums) + (if (null albums) (message "naviel: no starred albums") + (push `(:type album-list2 :data "starred" :label "Starred Albums") naviel--browser-stack) + (naviel--browser-render (format "Starred Albums (%d)" (length albums)) + albums #'naviel--format-album-with-artist 'album)))))) ;;;###autoload (defun naviel-genres () (interactive) - (message "naviel: loading genres...") + (message "naviel: loading genres…") (naviel--get-genres (lambda (genres) (if (null genres) (message "naviel: no genres found") - (setq genres (sort genres (lambda (a b) (> (string-to-number (or (plist-get a :songs) "0")) (string-to-number (or (plist-get b :songs) "0")))))) + (setq genres + (sort genres (lambda (a b) + (> (string-to-number (or (plist-get a :songs) "0")) + (string-to-number (or (plist-get b :songs) "0")))))) (push `(:type genre-list :data nil :label "Genres") naviel--browser-stack) - (naviel--browser-render (format "Genres (%d)" (length genres)) genres #'naviel--format-genre 'genre))))) - -;;;###autoload -(defun naviel-help () - (interactive) - (with-help-window "*Naviel Help*" - (princ "Naviel - Navidrome client for Emacs\n") - (princ (make-string 52 ?─)) (princ "\n\n") - (princ "Navigation\n") - (princ " RET Play item / expand artist, album, playlist, or genre\n") - (princ " a Append item to queue without replacing it\n") - (princ " l Fetch and show lyrics for song at point\n") - (princ " b Go back one level\n") - (princ " g Refresh current view from server\n") - (princ " s Search library\n") - (princ " r Play random (C-u r for custom count)\n") - (princ " q Quit browser\n\n") - (princ "Library views\n") - (princ " P Browse playlists\n") - (princ " N Recently added albums (C-u N for custom count)\n") - (princ " H Recently played albums (C-u H for custom count)\n") - (princ " G Browse genres\n\n") - (princ "Playback\n") - (princ " SPC Toggle pause / resume\n") - (princ " n Next track\n") - (princ " p Previous track\n") - (princ " [ Seek backward (naviel-seek-step seconds)\n") - (princ " ] Seek forward (naviel-seek-step seconds)\n") - (princ " + = Volume up\n") - (princ " - Volume down\n") - (princ " R Cycle repeat: off -> one -> all -> off\n\n") - (princ "Queue\n") - (princ " Q Toggle queue window\n") - (princ " S Shuffle queue in place\n\n") - (princ "Queue window\n") - (princ " RET Jump to track\n") - (princ " d Remove track from queue\n") - (princ " D Clear entire queue\n") - (princ " u Move track up one position\n\n"))) + (naviel--browser-render (format "Genres (%d)" (length genres)) + genres #'naviel--format-genre 'genre))))) ;;; Mode line integration + (defvar naviel-mode-line-format '(:eval (naviel--mode-line-string))) ;;;###autoload -- cgit v1.2.3