├── README.md ├── helm-emms.el └── images └── helm-emms-directories.jpg /README.md: -------------------------------------------------------------------------------- 1 | [![MELPA](https://melpa.org/packages/helm-emms-badge.svg)](https://melpa.org/#/helm-emms) 2 | [![MELPA Stable](https://stable.melpa.org/packages/helm-emms-badge.svg)](https://stable.melpa.org/#/helm-emms) 3 | 4 | 5 | # helm-emms 6 | 7 | Basic helm interface to [emms](https://www.gnu.org/software/emms/) 8 | 9 | ![directories](images/helm-emms-directories.jpg) 10 | 11 | # Dependencies 12 | 13 | This package require `emms` and `helm`. 14 | 15 | # Install 16 | 17 | You can get it from Melpa, otherwise when installing from source, 18 | ensure all dependencies are installed and working, then put 19 | "helm-emms.el" somewhere in your load-path, compile it and require 20 | `helm-emms`, or even better use `(autoload 'helm-emms "helm-emms" nil t)`. 21 | 22 | # Usage 23 | 24 | Ensure first `emms` is working as expected, and configure it as described at 25 | [Emms home page](https://www.gnu.org/software/emms/) 26 | 27 | Then M-x helm-emms. 28 | 29 | That's all, have fun. 30 | -------------------------------------------------------------------------------- /helm-emms.el: -------------------------------------------------------------------------------- 1 | ;;; helm-emms.el --- Emms for Helm. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2012 ~ 2014 Thierry Volpiatto 4 | 5 | ;; Author: Thierry Volpiatto 6 | ;; Version: 1.3 7 | ;; Package-Requires: ((helm "1.5") (emms "6.0") (cl-lib "0.5") (emacs "24.1")) 8 | ;; Keywords: multimedia, emms 9 | ;; URL: https://github.com/emacs-helm/helm-emms 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; This package provides a basic helm interface to emms, the Emacs Multimedia 27 | ;; System. This interface is accessible via the interactive `helm-emms' command 28 | ;; and provides direct access to music directories, individual music files, and 29 | ;; EMMS streams. See the `helm-emms' customization group for available 30 | ;; customizations, or simply call `helm-emms' to get started. 31 | 32 | ;;; Code: 33 | 34 | (require 'cl-lib) 35 | (require 'helm) 36 | (require 'helm-adaptive) 37 | (require 'helm-utils) 38 | (require 'emms) 39 | 40 | (declare-function emms-play-url "ext:emms-streams") 41 | (declare-function emms-playlist-tracks-in-region "ext:emms" (beg end)) 42 | (declare-function emms-playlist-first "ext:emms") 43 | (declare-function emms-playlist-mode-play-smart "ext:emms-playlist-mode") 44 | (declare-function emms-playlist-new "ext:emms" (&optional name)) 45 | (declare-function emms-player-simple-regexp "ext:emms-player-simple" (&rest extensions)) 46 | (declare-function emms-browser-make-cover "ext:emms-browser") 47 | (declare-function emms-browser-get-cover-from-path "ext:emms-browser") 48 | (declare-function emms-playlist-current-clear "ext:emms") 49 | (declare-function emms-play-directory "ext:emms-source-file") 50 | (declare-function emms-play-file "ext:emms-source-file") 51 | (declare-function emms-add-directory "ext:emms-source-file") 52 | (declare-function emms-add-file "ext:emms-source-file") 53 | (declare-function emms-source-playlist-parse-native "ext:emms-source-playlist") 54 | (defvar emms-player-playing-p) 55 | (defvar emms-browser-default-covers) 56 | (defvar emms-source-file-default-directory) 57 | (defvar emms-track-description-function) 58 | (defvar emms-cache-db) 59 | (defvar emms-playlist-buffer) 60 | (defvar emms-streams-file) 61 | (defvar emms-streams-built-in-list) 62 | 63 | 64 | (defgroup helm-emms nil 65 | "Predefined configurations for `helm.el'." 66 | :group 'helm) 67 | 68 | (defface helm-emms-playlist 69 | '((t (:foreground "Springgreen4" :underline t))) 70 | "Face used for tracks and directories in current emms playlist." 71 | :group 'helm-emms) 72 | 73 | (defcustom helm-emms-use-track-description-function nil 74 | "If non-nil, use `emms-track-description-function'. 75 | If you have defined a custom function for track descriptions, you 76 | may want to use it in helm-emms as well." 77 | :group 'helm-emms 78 | :type 'boolean) 79 | 80 | (defcustom helm-emms-default-sources '(helm-source-emms-dired 81 | helm-source-emms-files 82 | helm-source-emms-streams) 83 | "The default source for `helm-emms'." 84 | :group 'helm-emms 85 | :type 'boolean) 86 | 87 | (defcustom helm-emms-music-extensions '("mp3" "ogg" "flac" "wav" "wma") 88 | "Music files default extensions used by helm to find your music." 89 | :group 'helm-emms 90 | :type '(repeat string)) 91 | 92 | (defcustom helm-emms-directory-files-recursive-fn 'helm-emms-walk-directory 93 | "The function used to initially parse the user music directory. 94 | It takes one argument DIR. The default function 95 | `helm-emms-walk-directory' use lisp to recursively find all directories 96 | which may be slow on large music directories." 97 | :group 'helm-emms 98 | :type '(choice (function :tag "Native" helm-emms-walk-directory) 99 | (function :tag "System \"find\" (faster)" helm-emms-walk-directory-with-find)) ) 100 | 101 | (defcustom helm-emms-find-program "find" 102 | "The \"find\" program. 103 | This is notably used by `helm-emms-walk-directory-with-find'." 104 | :group 'helm-emms 105 | :type 'string) 106 | 107 | (defcustom helm-emms-dired-directories (list emms-source-file-default-directory) 108 | "Directories scanned by `helm-source-emms-dired'." 109 | :group 'helm-emms 110 | :type '(repeat string)) 111 | 112 | (defcustom helm-emms-streams-list nil 113 | "List of streams for emms streams. 114 | When nil use `emms-streams-built-in-list' entries as default." 115 | :type '(alist :key-type string :value-type string) 116 | :group 'helm-emms) 117 | 118 | (defun helm-emms--read-streams-from-streams-file () 119 | "Read streams from `emms-streams-file' as track list. 120 | Return nil if `emms-streams-file' is nil or not a readable file." 121 | ;; `emms-streams-file' is a native EMMS playlist of streams, read it as it is. 122 | (when (and emms-streams-file 123 | (file-readable-p emms-streams-file)) 124 | (car (read-from-string (with-temp-buffer 125 | (insert-file-contents emms-streams-file) 126 | (buffer-substring (point-min) (point-max))))))) 127 | 128 | (defun helm-emms-stream-setup-default-alist () 129 | "Setup default value of `helm-emms-streams-list'. 130 | Source `emms-streams-file' if non-nil to retrieve default 131 | entries, or use `emms-streams-built-in-list'." 132 | (cl-loop for elm in (or (helm-emms--read-streams-from-streams-file) 133 | emms-streams-built-in-list) 134 | for assoc = (assoc-default 'metadata elm) 135 | collect (cons (car assoc) (cadr assoc)))) 136 | 137 | (defvar helm-source-emms-streams 138 | (helm-build-sync-source "Emms Streams" 139 | :init (lambda () 140 | (require 'emms-streams) 141 | (unless helm-emms-streams-list 142 | (setq helm-emms-streams-list 143 | (helm-emms-stream-setup-default-alist)))) 144 | :candidates (lambda () helm-emms-streams-list) 145 | :action '(("Play" . emms-play-url) 146 | ("Add new stream (C-u play)" . helm-emms-add-new-stream) 147 | ("Delete stream(s)" . helm-emms-delete-stream)) 148 | :filtered-candidate-transformer 'helm-adaptive-sort 149 | :group 'helm-emms)) 150 | 151 | (defun helm-emms-add-new-stream (_candidate) 152 | "Action to add a new stream to `helm-emms-streams-list'." 153 | (let ((count 0) 154 | (prefarg (or helm-current-prefix-arg 155 | current-prefix-arg))) 156 | (cl-labels ((record 157 | () 158 | (let ((name (read-string "Stream name: ")) 159 | (url (read-string "Stream url: "))) 160 | (customize-save-variable 'helm-emms-streams-list 161 | (append (list (cons name url)) 162 | helm-emms-streams-list)) 163 | (cl-incf count) 164 | (when (y-or-n-p "Add a new stream? ") 165 | (record)) 166 | (when prefarg 167 | (emms-play-url url))))) 168 | (record)) 169 | (message "%s new stream(s) added" count))) 170 | 171 | (defun helm-emms-delete-stream (_candidate) 172 | "Delete marked streams." 173 | (cl-loop for url in (helm-marked-candidates) 174 | do (customize-save-variable 'helm-emms-streams-list 175 | (delete (rassoc url helm-emms-streams-list) 176 | helm-emms-streams-list)))) 177 | 178 | ;; Don't forget to set `helm-emms-dired-directories'. 179 | (defvar helm-emms--dired-cache nil) 180 | (defvar helm-emms--directories-added-to-playlist nil) 181 | (defvar helm-source-emms-dired 182 | (helm-build-sync-source "Music Directories" 183 | :init (lambda () 184 | (cl-assert helm-emms-dired-directories nil 185 | "Incorrect EMMS setup please setup `helm-emms-dired-directories' variable") 186 | ;; User may have a symlinked directory to an external 187 | ;; drive or whatever (Issue #11). 188 | (setq helm-emms--dired-cache 189 | (cl-loop for dir in (mapcar #'file-truename helm-emms-dired-directories) 190 | when (file-exists-p dir) 191 | append (funcall helm-emms-directory-files-recursive-fn dir))) 192 | (add-hook 'emms-playlist-cleared-hook 193 | 'helm-emms--clear-playlist-directories)) 194 | :candidates 'helm-emms--dired-cache 195 | :persistent-action 'helm-emms-dired-persistent-action 196 | :persistent-help "Play or add directory to playlist (C-u clear playlist)" 197 | :action 198 | `(("Play Directories" 199 | . ,(lambda (_directory) 200 | (emms-stop) 201 | (emms-playlist-current-clear) 202 | (cl-loop with mkds = (helm-marked-candidates) 203 | with current-prefix-arg = nil 204 | with helm-current-prefix-arg = nil 205 | for dir in mkds 206 | do (helm-emms-add-directory-to-playlist dir)))) 207 | ("Add directories to playlist (C-u clear playlist)" 208 | . ,(lambda (_directory) 209 | (let ((mkds (helm-marked-candidates))) 210 | (cl-loop for dir in mkds 211 | do (helm-emms-add-directory-to-playlist dir))))) 212 | ("Open dired in file's directory" . ,(lambda (directory) 213 | (helm-open-dired directory)))) 214 | :filtered-candidate-transformer '(helm-adaptive-sort helm-emms-dired-transformer) 215 | :group 'helm-emms)) 216 | 217 | (defun helm-emms-walk-directory (dir) 218 | "The default function to recursively find directories in music directory." 219 | (helm-walk-directory dir :directories 'only :path 'full)) 220 | 221 | (defun helm-emms-walk-directory-with-find (dir) 222 | "Like `helm-emms-walk-directory' but uses the \"find\" external command. 223 | The path to the command is set in `helm-emms-find-program'. 224 | 225 | Warning: This won't work with directories containing a line break." 226 | (process-lines helm-emms-find-program dir "-mindepth" "1" "-type" "d")) 227 | 228 | (defun helm-emms--clear-playlist-directories () 229 | (setq helm-emms--directories-added-to-playlist nil)) 230 | 231 | (defun helm-emms-dired-persistent-action (directory) 232 | "Play or add DIRECTORY files to emms playlist. 233 | 234 | If emms is playing add all files of DIRECTORY to playlist, 235 | otherwise play directory." 236 | (if emms-player-playing-p 237 | (progn (emms-add-directory directory) 238 | (message "All files from `%s' added to playlist" 239 | (helm-basename directory))) 240 | (emms-play-directory directory)) 241 | (push directory helm-emms--directories-added-to-playlist) 242 | (helm-force-update)) 243 | 244 | (defun helm-emms-add-directory-to-playlist (directory) 245 | "Add all files in DIRECTORY to emms playlist." 246 | (let ((files (helm-emms-directory-files directory t))) 247 | (helm-emms-add-files-to-playlist files) 248 | (push directory helm-emms--directories-added-to-playlist))) 249 | 250 | (defun helm-emms-add-files-to-playlist (files) 251 | "Add FILES list to playlist. 252 | 253 | If a prefix arg is provided clear previous playlist." 254 | (with-current-emms-playlist 255 | (when (or helm-current-prefix-arg current-prefix-arg) 256 | (emms-stop) 257 | (emms-playlist-current-clear)) 258 | (dolist (f files) (emms-add-file f)) 259 | (unless emms-player-playing-p 260 | (helm-emms-play-current-playlist)))) 261 | 262 | (defun helm-emms-directory-files (directory &optional full nosort) 263 | "List files in DIRECTORY retaining only music files. 264 | 265 | Returns nil when no music files are found." 266 | (directory-files 267 | directory full 268 | (format ".*%s" (apply #'emms-player-simple-regexp 269 | helm-emms-music-extensions)) 270 | nosort)) 271 | 272 | (defun helm-emms-dired-transformer (candidates _source) 273 | (cl-loop with files 274 | for d in candidates 275 | for cover = (pcase (emms-browser-get-cover-from-path d 'small) 276 | ((and c (guard (and c (file-exists-p c)))) c) 277 | (_ (car emms-browser-default-covers))) 278 | for inplaylist = (member d helm-emms--directories-added-to-playlist) 279 | for bn = (helm-basename d) 280 | when (setq files (helm-emms-directory-files d)) collect 281 | (cons (let* ((cover-image (and cover (emms-browser-make-cover cover))) 282 | (title (if cover (propertize d 'display bn) d)) 283 | (title-with-face (if inplaylist 284 | (propertize title 'face 'helm-emms-playlist) 285 | title))) 286 | (propertize (concat cover-image title-with-face) 287 | 'help-echo (mapconcat 'identity files "\n"))) 288 | d))) 289 | 290 | (defvar helm-emms-current-playlist nil) 291 | 292 | (defun helm-emms-files-modifier (candidates _source) 293 | (cl-loop for (name . file) in candidates 294 | for curtrack = (emms-playlist-current-selected-track) 295 | for playing-file = (assoc-default 'name curtrack) 296 | if (member file helm-emms-current-playlist) 297 | collect (cons 298 | (if (string= playing-file file) 299 | (propertize name 'face 'emms-browser-track-face) 300 | (propertize name 'face 'helm-emms-playlist)) 301 | file) 302 | into currents 303 | else collect (cons name file) into others 304 | finally return (append 305 | (cl-loop for i in helm-emms-current-playlist 306 | when (rassoc i currents) 307 | collect it) 308 | others))) 309 | 310 | (defun helm-emms-play-current-playlist () 311 | "Play current playlist." 312 | (emms-playlist-first) 313 | (emms-playlist-mode-play-smart)) 314 | 315 | (defun helm-emms-set-current-playlist () 316 | (when (or (not emms-playlist-buffer) 317 | (not (buffer-live-p emms-playlist-buffer))) 318 | (setq emms-playlist-buffer (emms-playlist-new))) 319 | (setq helm-emms-current-playlist 320 | (with-current-buffer emms-playlist-buffer 321 | (save-excursion 322 | (goto-char (point-min)) 323 | (cl-loop for i in (reverse (emms-playlist-tracks-in-region 324 | (point-min) (point-max))) 325 | when (assoc-default 'name i) 326 | collect it))))) 327 | 328 | (defvar helm-source-emms-files 329 | (helm-build-sync-source "Emms files" 330 | :init 'helm-emms-set-current-playlist 331 | :candidates (lambda () 332 | (cl-loop for v being the hash-values in emms-cache-db 333 | for name = (assoc-default 'name v) 334 | for artist = (or (assoc-default 'info-artist v) "unknown") 335 | for genre = (or (assoc-default 'info-genre v) "unknown") 336 | for tracknum = (or (assoc-default 'info-tracknumber v) "unknown") 337 | for song = (or (assoc-default 'info-title v) "unknown") 338 | for info = (if helm-emms-use-track-description-function 339 | (funcall emms-track-description-function v) 340 | (concat artist " - " genre " - " tracknum ": " song)) 341 | unless (string-match "^\\(http\\|mms\\):" name) 342 | collect (cons info name))) 343 | :filtered-candidate-transformer 'helm-emms-files-modifier 344 | :candidate-number-limit 9999 345 | :persistent-action #'helm-emms-files-persistent-action 346 | :persistent-help "Play file(s) or add to playlist" 347 | :action `(("Play file(s)" 348 | . ,(lambda (_candidate) 349 | (emms-play-file (car (helm-marked-candidates))) 350 | (helm-emms-add-files-to-playlist 351 | (cdr (helm-marked-candidates))))) 352 | ("Add to playlist (C-u clear current and play)" 353 | . ,(lambda (_candidate) 354 | (helm-emms-add-files-to-playlist 355 | (helm-marked-candidates)))) 356 | ("Delete tracks from playlist" 357 | . helm-emms-delete-tracks)) 358 | :group 'helm-emms)) 359 | 360 | (defun helm-emms-goto-track (candidate) 361 | (let ((track (emms-track 'file (expand-file-name candidate)))) 362 | (with-current-buffer emms-playlist-buffer 363 | (goto-char (point-min)) 364 | (when (re-search-forward 365 | (format "%s" 366 | (regexp-quote 367 | (helm-basename (assoc-default 'name track) t))) 368 | nil t) 369 | (forward-line 0))))) 370 | 371 | (defun helm-emms-delete-tracks (_candidate) 372 | (with-current-buffer emms-playlist-buffer 373 | (let ((inhibit-read-only t)) 374 | (cl-loop for track in (helm-marked-candidates) 375 | do 376 | (when (helm-emms-goto-track track) 377 | (emms-playlist-simple-delete-track))) 378 | (helm-emms-delete-blank-lines)))) 379 | 380 | (defun helm-emms-delete-blank-lines () 381 | (save-excursion 382 | (goto-char (point-min)) 383 | (while (and (re-search-forward "^$" nil t) (not (eobp))) 384 | (delete-blank-lines)))) 385 | 386 | (defun helm-emms-files-persistent-action (candidate) 387 | (let ((recenter t)) 388 | (if (or emms-player-playing-p 389 | (not (helm-emms-playlist-empty-p))) 390 | (with-current-emms-playlist 391 | (let (track) 392 | (save-excursion 393 | (goto-char (point-min)) 394 | (while (and (not (string= 395 | candidate 396 | (setq track 397 | (assoc-default 398 | 'name (emms-playlist-track-at 399 | (point)))))) 400 | (not (eobp))) 401 | (forward-line 1)) 402 | (if (string= candidate track) 403 | (progn 404 | (setq recenter (with-helm-window 405 | (count-lines (window-start) (point)))) 406 | (emms-playlist-select (point)) 407 | (when emms-player-playing-p 408 | (emms-stop)) 409 | (emms-start)) 410 | (emms-add-file candidate))))) 411 | (emms-play-file candidate)) 412 | (helm-force-update nil recenter))) 413 | 414 | (defun helm-emms-playlist-empty-p () 415 | (with-current-emms-playlist 416 | (null (emms-playlist-track-at (point))))) 417 | 418 | ;;;###autoload 419 | (defun helm-emms () 420 | "Preconfigured `helm' for emms sources." 421 | (interactive) 422 | (helm :sources helm-emms-default-sources 423 | :buffer "*Helm Emms*")) 424 | 425 | 426 | (provide 'helm-emms) 427 | 428 | ;; Local Variables: 429 | ;; byte-compile-warnings: (not cl-functions obsolete) 430 | ;; coding: utf-8 431 | ;; indent-tabs-mode: nil 432 | ;; End: 433 | 434 | ;;; helm-emms.el ends here 435 | -------------------------------------------------------------------------------- /images/helm-emms-directories.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-helm/helm-emms/2e8b53a46b32f956efacfda2dbf3f1b0db867472/images/helm-emms-directories.jpg --------------------------------------------------------------------------------