summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-04-05 20:33:57 +0200
committerDavid Moc <personal@cdatgoose.org>2026-04-05 20:33:57 +0200
commit0a924627e83335695725b3797c74fa82fccba3f9 (patch)
tree70ef1071cdafb3e0659c70d88c4878c4d11b269c
parentf2d36a2e6ebf3b7d6cee6714481726b6f2ad4e3d (diff)
Changed the volume, adding coverart, added starring and radio.
Signed-off-by: David Moc <personal@cdatgoose.org>
-rwxr-xr-xnaviel.el1237
1 files changed, 932 insertions, 305 deletions
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