branch: externals-release/listen
commit a3b90ed6d94a237c0f5b60b5345933b3aa19b1e8
Merge: 647302eb98 e0a243d3c6
Author: Adam Porter <[email protected]>
Commit: Adam Porter <[email protected]>

    Release: v0.10
---
 README.org      |  67 +++++-------
 docs/README.org |  30 ++++--
 listen-info.el  |  36 ++-----
 listen-lib.el   | 139 +++++++++++++++++++++++--
 listen-mpd.el   |   2 +-
 listen-mpv.el   | 313 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 listen-queue.el | 259 +++++++++++++++++++++++++++++++---------------
 listen-vlc.el   |  24 +++--
 listen.el       | 183 +++++++++++++++++++++++++--------
 9 files changed, 838 insertions(+), 215 deletions(-)

diff --git a/README.org b/README.org
index 86b4769056..80be6676d5 100644
--- a/README.org
+++ b/README.org
@@ -5,11 +5,11 @@
 #+texinfo_dir_title: Listen: (listen)
 #+texinfo_dir_desc: Audio/Music player
 
-[[https://elpa.gnu.org/packages/listen.html][file:https://elpa.gnu.org/packages/listen.svg]]
+[[https://elpa.gnu.org/packages/listen.html][file:https://elpa.gnu.org/packages/listen.svg]]
 
[[https://elpa.gnu.org/devel/listen.html][file:https://elpa.gnu.org/devel/listen.svg]]
 
 This package aims to provide a simple audio/music player for Emacs.  It should 
"just work," with little-to-no configuration, have intuitive commands, and be 
easily extended and customized.  (Contrast to setting up EMMS, or having to 
configure external players like MPD.)  A Transient menu, under the command 
~listen~, is the primary entry point.
 
-The only external dependency is VLC, which is currently the only player 
backend that is supported.  (Other backends may easily be added; see library 
~listen-vlc~ for example.)  Track metadata is read using EMMS's native Elisp 
metadata library, which has been imported into this package.
+The only external dependency is either [[https://mpv.io/][MPV]] or 
[[https://www.videolan.org/vlc/][VLC]], which are the supported player 
backends.  Track metadata is read using EMMS's native Elisp metadata library, 
which has been imported into this package.
 
 Queues are provided as the means to play consecutive tracks, and they are 
shown in a ~vtable~-based view buffer.  They are persisted between sessions 
using the ~persist~ library, and they may be bookmarked.
 
@@ -18,7 +18,6 @@ The primary interface to one's music library is through the 
filesystem, by selec
 A simple "library" view is provided that shows a list of files organized into 
a hierarchy by genre, date, artist, album, etc.  (This will be made more 
configurable and useful in the future.)
 
 Note a silly limitation: a track may be present in a queue only once (but who 
would want to have a track more than once in a playlist).
-
 * Contents
 :CONTENTS:
 - [[#screenshots][Screenshots]]
@@ -28,29 +27,25 @@ Note a silly limitation: a track may be present in a queue 
only once (but who wo
 - [[#changelog][Changelog]]
 - [[#development][Development]]
 :END:
-
 * Screenshots
 
 [[file:images/screenshot-modus-vivendi-tinted.png]]
-
 * Installation
 
 *Requirements:*
 - Emacs version 29.1 or later.
-- [[https://www.videolan.org/vlc/][VLC]]: used to play audio.
+- [[https://mpv.io/][MPV]] or [[https://www.videolan.org/vlc/][VLC]]: used to 
play audio.
 - Optional: ~ffprobe~ (part of [[https://ffmpeg.org/ffprobe.html][FFmpeg]]) is 
used to read tracks' duration when available.
-
 ** GNU ELPA
 
 Listen.el is published in [[http://elpa.gnu.org/][GNU ELPA]] as 
[[https://elpa.gnu.org/packages/listen.html][listen]], so it may be installed 
in Emacs with the command ~M-x package-install RET listen RET~.  This is the 
recommended way to install Listen.el, as it will install the current stable 
release.
 
 The latest development build may be installed from 
[[https://elpa.gnu.org/devel/listen.html][ELPA-devel]] or from Git (see below).
-
 ** Git
 
 The ~master~ branch of the Git repository is intended to be usable at all 
times; only minor bugs are expected to be found in it before a new stable 
release is made.
 
-To install, it is recommended to use 
[[https://github.com/quelpa/quelpa-use-package][quelpa-use-package]], like this 
(using 
[[https://github.com/alphapapa/unpackaged.el#upgrade-a-quelpa-use-package-forms-package][this
 helpful command]] for upgrading versions):
+To install from Git, it is recommended to use 
[[https://github.com/quelpa/quelpa-use-package][quelpa-use-package]], like this 
(using 
[[https://github.com/alphapapa/unpackaged.el#upgrade-a-quelpa-use-package-forms-package][this
 helpful command]] for upgrading versions):
 
 #+begin_src elisp
   ;; Install and load `quelpa-use-package'.
@@ -63,11 +58,9 @@ To install, it is recommended to use 
[[https://github.com/quelpa/quelpa-use-pack
 #+end_src
 
 One might also use systems like 
[[https://github.com/progfolio/elpaca][Elpaca]] or 
[[https://github.com/radian-software/straight.el][Straight]] (which is also 
used by [[https://github.com/doomemacs/doomemacs][DOOM]]), but the author 
cannot offer support for them.
-
 * Configuration
 
 Listen is intended to work with little-to-no configuration.  You can set the 
~listen-directory~ to the location of your music library if it's not at 
~~/Music~.  See ~M-x customize-group RET listen RET~.
-
 * Usage
 Use the command ~listen~ to show the Transient menu.  From there, it 
is--hopefully--self-explanatory.  Please feel free to give feedback if it 
doesn't seem so.  For more information, see the following sections.
 
@@ -78,7 +71,6 @@ Use the command ~listen~ to show the Transient menu.  From 
there, it is--hopeful
 - [[#mode][Mode]]
 - [[#tips][Tips]]
 :END:
-
 ** Queues
 
 While ~listen~ can simply play one track and stop, playing multiple tracks 
sequentially is provided by /queues/ (what other players may call /playlists/). 
 A queue is a list of tracks, each of which is backed by a file on disk, and 
which may have associated metadata (provided by reading the file in Emacs with 
the ~listen-info~ library, or from an external source, like an MPD server).
@@ -88,7 +80,6 @@ Queues are automatically persisted to disk in the variable 
~listen-queues~.
 A new, empty queue may be made with the command ~listen-queue-new~, but it's 
usually more convenient to use a command that adds tracks to a queue and enter 
a new queue name.
 
 A queue's tracks may be de-duplicated using the command 
~listen-queue-deduplicate~.  Tracks that appear to have the same metadata 
(artist, album, and title, compared case-insensitively) are de-duplicated.  
Also, any tracks no longer backed by a file are removed.
-
 *** Adding tracks to a queue
 
 Tracks can be added to a queue from various sources using these commands:
@@ -96,11 +87,9 @@ Tracks can be added to a queue from various sources using 
these commands:
 - Files and directories: ~listen-queue-add-files~.  Individual files may be 
chosen, or a directory may be, which will be searched recursively for tracks, 
which are added to the selected queue.
 - From an MPD server: ~listen-queue-add-from-mpd~.  An MPD search query will 
be read with completion, and matching tracks are added to the selected queue.
 - From a playlist file: ~listen-queue-add-from-playlist-file~.  The playlist 
file is read, and its tracks are added to the selected queue.
-
 *** Queue buffer
 
 A queue may be shown in a buffer with the command ~listen-queue~, which shows 
its tracks in a [[info:vtable#Introduction][vtable]] with columns for metadata 
and filename.
-
 **** Commands
 
 In the buffer, you can use these commands:
@@ -125,11 +114,9 @@ In the buffer, you can use these commands:
 | Revert queue's tracks from disk | ~C-u g~                     |
 | Pause the player                | ~listen-pause~ (~SPC~)      |
 | Show the menu                   | ~listen-menu~ (~?~)         |
-
 **** Bookmarks
 
 Queue buffers may be bookmarked with ~bookmark-set~ (~C-x r m~).  The bookmark 
record refers to the queue by name, so if the queue is renamed or discarded, 
the bookmark will remain.
-
 *** Queue list buffer
 
 The queue list buffer may be shown with the command ~listen-queue-list~.  In 
the list buffer, you can use these commands:
@@ -144,7 +131,6 @@ The queue list buffer may be shown with the command 
~listen-queue-list~.  In the
 | Revert the queue list | ~listen-queue-list~ (~g~) |
 | Pause the player      | ~listen-pause~ (~SPC~)    |
 | Show the menu         | ~listen-menu~ (~?~)       |
-
 ** Library
 
 To help with exploring and managing a music library, ~listen~ provides various 
"library" features.  Tracks can be passed between library and queue buffers and 
operated on with similar commands and bindings.
@@ -152,11 +138,9 @@ To help with exploring and managing a music library, 
~listen~ provides various "
 ~listen~ does not maintain its own database of audio files; they are simply 
read from the filesystem as needed.  But if a local MPD server is available, 
tracks can be loaded from its database (which does a fine job of indexing audio 
files and their metadata); this is generally much faster, because it avoids 
having to read tracks' metadata with Emacs Lisp or their durations with 
~ffprobe~.
 
 ~listen~ does not provide features to modify tracks' metadata, but it provides 
commands to run shell commands on tracks' filenames, which works well with 
external tools like [[https://picard.musicbrainz.org/][Picard]].
-
 *** Library buffer
 
 A library buffer provides a hierarchical view of tracks grouped by their 
metadata using [[info:taxy#Top][Taxy]], rendered with 
[[info:magit-section#Top][Magit Section]].  Each section can be folded, and it 
shows the number of tracks in it and its subgroups.
-
 **** Showing a library buffer
 
 Tracks from various sources can be shown in a library using these commands:
@@ -164,7 +148,6 @@ Tracks from various sources can be shown in a library using 
these commands:
 - Files and directories: ~listen-library~.  Individual files may be chosen, or 
a directory may be, which will be searched recursively for tracks.
 - From an MPD server: ~listen-library-from-mpd~.  An MPD search query will be 
read with completion, and matching tracks are read from the MPD server.
 - From a playlist file: ~listen-library-from-playlist-file~.  Tracks are read 
from the given playlist file.
-
 **** Commands
 
 In the library buffer, you can use these commands:
@@ -181,27 +164,22 @@ In the library buffer, you can use these commands:
 | Revert the library buffer | ~listen-library-revert~ (~g~) |
 | Pause the player          | ~listen-pause~ (~SPC~)        |
 | Show the menu             | ~listen-menu~ (~?~)           |
-
 **** Bookmarks
 
 Library buffers may be bookmarked with ~bookmark-set~ (~C-x r m~).  The 
bookmark record refers to the buffer by the way it was created (e.g. the 
filename paths, queue name, MPD query, or playlist file the tracks came from), 
so jumping to the bookmark will show an updated view, as if calling the 
original command with the same arguments.
-
 ** Players
 
-~listen~ currently supports audio playback via the VLC backend.  Internally, 
any number of simultaneous player instances could be controlled, but ~listen~'s 
UI provides the means to control one at a time.
+~listen~ supports audio playback via MPV or VLC backends.  Internally, any 
number of simultaneous player instances could be controlled, but ~listen~'s UI 
provides the means to control one at a time.
 
 Controlling the player is mainly done through the main 
[[info:transient#Top][Transient]] menu, through the command ~listen~.  However, 
all of the commands provided in it are also available as interactive commands, 
which could be bound by the user in any keymap (see, e.g. 
[[elisp:(apropos-command "^listen-")][M-x apropos-command RET ^listen- RET]]).
 
 The player is run in a child process, which is started when playback begins.  
The ~listen-quit~ command terminates the player process.
-
 *** Volume
 
 The ~listen-volume~ command is used to set the current player's volume.  Its 
argument should be an integer percentage.  Some players, e.g. VLC, may allow 
settings above 100% to boost output beyond normal levels.
-
 *** Seeking
 
 The ~listen-seek~ command is used to seek to a position in the current track.  
Its argument should be a timestamp in MM:SS format, and it may include a ~-~ or 
~+~ prefix to indicate a position relative to the current one.
-
 *** Repeat modes
 
 Three repeat modes are provided, controlled by the option 
~listen-queue-repeat-mode~, which may have these values:
@@ -211,18 +189,32 @@ Three repeat modes are provided, controlled by the option 
~listen-queue-repeat-m
 - ~shuffle~ :: When the last track in the current queue finishes playing, the 
queue is shuffled and played again.
 
 The repeat mode is most easily set using the commands in the ~listen~ menu.
-
 ** Mode
 
 The ~listen-mode~ minor mode runs a timer which plays the next track in the 
current queue when a track finishes playing (when playing a queue).  It is 
automatically activated when playing a queue.  It also shows the current track 
in the ~global-mode-string~, which may be displayed in the mode line or tab bar.
-
 ** Tips
 
-- Since VLC is used as a backend, 
[[https://www.freedesktop.org/wiki/Specifications/mpris-spec/][MPRIS]]-based 
player info and controls "just work", so you can use things like media hotkeys 
and various widgets to control ~listen~'s playback.
+- When using VLC as a backend, 
[[https://www.freedesktop.org/wiki/Specifications/mpris-spec/][MPRIS]]-based 
player info and controls "just work", so you can use things like media hotkeys 
and various widgets to control ~listen~'s playback.  When using MPV as a 
backend, see the [[https://github.com/hoyon/mpv-mpris][mpv-mpris]] package.
 - Similarly, you might even see an icon in your task switcher indicating that 
Emacs is playing sound (e.g. with KDE Plasma).
-
 * Changelog
 
+** v0.10
+
+*Additions*
+- [[https://mpv.io/][MPV]] support (works asynchronously, to improve 
performance and avoid blocking Emacs).
+- Command ~listen-status~, which shows a status buffer (contributions to it 
are welcome).
+- Command ~listen-queue-add-tracks~, when used in a Dired buffer, uses the 
marked files or the one at point.
+- Option ~listen-backend~, which sets the backend to use: MPV or VLC.  (The 
default is to auto-detect which is available at load time, with MPV being 
preferred due to more robust IPC support.)
+- Faces for parts of mode line lighter.
+
+*Changes*
+- Improve performance of adding large numbers of tracks to large queues (using 
a hash table for deduplication).
+
+*Fixes*
+- Playing next track in queue after current track has been removed.
+- Command ~listen-queue-goto-current~ automatically shows the queue's buffer.
+- Updating vtables for Emacs versions before 30.
+- Minor fixes for Emacs 30 compatibility.
 ** v0.9
 
 /Released without additional changes due to change in ELPA recipe./
@@ -230,12 +222,10 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 *Fixes*
 - Currently playing column in queue list buffer.
 - Autoload of ~listen~ / ~listen-menu~ commands (See 
[[https://github.com/magit/transient/issues/280][Transient issue]].  Thanks to 
Jonas Bernoulli.).
-
 ** v0.8.1
 
 *Fixes*
 - Autoload of ~listen~ / ~listen-menu~ commands.
-
 ** v0.8
 
 *Additions*
@@ -252,7 +242,6 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 - Set metadata slot when reverting track from disk.
 - Don't highlight current track in non-playing queues.
 - Increase minimum ~ffprobe~ timeout for a single track.
-
 ** v0.7
 
 *Additions*
@@ -272,7 +261,6 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 - Queue bookmark handler.
 - Open library buffer with point at beginning.
 - In queue buffer, sort track numbers numerically.
-
 ** v0.6
 
 *Additions*
@@ -288,12 +276,10 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 
 *Fixes*
 - Reading new queue name when no queue is playing.
-
 ** v0.5.1
 
 *Fixes*
 - Viewing queues which aren't currently playing.
-
 ** v0.5
 
 *Additions*
@@ -315,7 +301,6 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 - Increase timeout for reading track durations.
 - Command ~listen-queue-deduplicate~ first removes any tracks not backed by a 
file.
 - In queue buffer, mark current track by comparing filename (rather than 
internal track identity).
-
 ** v0.4
 
 *Additions*
@@ -326,7 +311,6 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 *Fixes*
 - Transposing a track in a queue keeps point on the track.
 - Autoloading of ~listen~ command.
-
 ** v0.3
 
 *Additions*
@@ -348,7 +332,6 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 
 *Credits*
 - Thanks to [[https://amodernist.com/][Philip Kaludercic]] for reviewing.
-
 ** v0.2
 
 *Additions*
@@ -360,19 +343,15 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 - The queue could sometimes skip tracks when playing.
 - Improve handling of tracks that are changed during playback (e.g. metadata).
 - Update copyright statements in all libraries.
-
 ** v0.1
 
 Initial release.
-
 * Development
 
 Feedback and patches are welcome.
-
 ** Copyright assignment
 
 Listen.el is published in GNU ELPA and is considered part of GNU Emacs.  
Therefore, cumulative contributions of more than 15 lines of code require that 
the author assign copyright of such contributions to the FSF.  Authors who are 
interested in doing so may contact [[mailto:[email protected]][[email protected]]] to 
request the appropriate form.
-
 ** Known issues
 
 - Queue buffers that are not visible during playback are not updated 
automatically (i.e. to show the currently playing track).  This is due to a 
limitation of the ~vtable~ library (see 
[[https://debbugs.gnu.org/cgi/bugreport.cgi?bug=69837][bug #69837]]).
diff --git a/docs/README.org b/docs/README.org
index d37bf3d002..fe87a3b7fd 100644
--- a/docs/README.org
+++ b/docs/README.org
@@ -10,11 +10,11 @@
 #+TEXINFO_DIR_DESC: Audio/Music player
 
 # ELPA badge image.
-[[https://elpa.gnu.org/packages/listen.html][file:https://elpa.gnu.org/packages/listen.svg]]
+[[https://elpa.gnu.org/packages/listen.html][file:https://elpa.gnu.org/packages/listen.svg]]
 
[[https://elpa.gnu.org/devel/listen.html][file:https://elpa.gnu.org/devel/listen.svg]]
 
 This package aims to provide a simple audio/music player for Emacs.  It should 
"just work," with little-to-no configuration, have intuitive commands, and be 
easily extended and customized.  (Contrast to setting up EMMS, or having to 
configure external players like MPD.)  A Transient menu, under the command 
~listen~, is the primary entry point.
 
-The only external dependency is VLC, which is currently the only player 
backend that is supported.  (Other backends may easily be added; see library 
~listen-vlc~ for example.)  Track metadata is read using EMMS's native Elisp 
metadata library, which has been imported into this package.
+The only external dependency is either [[https://mpv.io/][MPV]] or 
[[https://www.videolan.org/vlc/][VLC]], which are the supported player 
backends.  Track metadata is read using EMMS's native Elisp metadata library, 
which has been imported into this package.
 
 Queues are provided as the means to play consecutive tracks, and they are 
shown in a ~vtable~-based view buffer.  They are persisted between sessions 
using the ~persist~ library, and they may be bookmarked.
 
@@ -45,7 +45,7 @@ Note a silly limitation: a track may be present in a queue 
only once (but who wo
 
 *Requirements:*
 + Emacs version 29.1 or later.
-+ [[https://www.videolan.org/vlc/][VLC]]: used to play audio.
++ [[https://mpv.io/][MPV]] or [[https://www.videolan.org/vlc/][VLC]]: used to 
play audio.
 + Optional: ~ffprobe~ (part of [[https://ffmpeg.org/ffprobe.html][FFmpeg]]) is 
used to read tracks' duration when available.
 
 ** GNU ELPA
@@ -58,7 +58,7 @@ The latest development build may be installed from 
[[https://elpa.gnu.org/devel/
 
 The ~master~ branch of the Git repository is intended to be usable at all 
times; only minor bugs are expected to be found in it before a new stable 
release is made.
 
-To install, it is recommended to use 
[[https://github.com/quelpa/quelpa-use-package][quelpa-use-package]], like this 
(using 
[[https://github.com/alphapapa/unpackaged.el#upgrade-a-quelpa-use-package-forms-package][this
 helpful command]] for upgrading versions):
+To install from Git, it is recommended to use 
[[https://github.com/quelpa/quelpa-use-package][quelpa-use-package]], like this 
(using 
[[https://github.com/alphapapa/unpackaged.el#upgrade-a-quelpa-use-package-forms-package][this
 helpful command]] for upgrading versions):
 
 #+BEGIN_SRC elisp
   ;; Install and load `quelpa-use-package'.
@@ -200,7 +200,7 @@ Library buffers may be bookmarked with 
{{{command-binding(bookmark-set,C-x r m)}
 
 ** Players
 
-~listen~ currently supports audio playback via the VLC backend.  Internally, 
any number of simultaneous player instances could be controlled, but ~listen~'s 
UI provides the means to control one at a time.
+~listen~ supports audio playback via MPV or VLC backends.  Internally, any 
number of simultaneous player instances could be controlled, but ~listen~'s UI 
provides the means to control one at a time.
 
 Controlling the player is mainly done through the main 
[[info:transient#Top][Transient]] menu, through the command ~listen~.  However, 
all of the commands provided in it are also available as interactive commands, 
which could be bound by the user in any keymap (see, e.g. 
[[elisp:(apropos-command "^listen-")][M-x apropos-command RET ^listen- RET]]).
 
@@ -230,11 +230,29 @@ The ~listen-mode~ minor mode runs a timer which plays the 
next track in the curr
 
 ** Tips
 
-+ Since VLC is used as a backend, 
[[https://www.freedesktop.org/wiki/Specifications/mpris-spec/][MPRIS]]-based 
player info and controls "just work", so you can use things like media hotkeys 
and various widgets to control ~listen~'s playback.
++ When using VLC as a backend, 
[[https://www.freedesktop.org/wiki/Specifications/mpris-spec/][MPRIS]]-based 
player info and controls "just work", so you can use things like media hotkeys 
and various widgets to control ~listen~'s playback.  When using MPV as a 
backend, see the [[https://github.com/hoyon/mpv-mpris][mpv-mpris]] package.
 + Similarly, you might even see an icon in your task switcher indicating that 
Emacs is playing sound (e.g. with KDE Plasma).
   
 * Changelog
 
+** v0.10
+
+*Additions*
++ [[https://mpv.io/][MPV]] support (works asynchronously, to improve 
performance and avoid blocking Emacs).
++ Command ~listen-status~, which shows a status buffer (contributions to it 
are welcome).
++ Command ~listen-queue-add-tracks~, when used in a Dired buffer, uses the 
marked files or the one at point.
++ Option ~listen-backend~, which sets the backend to use: MPV or VLC.  (The 
default is to auto-detect which is available at load time, with MPV being 
preferred due to more robust IPC support.)
++ Faces for parts of mode line lighter.
+
+*Changes*
++ Improve performance of adding large numbers of tracks to large queues (using 
a hash table for deduplication).
+
+*Fixes*
++ Playing next track in queue after current track has been removed.
++ Command ~listen-queue-goto-current~ automatically shows the queue's buffer.
++ Updating vtables for Emacs versions before 30.
++ Minor fixes for Emacs 30 compatibility.
+
 ** v0.9
 
 /Released without additional changes due to change in ELPA recipe./
diff --git a/listen-info.el b/listen-info.el
index 75eea5d697..1d78d10058 100644
--- a/listen-info.el
+++ b/listen-info.el
@@ -337,7 +337,7 @@ Return comments in a list of (FIELD . VALUE) cons cells.  
See
 `listen-info--split-vorbis-comment' for details."
   (let* ((packets (listen-info--decode-ogg-packets filename 2))
          (headers (listen-info--decode-ogg-headers packets
-                                                        stream-type))
+                                                   stream-type))
          (comments (bindat-get-field headers
                                      'comment-header
                                      'user-comments)))
@@ -362,8 +362,7 @@ different streams will be mixed together without an error."
         (offset 0)
         (stream (vector)))
     (while (< num-packets packets)
-      (let ((page (listen-info--decode-ogg-page filename
-                                                     offset)))
+      (let ((page (listen-info--decode-ogg-page filename offset)))
         (cl-incf num-packets (or (plist-get page :num-packets) 0))
         (cl-incf offset (plist-get page :num-bytes))
         (setq stream (vconcat stream (plist-get page :stream)))
@@ -746,10 +745,7 @@ fields."
           ;; Skip the extended header.
           (cl-incf offset
                    (listen-info--checked-id3v2-ext-header-size filename)))
-        (listen-info--decode-id3v2-frames filename
-                                               offset
-                                               (+ tag-size 10)
-                                               unsync))
+        (listen-info--decode-id3v2-frames filename offset (+ tag-size 10) 
unsync))
     (error nil)))
 
 (defun listen-info--decode-id3v2-header (filename)
@@ -782,9 +778,7 @@ Return metadata in a list of (FIELD . VALUE) cons cells."
         comments)
     (condition-case nil
         (while (< offset limit)
-          (let* ((frame-data (listen-info--decode-id3v2-frame filename
-                                                                   offset
-                                                                   unsync))
+          (let* ((frame-data (listen-info--decode-id3v2-frame filename offset 
unsync))
                  (next-frame-offset (car frame-data))
                  (comment (cdr frame-data)))
             (when comment (push comment comments))
@@ -797,21 +791,16 @@ Return metadata in a list of (FIELD . VALUE) cons cells."
   (if (= listen-info--id3v2-version 2) 6 10))
 
 (defun listen-info--decode-id3v2-frame (filename offset unsync)
-  (let* ((header (listen-info--decode-id3v2-frame-header filename
-                                                              offset))
+  (let* ((header (listen-info--decode-id3v2-frame-header filename offset))
          (info-id (listen-info--id3v2-frame-info-id header))
          (data-offset (car header))
          (size (bindat-get-field (cdr header) 'size)))
     (if (or info-id unsync)
         ;; Note that if unsync is t, we have to always read the frame
         ;; to determine next-frame-offset.
-        (let* ((data (listen-info--read-id3v2-frame-data filename
-                                                              data-offset
-                                                              size
-                                                              unsync))
+        (let* ((data (listen-info--read-id3v2-frame-data filename data-offset 
size unsync))
                (next-frame-offset (car data))
-               (value (listen-info--decode-id3v2-frame-data (cdr data)
-                                                                 info-id)))
+               (value (listen-info--decode-id3v2-frame-data (cdr data) 
info-id)))
           (cons next-frame-offset value))
       ;; Skip the frame.
       (cons (+ data-offset size) nil))))
@@ -835,14 +824,11 @@ If there is no such identifier, return nil."
   (cdr (assoc (bindat-get-field frame 'id)
               listen-info--id3v2-frame-to-info)))
 
-(defun listen-info--read-id3v2-frame-data (filename
-                                                begin
-                                                num-bytes
-                                                unsync)
+(defun listen-info--read-id3v2-frame-data (filename begin num-bytes unsync)
   "Read NUM-BYTES of raw id3v2 frame data from FILENAME.
-Start reading from offset BEGIN.  If UNSYNC is t, all 'FF 00'
-byte combinations are replaced by 'FF'.  Replaced byte pairs are
-counted as one, instead of two, towards NUM-BYTES.
+Start reading from offset BEGIN.  If UNSYNC is t, all \"FF 00\" byte
+combinations are replaced by \"FF\".  Replaced byte pairs are counted as
+one, instead of two, towards NUM-BYTES.
 
 Return a cons cell (OFFSET . DATA), where OFFSET is the byte
 offset after NUM-BYTES bytes have been read, and DATA is the raw
diff --git a/listen-lib.el b/listen-lib.el
index be9a7e7331..91469ff418 100644
--- a/listen-lib.el
+++ b/listen-lib.el
@@ -19,14 +19,76 @@
 
 ;;; Commentary:
 
-;; 
+;; Library functions for Listen.
 
 ;;; Code:
 
 (require 'cl-lib)
+(require 'map)
+(require 'pcase)
+(require 'warnings)
 
 ;;;; Macros
 
+(cl-defmacro listen-debug (&rest args)
+  "Display a debug warning showing the runtime value of ARGS.
+The warning automatically includes the name of the containing
+function, and it is only displayed if `warning-minimum-log-level'
+is `:debug' at expansion time (otherwise the macro expands to a
+call to `ignore' with ARGS and is eliminated by the
+byte-compiler).  When debugging, the form also returns nil so,
+e.g. it may be used in a conditional in place of nil.
+
+Each of ARGS may be a string, which is displayed as-is, or a
+symbol, the value of which is displayed prefixed by its name, or
+a Lisp form, which is displayed prefixed by its first symbol.
+
+Before the actual ARGS arguments, you can write keyword
+arguments, i.e. alternating keywords and values.  The following
+keywords are supported:
+
+  :buffer BUFFER   Name of buffer to pass to `display-warning'.
+  :level  LEVEL    Level passed to `display-warning', which see.
+                   Default is :debug."
+  ;; TODO: Can we use a compiler macro to handle this more elegantly?
+  (pcase-let* ((fn-name (when byte-compile-current-buffer
+                          (with-current-buffer byte-compile-current-buffer
+                            ;; This is a hack, but a nifty one.
+                            (save-excursion
+                              (beginning-of-defun)
+                              (cl-second (read (current-buffer)))))))
+               (plist-args (cl-loop while (keywordp (car args))
+                                    collect (pop args)
+                                    collect (pop args)))
+               ((map (:buffer buffer) (:level level)) plist-args)
+               (level (or level :debug))
+               (string (cl-loop for arg in args
+                                concat (pcase arg
+                                         ((pred stringp) "%S ")
+                                         ((pred symbolp)
+                                          (concat (upcase (symbol-name arg)) 
":%S "))
+                                         ((pred listp)
+                                          (concat "(" (upcase (symbol-name 
(car arg)))
+                                                  (pcase (length arg)
+                                                    (1 ")")
+                                                    (_ "...)"))
+                                                  ":%S "))))))
+    (if (eq :debug warning-minimum-log-level)
+        `(let ((fn-name ,(if fn-name
+                             `',fn-name
+                           ;; In an interpreted function: use 
`backtrace-frame' to get the
+                           ;; function name (we have to use a little hackery 
to figure out
+                           ;; how far up the frame to look, but this seems to 
work).
+                           `(cl-loop for frame in (backtrace-frames)
+                                     for fn = (cl-second frame)
+                                     when (not (or (subrp fn)
+                                                   (special-form-p fn)
+                                                   (eq 'backtrace-frames fn)))
+                                     return (make-symbol (format "%s 
[interpreted]" fn))))))
+           (display-warning fn-name (format ,string ,@args) ,level ,buffer)
+           nil)
+      `(ignore ,@args))))
+
 (defmacro listen-once-per (value-form &rest body)
   "Evaluate BODY at most once while VALUE-FORM has the same value."
   (declare (indent defun))
@@ -45,9 +107,22 @@
 (cl-defstruct listen-player
   ;; TODO: Add queue slot.
   process command args
+  (status
+   nil :documentation "Symbol representing player's playback status.
+For example, `playing', `paused', `stopped', or nil if unknown.")
+  (etc nil :documentation "Alist used to store other information about the 
player.")
+  (path nil :documentation "Filename path or URL to currently playing track, 
if any.")
+  (metadata nil :documentation "Metadata alist.")
+  (volume nil :documentation "Volume in percent.")
   (max-volume
    100 :documentation "Maximum volume in percent (may be greater than 100 for 
some players).")
-  etc)
+  (playback-started-at
+   nil :documentation "Time at which playback started (used to compute 
elapsed/remaining).")
+  (playback-started-from
+   nil :documentation "Track position at which playback last started/unpaused, 
in seconds.
+Used to compute elapsed/remaining.")
+  (duration
+   nil :documentation "Duration of current track, in seconds (used to compute 
elapsed/remaining)."))
 
 (cl-defstruct listen-queue
   name tracks current etc)
@@ -107,14 +182,41 @@ return a list of values; otherwise return the sole value."
 (defface listen-rating '((t :inherit font-lock-escape-face))
   "Track rating.")
 
+(defgroup listen-lighter-faces nil
+  "Faces used in the mode line lighter."
+  :group 'listen-faces)
+
+(defface listen-lighter-artist '((t :inherit listen-artist))
+  "Track artist.")
+
+(defface listen-lighter-title '((t :inherit listen-title))
+  "Track title.")
+
+(defface listen-lighter-album '((t :inherit listen-album))
+  "Track album.")
+
+(defface listen-lighter-filename '((t :inherit listen-filename))
+  "Track filename.")
+
+(defface listen-lighter-genre '((t :inherit listen-genre))
+  "Track genre.")
+
+(defface listen-lighter-rating '((t :inherit listen-rating))
+  "Track rating.")
+
+(defface listen-lighter-time '((t :inherit fixed-pitch))
+  "Track time elapsed/remaining.")
+
+(defface listen-lighter-extra '((t :inherit font-lock-comment-face))
+  "See `listen-lighter-extra-functions'.")
+
 ;;;; Functions
 
-;; FIXME: Declare this differently or something.
-(declare-function make-listen-player-vlc "listen-vlc")
 (defun listen-current-player ()
   "Return variable `listen-player' or a newly set one if nil."
+  (defvar listen-backend)
   (or listen-player
-      (setf listen-player (make-listen-player-vlc))))
+      (setf listen-player (funcall listen-backend))))
 
 (cl-defun listen-current-track (&optional (player listen-player))
   "Return track playing on PLAYER, if any."
@@ -125,7 +227,32 @@ return a list of values; otherwise return the sole value."
 
 (defun listen-format-seconds (seconds)
   "Return SECONDS formatted as an hour:minute:second-style duration."
-  (format-seconds "%h:%z%.2m:%.2s" seconds))
+  (format-seconds "%h:%z%m:%.2s" seconds))
+
+(define-hash-table-test
+ 'listen-track-equal
+ #'equal
+ (lambda (track)
+   (sxhash-equal (expand-file-name (listen-track-filename track)))))
+
+(cl-defun listen-delete-dups (list &optional (test 'listen-track-equal))
+  "Return LIST having destructively removed duplicates.
+Similar to `delete-dups', but TEST may be specified.
+Unlike `delete-dups', this function always uses a hash table to find
+duplicates; therefore TEST should be compatible with `make-hash-table',
+which see."
+  ;; Copies the body of `delete-dups', passing through TEST, and removing the 
length-based
+  ;; non-hash-table case..
+  (let ((hash (make-hash-table :test test))
+        (tail list) retail)
+    (puthash (car list) t hash)
+    (while (setq retail (cdr tail))
+      (let ((elt (car retail)))
+        (if (gethash elt hash)
+            (setcdr tail (cdr retail))
+          (puthash elt t hash)
+          (setq tail retail))))
+    list))
 
 ;;;; Methods
 
diff --git a/listen-mpd.el b/listen-mpd.el
index 26ce171a6e..a0b87259dc 100644
--- a/listen-mpd.el
+++ b/listen-mpd.el
@@ -57,7 +57,7 @@ applied to the buffer."
   (listen-library tracks :name name))
 
 (declare-function listen-queue-add-files "listen-queue")
-(declare-function listen-queue-complete "listen-queue-complete")
+(declare-function listen-queue-complete "listen-queue")
 ;;;###autoload
 (cl-defun listen-queue-add-from-mpd (tracks queue)
   "Add TRACKS (selected from MPD library) to QUEUE."
diff --git a/listen-mpv.el b/listen-mpv.el
new file mode 100755
index 0000000000..7f8218dc5a
--- /dev/null
+++ b/listen-mpv.el
@@ -0,0 +1,313 @@
+;;; listen-mpv.el --- MPV support for Emacs Music Player                    
-*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024  Free Software Foundation, Inc.
+
+;; Author: Adam Porter <[email protected]>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; 
+
+;;; Code:
+
+;;;; Requirements
+
+(require 'cl-lib)
+(require 'json)
+(require 'map)
+
+(require 'listen-lib)
+
+;;;; Customization
+
+(defgroup listen-mpv nil
+  "MPV-related options."
+  :group 'listen)
+
+(defcustom listen-mpv-volume 50
+  "Initial volume for MPV instance."
+  :type 'natnum)
+
+;;;; Types
+
+(cl-defstruct
+    (listen-player-mpv
+     (:include listen-player
+               (command "mpv")
+               (args '("--no-msg-color" "--idle" "--audio-display=no"))
+               (max-volume 100)
+               (etc '((:request-id . 0))))))
+
+;;;; Functions
+
+(cl-defmethod listen--info ((player listen-player-mpv))
+  "Return metadata from MPV PLAYER, or nil if a track is not playing."
+  (or (listen-player-metadata player)
+      (listen--update-metadata player)))
+
+(cl-defmethod listen--update-metadata ((player listen-player-mpv) &optional 
then)
+  "Update PLAYER's metadata slot, then call THEN without arguments."
+  (let ((callback (lambda (metadata)
+                    (pcase metadata
+                      ((and (or `nil :unknown) value)
+                       ;; May happen between tracks.
+                       (listen-debug "Metadata response was" value))
+                      (_
+                       (setf (listen-player-metadata player)
+                             (map-apply (lambda (key value)
+                                          (cons (intern (downcase (symbol-name 
key))) value))
+                                        metadata))
+                       (when then
+                         (funcall then)))))))
+    (if then
+        (listen-mpv--get-property player "metadata" :then callback)
+      (funcall callback (listen-mpv--get-property player "metadata")))))
+
+(cl-defmethod listen--filename ((player listen-player-mpv))
+  "Return filename of PLAYER's current track."
+  (let ((new-status (listen-mpv--get-property player "path")))
+    (when (string-match (rx bol "( new input: file://" (group (1+ nonl)) " )" 
) new-status)
+      (match-string 1 new-status))))
+
+(cl-defmethod listen--title ((player listen-player-mpv))
+  (map-elt (listen-player-metadata player) 'title))
+
+(cl-defmethod listen--ensure ((player listen-player-mpv))
+  "Ensure PLAYER is ready."
+  (pcase-let* (((cl-struct listen-player command args process) player)
+               (socket (make-temp-name (expand-file-name "listen-mpv-socket-" 
temporary-file-directory)))
+               (args (append args (list (format "--input-ipc-server=%s" socket)
+                                        "--msg-level=ipc=debug"
+                                        (format "--volume=%s" 
listen-mpv-volume)
+                                        "--terminal=no"))))
+    (unless (process-live-p process)
+      (let ((process-buffer (generate-new-buffer " *listen-player-mpv*"))
+            (socket-buffer (generate-new-buffer " 
*listen-player-mpv-socket*")))
+        (buffer-disable-undo process-buffer)
+        (buffer-disable-undo socket-buffer)
+        (setf (listen-player-process player)
+              (apply #'start-process "listen-player-mpv" process-buffer
+                     command args))
+        ;; FIXME: Test in a short loop rather than just sleeping for a second.
+        (sleep-for 1)
+        (setf (map-elt (listen-player-etc player) :network-process)
+              (make-network-process :name "listen-player-mpv-socket" :family 
'local
+                                    :remote socket :noquery t
+                                    :buffer socket-buffer
+                                    :service nil)
+              (process-filter (map-elt (listen-player-etc player) 
:network-process))
+              (lambda (proc text)
+                (listen--filter player proc text))
+              (process-sentinel (map-elt (listen-player-etc player) 
:network-process))
+              (lambda (proc msg)
+                (display-warning 'listen-mpv
+                                 (format-message "listen-process-sentinel: 
PROC:%S  MSG:%S"
+                                                 proc msg)
+                                 :debug "*listen-mpv*")
+                (internal-default-process-sentinel proc msg))))
+      (set-process-query-on-exit-flag (listen-player-process player) nil)
+      ;; Observe relevant properties.
+      (dolist (property '("volume" "mute" "pause" "playback-time" "duration" 
"path" "metadata"))
+        (listen--send* player `("observe_property" ,property) :then 
#'ignore)))))
+
+(cl-defmethod listen--filter ((player listen-player-mpv) proc text)
+  (listen-debug :buffer "*listen-mpv*" (listen-player-process player) proc 
text)
+  (cl-labels ((next-message ()
+                (if-let ((msg (ignore-errors (let ((json-false nil))
+                                               (json-read)))))
+                    (progn
+                      (listen-debug :buffer "*listen-mpv*" "Parsed" msg)
+                      (delete-region (point-min) (point))
+                      msg)
+                  ;; Unparseable: return point so we can try again later.
+                  (listen-debug :buffer "*listen-mpv*" "Unparseable")
+                  (goto-char (point-min))
+                  nil)))
+    (with-current-buffer (process-buffer proc)
+      (goto-char (point-max))
+      (insert text)
+      (goto-char (point-min))
+      (while-let ((msg (next-message)))
+        (listen--act player msg)))))
+
+(cl-defmethod listen--act ((player listen-player-mpv) msg)
+  (listen-debug :buffer "*listen-mpv*" (listen-player-process player) msg)
+  (pcase-let (((map event request_id _reason data _error name) msg))
+    (pcase event
+      ((or "start-file" "playback-restart")
+       (listen--status-is player 'playing)
+       (listen--update-metadata player)
+       ;; TODO: Maybe move these to --status-is?
+       (setf (listen-player-duration player) (listen-mpv--get-property player 
"duration"))
+       (setf (listen-player-volume player) (listen-mpv--get-property player 
"volume")))
+      ((or "end-file" "idle") (listen--status-is player 'stopped))
+      ((or 'nil "data")
+       (if-let ((callback (map-elt (map-elt (listen-player-etc player) 
:requests) request_id)))
+           (prog1
+               (funcall callback msg)
+             (setf (map-elt (listen-player-etc player) :requests)
+                   (map-delete (map-elt (listen-player-etc player) :requests) 
request_id)))
+         (listen-debug :buffer "*listen-mpv*" "No callback for" msg)))
+      ("property-change"
+       ;; NOTE: Even though we explicitly observe these properties, if they 
change as a result of a
+       ;; command that we send, MPV does not send messages for these 
properties changing (e.g. if we
+       ;; tell it to pause, we don't get a pause property-change event).
+       (pcase name
+         ("duration" (setf (listen-player-duration player) data))
+         ("metadata" (setf (listen-player-metadata player) data))
+         ("path" (setf (listen-player-path player) data))
+         ("pause"
+          (listen--status-is
+           player (pcase data
+                    ('t 'paused)
+                    ('nil 'playing)
+                    (_ (listen-debug :buffer "*listen-mpv*" "Unrecognized 
pause" data)))))
+         ;; ("playback-time" (setf (listen-player-position player) data
+         ;;                        (listen-player-playback-started-from 
player) data))
+         ("volume" (setf (listen-player-volume player) data))))
+      (_ (listen-debug :buffer "*listen-mpv*" "Unrecognized event" event)))))
+
+(cl-defmethod listen--status-is ((player listen-player-mpv) new-status)
+  "Update PLAYER's status slot according to NEW-STATUS and return it.
+When NEW-STATUS is `playing', updates started-at and started-from slots."
+  (pcase-exhaustive new-status
+    ('paused nil)
+    ('playing
+     (setf (listen-player-playback-started-at player) (current-time)
+           (listen-player-playback-started-from player)
+           (listen-mpv--get-property player "playback-time")))
+    ('stopped (setf (listen-player-playback-started-at player) nil
+                    (listen-player-playback-started-from player) nil)))
+  (setf (listen-player-status player) new-status))
+
+(cl-defmethod listen--play ((player listen-player-mpv) file)
+  "Play FILE with PLAYER.
+Stops playing, clears playlist, adds FILE, and plays it."
+  (listen--send* player `("loadfile" ,(expand-file-name file)) :then #'ignore))
+
+;; (cl-defmethod listen--stop ((player listen-player-mpv))
+;;   "Stop playing with PLAYER."
+;;   (listen--send player "stop"))
+
+(cl-defmethod listen--status ((player listen-player-mpv))
+  (listen-player-status player))
+
+(cl-defmethod listen--pause ((player listen-player-mpv))
+  "Pause playing with PLAYER."
+  (let ((new-status (pcase (listen-player-status player)
+                      ('playing "yes")
+                      ('paused "no")
+                      ('nil "no"))))
+    (listen-mpv--set-property
+     player "pause" new-status
+     :then (lambda (msg)
+             (pcase (map-elt msg 'error)
+               ("success" (listen--status-is
+                           player (pcase-exhaustive new-status ("yes" 'paused) 
("no" 'playing))))
+               (_ (display-warning 'listen--pause (format-message "Unexpected 
response: %S" msg)
+                                   :warning "*listen-mpv*")))))))
+
+(cl-defmethod listen--playing-p ((player listen-player-mpv))
+  "Return non-nil if PLAYER is playing."
+  (equal (listen-player-status player) 'playing))
+
+(cl-defmethod listen--elapsed ((player listen-player-mpv))
+  "Return seconds elapsed for PLAYER's track."
+  (if (listen--playing-p player)
+      (setf (map-elt (listen-player-etc player) :elapsed)
+            (+ (time-to-seconds
+                (time-subtract (current-time) 
(listen-player-playback-started-at player)))
+               (listen-player-playback-started-from player)))
+    (map-elt (listen-player-etc player) :elapsed)))
+
+(cl-defmethod listen--length ((player listen-player-mpv))
+  "Return length of PLAYER's track in seconds."
+  (listen-player-duration player))
+
+(cl-defmethod listen--send ((player listen-player-mpv) command &rest args)
+  "Not implemented for MPV; use `listen--send*'.
+For checkdoc: PLAYER, COMMAND, ARGS."
+  (ignore player command args)
+  (error "Method `listen--send' is not implemented for player 
`listen-player-mpv'; use `listen--send*'"))
+
+(cl-defmethod listen--send* ((player listen-player-mpv) command-args &key then)
+  "Send COMMAND-ARGS to PLAYER.
+The first string in COMMAND-ARGS is the MPV command, and the remaining
+ones are arguments to it.  If THEN is provided, it should be a function
+which will be called asynchronously with the message alist returned by
+MPV, and the request ID number is returned from this function;
+otherwise, the MPV command is called synchronously and the message alist
+is returned from this function."
+  (listen--ensure player)
+  (cl-macrolet
+      ((wrap-callback (callback)
+         `(lambda (msg)
+            (unwind-protect
+                (funcall ,callback msg)
+              (setf (map-elt (listen-player-etc player) :requests)
+                    (map-delete (map-elt (listen-player-etc player) :requests) 
request-id))))))
+    (pcase-let* (((cl-struct listen-player (etc (map :network-process))) 
player)
+                 (request-id (cl-incf (map-elt (listen-player-etc player) 
:request-id)))
+                 (`(,command . ,args) command-args)
+                 (json (json-encode `(("command" ,command ,@args)
+                                      ("request_id" . ,request-id)))))
+      (listen-debug :buffer "*listen-mpv*" (listen-player-process player) json)
+      (process-send-string network-process json)
+      (process-send-string network-process "\n")
+      ;; TODO: Maybe check for success/error.
+      (if then
+          (progn
+            (setf (map-elt (map-elt (listen-player-etc player) :requests) 
request-id)
+                  (wrap-callback then))
+            request-id)
+        (let ((value :unknown))
+          (setf (map-elt (map-elt (listen-player-etc player) :requests) 
request-id)
+                (wrap-callback
+                 (lambda (msg)
+                   ;; Save the callback's value to the map so we can retrieve 
it.
+                   (setf value (map-elt msg 'data)))))
+          (accept-process-output (listen-player-process player) 0.05)
+          ;; Return the then's value.
+          value)))))
+
+(cl-defmethod listen--seek ((player listen-player-mpv) seconds)
+  "Seek PLAYER to SECONDS."
+  (listen--send* player `("seek" ,seconds "absolute") :then #'ignore))
+
+(cl-defmethod listen--volume ((player listen-player-mpv) &optional volume)
+  "Return or set PLAYER's VOLUME.
+VOLUME is an integer percentage."
+  (pcase-let (((cl-struct listen-player max-volume) player))
+    (if volume
+        (progn
+          (unless (<= 0 volume max-volume)
+            (error "VOLUME must be 0-%s" max-volume))
+          ;; We assume that the command will work, and we set the volume that 
is being set,
+          ;; because the Transient description uses the value from the player 
slot, and the
+          ;; callback can't make the Transient update itself.
+          (listen-mpv--set-property player "volume" volume)
+          (setf (listen-player-volume player) volume))
+      (listen-player-volume player))))
+
+(cl-defmethod listen-mpv--get-property ((player listen-player-mpv) property 
&key then)
+  (listen--send* player `("get_property" ,property) :then then))
+
+(cl-defmethod listen-mpv--set-property ((player listen-player-mpv) property 
value &key then)
+  (listen--send* player `("set_property" ,property ,value) :then then))
+
+(provide 'listen-mpv)
+
+;;; listen-mpv.el ends here
diff --git a/listen-queue.el b/listen-queue.el
index 9b3dc6c414..a7b74d9a7a 100644
--- a/listen-queue.el
+++ b/listen-queue.el
@@ -131,7 +131,7 @@ Useful for when `save-excursion' does not preserve point."
   (if-let ((buffer (listen-queue-buffer queue)))
       (progn
         (pop-to-buffer buffer)
-        (listen-queue-goto-current))
+        (listen-queue-goto-current queue))
     (with-current-buffer
         (setf buffer (get-buffer-create (format "*Listen Queue: %s*" 
(listen-queue-name queue))))
       (let ((inhibit-read-only t))
@@ -139,7 +139,7 @@ Useful for when `save-excursion' does not preserve point."
         (setf listen-queue queue)
         (erase-buffer)
         (when (listen-queue-tracks listen-queue)
-          (make-vtable
+          (listen-make-vtable
            :columns
            (list (list :name "▶" :primary 'descend
                        :getter
@@ -236,7 +236,7 @@ Useful for when `save-excursion' does not preserve point."
                                 (call-interactively 
#'listen-library-from-queue))
                           "!" (lambda (_) (call-interactively 
#'listen-queue-shell-command)))))
         (listen-queue--annotate-buffer)
-        (listen-queue-goto-current)))
+        (listen-queue-goto-current queue)))
     ;; NOTE: We pop to the buffer outside of `with-current-buffer' so
     ;; `listen-queue--bookmark-handler' works correctly.
     (pop-to-buffer buffer)))
@@ -251,6 +251,7 @@ To be called in a queue's buffer."
          (inhibit-read-only t))
     (setf (map-elt (listen-queue-etc queue) :duration) duration)
     (vtable-end-of-table)
+    (delete-region (point) (point-max))
     (when duration
       (insert (format "Duration: %s" (listen-format-seconds duration))))
     (listen-queue--highlight-current)))
@@ -312,7 +313,7 @@ If BACKWARDP, move it backward."
                  (list track)
                  (seq-subseq (listen-queue-tracks queue) position)))
     (vtable-insert-object (vtable-current-table) track previous-track)
-    (vtable-update-object (vtable-current-table) previous-track 
previous-track)))
+    (listen-queue--vtable-update-object (vtable-current-table) previous-track 
previous-track)))
 
 (defun listen-queue--update-buffer (queue)
   "Update QUEUE's buffer, if any."
@@ -323,62 +324,20 @@ If BACKWARDP, move it backward."
       (when (vtable-current-table)
         (vtable-revert-command))
       (listen-queue--annotate-buffer))
-    (listen-queue-goto-current)))
+    (listen-queue-goto-current queue)))
 
-(defun listen-queue-update-track (track queue)
-  "Update TRACK in QUEUE.
-Reverts TRACK's metadata from the file and updates it in QUEUE,
-including QUEUE's buffer, if any."
-  ;; TODO: Use where appropriate.
-  (listen-queue-revert-track track)
+(defun listen-queue-goto-current (queue)
+  "Jump to current track."
+  (interactive (list (listen-queue-complete)))
+  (unless (listen-queue-buffer queue)
+    (listen-queue queue))
   (listen-queue-with-buffer queue
-    (listen-save-position
+    (when-let ((current-track (listen-queue-current queue)))
+      ;; Ensure point is within the vtable.
       (goto-char (point-min))
-      (vtable-update-object (vtable-current-table) track track))))
-
-(declare-function listen-mode "listen")
-(declare-function listen-play "listen")
-;;;###autoload
-(cl-defun listen-queue-play (queue &optional (track (car (listen-queue-tracks 
queue))))
-  "Play QUEUE and optionally TRACK in it.
-Interactively, selected queue with completion; and with prefix,
-select track as well."
-  (interactive
-   (let* ((queue (listen-queue-complete))
-          (track (if current-prefix-arg
-                     (listen-queue-complete-track queue)
-                   (car (listen-queue-tracks queue)))))
-     (list queue track)))
-  (let ((player (listen-current-player)))
-    (listen-play player (listen-track-filename track))
-    (let ((previous-track (listen-queue-current queue)))
-      (setf (listen-queue-current queue) track
-            (map-elt (listen-player-etc player) :queue) queue)
-      (listen-queue-with-buffer queue
-        ;; HACK: Only update the vtable if its buffer is visible.
-        (when-let ((buffer-window (get-buffer-window (current-buffer))))
-          (with-selected-window buffer-window
-            (listen-save-position
-              (goto-char (point-min))
-              (ignore-errors
-                ;; HACK: Ignore errors, because if the window size has 
changed, the vtable's cache
-                ;; will miss and it will signal an error.
-                (when previous-track
-                  (listen-queue--vtable-update-object (vtable-current-table)
-                                                      previous-track 
previous-track))
-                (listen-queue--vtable-update-object (vtable-current-table) 
track track)))
-            (listen-queue--highlight-current))))))
-  (unless listen-mode
-    (listen-mode))
-  queue)
-
-(defun listen-queue-goto-current ()
-  "Jump to current track."
-  (interactive)
-  (when-let ((current-track (listen-queue-current listen-queue)))
-    ;; Ensure point is within the vtable.
-    (goto-char (point-min))
-    (vtable-goto-object current-track)))
+      (vtable-goto-object current-track))
+    (unless (get-buffer-window (current-buffer))
+      (display-buffer (current-buffer)))))
 
 (defun listen-queue-complete-track (queue)
   "Return track selected from QUEUE with completion."
@@ -438,16 +397,26 @@ which see."
   (interactive (list (listen-queue-complete :prompt "Discard queue: ")))
   (cl-callf2 delete queue listen-queues))
 
+(defun listen-complete-files ()
+  ;; FIXME: Use this function in more places as appropriate.
+  "Return files selected with completion.
+In a Dired buffer, use `dired-get-marked-files'."
+  (declare-function dired-get-marked-files "dired")
+  (cl-case major-mode
+    (dired-mode (dired-get-marked-files))
+    (otherwise (let ((path (expand-file-name (read-file-name "Enqueue 
file/directory: "
+                                                             listen-directory 
nil t))))
+                 (if (file-directory-p path)
+                     (directory-files-recursively path ".")
+                   (list path))))))
+
 ;;;###autoload
 (cl-defun listen-queue-add-files (files queue)
-  "Add FILES to QUEUE."
+  "Add FILES to QUEUE.
+Completes files with `listen-complete-files', which see."
   (interactive
-   (let ((queue (listen-queue-complete :allow-new-p t))
-         (path (expand-file-name (read-file-name "Enqueue file/directory: " 
listen-directory nil t))))
-     (list (if (file-directory-p path)
-               (directory-files-recursively path ".")
-             (list path))
-           queue)))
+   (let ((queue (listen-queue-complete :allow-new-p t)))
+     (list (listen-complete-files) queue)))
   (cl-callf append (listen-queue-tracks queue) (listen-queue-tracks-for files))
   (listen-queue queue)
   queue)
@@ -459,10 +428,7 @@ the queue's buffer is updated, if any."
   (cl-callf append (listen-queue-tracks queue) tracks)
   ;; TODO: Consider updating the metadata of any duplicate tracks.
   (setf (listen-queue-tracks queue)
-        (cl-delete-duplicates (listen-queue-tracks queue)
-                              :key (lambda (track)
-                                     (expand-file-name (listen-track-filename 
track)))
-                              :test #'file-equal-p))
+        (listen-delete-dups (listen-queue-tracks queue) 'listen-track-equal))
   (listen-queue--update-buffer queue))
 
 (cl-defun listen-queue-add-from-playlist-file (filename queue)
@@ -552,9 +518,10 @@ with \"ffprobe\"."
     ;; If `listen-queue-track' (and thereby `listen-queue-tracks-for') returns 
nil for a track
     ;; (e.g. if its metadata can't be read), leave it alone (e.g. its metadata 
might have come from
     ;; by MPD).
+    ;; FIXME: Store metadata in its own slot and don't misuse etc slot.
+    (setf (listen-track-etc track) (listen-track-etc new-track)
+          (listen-track-metadata track) (listen-track-etc new-track))
     (dolist (slot '(artist title album number date genre etc))
-      ;; FIXME: Store metadata in its own slot and don't misuse etc slot.
-      (setf (listen-track-metadata track) (listen-track-etc new-track))
       (setf (cl-struct-slot-value 'listen-track slot track)
             (cl-struct-slot-value 'listen-track slot new-track)))))
 
@@ -627,12 +594,24 @@ tracks no longer backed by a file are removed."
       ;; the track metadata and refreshes the queue from disk while
       ;; the track is playing), in which case it won't be able to find
       ;; the track in the queue, so look again by comparing filenames.
-      (seq-elt (listen-queue-tracks queue)
-               (1+ (seq-position (listen-queue-tracks queue)
-                                 (listen-queue-current queue)
-                                 (lambda (a b)
-                                   (equal (listen-track-filename a)
-                                          (listen-track-filename b))))))))
+      (let ((current-track-position
+             (or (seq-position (listen-queue-tracks queue)
+                               (listen-queue-current queue)
+                               (lambda (a b)
+                                 (equal (expand-file-name 
(listen-track-filename a))
+                                        (expand-file-name 
(listen-track-filename b)))))
+                 (progn
+                   (display-warning 'listen-queue
+                                    (format-message "listen: Can't find track 
(%S) in queue (%S)"
+                                                    (listen-queue-current 
queue)
+                                                    (listen-queue-name queue))
+                                    :debug)
+                   (if-let ((track-number (map-elt (listen-queue-etc queue) 
:track-number)))
+                       ;; If the track was removed, the next track in the 
queue should have
+                       ;; taken its place, and we don't want to skip it, so 
subtract one.
+                       (1- track-number)
+                     (error "listen: Couldn't find track in queue, and track 
number is nil"))))))
+        (seq-elt (listen-queue-tracks queue) (1+ current-track-position)))))
 
 (declare-function listen-shell-command "listen")
 (defun listen-queue-shell-command (command filenames)
@@ -743,7 +722,7 @@ tracks in the queue unchanged)."
       (cl-labels ((get (slot)
                     (cons (capitalize (symbol-name slot))
                           (cl-struct-slot-value 'listen-track slot track))))
-        (make-vtable
+        (listen-make-vtable
          :columns
          (list (list :name "Key" :getter (lambda (row _table) (car row)))
                (list :name "Value" :getter (lambda (row _table) (cdr row))))
@@ -850,10 +829,11 @@ MAX-PROCESSES limits the number of parallel probing 
processes."
                                      "-show_entries" "format=duration"
                                      (expand-file-name (listen-track-filename 
track))))
                       (process (make-process
-                                :name "listen:ffprobe" :noquery t :type 'pipe 
:buffer (current-buffer)
-                                :sentinel sentinel :command (if 
listen-queue-nice-p
-                                                                (cons "nice" 
command)
-                                                              command))))
+                                :name "listen:ffprobe" :noquery t 
:connection-type 'pipe
+                                :buffer (current-buffer) :sentinel sentinel
+                                :command (if listen-queue-nice-p
+                                             (cons "nice" command)
+                                           command))))
                  process))))
          (probe-more ()
            (while (and tracks (length< processes max-processes))
@@ -914,7 +894,7 @@ Delay according to `listen-queue-delay-time-range', which 
see."
       (toggle-truncate-lines 1)
       (setq-local bookmark-make-record-function 
#'listen-queue-list--bookmark-make-record)
       (when listen-queues
-        (make-vtable
+        (listen-make-vtable
          :columns
          (list (list :name "▶" :primary 'descend
                      :getter (lambda (queue _table)
@@ -960,10 +940,41 @@ Delay according to `listen-queue-delay-time-range', which 
see."
     ;; `listen-queue--bookmark-handler' works correctly.
     (pop-to-buffer buffer)))
 
-;;;; Compatibility
+;;;; Vtable
+
+(require 'vtable)
+
+(cl-defmacro listen-with-vtable-at (position &rest body)
+  "FIXME: Docstring."
+  (declare (indent defun))
+  (let ((positionᵥ (gensym)))
+    `(let ((,positionᵥ ,position))
+       (save-excursion
+         (goto-char ,positionᵥ)
+         (cl-letf* (((symbol-function 'frame-terminal)
+                     (lambda (&optional _)
+                       listen-vtable-frame-terminal))
+                    ((symbol-function 'window-width)
+                     (lambda (&optional _ _)
+                       listen-vtable-window-width))
+                    (table (vtable-current-table))
+                    ((symbol-function 'vtable-current-table)
+                     (lambda ()
+                       table))
+                    ((symbol-function 'vtable--recompute-numerical)
+                     #'listen-queue--vtable--recompute-numerical))
+           ,@body)))))
+
+(defvar-local listen-vtable-frame-terminal nil)
+(defvar-local listen-vtable-window-width nil)
+
+(defun listen-make-vtable (&rest args)
+  (apply #'make-vtable args)
+  (setq-local listen-vtable-frame-terminal (frame-terminal)
+              listen-vtable-window-width (window-width)))
 
 (defalias 'listen-queue--vtable-update-object
-  (if (version<= emacs-version "29.2")
+  (if (version<= emacs-version "30")
       ;; See <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=69664>.
       (lambda (table object old-object)
         "Replace OLD-OBJECT in TABLE with OBJECT."
@@ -1010,6 +1021,86 @@ Delay according to `listen-queue-delay-time-range', 
which see."
             (error "Can't find cached object in vtable"))))
     #'vtable-update-object))
 
+;;;;; Functions using these vtable workarounds
+
+(defun listen-queue-update-track (track queue)
+  "Update TRACK in QUEUE.
+Reverts TRACK's metadata from the file and updates it in QUEUE,
+including QUEUE's buffer, if any."
+  ;; TODO: Use where appropriate.
+  (listen-queue-revert-track track)
+  (listen-queue-with-buffer queue
+    (listen-save-position
+      (listen-with-vtable-at (point-min)
+        (listen-queue--vtable-update-object table track track)))))
+
+;;;###autoload
+(cl-defun listen-queue-play (queue &optional (track (car (listen-queue-tracks 
queue))))
+  "Play QUEUE and optionally TRACK in it.
+Interactively, selected queue with completion; and with prefix,
+select track as well."
+  (interactive
+   (let* ((queue (listen-queue-complete))
+          (track (if current-prefix-arg
+                     (listen-queue-complete-track queue)
+                   (car (listen-queue-tracks queue)))))
+     (list queue track)))
+  (declare-function listen-mode "listen")
+  (declare-function listen-play "listen")
+  (let ((player (listen-current-player)))
+    (listen-play player (listen-track-filename track))
+    ;; Remember queue position of track so if it gets removed, we can still go 
to the next track.
+    (setf (map-elt (listen-queue-etc queue) :track-number)
+          (seq-position (listen-queue-tracks queue) track))
+    (let ((previous-track (listen-queue-current queue)))
+      (setf (listen-queue-current queue) track
+            (map-elt (listen-player-etc player) :queue) queue)
+      (listen-queue-with-buffer queue
+        (listen-save-position
+          (listen-with-vtable-at (point-min)
+            (when previous-track
+              (listen-queue--vtable-update-object table previous-track 
previous-track))
+            (listen-queue--vtable-update-object table track track)))
+        (listen-queue--highlight-current))))
+  (unless listen-mode
+    (listen-mode))
+  queue)
+
+(defalias 'listen-queue--vtable--recompute-numerical
+  ;; TODO: Remove this when requiring Emacs 30+.
+  ;; See <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=69927>.
+  (if (version< emacs-version "30.1")
+      (lambda (table line)
+        "Recompute numericalness of columns if necessary."
+        (let ((columns (vtable-columns table))
+              (recompute nil))
+          (seq-do-indexed
+           (lambda (elem index)
+             (when (and (vtable-column--numerical (elt columns index))
+                        (not (numberp (car elem))))
+               (setq recompute t)))
+           line)
+          (when recompute
+            (vtable--compute-columns table))))
+    ;; HACK: This is unique, one the likes of which I've never quite seen 
before.  This is to work
+    ;; around errors like "(cyclic-function-indirection 
vtable--recompute-numerical)" and "Symbol’s
+    ;; chain of function indirections contains a loop: 
vtable--recompute-numerical".  Because we
+    ;; also, in the macro `listen-with-vtable-at', dynamically rebind the 
function
+    ;; `vtable--recompute-numerical' with `cl-letf*', which normally creates a 
loop, we use
+    ;; `cl-letf' here also, to save a reference to the original function 
definition, which we make
+    ;; our own alias to.  Then when the expansion of `cl-letf*' in 
`listen-with-vtable-at' rebinds
+    ;; the function slot of `vtable--recompute-numerical', it binds it to the 
original function,
+    ;; rather than to the symbol (which would cause the cyclic 
indirection/loop).
+
+    ;; Now, you may think this is ugly or ridiculous, but it has a legitimate 
purpose: to provide a
+    ;; fix for users of older Emacs versions, while also being compatible with 
the Emacs version
+    ;; that has the fix included.  And how many other languages and platforms 
would even allow this?
+    ;; (Remember that `cl-letf' rebinds the symbol's function slot, so that 
while the macro's
+    ;; expansion is on the stack, anything else--in the whole system--that 
calls the rebound
+    ;; function calls our replacement for it--not just where we, ourselves, 
directly reference it.)
+    (cl-letf ((orig-fn (symbol-function 'vtable--recompute-numerical)))
+      orig-fn)))
+
 ;;;; Footer
 
 (provide 'listen-queue)
diff --git a/listen-vlc.el b/listen-vlc.el
index 7d61671a8a..9da0456209 100755
--- a/listen-vlc.el
+++ b/listen-vlc.el
@@ -48,12 +48,18 @@
 ;;;; Functions
 
 (cl-defmethod listen--info ((player listen-player-vlc))
+  "Return metadata from VLC PLAYER, or nil if a track is not playing."
+  (or (listen-player-metadata player)
+      (listen--update-metadata player)))
+
+(cl-defmethod listen--update-metadata ((player listen-player-vlc))
   (with-temp-buffer
     (save-excursion
       (insert (listen--send player "info")))
-    (cl-loop while (re-search-forward (rx bol "| " (group (1+ (not blank))) ": 
"
-                                          (group (1+ (not (any "
"))))) nil t)
-             collect (cons (match-string 1) (match-string 2)))))
+    (setf (listen-player-metadata player)
+          (cl-loop while (re-search-forward (rx bol "| " (group (1+ (not 
blank))) ": "
+                                                (group (1+ (not (any "
"))))) nil t)
+                   collect (cons (intern (downcase (match-string 1))) 
(match-string 2))))))
 
 (cl-defmethod listen--filename ((player listen-player-vlc))
   "Return filename of PLAYER's current track."
@@ -86,7 +92,10 @@ Stops playing, clears playlist, adds FILE, and plays it."
 (cl-defmethod listen--status ((player listen-player-vlc))
   (let ((status (listen--send player "status")))
     (when (string-match (rx "( state " (group (1+ alnum)) " )") status)
-      (match-string 1 status))))
+      (pcase (match-string 1 status)
+        ("paused" 'paused)
+        ("playing" 'playing)
+        ("stopped" 'stopped)))))
 
 (cl-defmethod listen--pause ((player listen-player-vlc))
   "Pause playing with PLAYER."
@@ -133,9 +142,10 @@ VOLUME is an integer percentage."
     (if volume
         (progn
           (unless (<= 0 volume max-volume)
-            (user-error "VOLUME must be 0-%s" max-volume))
-          (listen--send player (format "volume %s" (* 255 (/ volume 100.0)))))
-      (* 100 (/ (string-to-number (listen--send player "volume")) 255.0)))))
+            (error "VOLUME must be 0-%s" max-volume))
+          (listen--send player (format "volume %s" (* 255 (/ volume 100.0))))
+          (setf (listen-player-volume player) volume))
+      (listen-player-volume player))))
 
 (provide 'listen-vlc)
 
diff --git a/listen.el b/listen.el
index 40de75ff22..be7a653b4f 100755
--- a/listen.el
+++ b/listen.el
@@ -6,7 +6,7 @@
 ;; Maintainer: Adam Porter <[email protected]>
 ;; Keywords: multimedia
 ;; Package-Requires: ((emacs "29.1") (persist "0.6") (taxy "0.10") 
(taxy-magit-section "0.13") (transient "0.5.3"))
-;; Version: 0.9
+;; Version: 0.10
 ;; URL: https://github.com/alphapapa/listen.el
 
 ;; This program is free software; you can redistribute it and/or modify
@@ -60,6 +60,8 @@
 (require 'map)
 
 (require 'listen-lib)
+;; TODO: Can we load these as-needed?
+(require 'listen-mpv)
 (require 'listen-vlc)
 
 ;;;; Variables
@@ -119,6 +121,14 @@ Called with one argument, the player (if the player has a 
queue,
 its current track will be the one that just finished playing)."
   :type 'hook)
 
+(defcustom listen-backend
+  (cond ((executable-find "mpv") #'make-listen-player-mpv)
+        ((executable-find "vlc") #'make-listen-player-vlc)
+        (t (display-warning 'listen-backend "Unable to find MPV or VLC." 
:error)))
+  "Player backend."
+  :type '(choice (const :tag "MPV" make-listen-player-mpv)
+                 (const :tag "VLC" make-listen-player-vlc)))
+
 ;;;; Commands
 
 (defun listen-quit (player)
@@ -126,16 +136,16 @@ its current track will be the one that just finished 
playing)."
 Interactively, uses the default player."
   (interactive
    (list (listen-current-player)))
-  (delete-process (listen-player-process player))
   (when (eq player listen-player)
     (setf listen-player nil))
+  (delete-process (listen-player-process player))
   (listen-mode--update))
 
-(declare-function listen-queue-next "listen-queue")
 (defun listen-next (player)
   "Play next track in PLAYER's queue.
 Interactively, uses the default player."
   (interactive (list (listen-current-player)))
+  (declare-function listen-queue-next "listen-queue")
   (listen-queue-next (map-elt (listen-player-etc player) :queue)))
 
 (defun listen-pause (player)
@@ -208,6 +218,9 @@ Interactively, jump to current queue's current track."
 ;;;; Mode
 
 (defvar listen-mode-lighter nil)
+;; Setting this symbol property allows faces and display properties to affect 
the lighter in the
+;; mode line and tab bar.
+(put 'listen-mode-lighter 'risky-local-variable t)
 
 ;;;###autoload
 (define-minor-mode listen-mode
@@ -231,35 +244,44 @@ Interactively, jump to current queue's current track."
 (defun listen-mode-lighter ()
   "Return lighter for `listen-mode'.
 According to `listen-lighter-format', which see."
-  (when-let ((listen-player)
-             ((listen--running-p listen-player))
-             ((listen--playing-p listen-player))
-             (info (listen--info listen-player)))
+  (when-let* ((player listen-player)
+              ((listen--running-p player))
+              ((pcase (listen--status player)
+                 ((or 'playing 'paused) t)))
+              (metadata (listen--info player)))
     (format-spec listen-lighter-format
                  `((?a . ,(lambda ()
-                            (or (alist-get "artist" info nil nil #'equal) "")))
+                            (propertize (or (alist-get 'artist metadata nil 
nil #'equal) "")
+                                        'face 'listen-lighter-artist)))
                    (?A . ,(lambda ()
-                            (or (alist-get "album" info nil nil #'equal) "")))
+                            (propertize (or (alist-get 'album metadata nil nil 
#'equal) "")
+                                        'face 'listen-lighter-album)))
                    (?t . ,(lambda ()
-                            (if-let ((title (alist-get "title" info nil nil 
#'equal)))
-                                (truncate-string-to-width title 
listen-lighter-title-max-length
-                                                          nil nil t)
+                            (if-let ((title (alist-get 'title metadata nil nil 
#'equal)))
+                                (propertize
+                                 (truncate-string-to-width title 
listen-lighter-title-max-length
+                                                           nil nil t)
+                                 'face 'listen-lighter-title)
                               "")))
                    (?e . ,(lambda ()
-                            (listen-format-seconds (listen--elapsed 
listen-player))))
+                            (propertize (listen-format-seconds 
(listen--elapsed listen-player))
+                                        'face 'listen-lighter-time)))
                    (?r . ,(lambda ()
-                            (concat "-" (listen-format-seconds
-                                         (- (listen--length listen-player)
-                                            (listen--elapsed 
listen-player))))))
+                            (propertize (concat "-" (listen-format-seconds
+                                                     (- (listen--length 
listen-player)
+                                                        (listen--elapsed 
listen-player))))
+                                        'face 'listen-lighter-time)))
                    (?s . ,(lambda ()
-                            (pcase (listen--status listen-player)
-                              ("playing" "▶")
-                              ("paused" "⏸")
-                              ("stopped" "■")
-                              (_ ""))))
+                            (propertize (pcase (listen--status listen-player)
+                                          ('playing "▶")
+                                          ('paused "⏸")
+                                          ('stopped "■")
+                                          (_ ""))
+                                        'face 'bold)))
                    (?E . ,(lambda ()
                             (if-let ((extra (mapconcat #'funcall 
listen-lighter-extra-functions " ")))
-                                (concat " " extra)
+                                (propertize (concat " " extra)
+                                            'face 'listen-lighter-extra)
                               "")))))))
 
 (defun listen-lighter-format-rating ()
@@ -272,24 +294,20 @@ According to `listen-lighter-format', which see."
     (unless (equal "-1" rating)
       (format "[%s]" (* 5 (string-to-number rating))))))
 
-(declare-function listen-queue-play "listen-queue")
-(declare-function listen-queue-next-track "listen-queue")
 (defun listen-mode--update (&rest _ignore)
   "Play next track and/or update variable `listen-mode-lighter'."
-  (let (playing-next-p)
-    (when listen-player
-      (unless (or (listen--playing-p listen-player)
-                  ;; HACK: It seems that sometimes the player gets restarted
-                  ;; even when paused: this extra check should prevent that.
-                  (member (listen--status listen-player) '("playing" 
"paused")))
-        (setf playing-next-p
-              (run-hook-with-args 'listen-track-end-functions listen-player))))
-    (setf listen-mode-lighter
-          (when (and listen-player (listen--running-p listen-player))
-            (listen-mode-lighter)))
-    (when playing-next-p
-      ;; TODO: Remove this (I think it's not necessary anymore).
-      (force-mode-line-update 'all))))
+  (declare-function listen-queue-play "listen-queue")
+  (declare-function listen-queue-next-track "listen-queue")
+  (when (and listen-player (listen--running-p listen-player))
+    (unless (or (listen--playing-p listen-player)
+                ;; HACK: It seems that sometimes the player gets restarted
+                ;; even when paused: this extra check should prevent that.
+                (member (listen--status listen-player) '(playing paused)))
+      (run-hook-with-args 'listen-track-end-functions listen-player)))
+  (setf listen-mode-lighter
+        (when (and listen-player (listen--running-p listen-player))
+          (listen-mode-lighter)))
+  (force-mode-line-update 'all))
 
 (defun listen-play-next (player)
   "Play PLAYER's queue's next track and return non-nil if playing."
@@ -337,8 +355,8 @@ TIME is a string like \"SS\", \"MM:SS\", or \"HH:MM:SS\"."
 
 (defvar listen-queue-repeat-mode)
 
-;; It seems that autoloading the transient prefix command doesn't work
-;; as expected, so we'll try this workaround.
+;; TODO(someday): Simplify autoload when requiring Emacs 30.  See
+;; <https://github.com/magit/transient/issues/280>.
 
 ;;;###autoload (autoload 'listen-menu "listen" nil t)
 (transient-define-prefix listen-menu ()
@@ -373,8 +391,9 @@ TIME is a string like \"SS\", \"MM:SS\", or \"HH:MM:SS\"."
           listen-player)
     :description
     (lambda ()
-      (if listen-player
-          (format "Volume: %.0f%%" (listen--volume listen-player))
+      (if-let ((listen-player)
+               (volume (listen--volume listen-player)))
+          (format "Volume: %.0f%%" volume)
         "Volume: N/A"))
     ("=" "Set" listen-volume)
     ("v" "Down" (lambda ()
@@ -454,6 +473,86 @@ TIME is a string like \"SS\", \"MM:SS\", or \"HH:MM:SS\"."
 ;;;###autoload
 (defalias 'listen #'listen-menu)
 
+;;;; Status buffer
+
+(cl-defun listen-status (player &key (displayp t))
+  "Show status buffer for PLAYER.
+If DISPLAYP, show the buffer; otherwise just update existing one."
+  (interactive (list listen-player))
+  (cl-macrolet ((with-face (string face)
+                  `(propertize ,string 'face ,face)))
+    (cl-labels ((buffer-for (player)
+                  (let ((buffer-name
+                         (if (eq player listen-player)
+                             ;; Default player.
+                             "*Listen Status*"
+                           (or (cl-loop for buffer in (buffer-list)
+                                        when (eq player (buffer-local-value 
'listen-player buffer))
+                                        return (buffer-name buffer))
+                               (concat (generate-new-buffer-name "*Listen 
Status ") "*")))))
+                    (or (get-buffer buffer-name)
+                        (with-current-buffer (generate-new-buffer buffer-name)
+                          (listen-player-mode)
+                          (current-buffer)))))
+                (metadata (key track)
+                  (or (listen-track-metadata-get key track) "")))
+      (with-current-buffer (buffer-for player)
+        (setq-local listen-player player)
+        (let ((inhibit-read-only t)
+              ;; FIXME: When playing a file without a queue.
+              (track (listen-queue-current (map-elt (listen-player-etc player) 
:queue)))
+              (pos (point)))
+          (erase-buffer)
+          (if (not (listen--playing-p player))
+              (insert "Not playing")
+            (insert (with-face "Artist: " 'bold)
+                    (with-face (metadata "artist" track) 'listen-artist) "\n")
+            (insert (with-face " Title: " 'bold)
+                    (propertize (metadata "title" track)
+                                'face 'listen-title
+                                'wrap-prefix "        ") "\n")
+            (insert (with-face " Album: " 'bold)
+                    (propertize (metadata "album" track)
+                                'face 'listen-album
+                                'wrap-prefix "        ") "\n")
+            (insert (with-face "  Time: " 'bold) (listen-format-seconds 
(listen--elapsed player))
+                    " / " (listen-format-seconds (listen-track-duration track))
+                    " (-" (listen-format-seconds (- (listen-track-duration 
track)
+                                                    (listen--elapsed player))) 
")" "\n")
+            (insert (with-face "  File: " 'bold)
+                    (propertize (listen-track-filename track)
+                                'face 'listen-filename
+                                'wrap-prefix "        ")))
+          (goto-char pos))
+        (when displayp
+          (display-buffer (current-buffer)))))))
+
+(defvar-local listen-player-timer nil)
+
+(define-derived-mode listen-player-mode special-mode "Listen-Player"
+  :group 'listen
+  :interactive nil
+  (setq-local buffer-read-only t
+              buffer-undo-list t
+              bookmark-make-record-function
+              (lambda ()
+                (if (eq (default-value 'listen-player)
+                        (buffer-local-value 'listen-player (current-buffer)))
+                    (list "*Listen Player*"
+                          (cons 'handler 'listen-player))
+                  (user-error "Only the default player's buffer may be 
bookmarked")))
+              revert-buffer-function (lambda (&rest _)
+                                       (interactive)
+                                       (listen-status listen-player :displayp 
nil)))
+  (add-hook 'kill-buffer-hook (lambda ()
+                                (when (timerp listen-player-timer)
+                                  (cancel-timer listen-player-timer)))
+            nil 'local)
+  (setq-local listen-player-timer (run-at-time nil 1 revert-buffer-function))
+  (visual-line-mode))
+
+;;; Footer:
+
 (provide 'listen)
 
 ;;; listen.el ends here

Reply via email to