| 1 | ;;; emms-source-playlist.el --- EMMS sources from playlist files -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;; Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, |
| 4 | ;; 2009, 2023 Free Software Foundation, Inc. |
| 5 | |
| 6 | ;; Author: Jorgen Schäfer <forcer@forcix.cx> |
| 7 | ;; Keywords: emms, mp3, mpeg, multimedia |
| 8 | |
| 9 | ;; This file is part of EMMS. |
| 10 | |
| 11 | ;; EMMS 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, or (at your option) |
| 14 | ;; any later version. |
| 15 | ;; |
| 16 | ;; EMMS 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 EMMS; see the file COPYING. If not, write to the |
| 23 | ;; Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, |
| 24 | ;; Boston, MA 02110-1301, USA. |
| 25 | |
| 26 | ;;; Commentary: |
| 27 | |
| 28 | ;; This file contains track sources for EMMS which read playlist |
| 29 | ;; files. EMMS' own playlist files are supported as well as .m3u and |
| 30 | ;; .pls files. |
| 31 | |
| 32 | ;;; Code: |
| 33 | |
| 34 | ;; Version control |
| 35 | (defvar emms-source-playlist-version "0.5 $Revision: 1.30 $" |
| 36 | "emms-source-playlist.el version string") |
| 37 | ;; $Id: emms-source-file.el,v 1.30 2005/08/11 06:16:15 yonirabkin Exp $ |
| 38 | |
| 39 | (require 'emms) |
| 40 | (require 'emms-source-file) |
| 41 | |
| 42 | (defcustom emms-source-playlist-formats '(native pls m3u) |
| 43 | "*A list of playlist formats. |
| 44 | Each entry must have at least three corresponding functions. |
| 45 | |
| 46 | First, a function named `emms-source-playlist-FORMAT-p' which |
| 47 | returns non-nil if the current buffer is of the type FORMAT. It |
| 48 | is called with no arguments. |
| 49 | |
| 50 | Second, a function named `emms-source-playlist-parse-FORMAT' |
| 51 | which parses the current buffer into tracks. It is called with |
| 52 | no arguments. |
| 53 | |
| 54 | Third, a function named `emms-source-playlist-unparse-FORMAT' |
| 55 | which creates an output file in the type FORMAT that contains the |
| 56 | tracks of a playlist buffer. It is called with two arguments: |
| 57 | The playlist buffer and the file buffer. |
| 58 | |
| 59 | It is also recommended to have a function named |
| 60 | `emms-source-playlist-FORMAT-files' which returns a list of the |
| 61 | files contained in the playlist." |
| 62 | :type '(repeat (symbol :tag "Format")) |
| 63 | :group 'emms) |
| 64 | |
| 65 | (defcustom emms-source-playlist-default-format nil |
| 66 | "*The default format to use for saving playlists. |
| 67 | If this is nil, you will be prompted for a format to use." |
| 68 | :type '(choice (const :tag "Prompt each time" nil) |
| 69 | (const :tag "Native" native) |
| 70 | (const :tag "m3u" m3u) |
| 71 | (const :tag "pls" pls) |
| 72 | (symbol :tag "Other")) |
| 73 | :group 'emms) |
| 74 | |
| 75 | (defcustom emms-source-playlist-ask-before-overwrite t |
| 76 | "*Ask before saving over an existing playlist. |
| 77 | If this is nil, existing playlists will be quietly overwritten." |
| 78 | :type 'boolean |
| 79 | :group 'emms) |
| 80 | |
| 81 | (defvar emms-source-playlist-native-header-line |
| 82 | ";;; This is an EMMS playlist file" |
| 83 | "Line which identifies a native emms playlist.") |
| 84 | |
| 85 | ;;; General playlist |
| 86 | |
| 87 | (defsubst emms-source-playlist-p-sym (format) |
| 88 | (intern (concat "emms-source-playlist-" (symbol-name format) "-p"))) |
| 89 | |
| 90 | (defsubst emms-source-playlist-parse-sym (format) |
| 91 | (intern (concat "emms-source-playlist-parse-" (symbol-name format)))) |
| 92 | |
| 93 | (defsubst emms-source-playlist-unparse-sym (format) |
| 94 | (intern (concat "emms-source-playlist-unparse-" (symbol-name format)))) |
| 95 | |
| 96 | (defsubst emms-source-playlist-files-sym (format) |
| 97 | (intern (concat "emms-source-playlist-" (symbol-name format) "-files"))) |
| 98 | |
| 99 | (defun emms-source-playlist-p (format &optional parse-files) |
| 100 | (let ((sym (emms-source-playlist-p-sym format))) |
| 101 | (when (and (functionp sym) |
| 102 | (or (not parse-files) |
| 103 | (functionp (emms-source-playlist-files-sym format)))) |
| 104 | (funcall sym)))) |
| 105 | |
| 106 | (defun emms-source-playlist-parse (format file) |
| 107 | (funcall (emms-source-playlist-parse-sym format) file)) |
| 108 | |
| 109 | (defun emms-source-playlist-unparse (format playlist-buf file-buf) |
| 110 | (funcall (emms-source-playlist-unparse-sym format) playlist-buf file-buf)) |
| 111 | |
| 112 | (defun emms-source-playlist-files (format) |
| 113 | (let ((sym (emms-source-playlist-files-sym format))) |
| 114 | (if (functionp sym) |
| 115 | (funcall sym) |
| 116 | (error "The `%s' format cannot parse files from a playlist" format)))) |
| 117 | |
| 118 | (defvar emms-source-playlist-format-history nil |
| 119 | "List of recently-entered formats; used by `emms-playlist-save'.") |
| 120 | |
| 121 | (defun emms-source-playlist-read-format () |
| 122 | "Read a playlist format from the user. |
| 123 | If `emms-source-playlist-default-format' is non-nil, use it |
| 124 | instead of prompting the user." |
| 125 | (or emms-source-playlist-default-format |
| 126 | (let ((format |
| 127 | (emms-completing-read |
| 128 | (concat "Playlist format: (default: " |
| 129 | (if emms-source-playlist-format-history |
| 130 | (car emms-source-playlist-format-history) |
| 131 | "native") |
| 132 | ") ") |
| 133 | (mapcar #'symbol-name emms-source-playlist-formats) |
| 134 | nil nil nil 'emms-source-playlist-format-history |
| 135 | (if emms-source-playlist-format-history |
| 136 | (car emms-source-playlist-format-history) |
| 137 | "native")))) |
| 138 | ;; Sometimes the completion function can put partial results |
| 139 | ;; onto the history, so pop the last one off and include the |
| 140 | ;; completed version instead. |
| 141 | (setq emms-source-playlist-format-history |
| 142 | (cons format |
| 143 | (cdr emms-source-playlist-format-history))) |
| 144 | (intern format)))) |
| 145 | |
| 146 | (defun emms-playlist-save (format file) |
| 147 | "Store the current playlist to FILE as the type FORMAT. |
| 148 | The default format is specified by `emms-source-playlist-default-format'." |
| 149 | (interactive (list (emms-source-playlist-read-format) |
| 150 | (read-file-name "Store as: " |
| 151 | emms-source-file-default-directory |
| 152 | emms-source-file-default-directory |
| 153 | nil))) |
| 154 | (if (or (eq emms-playlist-buffer (current-buffer)) |
| 155 | (and (not (eq emms-playlist-buffer (current-buffer))) |
| 156 | (y-or-n-p |
| 157 | (format "Current playlist buffer (%s) is not the one you are visiting (%s). Save anyway?" |
| 158 | emms-playlist-buffer (current-buffer))))) |
| 159 | (with-temp-buffer |
| 160 | (emms-source-playlist-unparse format |
| 161 | (with-current-emms-playlist |
| 162 | (current-buffer)) |
| 163 | (current-buffer)) |
| 164 | (let ((backup-inhibited t)) |
| 165 | (write-file file emms-source-playlist-ask-before-overwrite))) |
| 166 | (message "aborting save"))) |
| 167 | |
| 168 | (defun emms-source-playlist-determine-format (&optional parse-files) |
| 169 | "Determine the playlist format of the current buffer. |
| 170 | If PARSE-FILES is specified, the given format must be able to |
| 171 | return a list of the files contained in the playlist." |
| 172 | (catch 'return |
| 173 | (let ((formats emms-source-playlist-formats)) |
| 174 | (while formats |
| 175 | (when (emms-source-playlist-p (car formats) parse-files) |
| 176 | (throw 'return (car formats))) |
| 177 | (setq formats (cdr formats)))))) |
| 178 | |
| 179 | ;;;###autoload (autoload 'emms-play-playlist "emms-source-playlist" nil t) |
| 180 | ;;;###autoload (autoload 'emms-add-playlist "emms-source-playlist" nil t) |
| 181 | (define-emms-source playlist (file) |
| 182 | "An EMMS source for playlists. |
| 183 | See `emms-source-playlist-formats' for a list of supported formats." |
| 184 | (interactive (list (read-file-name "Playlist file: " |
| 185 | emms-source-file-default-directory |
| 186 | emms-source-file-default-directory |
| 187 | t))) |
| 188 | (mapc #'emms-playlist-insert-track |
| 189 | (with-temp-buffer |
| 190 | (emms-insert-file-contents file) |
| 191 | (goto-char (point-min)) |
| 192 | (let ((format (emms-source-playlist-determine-format))) |
| 193 | (if format |
| 194 | (emms-source-playlist-parse format file) |
| 195 | (error "Not a recognized playlist format")))))) |
| 196 | |
| 197 | ;;; Emms native playlists |
| 198 | |
| 199 | ;; An Emms native playlist file starts with the contents of |
| 200 | ;; `emms-source-playlist-native-header-line' and is followed by |
| 201 | ;; tracks in sexp format. |
| 202 | |
| 203 | (defun emms-source-playlist-native-p () |
| 204 | "Return non-nil if the current buffer contains a native EMMS playlist." |
| 205 | (save-excursion |
| 206 | (goto-char (point-min)) |
| 207 | (looking-at (concat "^" emms-source-playlist-native-header-line)))) |
| 208 | |
| 209 | (defun emms-source-playlist-parse-native (file) |
| 210 | "Parse the native EMMS playlist in the current buffer." |
| 211 | (ignore file) |
| 212 | (let ((tracks (save-excursion |
| 213 | (goto-char (point-min)) |
| 214 | (read (current-buffer))))) |
| 215 | (mapc (lambda (track) |
| 216 | (when (fboundp emms-cache-set-function) |
| 217 | (funcall emms-cache-set-function |
| 218 | (emms-track-type track) |
| 219 | (emms-track-name track) |
| 220 | track))) |
| 221 | tracks) |
| 222 | tracks)) |
| 223 | |
| 224 | (defun emms-source-playlist-unparse-native (in out) |
| 225 | "Unparse a native playlist from IN to OUT. |
| 226 | IN should be a buffer with a EMMS playlist in it. |
| 227 | OUT should be the buffer where tracks are stored in the native EMMS format." |
| 228 | (with-current-buffer in ;; Don't modify the position |
| 229 | (save-excursion ;; in the IN buffer |
| 230 | (with-current-buffer out |
| 231 | (insert emms-source-playlist-native-header-line |
| 232 | " Play it with M-x emms-play-playlist\n") |
| 233 | (insert "(")) |
| 234 | (let ((firstp t)) |
| 235 | (goto-char (point-min)) |
| 236 | (emms-walk-tracks |
| 237 | (let ((track (emms-playlist-track-at (point)))) |
| 238 | (with-current-buffer out |
| 239 | (if (not firstp) |
| 240 | (insert "\n ") |
| 241 | (setq firstp nil)) |
| 242 | (prin1 track (current-buffer)))))) |
| 243 | (with-current-buffer out |
| 244 | (insert ")\n"))))) |
| 245 | |
| 246 | ;;;###autoload (autoload 'emms-play-native-playlist "emms-source-playlist" nil t) |
| 247 | ;;;###autoload (autoload 'emms-add-native-playlist "emms-source-playlist" nil t) |
| 248 | (define-emms-source native-playlist (file) |
| 249 | "An EMMS source for a native EMMS playlist file." |
| 250 | (interactive (list (read-file-name "Playlist file: " |
| 251 | emms-source-file-default-directory |
| 252 | emms-source-file-default-directory |
| 253 | t))) |
| 254 | (mapc #'emms-playlist-insert-track |
| 255 | (with-temp-buffer |
| 256 | (emms-insert-file-contents file) |
| 257 | (goto-char (point-min)) |
| 258 | (when (not (emms-source-playlist-native-p)) |
| 259 | (error "Not a native EMMS playlist file.")) |
| 260 | (emms-source-playlist-parse-native file)))) |
| 261 | |
| 262 | ;;; m3u files |
| 263 | |
| 264 | ;; Format: |
| 265 | ;; Either a list of filename-per-line, ignore lines beginning with # |
| 266 | ;; or: |
| 267 | ;; #EXTM3U |
| 268 | ;; #EXTINF:<length in seconds>,<name> |
| 269 | ;; <filename> |
| 270 | |
| 271 | ; emms-source-playlist-m3u-p |
| 272 | ; emms-source-playlist-parse-m3u |
| 273 | ; emms-source-playlist-m3u-files |
| 274 | ; emms-source-playlist-unparse-m3u |
| 275 | |
| 276 | (defun emms-source-playlist-m3u-p () |
| 277 | "Return non-nil if the current buffer contains an m3u playlist. |
| 278 | |
| 279 | We currently have no metric for determining whether a buffer is |
| 280 | an .m3u playlist based on its contents alone, so we assume that |
| 281 | the more restrictive playlist formats have already been |
| 282 | detected and simply return non-nil always." |
| 283 | t) |
| 284 | |
| 285 | (defun emms-source-playlist-parse-m3u (playlist-file) |
| 286 | "Parse the m3u playlist in the current buffer. |
| 287 | Files will be relative to the directory of PLAYLIST-FILE, unless |
| 288 | they have absolute paths." |
| 289 | (let ((dir (file-name-directory playlist-file))) |
| 290 | (mapcar (lambda (file) |
| 291 | (if (string-match "\\`\\(http[s]?\\|mms\\)://" file) |
| 292 | (emms-track 'url file) |
| 293 | (emms-track 'file (expand-file-name file dir)))) |
| 294 | (emms-source-playlist-m3u-files)))) |
| 295 | |
| 296 | (defun emms-source-playlist-m3u-files () |
| 297 | "Extract a list of filenames from the given m3u playlist. |
| 298 | |
| 299 | Empty lines and lines starting with '#' are ignored." |
| 300 | (let ((files nil)) |
| 301 | (save-excursion |
| 302 | (goto-char (point-min)) |
| 303 | (while (re-search-forward "^[^# \n].*$" nil t) |
| 304 | (setq files (cons (match-string 0) files)))) |
| 305 | (nreverse files))) |
| 306 | |
| 307 | (defun emms-source-playlist-unparse-m3u (in out) |
| 308 | "Unparse an m3u playlist from IN to OUT. |
| 309 | IN should be a buffer containing an m3u playlist. |
| 310 | OUT should be the buffer where tracks are stored in m3u format." |
| 311 | (with-current-buffer in ;; Don't modify the position |
| 312 | (save-excursion ;; in the IN buffer |
| 313 | (goto-char (point-min)) |
| 314 | (emms-walk-tracks |
| 315 | (let ((track (emms-playlist-track-at (point)))) |
| 316 | (with-current-buffer out |
| 317 | (insert (emms-track-name track) ?\n))))))) |
| 318 | |
| 319 | ;;;###autoload (autoload 'emms-play-m3u-playlist "emms-source-playlist" nil t) |
| 320 | ;;;###autoload (autoload 'emms-add-m3u-playlist "emms-source-playlist" nil t) |
| 321 | (define-emms-source m3u-playlist (file) |
| 322 | "An EMMS source for an m3u playlist file." |
| 323 | (interactive (list (read-file-name "Playlist file: " |
| 324 | emms-source-file-default-directory |
| 325 | emms-source-file-default-directory |
| 326 | t))) |
| 327 | (mapc #'emms-playlist-insert-track |
| 328 | (with-temp-buffer |
| 329 | (emms-insert-file-contents file) |
| 330 | (goto-char (point-min)) |
| 331 | (when (not (emms-source-playlist-m3u-p)) |
| 332 | (error "Not an m3u playlist file.")) |
| 333 | (emms-source-playlist-parse-m3u file)))) |
| 334 | |
| 335 | ;;; pls files |
| 336 | |
| 337 | ;; Format: |
| 338 | ;; A list of one filename per line. |
| 339 | ;; [playlist] |
| 340 | ;; NumberOfEntries=<num_entries> |
| 341 | ;; File<position>=<filename> |
| 342 | |
| 343 | ; emms-source-playlist-pls-p |
| 344 | ; emms-source-playlist-parse-pls |
| 345 | ; emms-source-playlist-pls-files |
| 346 | ; emms-source-playlist-unparse-pls |
| 347 | |
| 348 | (defun emms-source-playlist-pls-p () |
| 349 | "Return non-nil if the current buffer contains a pls playlist." |
| 350 | (save-excursion |
| 351 | (goto-char (point-min)) |
| 352 | (if (re-search-forward "^File[0-9]*=.+$" nil t) |
| 353 | t |
| 354 | nil))) |
| 355 | |
| 356 | (defun emms-source-playlist-parse-pls (playlist-file) |
| 357 | "Parse the pls playlist in the current buffer. |
| 358 | Files will be relative to the directory of PLAYLIST-FILE, unless |
| 359 | they have absolute paths." |
| 360 | (let ((dir (file-name-directory playlist-file))) |
| 361 | (mapcar (lambda (file) |
| 362 | (if (string-match "\\`\\(http[s]?\\|mms\\)://" file) |
| 363 | (emms-track 'url file) |
| 364 | (if (string-match "\\`file://" file) ;; handle file:// uris |
| 365 | (let ((file (url-unhex-string (substring file 7)))) |
| 366 | (emms-track 'file file)) |
| 367 | (emms-track 'file (expand-file-name file dir))))) |
| 368 | (emms-source-playlist-pls-files)))) |
| 369 | |
| 370 | |
| 371 | (defun emms-source-playlist-pls-files () |
| 372 | "Extract a list of filenames from the given pls playlist. |
| 373 | |
| 374 | Empty lines and lines starting with '#' are ignored." |
| 375 | (let ((files nil)) |
| 376 | (save-excursion |
| 377 | (goto-char (point-min)) |
| 378 | (while (re-search-forward "^File[0-9]*=\\(.+\\)$" nil t) |
| 379 | (setq files (cons (match-string 1) files)))) |
| 380 | (nreverse files))) |
| 381 | |
| 382 | (defun emms-source-playlist-unparse-pls (in out) |
| 383 | "Unparse a pls playlist from IN to OUT. |
| 384 | IN should be a buffer conatining a pls playlist. |
| 385 | OUT should be the buffer where tracks are stored in pls format." |
| 386 | (with-current-buffer in ;; Don't modify the position |
| 387 | (save-excursion ;; in the IN buffer |
| 388 | (let ((pos 0) |
| 389 | beg) |
| 390 | (with-current-buffer out |
| 391 | (insert "[playlist]\n") |
| 392 | (setq beg (point))) |
| 393 | (goto-char (point-min)) |
| 394 | (emms-walk-tracks |
| 395 | (let ((track (emms-playlist-track-at (point)))) |
| 396 | (setq pos (1+ pos)) |
| 397 | (with-current-buffer out |
| 398 | (insert "File" (number-to-string pos) "=" |
| 399 | (emms-track-name track) ?\n)))) |
| 400 | (with-current-buffer out |
| 401 | (goto-char beg) |
| 402 | (insert "NumberOfEntries=" (number-to-string pos) ?\n)))))) |
| 403 | |
| 404 | ;;;###autoload (autoload 'emms-play-pls-playlist "emms-source-playlist" nil t) |
| 405 | ;;;###autoload (autoload 'emms-add-pls-playlist "emms-source-playlist" nil t) |
| 406 | (define-emms-source pls-playlist (file) |
| 407 | "An EMMS source for a pls playlist file." |
| 408 | (interactive (list (read-file-name "Playlist file: " |
| 409 | emms-source-file-default-directory |
| 410 | emms-source-file-default-directory |
| 411 | t))) |
| 412 | (mapc #'emms-playlist-insert-track |
| 413 | (with-temp-buffer |
| 414 | (emms-insert-file-contents file) |
| 415 | (goto-char (point-min)) |
| 416 | (when (not (emms-source-playlist-pls-p)) |
| 417 | (error "Not a pls playlist file.")) |
| 418 | (emms-source-playlist-parse-pls file)))) |
| 419 | |
| 420 | ;;; extm3u files |
| 421 | |
| 422 | ;; Format: |
| 423 | ;; #EXTM3U |
| 424 | ;; #EXTINF:<length in seconds>,<name> |
| 425 | ;; <filename> |
| 426 | |
| 427 | ; emms-source-playlist-extm3u-p |
| 428 | ; emms-source-playlist-parse-extm3u |
| 429 | ; emms-source-playlist-unparse-extm3u |
| 430 | |
| 431 | ;; (erase-buffer) |
| 432 | ;; (insert "#EXTM3U\n") |
| 433 | ;; (mapc (lambda (track) |
| 434 | ;; (let ((time (or (emms-track-get track 'info-mtime) "")) |
| 435 | ;; (artist (emms-track-get track 'info-artist)) |
| 436 | ;; (title (emms-track-get track 'info-title)) |
| 437 | ;; (name (emms-track-get track 'name))) |
| 438 | ;; (insert (format "#EXTINF: %s,%s - %s\n%s\n" |
| 439 | ;; time artist title name)))) |
| 440 | ;; tracklist) |
| 441 | ;; (save-buffer) |
| 442 | ;; (kill-buffer (current-buffer))))) |
| 443 | |
| 444 | ;; Not implemented yet |
| 445 | |
| 446 | ;;; Adding playlists as files |
| 447 | |
| 448 | ;;;###autoload (autoload 'emms-play-playlist-file "emms-source-playlist" nil t) |
| 449 | ;;;###autoload (autoload 'emms-add-playlist-file "emms-source-playlist" nil t) |
| 450 | (define-emms-source playlist-file (file) |
| 451 | "An EMMS source for playlist files. |
| 452 | This adds the given file to the current EMMS playlist buffer, |
| 453 | without adding its contents. |
| 454 | |
| 455 | See `emms-source-playlist-formats' for a list of supported formats." |
| 456 | (interactive (list (read-file-name "Playlist file: " |
| 457 | emms-source-file-default-directory |
| 458 | emms-source-file-default-directory |
| 459 | t))) |
| 460 | (emms-playlist-insert-track |
| 461 | (emms-track 'playlist (expand-file-name file)))) |
| 462 | |
| 463 | ;;;###autoload (autoload 'emms-play-playlist-directory |
| 464 | ;;;###autoload "emms-source-playlist" nil t) |
| 465 | ;;;###autoload (autoload 'emms-add-playlist-directory |
| 466 | ;;;###autoload "emms-source-playlist" nil t) |
| 467 | (define-emms-source playlist-directory (dir) |
| 468 | "An EMMS source for a whole directory tree of playlist files. |
| 469 | If DIR is not specified, it is queried from the user." |
| 470 | (interactive (list |
| 471 | (emms-read-directory-name "Play directory: " |
| 472 | emms-source-file-default-directory |
| 473 | emms-source-file-default-directory |
| 474 | t))) |
| 475 | (mapc (lambda (file) |
| 476 | (unless (or (let ((case-fold-search nil)) |
| 477 | (string-match emms-source-file-exclude-regexp file)) |
| 478 | (file-directory-p file)) |
| 479 | (emms-playlist-insert-track |
| 480 | (emms-track 'playlist (expand-file-name file))))) |
| 481 | (directory-files dir t "^[^.]"))) |
| 482 | |
| 483 | ;;;###autoload (autoload 'emms-play-playlist-directory-tree |
| 484 | ;;;###autoload "emms-source-playlist" nil t) |
| 485 | ;;;###autoload (autoload 'emms-add-playlist-directory-tree |
| 486 | ;;;###autoload "emms-source-file" nil t) |
| 487 | (define-emms-source playlist-directory-tree (dir) |
| 488 | "An EMMS source for multiple directory trees of playlist files. |
| 489 | If DIR is not specified, it is queried from the user." |
| 490 | (interactive (list |
| 491 | (emms-read-directory-name "Play directory tree: " |
| 492 | emms-source-file-default-directory |
| 493 | emms-source-file-default-directory |
| 494 | t))) |
| 495 | (mapc (lambda (file) |
| 496 | (unless (let ((case-fold-search nil)) |
| 497 | (string-match emms-source-file-exclude-regexp file)) |
| 498 | (emms-playlist-insert-track |
| 499 | (emms-track 'playlist file)))) |
| 500 | (emms-source-file-directory-tree (expand-file-name dir) "^[^.]"))) |
| 501 | |
| 502 | (provide 'emms-source-playlist) |
| 503 | ;;; emms-source-playlist.el ends here |