[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/ampc 0bf832e424 092/111: * Add tagger. ampc-tagger.cpp:
From: |
Stefan Monnier |
Subject: |
[elpa] externals/ampc 0bf832e424 092/111: * Add tagger. ampc-tagger.cpp: New file. |
Date: |
Tue, 20 Feb 2024 18:16:43 -0500 (EST) |
branch: externals/ampc
commit 0bf832e424bf584e74c71eb901d6d5ace10943ff
Author: Christopher Schmidt <christopher@ch.ristopher.com>
Commit: Christopher Schmidt <christopher@ch.ristopher.com>
* Add tagger. ampc-tagger.cpp: New file.
.gitignore: Add ampc_tagger.
ampc.el (ampc-tagger-music-directories, ampc-tagger-executable)
(ampc-tagger-backup-directory, ampc-tagger-grab-hook,
ampc-tagger-grabbed-hook)
(ampc-tagger-store-hook, ampc-tagger-stored-hook)
(ampc-tagger-previous-configuration, ampc-tagger-version-verified)
(ampc-tagger-genres, ampc-files-list-mode-map, ampc-tagger-mode-map)
(ampc-tagger-dired-mode-map, ampc-tagger-completion-at-point): New
variables.
(ampc-tagger-tag-face, ampc-tagger-keyword-face): New faces.
(ampc-views): Add tagger view.
(ampc-tagger-version, ampc-tagger-tags): New constants.
(ampc-mode-map): Bind ampc-tagger.
(ampc-tagger-log): New macro.
(ampc-files-list-mode, ampc-tagger-mode, ampc-tagger-log-mode): New major
modes.
(ampc-tagger-dired-mode): New minor mode.
(ampc-tagger-report, ampc-tagger-call, ampc-tagger-tags-modified)
(ampc-tagger-make-backup, ampc-tagger-get-values, ampc-tagger-update)
(ampc-tag-files, ampc-tagger-complete-tag, ampc-tagger-complete-value)
(ampc-tagger-rename-artist-title): New functions.
(ampc-post-mark-change-update): Handle files list buffers.
(ampc-tagger-reset, ampc-tagger-save, ampc-tagger-quit, ampc-tagger)
(ampc-tagger-dired, ampc-tagger-completion-all-files): New commands.
(ampc-in-ampc-p): Add optional argument or-in-tagger.
All callers changed to make use of the new argument if applicable.
(ampc): Use the second view specified in ampc-views at startup.
---
ampc.el | 752 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
ampc_tagger.cpp | 218 ++++++++++++++++
2 files changed, 946 insertions(+), 24 deletions(-)
diff --git a/ampc.el b/ampc.el
index 758f00d3c9..fb2a72fb0d 100644
--- a/ampc.el
+++ b/ampc.el
@@ -40,6 +40,31 @@
;; Byte-compile ampc (M-x byte-compile-file RET /path/to/ampc.el RET) to
improve
;; its performance!
+;;; *** tagger
+;; ampc is not only a frontend to MPD but also a full-blown audio file tagger.
+;; To use this feature you have to build the backend application,
`ampc_tagger',
+;; which in turn uses TagLib (http://taglib.github.com/), a dual-licended
+;; (LGPL/MPL) audio meta-data library written in C++. TagLib has no
+;; dependencies on its own.
+;;
+;; To build `ampc_tagger', locate ampc_tagger.cpp. The file can be found in
the
+;; directory in which this file, ampc.el, is located. Compile the file and
+;; either customize `ampc-tagger-executable' to point to the binary file or
move
+;; the executable in a suitable directory so Emacs finds it via consulting
+;; `exec-path'.
+;;
+;; g++ -O2 ampc_tagger.cpp -oampc_tagger -ltag && sudo cp ampc_tagger
/usr/local/bin && rm ampc_tagger
+;;
+;; You have to customize `ampc-tagger-music-directories' in order to use the
+;; tagger. This variable should be a list of directories in which your music
+;; files are located. Usually this list should have only one entry, the value
+;; of your mpd.conf's `music_directory'.
+;;
+;; If `ampc-tagger-backup-directory' is non-nil, the tagger saved copies of all
+;; files that are about to be modified to this directory. Emacs's regular
+;; numeric backup filename syntax is used for the backup file names. By
default
+;; `ampc-tagger-backup-directory' is set to "~/.emacs.d/ampc-backups/".
+
;;; ** usage
;; To invoke ampc call the command `ampc', e.g. via M-x ampc RET. The first
;; argument to `ampc' is the host, the second is the port. Both values default
@@ -133,10 +158,87 @@
;; MPD. To toggle the enabled property of the selected outputs, press `a'
;; (ampc-toggle-output-enabled) or `<mouse-3>'.
-;;; *** global keys
+;;; ** tagger
+;; To start the tagging subsystem, press `I' (ampc-tagger). This key binding
+;; works in every buffer associated with ampc. First, the command tries to
+;; determine which files you want to tag. The files are collected using either
+;; the selected entries within the current buffer, the file associated with the
+;; entry at point, or, if both sources did not provide any files, the audio
file
+;; that is currently played by MPD. Next, the tagger view is created. On the
+;; right there is the buffer that contain the tag data. Each line in this
+;; buffer represents a tag with a value. Tag and value are separated by a
+;; colon. Valid tags are "Title", "Artist", "Album", "Comment", "Genre",
"Year"
+;; and "Track". The value can be an arbitrary string. Whitespaces in front
and
+;; at the end of the value are ignored. If the value is "<keep>", the tag line
+;; is ignored.
+;;
+;; To save the specified tag values back to the files, press `C-c C-c'
+;; (ampc-tagger-save). To exit the tagger and restore the previous window
+;; configuration, press `C-c C-q'. `C-u C-c C-c' saved the tags and exits the
+;; tagger. Only tags that are actually specified within the tagger buffer
+;; written back to the file. Other tags will not be touched by ampc. For
+;; example, to clear the "Commentary" tag, you need to specify the line
+;;
+;; Commentary:
+;;
+;; In the tagger buffer. Omitting this line will make the tagger not touch the
+;; "Commentary" tag at all.
+;;
+;; On the right there is the files list buffer. The selection of this buffer
+;; specifies which files the command `ampc-tag-save' will write to. If no file
+;; is selected, the file at point in the file list buffer is used.
+;;
+;; To reset the values of the tags specified in the tagger buffer to the common
+;; values of all selected files specified by the selection of the files list
+;; buffer, press `C-c C-r' (ampc-tagger-reset). With a prefix argument,
+;; `ampc-tagger-reset' restores missing tags as well.
+;;
+;; You can use tab-completion within the tagger buffer for both tags and tag
+;; values.
+;;
+;; You can also use the tagging subsystem on its own without a running ampc
+;; instance. To start the tagger, call `ampc-tag-files'. This function
accepts
+;; one argument, a list of absolute file names which are the files to tag.
ampc
+;; provides a minor mode for dired, `ampc-tagger-dired-mode'. If this mode is
+;; enabled within a dired buffer, pressing `C-c C-t' (ampc-tagger-dired) will
+;; start the tagger on the current selection.
+;;
+;; The following ampc-specific hooks are run during tagger usage:
+;;
+;; `ampc-tagger-grab-hook': Run by the tagger before grabbing tags of a file.
+;; Each function is called with one argument, the file name.
+;;
+;; `ampc-tagger-grabbed-hook': Run by the tagger after grabbing tags of a file.
+;; Each function is called with one argument, the file name.
+;;
+;; `ampc-tagger-store-hook': Run by the tagger before writing tags back to a
+;; file. Each function is called with two arguments, FOUND-CHANGED and DATA.
+;; FOUND-CHANGED is non-nil if the tags that are about to be written differ
from
+;; the ones in the file. DATA is a cons. The car specifies the full file name
+;; of the file that is about to be written to, the cdr is an alist that
+;; specifies the tags that are about to be (over-)written. The car of each
+;; entry in this list is a symbol specifying the tag (one of the ones in
+;; `ampc-tagger-tags'), the cdr a string specifying the value. The cdr of DATA
+;; may be modified. If FOUND-CHANGED is nil and the cdr of DATA is not
modified
+;; throughout the hook is run, the file is not touched.
+;; `ampc-tagger-stored-hook' is still run, though.
+;;
+;; `ampc-tagger-stored-hook': Run by the tagger after writing tags back to a
+;; file. Each function is called with two arguments, FOUND-CHANGED and DATA.
+;; These are the same arguments that were already passed to
+;; `ampc-tagger-store-hook'. The car of DATA, the file name, may be modified.
+;;
+;; These hooks can be used to handle vc locking and unlocking of files. For
+;; renaming files according to their (new) tag values, ampc provides the
+;; function `ampc-tagger-rename-artist-title' which may be added to
+;; `ampc-tagger-stored-hook'. The new file name generated by this function is
+;; "Artist"_-_"Title"."extension". Characters within "Artist" and "Title" that
+;; are not alphanumeric are substituted with underscores.
+
+;;; ** global keys
;; Aside from `J', `M', `K', `<' and `L', which may be used to select different
-;; views, ampc defines the following global keys, which may be used in every
-;; window associated with ampc:
+;; views, and `I' which starts the tagger, ampc defines the following global
+;; keys. These binding are available in every buffer associated with ampc:
;;
;; `k' (ampc-toggle-play): Toggle play state. If MPD does not play a song,
;; start playing the song at point if the current buffer is the playlist
buffer,
@@ -292,6 +394,23 @@ used by ampc. The function is called with one string
argument,
the tag value, and should return the treated value."
:type '(alist :key-type string :value-type function))
+(defcustom ampc-tagger-music-directories nil
+ "List of base directories in which your music files are located.
+Usually this list should have only one entry, the value of your
+mpd.conf's `music_directory'"
+ :type '(list directory))
+
+(defcustom ampc-tagger-executable "ampc_tagger"
+ "The name or full path to ampc's tagger executable."
+ :type 'string)
+
+(defcustom ampc-tagger-backup-directory
+ (file-name-directory (locate-user-emacs-file "ampc-backups/"))
+ "The directory in which the tagger copies files before modifying.
+If nil, disable backups."
+ :type '(choice (const :tag "Disable backups" nil)
+ (directory :tag "Directory")))
+
;;; **** hooks
(defcustom ampc-before-startup-hook nil
"A hook run before startup.
@@ -330,6 +449,37 @@ and the keys in `ampc-status-tags'. Not all keys may be
present
all the time!"
:type 'hook)
+(defcustom ampc-tagger-grab-hook nil
+ "Hook run by the tagger before grabbing tags of a file.
+Each function is called with one argument, the file name."
+ :type 'hook)
+(defcustom ampc-tagger-grabbed-hook nil
+ "Hook run by the tagger after grabbing tags of a file.
+Each function is called with one argument, the file name."
+ :type 'hook)
+
+(defcustom ampc-tagger-store-hook nil
+ "Hook run by the tagger before writing tags back to a file.
+Each function is called with two arguments, FOUND-CHANGED and
+DATA. FOUND-CHANGED is non-nil if the tags that are about to be
+written differ from the ones in the file. DATA is a cons. The
+car specifies the full file name of the file that is about to be
+written to, the cdr is an alist that specifies the tags that are
+about to be (over-)written. The car of each entry in this list
+is a symbol specifying the tag (one of the ones in
+`ampc-tagger-tags'), the cdr a string specifying the value. The
+cdr of DATA may be modified. If FOUND-CHANGED is nil and the cdr
+of DATA is not modified throughout the hook is run, the file is
+not touched. `ampc-tagger-stored-hook' is still run, though."
+ :type 'hook)
+(defcustom ampc-tagger-stored-hook nil
+ "Hook run by the tagger after writing tags back to a file.
+Each function is called with two arguments, FOUND-CHANGED and
+DATA. These are the same arguments that were already passed to
+`ampc-tagger-store-hook'. The car of DATA, the file name, may be
+modified."
+ :type 'hook)
+
;;; *** faces
(defface ampc-mark-face '((t (:inherit font-lock-constant-face)))
"Face of the mark.")
@@ -342,6 +492,11 @@ all the time!"
(defface ampc-current-song-marked-face '((t (:inherit region)))
"Face of the current song if marked.")
+(defface ampc-tagger-tag-face '((t (:inherit font-lock-constant-face)))
+ "Face of tags within the tagger.")
+(defface ampc-tagger-keyword-face '((t (:inherit font-lock-keyword-face)))
+ "Face of tags within the tagger.")
+
;;; *** internal variables
(defvar ampc-views
(let* ((songs '(1.0 song :properties (("Track" :title "#" :width 4)
@@ -365,7 +520,20 @@ all the time!"
("Artist" :min 15 :max 40)
("Album" :min 15 :max 40)
("Time" :width 6)))))
- `(("Current playlist view (Genre|Artist|Album)"
+ `((tagger
+ horizontal
+ (0.65 files-list
+ :properties ((filename :shrink t :title "File" :min 20 :max 40)
+ ("Title" :min 15 :max 40)
+ ("Artist" :min 15 :max 40)
+ ("Album" :min 15 :max 40)
+ ("Genre" :min 15 :max 40)
+ ("Year" :width 5)
+ ("Track" :title "#" :width 4)
+ ("Comment" :min 15 :max 40))
+ :dedicated nil)
+ (1.0 tagger))
+ ("Current playlist view (Genre|Artist|Album)"
,(kbd "J")
horizontal
(0.4 vertical
@@ -425,6 +593,14 @@ all the time!"
(defvar ampc-internal-db nil)
(defvar ampc-status nil)
+(defvar ampc-tagger-previous-configuration nil)
+(defvar ampc-tagger-version-verified nil)
+(defvar ampc-tagger-completion-all-files nil)
+(defvar ampc-tagger-genres nil)
+
+(defconst ampc-tagger-version "0.1")
+(defconst ampc-tagger-tags '(Title Artist Album Comment Genre Year Track))
+
;;; *** mode maps
(defvar ampc-mode-map
(let ((map (make-sparse-keymap)))
@@ -452,11 +628,13 @@ all the time!"
(define-key map (kbd "q") 'ampc-quit)
(define-key map (kbd "z") 'ampc-suspend)
(define-key map (kbd "T") 'ampc-trigger-update)
+ (define-key map (kbd "I") 'ampc-tagger)
(loop for view in ampc-views
- do (define-key map (cadr view)
- `(lambda ()
- (interactive)
- (ampc-change-view ',view))))
+ do (when (stringp (car view))
+ (define-key map (cadr view)
+ `(lambda ()
+ (interactive)
+ (ampc-change-view ',view)))))
map))
(defvar ampc-item-mode-map
@@ -525,14 +703,44 @@ all the time!"
(define-key map (kbd "<mouse-3>") 'ampc-mouse-align-point)
map))
+(defvar ampc-files-list-mode-map
+ (let ((map (make-sparse-keymap)))
+ (suppress-keymap map)
+ (define-key map (kbd "t") 'ampc-toggle-marks)
+ (define-key map (kbd "C-c C-q") 'ampc-tagger-quit)
+ (define-key map (kbd "C-c C-c") 'ampc-tagger-save)
+ (define-key map (kbd "C-c C-r") 'ampc-tagger-reset)
+ (define-key map [remap ampc-tagger] nil)
+ (define-key map [remap ampc-quit] 'ampc-tagger-quit)
+ (loop for view in ampc-views
+ do (when (stringp (car view))
+ (define-key map (cadr view) nil)))
+ map))
+
+(defvar ampc-tagger-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-q") 'ampc-tagger-quit)
+ (define-key map (kbd "C-c C-c") 'ampc-tagger-save)
+ (define-key map (kbd "C-c C-r") 'ampc-tagger-reset)
+ (define-key map (kbd "<tab>") 'ampc-tagger-completion-at-point)
+ map))
+
+(defvar ampc-tagger-dired-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-t") 'ampc-tagger-dired)
+ map))
+
;;; **** menu
(easy-menu-define nil ampc-mode-map nil
`("ampc"
("Change view" ,@(loop for view in ampc-views
+ when (stringp (car view))
collect (vector (car view)
`(lambda ()
(interactive)
- (ampc-change-view ',view)))))
+ (ampc-change-view ',view)))
+ end))
+ ["Run tagger" ampc-tagger]
"--"
["Play" ampc-toggle-play
:visible (and ampc-status
@@ -707,12 +915,10 @@ all the time!"
(setf arg- (prefix-numeric-value arg-))
(ampc-align-point)
(loop until (eobp)
- for index from 0 to (1- (if (numberp arg-)
- arg-
- (prefix-numeric-value arg-)))
+ for index from 0 to (1- (abs arg-))
do (save-excursion
,@body)
- until (ampc-next-line)))))
+ until (if (< arg- 0) (ampc-previous-line) (ampc-next-line))))))
(defmacro ampc-iterate-source (data-buffer delimiter bindings &rest body)
(declare (indent 3) (debug t))
@@ -745,6 +951,18 @@ all the time!"
(with-current-buffer output-buffer
(ampc-insert (ampc-pad ,pad-data) ,@body)))))
+(defmacro ampc-tagger-log (&rest what)
+ (declare (indent 0) (debug t))
+ `(with-current-buffer (get-buffer-create "*Tagger Log*")
+ (ampc-tagger-log-mode)
+ (save-excursion
+ (goto-char (point-max))
+ (let ((inhibit-read-only t)
+ (what (concat ,@what)))
+ (when ampc-debug
+ (message "ampc: %s" what))
+ (insert what)))))
+
;;; *** modes
(define-derived-mode ampc-outputs-mode ampc-item-mode "ampc-o")
@@ -757,10 +975,32 @@ all the time!"
(define-derived-mode ampc-playlists-mode ampc-item-mode "ampc-pls")
+(define-derived-mode ampc-files-list-mode ampc-item-mode "ampc-files-list")
+(define-derived-mode ampc-tagger-mode nil "ampc-tagger"
(set (make-local-variable 'tool-bar-map) ampc-tool-bar-map)
+ (set (make-local-variable 'tab-stop-list)
+ (list (+ (loop for tag in ampc-tagger-tags
+ maximize (length (symbol-name tag)))
+ 2)))
+ (set (make-local-variable 'completion-at-point-functions)
+ '(ampc-tagger-complete-tag ampc-tagger-complete-value))
(setf truncate-lines ampc-truncate-lines
- font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
+ font-lock-defaults
+ `(((,(concat "^\\([ \t]*\\(?:"
+ (mapconcat 'symbol-name ampc-tagger-tags "\\|")
+ "\\)[ \t]*:\\)"
+ "\\(\\(?:[ \t]*"
+ "\\(?:"
+ (mapconcat 'identity ampc-tagger-genres "\\|") "\\|<keep>"
+ "\\)"
+ "[ \t]*$\\)?\\)")
+ (1 'ampc-tagger-tag-face)
+ (2 'ampc-tagger-keyword-face)))
+ t)))
+
+(define-derived-mode ampc-tagger-log-mode nil "ampc-tagger-log")
+
(define-derived-mode ampc-item-mode ampc-mode "ampc-item"
(setf font-lock-defaults '((("^\\(\\*\\)\\(.*\\)$"
(1 'ampc-mark-face)
@@ -786,7 +1026,30 @@ all the time!"
(1 'ampc-current-song-mark-face)
(2 'ampc-current-song-marked-face)))))
+;;;###autoload
+(define-minor-mode ampc-tagger-dired-mode
+ "Minor mode that adds a audio file meta data tagging key binding to dired."
+ nil
+ " ampc-tagger"
+ nil
+ (assert (derived-mode-p 'dired-mode)))
+
;;; *** internal functions
+(defun ampc-tagger-report (args status)
+ (unless (zerop status)
+ (let ((message (format (concat "ampc_tagger (%s %s) returned with a "
+ "non-zero exit status (%s)")
+ ampc-tagger-executable
+ (mapconcat 'identity args " ")
+ status)))
+ (ampc-tagger-log message "\n")
+ (error message))))
+
+(defun ampc-tagger-call (&rest args)
+ (ampc-tagger-report
+ args
+ (apply 'call-process ampc-tagger-executable nil t nil args)))
+
(defun ampc-int-insert-cmp (p1 p2)
(cond ((< p1 p2) 'insert)
((eq p1 p2) 'overwrite)
@@ -821,6 +1084,15 @@ all the time!"
(loop for w in (cdr windows)
do (delete-window w)))))
+(defun ampc-tagger-tags-modified (tags new-tags)
+ (loop with found-changed
+ for (tag . value) in new-tags
+ for prop = (assq tag tags)
+ do (unless (equal (cdr prop) value)
+ (setf (cdr prop) value
+ found-changed t))
+ finally return found-changed))
+
(defun ampc-change-view (view)
(if (equal ampc-outstanding-commands '((idle)))
(ampc-configure-frame (cddr view))
@@ -829,9 +1101,11 @@ all the time!"
(defun ampc-quote (string)
(concat "\"" (replace-regexp-in-string "\"" "\\\"" string) "\""))
-(defun ampc-in-ampc-p ()
- (when (ampc-on-p)
- ampc-type))
+(defun ampc-in-ampc-p (&optional or-in-tagger)
+ (or (when (ampc-on-p)
+ ampc-type)
+ (when or-in-tagger
+ (memq (car ampc-type) '(files-list tagger)))))
(defun ampc-add-impl (&optional data)
(ampc-on-files (lambda (file)
@@ -915,6 +1189,34 @@ all the time!"
0)))
(ampc-send-command 'status t))
+(defun* ampc-tagger-make-backup (file)
+ (unless ampc-tagger-backup-directory
+ (return-from ampc-tagger-make-backup))
+ (when (functionp ampc-tagger-backup-directory)
+ (funcall ampc-tagger-backup-directory file)
+ (return-from ampc-tagger-make-backup))
+ (unless (file-directory-p ampc-tagger-backup-directory)
+ (make-directory ampc-tagger-backup-directory t))
+ (let* ((real-file
+ (loop with real-file = file
+ for target = (file-symlink-p real-file)
+ while target
+ do (setf real-file (expand-file-name
+ target (file-name-directory real-file)))
+ finally return real-file))
+ (target
+ (loop with base = (file-name-nondirectory real-file)
+ for i from 1
+ for file = (expand-file-name
+ (concat base ".~"
+ (int-to-string i)
+ "~")
+ ampc-tagger-backup-directory)
+ while (file-exists-p file)
+ finally return file)))
+ (ampc-tagger-log "\tBackup file: " (abbreviate-file-name target) "\n")
+ (copy-file real-file target nil t)))
+
(defun* ampc-move (N &aux with-marks entries-to-move (up (< N 0)))
(save-excursion
(goto-char (point-min))
@@ -1013,6 +1315,86 @@ all the time!"
(when (memq (car ampc-type) '(song tag))
(ampc-set-dirty t))))
(ampc-fill-tag-song))
+ (files-list
+ (ampc-tagger-update))))
+
+(defun* ampc-tagger-get-values (tag all-files &aux result)
+ (ampc-with-buffer 'files-list
+ no-se
+ (save-excursion
+ (macrolet
+ ((add-file
+ ()
+ `(let ((value (cdr (assq tag (get-text-property (point) 'data)))))
+ (unless (member value result)
+ (push value result)))))
+ (if all-files
+ (loop until (eobp)
+ initially do (goto-char (point-min))
+ (ampc-align-point)
+ do (add-file)
+ until (ampc-next-line))
+ (ampc-with-selection nil
+ (add-file))))))
+ result)
+
+(defun ampc-tagger-update ()
+ (ampc-with-buffer 'tagger
+ (loop
+ while (search-forward-regexp (concat "^[ \t]*\\("
+ (mapconcat 'symbol-name
+ ampc-tagger-tags
+ "\\|")
+ "\\)[ \t]*:"
+ "[ \t]*\\(<keep>[ \t]*?\\)"
+ "\\(?:\n\\)?$")
+ nil
+ t)
+ for tag = (intern (match-string 1))
+ do (when (memq tag ampc-tagger-tags)
+ (let ((values (save-match-data (ampc-tagger-get-values tag nil))))
+ (when (eq (length values) 1)
+ (replace-match (car values) nil t nil 2)))))))
+
+(defun ampc-tagger-complete-tag ()
+ (save-excursion
+ (save-restriction
+ (narrow-to-region (line-beginning-position) (line-end-position))
+ (unless (search-backward-regexp "^.*:" nil t)
+ (when (search-backward-regexp "\\(^\\|[ \t]\\).*" nil t)
+ (when (looking-at "[ \t]")
+ (forward-char 1))
+ (list (point)
+ (search-forward-regexp ":\\|$")
+ (mapcar (lambda (tag) (concat (symbol-name tag) ":"))
+ ampc-tagger-tags)))))))
+
+(defun* ampc-tagger-complete-value (&aux tag)
+ (save-excursion
+ (save-restriction
+ (narrow-to-region (line-beginning-position) (line-end-position))
+ (save-excursion
+ (unless (search-backward-regexp (concat "^[ \t]*\\("
+ (mapconcat 'symbol-name
+ ampc-tagger-tags
+ "\\|")
+ "\\)[ \t]*:")
+ nil t)
+ (return-from ampc-tagger-complete-tag))
+ (setf tag (intern (match-string 1))))
+ (save-excursion
+ (search-backward-regexp "[: \t]")
+ (forward-char 1)
+ (list (point)
+ (search-forward-regexp "[ \t]\\|$")
+ (let ((values (cons "<keep>" (ampc-tagger-get-values
+ tag
+
ampc-tagger-completion-all-files))))
+ (when (eq tag 'Genre)
+ (loop for g in ampc-tagger-genres
+ do (unless (member g values)
+ (push g values))))
+ values))))))
(defun ampc-align-point ()
(unless (eobp)
@@ -1769,6 +2151,200 @@ all the time!"
(unless no-update
(ampc-update)))
+(defun ampc-tagger-rename-artist-title (_changed-tags data)
+ "Rename music file according to its tags.
+This function is meant to be inserted into
+`ampc-tagger-stored-hook'. The new file name is
+`Artist'_-_`Title'.`extension'. Characters within `Artist' and
+`Title' that are not alphanumeric are substituted with underscore."
+ (let* ((artist (replace-regexp-in-string
+ "[^a-zA-Z0-9]" "_"
+ (or (cdr (assq 'Artist (cdr data))) "")))
+ (title (replace-regexp-in-string
+ "[^a-zA-Z0-9]" "_"
+ (or (cdr (assq 'Title (cdr data))) "")))
+ (new-file
+ (expand-file-name (replace-regexp-in-string
+ "_\\(_\\)+"
+ "_"
+ (concat artist
+ (when (and (> (length artist) 0)
+ (> (length title) 0))
+ "_-_")
+ title
+ (file-name-extension (car data) t)))
+ (file-name-directory (car data)))))
+ (unless (equal (car data) new-file)
+ (ampc-tagger-log "Renaming file " (abbreviate-file-name (car data))
+ " to " (abbreviate-file-name new-file) "\n")
+ (rename-file (car data) new-file)
+ (setf (car data) new-file))))
+
+;;; *** interactives
+(defun ampc-tagger-completion-at-point (&optional all-files)
+ "Perform completion at point via `completion-at-point'.
+If optional prefix argument ALL-FILES is non-nil, use all files
+within the files list buffer as source for completion. The
+default behaviour is to use only the selected ones."
+ (interactive "P")
+ (let ((ampc-tagger-completion-all-files all-files))
+ (completion-at-point)))
+
+(defun ampc-tagger-reset (&optional reset-all-tags)
+ "Reset all tag values within the tagger, based on the selection of files.
+If optional prefix argument RESET-ALL-TAGS is non-nil, restore
+all tags."
+ (interactive "P")
+ (when reset-all-tags
+ (ampc-with-buffer 'tagger
+ no-se
+ (erase-buffer)
+ (loop for tag in ampc-tagger-tags
+ do (insert (ampc-pad (list (concat (symbol-name tag) ":") "dummy"))
+ "\n"))
+ (goto-char (point-min))
+ (re-search-forward ":\\( \\)+")))
+ (ampc-with-buffer 'tagger
+ (loop while (search-forward-regexp
+ (concat "^\\([ \t]*\\)\\("
+ (mapconcat 'symbol-name ampc-tagger-tags "\\|")
+ "\\)\\([ \t]*\\):\\([ \t]*.*\\)$")
+ nil
+ t)
+ do (replace-match "" nil nil nil 1)
+ (replace-match "" nil nil nil 3)
+ (replace-match (concat (make-string (- (car tab-stop-list)
+ (1+ (length (match-string
2))))
+ ? )
+ "<keep>")
+ nil nil nil 4)))
+ (ampc-tagger-update)
+ (ampc-with-buffer 'tagger
+ no-se
+ (when (looking-at "[ \t]+")
+ (goto-char (match-end 0)))))
+
+(defun* ampc-tagger-save (&optional quit &aux tags)
+ "Save tags.
+If optional prefix argument QUIT is non-nil, quit tagger
+afterwards. If the numeric value of QUIT is 16, quit tagger and
+do not trigger a database update"
+ (interactive "P")
+ (ampc-with-buffer 'tagger
+ (loop do (loop until (eobp)
+ while (looking-at "^[ \t]*$")
+ do (forward-line))
+ until (eobp)
+ do (unless (and (looking-at
+ (concat "^[ \t]*\\("
+ (mapconcat 'symbol-name
+ ampc-tagger-tags
+ "\\|")
+ "\\)[ \t]*:"
+ "[ \t]*\\(.*\\)[ \t]*$"))
+ (not (assq (intern (match-string 1)) tags)))
+ (error "Malformed line \"%s\""
+ (buffer-substring (line-beginning-position)
+ (line-end-position))))
+ (push (cons (intern (match-string 1))
+ (let ((val (match-string 2)))
+ (if (string= "<keep>" val)
+ t
+ (set-text-properties 0 (length val) nil val)
+ val)))
+ tags)
+ (forward-line)))
+ (callf2 rassq-delete-all t tags)
+ (with-temp-buffer
+ (loop for (tag . value) in tags
+ do (insert (symbol-name tag) "\n"
+ value "\n"))
+ (let ((input-buffer (current-buffer)))
+ (ampc-with-buffer 'files-list
+ no-se
+ (let ((reporter
+ (make-progress-reporter "Storing tags"
+ 0
+ (let ((count (count-matches "^\\* ")))
+ (if (zerop count)
+ 1
+ count))))
+ (step 0))
+ (ampc-with-selection nil
+ (let* ((data (get-text-property (point) 'data))
+ (old-tags (loop for (tag . data) in (cdr data)
+ collect (cons tag data)))
+ (found-changed (ampc-tagger-tags-modified (cdr data) tags)))
+ (let ((pre-hook-tags (cdr data)))
+ (run-hook-with-args 'ampc-tagger-store-hook found-changed data)
+ (setf found-changed
+ (or found-changed
+ (ampc-tagger-tags-modified pre-hook-tags
+ (cdr data)))))
+ (when found-changed
+ (ampc-tagger-log
+ "Storing tags for file "
+ (abbreviate-file-name (car data)) "\n"
+ "\tOld tags:\n"
+ (loop for (tag . value) in old-tags
+ concat (concat "\t\t"
+ (symbol-name tag) ": "
+ value "\n"))
+ "\tNew tags:\n"
+ (loop for (tag . value) in (cdr data)
+ concat (concat "\t\t"
+ (symbol-name tag) ": "
+ value "\n")))
+ (ampc-tagger-make-backup (car data))
+ (ampc-tagger-report
+ (list "--set" (car data))
+ (with-temp-buffer
+ (insert-buffer-substring input-buffer)
+ (prog1
+ (call-process-region (point-min) (point-max)
+ ampc-tagger-executable
+ nil t nil
+ "--set" (car data))
+ (when ampc-debug
+ (message "ampc-tagger: %s"
+ (buffer-substring
+ (point-min) (point))))))))
+ (run-hook-with-args 'ampc-tagger-stored-hook found-changed data)
+ (let ((inhibit-read-only t))
+ (move-beginning-of-line nil)
+ (forward-char 2)
+ (kill-line 1)
+ (insert
+ (ampc-pad (loop for p in (plist-get (cdr ampc-type)
+ :properties)
+ when (eq (car p) 'filename)
+ collect (file-name-nondirectory (car data))
+ else
+ collect (cdr (assq (intern (car p))
+ (cdr data)))
+ end))
+ "\n")
+ (forward-line -1)
+ (put-text-property (line-beginning-position)
+ (1+ (line-end-position))
+ 'data data))
+ (progress-reporter-update reporter (incf step))))
+ (progress-reporter-done reporter)))))
+ (when quit
+ (ampc-tagger-quit (eq (prefix-numeric-value quit) 16))))
+
+(defun ampc-tagger-quit (&optional no-update)
+ "Quit tagger and restore previous window configuration.
+With optional prefix NO-UPDATE, do not trigger a database update."
+ (interactive "P")
+ (set-window-configuration (or (car-safe ampc-tagger-previous-configuration)
+ ampc-tagger-previous-configuration))
+ (when (car-safe ampc-tagger-previous-configuration)
+ (unless no-update
+ (ampc-trigger-update))
+ (setf ampc-windows (cadr ampc-tagger-previous-configuration)))
+ (setf ampc-tagger-previous-configuration nil))
+
(defun ampc-move-to-tab ()
"Move point to next logical tab stop."
(interactive)
@@ -1835,7 +2411,7 @@ all the time!"
(defun* ampc-unmark-all (&aux (inhibit-read-only t))
"Remove all marks."
(interactive)
- (assert (ampc-in-ampc-p))
+ (assert (ampc-in-ampc-p t))
(save-excursion
(goto-char (point-min))
(loop while (search-forward-regexp "^\\* " nil t)
@@ -1852,7 +2428,7 @@ all the time!"
"Toggle marks.
Marked entries become unmarked, and vice versa."
(interactive)
- (assert (ampc-in-ampc-p))
+ (assert (ampc-in-ampc-p t))
(save-excursion
(loop for (a . b) in '(("* " . "T ")
(" " . "* ")
@@ -1882,14 +2458,14 @@ ARG defaults to one."
"Mark the next ARG'th entries.
ARG defaults to 1."
(interactive "p")
- (assert (ampc-in-ampc-p))
+ (assert (ampc-in-ampc-p t))
(ampc-mark-impl t arg))
(defun ampc-unmark (&optional arg)
"Unmark the next ARG'th entries.
ARG defaults to 1."
(interactive "p")
- (assert (ampc-in-ampc-p))
+ (assert (ampc-in-ampc-p t))
(ampc-mark-impl nil arg))
(defun ampc-set-volume (&optional arg)
@@ -2170,6 +2746,134 @@ selected), use playlist at point rather than the
selected one."
(message "No playlist at point")
(message "No playlist selected"))))
+;;;###autoload
+(defun ampc-tagger-dired (&optional arg)
+ "Start the tagging subsystem on dired's marked files.
+With optional prefix argument ARG, use the next ARG files."
+ (interactive "P")
+ (assert (derived-mode-p 'dired-mode))
+ (ampc-tag-files
+ (loop for file in (dired-map-over-marks (dired-get-filename) arg)
+ unless (file-directory-p file)
+ collect file
+ end)))
+
+;;;###autoload
+(defun ampc-tag-files (files)
+ "Start the tagging subsystem.
+FILES should be a list of absolute file names, the files to tag."
+ (unless files
+ (message "No files specified")
+ (return-from ampc-tagger-files t))
+ (when (memq (car ampc-type) '(files-list tagger))
+ (message "You are already within the tagger")
+ (return-from ampc-tagger-files t))
+ (let ((reporter (make-progress-reporter "Grabbing tags" 0 (length files))))
+ (loop for file in-ref files
+ for i from 1
+ do (run-hook-with-args 'ampc-tagger-grab-hook file)
+ (with-temp-buffer
+ (ampc-tagger-call "--get" file)
+ (setf file
+ (apply 'list
+ file
+ (loop for tag in ampc-tagger-tags
+ collect
+ (cons tag (or (ampc-extract (symbol-name tag))
+ ""))))))
+ (run-hook-with-args 'ampc-tagger-grabbed-hook file)
+ (progress-reporter-update reporter i))
+ (progress-reporter-done reporter))
+ (unless ampc-tagger-previous-configuration
+ (setf ampc-tagger-previous-configuration (current-window-configuration)))
+ (ampc-configure-frame (cdr (assq 'tagger ampc-views)) t)
+ (ampc-with-buffer 'files-list
+ (erase-buffer)
+ (loop for (file . props) in files
+ do (insert (propertize
+ (concat
+ " "
+ (ampc-pad
+ (loop for p in (plist-get (cdr ampc-type) :properties)
+ when (eq (car p) 'filename)
+ collect (file-name-nondirectory file)
+ else
+ collect (cdr (assq (intern (car p)) props))
+ end))
+ "\n")
+ 'data (cons file props))))
+ (ampc-set-dirty nil)
+ (ampc-toggle-marks))
+ (ampc-with-buffer 'tagger
+ no-se
+ (ampc-tagger-reset t)
+ (goto-char (point-min))
+ (search-forward-regexp ": *")
+ (ampc-set-dirty nil))
+ nil)
+
+(defun* ampc-tagger (&optional arg &aux files)
+ "Start the tagging subsystem.
+The files to tag are collected by using either the selected
+entries within the current buffer or the next ARG entries at
+point if numeric perfix argument ARG is non-nil, the file
+associated with the entry at point, or, if both sources did not
+provide any files, the audio file that is currently played by
+MPD."
+ (interactive "P")
+ (assert (ampc-in-ampc-p))
+ (unless ampc-tagger-version-verified
+ (with-temp-buffer
+ (ampc-tagger-call "--version")
+ (goto-char (point-min))
+ (let ((version (buffer-substring (line-beginning-position)
+ (line-end-position))))
+ (unless (equal version ampc-tagger-version)
+ (message (concat "The reported version of %s is not supported - "
+ "got \"%s\", want \"%s\"")
+ ampc-tagger-executable
+ version
+ ampc-tagger-version)
+ (return-from ampc-tagger))))
+ (setf ampc-tagger-version-verified t))
+ (unless ampc-tagger-genres
+ (with-temp-buffer
+ (ampc-tagger-call "--genres")
+ (loop while (search-backward-regexp "^\\(.+\\)$" nil t)
+ do (push (match-string 1) ampc-tagger-genres))))
+ (unless ampc-tagger-music-directories
+ (message (concat "ampc-tagger-music-directories is nil. Fill it via "
+ "M-x customize-variable RET ampc-tagger-music-directories
"
+ "RET"))
+ (return-from ampc-tagger))
+ (case (car ampc-type)
+ (current-playlist
+ (save-excursion
+ (ampc-with-selection arg
+ (callf nconc files (list (cdr (assoc "file" (get-text-property
+ (line-end-position)
+ 'data))))))))
+ ((playlist tag song)
+ (save-excursion
+ (ampc-with-selection arg
+ (ampc-on-files (lambda (file) (push file files)))))
+ (callf nreverse files))
+ (t
+ (let ((file (cdr (assoc 'file ampc-status))))
+ (when file
+ (setf files (list file))))))
+ (loop for file in-ref files
+ for read-file = (locate-file file ampc-tagger-music-directories)
+ do (unless read-file
+ (error "Cannot locate file %s in ampc-tagger-music-directories"
+ file)
+ (return-from ampc-tagger))
+ (setf file (expand-file-name read-file)))
+ (setf ampc-tagger-previous-configuration
+ (list (current-window-configuration) ampc-windows))
+ (when (ampc-tag-files files)
+ (setf ampc-tagger-previous-configuration nil)))
+
(defun ampc-store (&optional name-or-append)
"Store current playlist as NAME-OR-APPEND.
If NAME is non-nil and not a string, append selected entries
@@ -2216,14 +2920,14 @@ playlist name from the minibuffer."
"Go to previous ARG'th entry in the current buffer.
ARG defaults to 1."
(interactive "p")
- (assert (ampc-in-ampc-p))
+ (assert (ampc-in-ampc-p t))
(ampc-next-line (* (or arg 1) -1)))
(defun ampc-next-line (&optional arg)
"Go to next ARG'th entry in the current buffer.
ARG defaults to 1."
(interactive "p")
- (assert (ampc-in-ampc-p))
+ (assert (ampc-in-ampc-p t))
(forward-line arg)
(if (eobp)
(progn (forward-line -1)
@@ -2332,7 +3036,7 @@ default to the ones specified in `ampc-default-server'."
(setf ampc-outstanding-commands '((setup))))
(if suspend
(ampc-update)
- (ampc-configure-frame (cddar ampc-views)))
+ (ampc-configure-frame (cddadr ampc-views)))
(run-hooks 'ampc-connected-hook)
(when suspend
(ampc-suspend))
diff --git a/ampc_tagger.cpp b/ampc_tagger.cpp
new file mode 100644
index 0000000000..b62de7c00c
--- /dev/null
+++ b/ampc_tagger.cpp
@@ -0,0 +1,218 @@
+// ampc_tagger.el --- Asynchronous Music Player Controller Tagger
+
+// Copyright (C) 2012 Free Software Foundation, Inc.
+
+// Author: Christopher Schmidt <christopher@ch.ristopher.com>
+// Maintainer: Christopher Schmidt <christopher@ch.ristopher.com>
+// Created: 2012-07-17
+
+// This file is part of ampc.
+
+// 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 <http://www.gnu.org/licenses/>.
+
+#include <iostream>
+#include <sstream>
+
+#include <taglib/fileref.h>
+#include <taglib/tag.h>
+#include <taglib/id3v1genres.h>
+
+std::wstring const version=L"0.1";
+std::locale original_wcout_locale;
+
+bool get(std::string const& file)
+{
+ using namespace TagLib;
+
+ FileRef file_ref(file.c_str());
+ Tag* tag;
+ if(file_ref.isNull() || !(tag=file_ref.tag()))
+ {
+ std::wcerr << L"ERROR: Failed opening file." << std::endl;
+ return true;
+ }
+
+ std::wcout << L"Title: " << tag->title().toWString() << std::endl <<
+ L"Artist: " << tag->artist().toWString() << std::endl <<
+ L"Album: " << tag->album().toWString() << std::endl <<
+ L"Comment: " << tag->comment().toWString() << std::endl <<
+ L"Genre: " << tag->genre().toWString() << std::endl;
+ if(tag->year())
+ {
+ std::wcout << L"Year: ";
+ std::locale new_locale=std::wcout.imbue(original_wcout_locale);
+ std::wcout << tag->year();
+ std::wcout.imbue(new_locale);
+ std::wcout << std::endl;
+ }
+ if(tag->track())
+ {
+ std::wcout << L"Track: ";
+ std::locale new_locale=std::wcout.imbue(original_wcout_locale);
+ std::wcout << tag->track();
+ std::wcout.imbue(new_locale);
+ std::wcout << std::endl;
+ }
+
+ return false;
+}
+
+bool set(std::string const& file)
+{
+ using namespace TagLib;
+
+ FileRef file_ref(file.c_str());
+ Tag* tag;
+ if(file_ref.isNull() || !(tag=file_ref.tag()))
+ {
+ std::wcerr << L"ERROR: Failed opening file." << std::endl;
+ return true;
+ }
+
+ for(;;)
+ {
+ if(!std::wcin)
+ {
+ std::wcerr << L"ERROR: invalid input data." << std::endl;
+ return true;
+ }
+
+ std::wstring tag_to_set;
+ getline(std::wcin, tag_to_set);
+ if(tag_to_set == L"")
+ {
+ break;
+ }
+
+ std::wstring value;
+ getline(std::wcin, value);
+
+ std::wcout << L"Setting " << tag_to_set <<
+ L" to " << value << std::endl;
+
+ if(tag_to_set == L"Title")
+ {
+ tag->setTitle(value);
+ }
+ else if(tag_to_set == L"Artist")
+ {
+ tag->setArtist(value);
+ }
+ else if(tag_to_set == L"Album")
+ {
+ tag->setAlbum(value);
+ }
+ else if(tag_to_set == L"Comment")
+ {
+ tag->setComment(value);
+ }
+ else if(tag_to_set == L"Genre")
+ {
+ tag->setGenre(value);
+ }
+ else if(tag_to_set == L"Year")
+ {
+ unsigned int ival;
+ if(value == L"")
+ {
+ ival=0;
+ }
+ else
+ {
+ std::wistringstream val(value);
+ val >> ival;
+ }
+ tag->setYear(ival);
+ }
+ else if(tag_to_set == L"Track")
+ {
+ unsigned int ival;
+ if(value == L"")
+ {
+ ival=0;
+ }
+ else
+ {
+ std::wistringstream val(value);
+ val >> ival;
+ }
+ tag->setTrack(ival);
+ }
+ else
+ {
+ std::wcerr << L"Unknown tag " << tag_to_set << std::endl;
+ return true;
+ }
+ }
+
+ if(!file_ref.save())
+ {
+ std::wcerr << L"Failed saving file." << std::endl;
+ return true;
+ }
+
+ return false;
+}
+
+int main(int const argc, char**const argv)
+{
+ std::locale loc("");
+ original_wcout_locale=std::wcout.imbue(loc);
+ std::wcin.imbue(loc);
+ std::locale::global(loc);
+
+ std::string action;
+ if(argc >= 2)
+ {
+ action=argv[1];
+ }
+
+ if(action == "--version")
+ {
+ std::wcout << version << std::endl;
+ return 0;
+ }
+ else if(action == "--genres")
+ {
+ using namespace TagLib;
+ StringList genres=ID3v1::genreList();
+ for(StringList::ConstIterator genre=genres.begin();
+ genre!=genres.end();
+ ++genre)
+ {
+ std::wcout << genre->toWString() << std::endl;
+ }
+ return 0;
+ }
+ else if(action == "--get" && argc == 3)
+ {
+ return get(argv[2]) ? 1 : 0;
+ }
+ else if(action == "--set" && argc == 3)
+ {
+ return set(argv[2]) ? 1 : 0;
+ }
+ else
+ {
+ std::wcerr <<
+ L"Usage: ampc_tagger [--version|--genres|--set file|--get file]" <<
+ std::endl;
+ return 1;
+ }
+}
+
+// Local Variables:
+// fill-column: 80
+// indent-tabs-mode: nil
+// End:
- [elpa] externals/ampc 84efcaa16d 044/111: * ampc: Bump version to 0.1.2., (continued)
- [elpa] externals/ampc 84efcaa16d 044/111: * ampc: Bump version to 0.1.2., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc bdb75f9ab9 051/111: * ampc.el (ampc-mode-map): Add checkboxes to the toggle menu items., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 3c81462de0 058/111: * ampc.el: Add tool-bar., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc c3c36d6a5e 060/111: * ampc.el: Add mouse support., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc f0462f2d8c 061/111: * ampc.el (ampc): Change the name of the internal communication buffer to, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc bb5ba9b2bb 068/111: * ampc.el: Add ampc-mini, a command to select the song to play via, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc effd06f0d8 072/111: * ampc.el: Add mouse support for playlist commands., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc cd2a3a93a6 074/111: * ampc.el: Doc simplifications., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 0941e8d180 079/111: * ampc.el: Make ampc synchronous., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 2b4cbc7db9 088/111: * ampc.el: Use tab-stop-list for tabulated lists., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 0bf832e424 092/111: * Add tagger. ampc-tagger.cpp: New file.,
Stefan Monnier <=
- [elpa] externals/ampc 5aae56a25d 101/111: * packages/ampc/ampc.el: Add proper file trailer., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 259d4f6363 103/111: * ampc/ampc.el: Fix up warnings and use cl-lib. Change maintainer, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc e045d5a2fa 102/111: Remove ampc, then re-add, since I cannot find it anywhere else, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc a98412698b 105/111: * ampc/ampc.el (ampc-views): Add "Search view", Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 4a2a2e642f 109/111: ; Prefer HTTPS to HTTP in URLs, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc b15e6985a9 054/111: The package of ampc is ampc itself, not GNU Emacs. All files changed., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 1dda527e59 055/111: * ampc.el (ampc-item-mode-map): Clarify docs in regards to the arguments of, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 1bc7428150 069/111: * ampc.el (ampc-handle-command): Handle mini-currentsong., Stefan Monnier, 2024/02/20
- [elpa] externals/ampc c41fde637d 073/111: * ampc.el (ampc-goto-current-song): Goto point-min if there is no song currently, Stefan Monnier, 2024/02/20
- [elpa] externals/ampc 8e34ab9150 082/111: * ampc.el (ampc-update-playlist, ampc-fill-status, ampc-configure-frame), Stefan Monnier, 2024/02/20