aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-18 17:11:17 -0500
committerCraig Jennings <c@cjennings.net>2026-07-18 17:11:17 -0500
commit31e062c574c81a4b9f4e9e72da2df3bbfbf4ef36 (patch)
treef82ad0d83b0845507a4af10f1e6f40cfaea5e2f1
parent41be5b20be6b08dbad884795614ddb02161108ad (diff)
downloaddotemacs-31e062c574c81a4b9f4e9e72da2df3bbfbf4ef36.tar.gz
dotemacs-31e062c574c81a4b9f4e9e72da2df3bbfbf4ef36.zip
feat(music): add a delete-playlist command
- D in the playlist buffer deletes a saved playlist. - Candidates span both m3u sources: local library and MPD dir. - A typed yes/no confirms deletion. - Deleting the loaded playlist's file clears its association.
-rw-r--r--modules/music-config.el35
-rw-r--r--tests/test-music-config--delete-playlist-file.el135
2 files changed, 169 insertions, 1 deletions
diff --git a/modules/music-config.el b/modules/music-config.el
index 9b4dc149..78ea49d9 100644
--- a/modules/music-config.el
+++ b/modules/music-config.el
@@ -591,6 +591,21 @@ Returns the full path to the selected file, or nil if cancelled."
(unless (string= choice "(Cancel)")
(cdr (assoc choice m3u-files)))))
+(defun cj/music--delete-playlist-file (path)
+ "Delete the playlist file at PATH.
+Signals a `user-error' when PATH is nil or missing. When the playlist
+buffer's associated file is PATH, the association is cleared (the in-memory
+queue is untouched). Refreshes the radio metadata cache since an .m3u just
+left the roots."
+ (unless (and path (file-exists-p path))
+ (user-error "Playlist file does not exist: %s"
+ (if path (file-name-nondirectory path) "nil")))
+ (delete-file path)
+ (with-current-buffer (cj/music--ensure-playlist-buffer)
+ (when (equal cj/music-playlist-file path)
+ (setq cj/music-playlist-file nil)))
+ (cj/music--refresh-radio-name-map))
+
;;; Commands: add/select
(defun cj/music--music-files-recursive (directory)
@@ -839,6 +854,23 @@ reloaded playlist keeps its display name and cover art."
(message "Reloaded playlist: %s" name)))
+(defun cj/music-delete-playlist ()
+ "Delete an .m3u playlist file after strong confirmation.
+Candidates are the playlists `cj/music-playlist-load' offers -- every
+directory in `cj/music-m3u-roots' (the local library and MPD's playlist
+dir). Deleting the loaded playlist's file keeps the in-memory queue but
+clears its file association."
+ (interactive)
+ (let ((file (cj/music--select-m3u-file "Delete playlist: ")))
+ (if (not file)
+ (message "Playlist deletion cancelled")
+ (unless (cj/confirm-strong (format "Delete playlist %s? "
+ (file-name-nondirectory file)))
+ (user-error "Aborted deleting playlist"))
+ (cj/music--delete-playlist-file file)
+ (message "Deleted playlist: %s" (file-name-nondirectory file)))))
+
+
(defun cj/music-playlist-edit ()
"Open the playlist's M3U file in other window, prompting to save if modified."
(interactive)
@@ -1347,7 +1379,7 @@ The rule uses a resize-safe :align-to span, not a hardcoded character count."
(funcall mode-indicator "x" "consume" cj/music-consume-mode) "\n"
(propertize "Keys " 'face 'cj/music-header-face)
(propertize " : " 'face 'cj/music-header-face)
- (propertize "a:add c:clear L:load v:save S:stop SPC:pause <>:skip ↑↓:move C-↑↓:reorder q:dismiss"
+ (propertize "a:add c:clear L:load v:save D:delete S:stop SPC:pause <>:skip ↑↓:move C-↑↓:reorder q:dismiss"
'face 'cj/music-keyhint-face) "\n"
(propertize "Radio " 'face 'cj/music-header-face)
(propertize " : " 'face 'cj/music-header-face)
@@ -1592,6 +1624,7 @@ unless fancy."
("c" . cj/music-playlist-clear)
("C" . cj/music-playlist-clear)
("L" . cj/music-playlist-load)
+ ("D" . cj/music-delete-playlist)
("E" . cj/music-playlist-edit)
("g" . cj/music-playlist-reload)
("v" . cj/music-playlist-save)
diff --git a/tests/test-music-config--delete-playlist-file.el b/tests/test-music-config--delete-playlist-file.el
new file mode 100644
index 00000000..ace51a4d
--- /dev/null
+++ b/tests/test-music-config--delete-playlist-file.el
@@ -0,0 +1,135 @@
+;;; test-music-config--delete-playlist-file.el --- Tests for playlist file deletion -*- coding: utf-8; lexical-binding: t; -*-
+;;
+;; Author: Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; Unit tests for cj/music--delete-playlist-file function.
+;; Tests the internal helper that removes a playlist's .m3u file,
+;; clears the playlist buffer's file association when it pointed at
+;; the deleted file, and refreshes the radio metadata cache.
+;;
+;; Test organization:
+;; - Normal Cases: Existing file is deleted
+;; - Boundary Cases: Association cleared only when it matches; cache refresh
+;; - Error Cases: Nil path, nonexistent path
+;;
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Stub missing dependencies before loading music-config
+(defvar-keymap cj/custom-keymap
+ :doc "Stub keymap for testing")
+
+;; Add EMMS elpa directory to load path for batch testing
+(let ((emms-dir (car (file-expand-wildcards
+ (expand-file-name "elpa/emms-*" user-emacs-directory)))))
+ (when emms-dir
+ (add-to-list 'load-path emms-dir)))
+
+(require 'emms)
+(require 'emms-playlist-mode)
+(require 'music-config)
+
+;;; Test helpers
+
+(defun test-delete-playlist--setup ()
+ "Create test base dir and ensure playlist buffer exists."
+ (cj/create-test-base-dir)
+ (let ((buf (get-buffer-create cj/music-playlist-buffer-name)))
+ (with-current-buffer buf
+ (emms-playlist-mode)
+ (setq emms-playlist-buffer-p t))
+ (setq emms-playlist-buffer buf)
+ buf))
+
+(defun test-delete-playlist--teardown ()
+ "Clean up test playlist buffer and temp files."
+ (when-let ((buf (get-buffer cj/music-playlist-buffer-name)))
+ (with-current-buffer buf
+ (setq cj/music-playlist-file nil))
+ (kill-buffer buf))
+ (cj/delete-test-base-dir))
+
+;;; Normal Cases
+
+(ert-deftest test-music-config--delete-playlist-file-normal-removes-file ()
+ "Normal: an existing playlist file is deleted from disk."
+ (test-delete-playlist--setup)
+ (unwind-protect
+ (let ((file (cj/create-temp-test-file-with-content
+ "#EXTM3U\n/tmp/song.mp3\n" "playlist.m3u")))
+ (cj/music--delete-playlist-file file)
+ (should-not (file-exists-p file)))
+ (test-delete-playlist--teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-music-config--delete-playlist-file-boundary-clears-matching-association ()
+ "Boundary: deleting the associated playlist file clears the association."
+ (test-delete-playlist--setup)
+ (unwind-protect
+ (let ((file (cj/create-temp-test-file-with-content
+ "#EXTM3U\n" "playlist.m3u")))
+ (with-current-buffer (get-buffer cj/music-playlist-buffer-name)
+ (setq cj/music-playlist-file file))
+ (cj/music--delete-playlist-file file)
+ (with-current-buffer (get-buffer cj/music-playlist-buffer-name)
+ (should-not cj/music-playlist-file)))
+ (test-delete-playlist--teardown)))
+
+(ert-deftest test-music-config--delete-playlist-file-boundary-keeps-other-association ()
+ "Boundary: deleting a different file leaves the association untouched."
+ (test-delete-playlist--setup)
+ (unwind-protect
+ (let ((doomed (cj/create-temp-test-file-with-content
+ "#EXTM3U\n" "doomed.m3u"))
+ (kept (cj/create-temp-test-file-with-content
+ "#EXTM3U\n" "kept.m3u")))
+ (with-current-buffer (get-buffer cj/music-playlist-buffer-name)
+ (setq cj/music-playlist-file kept))
+ (cj/music--delete-playlist-file doomed)
+ (should (file-exists-p kept))
+ (with-current-buffer (get-buffer cj/music-playlist-buffer-name)
+ (should (equal cj/music-playlist-file kept))))
+ (test-delete-playlist--teardown)))
+
+(ert-deftest test-music-config--delete-playlist-file-boundary-refreshes-radio-cache ()
+ "Boundary: deletion clears the cached radio metadata."
+ (test-delete-playlist--setup)
+ (unwind-protect
+ (let ((file (cj/create-temp-test-file-with-content
+ "#EXTM3U\n" "playlist.m3u")))
+ (setq cj/music--radio-metadata-cache '(("stale" . "entry")))
+ (cj/music--delete-playlist-file file)
+ (should-not cj/music--radio-metadata-cache))
+ (test-delete-playlist--teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-music-config--delete-playlist-file-error-nil-path ()
+ "Error: nil path signals user-error."
+ (test-delete-playlist--setup)
+ (unwind-protect
+ (should-error (cj/music--delete-playlist-file nil) :type 'user-error)
+ (test-delete-playlist--teardown)))
+
+(ert-deftest test-music-config--delete-playlist-file-error-nonexistent-path ()
+ "Error: a path that does not exist signals user-error and touches nothing."
+ (test-delete-playlist--setup)
+ (unwind-protect
+ (let ((kept (cj/create-temp-test-file-with-content
+ "#EXTM3U\n" "kept.m3u")))
+ (with-current-buffer (get-buffer cj/music-playlist-buffer-name)
+ (setq cj/music-playlist-file kept))
+ (should-error
+ (cj/music--delete-playlist-file
+ (expand-file-name "no-such.m3u" cj/test-base-dir))
+ :type 'user-error)
+ (with-current-buffer (get-buffer cj/music-playlist-buffer-name)
+ (should (equal cj/music-playlist-file kept))))
+ (test-delete-playlist--teardown)))
+
+(provide 'test-music-config--delete-playlist-file)
+;;; test-music-config--delete-playlist-file.el ends here