Minor change.
[elisp.git] / media.el
1 ;; -*- mode: emacs-lisp -*-
2
3 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4 ;; This program is free software; you can redistribute it and/or         ;;
5 ;; modify it under the terms of the GNU General Public License as        ;;
6 ;; published by the Free Software Foundation; either version 3, or (at   ;;
7 ;; your option) any later version.                                       ;;
8 ;;                                                                       ;;
9 ;; This program is distributed in the hope that it will be useful, but   ;;
10 ;; WITHOUT ANY WARRANTY; without even the implied warranty of            ;;
11 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      ;;
12 ;; General Public License for more details.                              ;;
13 ;;                                                                       ;;
14 ;; You should have received a copy of the GNU General Public License     ;;
15 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.  ;;
16 ;;                                                                       ;;
17 ;; Written by and Copyright (C) Francois Fleuret                         ;;
18 ;; Contact <francois@fleuret.org> for comments & bug reports             ;;
19 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
20
21 ;; A simple front-end major mode for command line media players (only
22 ;; mplayer for now, feel free to write the code for others)
23 ;;
24 ;; The strict minimum is to set in your ~/.emacs the variable
25 ;; media/url-list to the list of directories where to pick the tunes
26 ;; and the URLs of streams. For the latter you can also specify a name
27 ;; that will appear in the interface instead of the URL itself.
28 ;;
29 ;; I have in my ~/.emacs
30 ;;
31 ;; (when (load "media" nil t)
32 ;;
33 ;;   (setq media/expert t
34 ;;         media/add-current-song-to-interrupted-when-killing t
35 ;;         media/duration-to-history 30
36 ;;         media/history-size 1000
37 ;;         media/playlist-file "~/private/media-playlists"
38 ;;         media/mplayer/args '("-framedrop" "-zoom" "-subfont-osd-scale" "3" "-osdlevel" "3")
39 ;;         media/mplayer/timing-request-period 1.0
40 ;;         media/url-list '("~/mp3"
41 ;;                          ("http://www.technomusic.com/live/hi/pls" . "Technomusic.com")
42 ;;                          ("http://www.fullhouseradio.com/listen.pls" . "Full House Radio")
43 ;;                          ("mms://live.france24.com/france24_fr.wsx" . "France 24")
44 ;;                          ))
45 ;;
46 ;;   (define-key global-map [(meta \\)] 'media)
47 ;; )
48 ;;
49 ;; If you put media.el and media-mplayer.el in an exotic directory,
50 ;; you have to tell emacs to look for them there by adding something
51 ;; like (add-to-list 'load-path "~/exotic/") before the (load "media")
52 ;; command.
53
54 (defgroup media ()
55   "Major mode to control media players"
56   :version "1.2.2")
57
58 (defcustom media/player-api "media-mplayer"
59   "The file to load for the abstract layer with the media player."
60   :type 'string
61   :group 'media)
62
63 (defcustom media/url-list '()
64   "List of directories to be imported and urls. Each element can be
65 either a string containing a directory or an url, or a cons cell the
66 first element of which is a string containing a url and the second a
67 title to display in the list (convenient for internet radios)."
68   :type 'list
69   :group 'media)
70
71 (defcustom media/playlist-file "~/.media-playlists"
72   "Where to save the playlists."
73   :type 'string
74   :group 'media)
75
76 (defcustom media/duration-to-history 5
77   "Duration in seconds after which the song should be put in the history."
78   :type 'integer
79   :group 'media)
80
81 (defcustom media/playlist-at-top nil
82   "Should the playlists be created at the top of the media buffer?"
83   :type 'bool
84   :group 'media)
85
86 (defcustom media/add-current-song-to-interrupted-when-killing nil
87   "Should we save the current song with time in the Interrupted playlist?"
88   :type 'bool
89   :group 'media)
90
91 (defcustom media/do-not-remove-nonexisting-entries nil
92   "Should we remove the entries corresponding to a non-existing file when saving the playlists?"
93   :type 'bool
94   :group 'media)
95
96 (defcustom media/history-size 0
97   "How many songs to keep in the history list."
98   :type 'integer
99   :group 'media)
100
101 (defcustom media/continue-mode nil
102   "Should the player start the next song in the buffer when the current terminates?"
103   :type 'boolean
104   :group 'media)
105
106 (defcustom media/continue-mode-hint "*"
107   "What to append to the MPlayer string when in repeat mode"
108   :type 'string
109   :group 'media)
110
111 (defcustom media/expert nil
112   "Should we bypass the keymap help when starting"
113   :type 'boolean
114   :group 'media)
115
116 (defvar media/current-information nil
117   "Contains the name of the current file playing, the frequency in Hz
118 and the bitrate. Should be nil if no information is available.")
119
120 (defvar media/current-song-in-stream nil
121   "Contains the title of the current song playing, as it may be
122 parsed from the stream.")
123
124 (defvar media/buffer nil
125   "The main buffer for the media player mode.")
126
127 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
128 ;; Hooks ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
129 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
130
131 (defcustom media/finished-hook '(media/song-terminates)
132   "Hook called when the current playing song/movie terminates."
133   :type 'hook
134   :group 'media)
135
136 (defcustom media/starting-hook nil
137   "Hook called after the media buffer has been set up."
138   :type 'hook
139   :group 'media)
140
141 (defcustom media/before-play-hook nil
142   "Hook called before starting the player on a new song."
143   :type 'hook
144   :group 'media)
145
146 (defcustom media/play-hook '(media/show-current-information)
147   "Hook called when a song starts to play."
148   :type 'hook
149   :group 'media)
150
151 (defcustom media/error-hook '(media/player-error)
152   "Hook called when a player error occurs."
153   :type 'hook
154   :group 'media)
155
156 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
157 ;; Faces ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
158 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
159
160 (defface media/playlist-face
161   '((((background dark)) (:foreground "blue" :bold t))
162     (((background light)) (:foreground "blue" :bold t)))
163   "Face for playlist names."
164   :group 'media)
165
166 (defface media/directory-face
167   '((((background dark)) (:foreground "green" :bold t))
168     (((background light)) (:foreground "forest green" :bold t)))
169   "Face for directories."
170   :group 'media)
171
172 (defface media/timestamp-face
173   '((((background dark)) (:foreground "turquoise"))
174     (((background light)) (:foreground "blue")))
175   "Face for the stored timestamps."
176   :group 'media)
177
178 (defface media/nonexisting-face
179   '((((background dark)) (:foreground "red"))
180     (((background light)) (:foreground "red3")))
181   "Face for non-existing files."
182   :group 'media)
183
184 (defface media/stream-face
185   '((((background dark)) (:foreground "green"))
186     (((background light)) (:foreground "green3")))
187   "Face for non-files urls."
188   :group 'media)
189
190 (defface media/current-tune-face
191   '((((background dark)) (:foreground "gray80" :background "black"))
192     (((background light)) (:foreground "black" :background "yellow")))
193   "Highlight of the currently playing tune."
194   :group 'media)
195
196 (defface media/instant-highlight-face
197   '((((background dark)) (:foreground "black" :background "lawn green"))
198     (((background light)) (:foreground "black" :background "lawn green")))
199   "Brief highlight when adding a tune to the \"Queue\" list."
200   :group 'media)
201
202 (defface media/mode-string-face
203   '((t (:foreground "darkblue" :bold t)))
204   "The face to display the media info in the modeline."
205   :group 'media)
206
207 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
208 ;; Various initializations
209 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
210
211 (setq media/current-overlay nil
212       media/instant-highlight-overlay nil
213       media/instant-highlight-timer nil
214       media/active-playlist nil)
215
216 (defun media/goto-top ()
217   (goto-char (text-property-any (point-min) (point-max) 'prologue nil)))
218
219 (defun media/goto-next-playlist-or-dir () (interactive)
220   (goto-char (min (next-single-char-property-change (point) 'playlist)
221                   (next-single-char-property-change (point) 'dir)))
222   (unless (< (point) (point-max)) (goto-char (point-min)))
223   (unless (or (get-text-property (point) 'playlist)
224               (get-text-property (point) 'dir))
225     (goto-char (min (next-single-char-property-change (point) 'playlist)
226                     (next-single-char-property-change (point) 'dir))))
227   )
228
229 (defun media/goto-previous-playlist-or-dir () (interactive)
230   (goto-char (max (previous-single-char-property-change (point) 'playlist)
231                   (previous-single-char-property-change (point) 'dir)))
232   (unless (> (point) (point-min)) (goto-char (point-max)))
233   (unless (or (get-text-property (point) 'playlist)
234               (get-text-property (point) 'dir))
235     (goto-char (max (previous-single-char-property-change (point) 'playlist)
236                     (previous-single-char-property-change (point) 'dir))))
237   )
238
239 (defun media/remove-instant-highlight ()
240   (move-overlay media/instant-highlight-overlay 0 0)
241   (setq media/instant-highlight-timer nil)
242   )
243
244 (defun media/instant-highlight (start end)
245   (move-overlay media/instant-highlight-overlay start end)
246   (when media/instant-highlight-timer
247     (cancel-timer media/instant-highlight-timer))
248   (setq media/instant-highlight-timer
249         (run-at-time 0.25 nil 'media/remove-instant-highlight)))
250
251 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
252 ;; Finding and playing URLs ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
253 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
254
255 (defun media/reset-current-information ()
256     (setq media/current-information nil
257           media/current-song-in-stream nil))
258
259 (defun media/format-url (url)
260   (if (string-match "^file:.*/\\([^/]+\\)$" url)
261       (match-string 1 url)
262     url)
263   )
264
265 (defun media/play-position (position) (interactive)
266   (let ((url (get-text-property position 'url))
267         (time (get-text-property position 'time)))
268     (if (not url) (media/remove-highlight)
269       (run-hook-with-args 'media/before-play-hook url)
270       (media/reset-current-information)
271       (media/api/play url)
272       ;; We keep the information of the url and the title
273       (setq media/played-information (cons url (get-text-property position 'title)))
274       (media/move-highlight position)
275       (when time (media/api/jump-at-time 'absolute time))
276       )))
277
278 (defun media/play-or-active-at-point () (interactive)
279   (if (get-text-property (point) 'url)
280       (media/play-position (point))
281     (let ((playlist (get-text-property (point) 'playlist)))
282       (when playlist
283         (setq media/active-playlist playlist)
284         (message "Active playlist is %s" media/active-playlist)))))
285
286 (defun media/goto-next () (interactive)
287   (let ((p (next-single-char-property-change (point) 'url)))
288     (while (and (< p (point-max)) (not (get-text-property p 'url)))
289       (setq p (next-single-char-property-change p 'url)))
290     (when (get-text-property p 'url)
291       (goto-char p))))
292
293 (defun media/play-next (&optional dont-move) (interactive)
294   (let ((p (next-single-char-property-change (overlay-start media/current-overlay) 'url)))
295     (while (and (< p (point-max)) (not (get-text-property p 'url)))
296       (setq p (next-single-char-property-change p 'url)))
297     (if (not (get-text-property p 'url))
298         (media/remove-highlight)
299       (media/play-position p)
300       (unless (or ;;(pos-visible-in-window-p p)
301                   dont-move)
302         (goto-char p)))))
303
304 (defun media/play-prev () (interactive)
305   (let ((p (previous-single-char-property-change (overlay-start media/current-overlay) 'url)))
306     (while (and (> p (point-min)) (not (get-text-property p 'url)))
307       (setq p (previous-single-char-property-change p 'url)))
308     (when (get-text-property p 'url)
309       (media/play-position p))
310     ;; (unless (pos-visible-in-window-p p)
311       (goto-char p)
312       ;; )
313     ))
314
315 (defun media/move-highlight (position)
316   (move-overlay media/current-overlay
317                 (previous-property-change (1+ position))
318                 ;; (next-property-change position)
319                 ;; (previous-single-char-property-change (1+ position) 'url)
320                 (next-single-char-property-change position 'url)
321                 ))
322
323 (defun media/remove-highlight ()
324   (move-overlay media/current-overlay 0 0))
325
326 (defun media/goto-current () (interactive)
327   (goto-char (overlay-start media/current-overlay)))
328
329 (defun media/jump-at-percent (&optional perc)
330   "Goes to a certain % of the song"
331   (interactive "P")
332   (media/api/jump-at-percent
333    (max 0
334         (min 100
335              (or perc
336                  (string-to-number (read-from-minibuffer "Percentage: ")))))))
337
338 (defun media/refresh-list (&optional dir) (interactive)
339   (when media/buffer
340     (let* ((current (overlay-end media/current-overlay))
341            (url (get-text-property current 'url))
342            ;; (playlist (get-text-property current 'playlist))
343            (w (get-buffer-window media/buffer)))
344
345       (if (not w) (media/full-refresh)
346         (let ((p (point))
347               (s (window-start w)))
348           (media/full-refresh)
349           (goto-char p)
350           (set-window-start w s)))
351
352       ))
353
354   ;; TODO: Move the overlay where they were before refresh
355
356   (message "Refreshed!"))
357
358 ;; TODO: Refresh only the directories which have to be
359
360 (defun media/rename-point () (interactive)
361   (let ((url (get-text-property (point) 'url)))
362     (when (and url (string-match "^file:/*\\(/.+\\)$" url))
363       (let* ((original (match-string-no-properties 1 url))
364              (new (read-from-minibuffer "New name: " original)))
365         (if (string= original new)
366             (message "Cancel")
367           (message "Renaming %s to %s" original new)
368           (rename-file original new)
369           (media/refresh-list (file-name-directory original))
370           (unless (string= (file-name-directory original) (file-name-directory new))
371             (media/refresh-list (file-name-directory new)))
372           )))))
373
374 (defun media/move-point-to-tmp () (interactive)
375   (let ((url (get-text-property (point) 'url)))
376     (unless (and url (string-match "^file:/*\\(/.+\\)$" url))
377       (error "No file here"))
378     (let* ((original (match-string-no-properties 1 url))
379            (new (replace-regexp-in-string "^.*/" "/tmp/" original)))
380       (if (string= original new)
381           (message "Cancel")
382         (message "Renaming %s into %s" original new)
383         (rename-file original new)
384         (media/refresh-list (file-name-directory original))
385         ))))
386
387 (setq media/id3-genre-table
388       [ "Blues" "Classic Rock" "Country" "Dance" "Disco" "Funk"
389         "Grunge" "Hip-Hop" "Jazz" "Metal" "New Age" "Oldies"
390         "Other" "Pop" "R&B" "Rap" "Reggae" "Rock"
391         "Techno" "Industrial" "Alternative" "Ska" "Death Metal" "Pranks"
392         "Soundtrack" "Euro-Techno" "Ambient" "Trip-Hop" "Vocal" "Jazz+Funk"
393         "Fusion" "Trance" "Classical" "Instrumental" "Acid" "House"
394         "Game" "Sound Clip" "Gospel" "Noise" "Alternative Rock" "Bass"
395         "Soul" "Punk" "Space" "Meditative" "Instrumental Pop"
396         "Instrumental Rock" "Ethnic" "Gothic" "Darkwave" "Techno-Industrial"
397         "Electronic" "Pop-Folk" "Eurodance" "Dream" "Southern Rock" "Comedy"
398         "Cult" "Gangsta" "Top 40" "Christian Rap" "Pop/Funk" "Jungle"
399         "Native US" "Cabaret" "New Wave" "Psychadelic" "Rave" "Showtunes"
400         "Trailer" "Lo-Fi" "Tribal" "Acid Punk" "Acid Jazz" "Polka" "Retro"
401         "Musical" "Rock & Roll" "Hard Rock" "Folk" "Folk-Rock" "National Folk"
402         "Swing" "Fast Fusion" "Bebob" "Latin" "Revival" "Celtic" "Bluegrass"
403         "Gothic Rock" "Progressive Rock" "Psychedelic Rock" "Symphonic Rock"
404         "Slow Rock" "Big Band" "Chorus" "Easy Listening" "Acoustic" "Humour"
405         "Speech" "Chanson" "Opera" "Chamber Music" "Sonata" "Symphony"
406         "Booty Bass" "Primus" "Porn Groove" "Satire" "Slow Jam" "Club" "Tango"
407         "Samba" "Folklore" "Ballad" "Power Ballad" "Rythmic Soul" "Freestyle"
408         "Duet" "Punk Rock" "Drum Solo" "Acapella" "Euro-House" "Dance Hall"
409         "Goa" "Drum & Bass" "Club-House" "Hardcore" "Terror" "Indie" "BritPop"
410         "Negerpunk" "Polsk Punk" "Beat" "Christian Gangsta Rap" "Heavy Metal"
411         "Black Metal" "Crossover" "Contemporary Christian" "Christian Rock"
412         "Merengue" "Salsa" "Trash Metal" ])
413
414 (defun media/get-file-id3-tags (file)
415   "Returns the id3 tags in a list (SONG ARTIST ALBUM YEAR NOTE GENRE),
416 returns nil if no id3 tags could be found."
417   (let ((size (elt (file-attributes file) 7)))
418     (unless (integerp size) (error "Can not read the file ID3 information (file probably too big)"))
419     (with-temp-buffer
420       (when (and (> size 128)
421                  (insert-file-contents-literally file nil (- size 128) size t)
422                  (string= (buffer-substring 1 4) "TAG"))
423         ;; Here we have the 128 last bytes of the file in a temporary
424         ;; buffer, and the three first characters are "TAG"
425         (append
426          ;; We get the 5 first id3s
427          (mapcar (lambda (pos)
428                    (replace-regexp-in-string
429                     "[\0 ]*$" ""
430                     (buffer-substring (car pos) (cdr pos))))
431                  '((4 . 34) (34 . 64) (64 . 94) (94 . 98) (98 . 127)))
432          ;; And we decode the last one with the genre table
433          (list
434           (condition-case nil
435               (elt media/id3-genre-table (string-to-char
436                                           (buffer-substring 128 129)))
437             (error "<Error>"))))))))
438
439 (defun media/show-id3-at-point ()
440   (interactive)
441   (let ((url (get-text-property (point) 'url)))
442     (when url
443       (if (not (string-match "^file:/*\\(/.+\\)$" url))
444           (message "This is not a file!")
445         (let* ((filename (match-string-no-properties 1 url)))
446           (if (file-exists-p filename)
447               (let ((id3tags (media/get-file-id3-tags filename)))
448                 (if id3tags
449                     (message
450                      "%s Song [%s] Artist [%s] Album [%s] Year [%s] Note [%s] Genre [%s]"
451                      filename
452                      (elt id3tags 0)
453                      (elt id3tags 1)
454                      (elt id3tags 2)
455                      (elt id3tags 3)
456                      (elt id3tags 4)
457                      (elt id3tags 5))
458                   (message "%s (no id3 tags) " filename)))
459             (message "No such file (%s)!" filename)))))))
460
461 (defun media/rename-point-according-to-id3 ()
462   "Renames the file located at point, according to the ID3 tags"
463   (interactive)
464   (let ((url (get-text-property (point) 'url)))
465     (when (and url (string-match "^file:/*\\(/.+\\)$" url))
466       (if (file-exists-p (match-string-no-properties 1 url))
467           (let* ((filename (match-string-no-properties 1 url))
468                  (id3tags (media/get-file-id3-tags filename)))
469             (if id3tags
470                 (let* ((original (match-string-no-properties 1 url))
471                        (new (read-from-minibuffer "New name: "
472                                                   (replace-regexp-in-string
473                                                    " " "_"
474                                                    (concat (replace-regexp-in-string
475                                                             "[^/]+$" "" (match-string-no-properties 1 url))
476                                                            (elt id3tags 1)
477                                                            "_-_"
478                                                            (elt id3tags 0)
479                                                            ".mp3")))))
480                   (if (string= original new)
481                       (message "Cancel")
482                     (message "Renaming %s into %s" original new)
483                     (rename-file original new)
484                     (media/refresh-list)
485                     ))
486               (message "%s (no id3 tags) " filename)))
487         (message "No such file!")))))
488
489 ;; TODO: Finish
490
491 (defun media/edit-id3-at-point ()
492   "Open a new buffer with the ID3 fields of the file on line editable."
493   (interactive)
494   (let ((url (get-text-property (point) 'url)))
495     (when (and url (string-match "^file:/*\\(/.+\\)$" url))
496       (if (file-exists-p (match-string-no-properties 1 url))
497           (let* ((filename (match-string-no-properties 1 url))
498                  (id3tags (or (media/get-file-id3-tags filename) '("-" "-" "-" "-" "-" "-"))))
499             (let ((map (make-sparse-keymap)))
500
501               (switch-to-buffer (get-buffer-create (generate-new-buffer-name "*media ID3 editor*")))
502
503               (text-mode)
504               (auto-fill-mode)
505
506               (mapc (lambda (s)
507                       (insert (if (numberp s) (elt id3tags s)
508                                 (propertize s 'read-only t 'rear-nonsticky '(read-only)))))
509
510                     '("SONG:   " 0 "\n"
511                       "ARTIST: " 1 "\n"
512                       "ALBUM:  " 2 "\n"
513                       "YEAR:   " 3 "\n"
514                       "NOTE:   " 4 "\n"
515                       "GENRE:  " 5 "\n"))
516
517               (goto-char (point-min))
518               (re-search-forward "SONG:   ")
519
520               (define-key map (kbd "TAB")
521                 (lambda () (interactive)
522                   (unless (re-search-forward ": +" nil t)
523                     (goto-char (point-min))
524                     (re-search-forward ": +" nil t))))
525
526               (define-key map [(control c) (control c)]
527                 (lambda () (interactive)
528                   (kill-this-buffer)
529                   )
530                 )
531
532               (define-key map [(control c) (control q)]
533                 (lambda () (interactive)
534                   (kill-this-buffer)
535                   (message "Cancel")
536                   ))
537
538               (use-local-map map)
539               (message "C-c C-c to save the information, C-c C-q to cancel")
540               )
541             )
542         )
543       )
544     )
545   )
546
547 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
548 ;; Adding objects in the buffer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
549 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
550
551 (defun media/separator ()
552   (unless (and (eq (char-before) ?\n) (eq (char-before (1- (point))) ?\n))
553     (insert "\n")))
554
555 (defun media/insert-url (url depth &optional info)
556   (insert
557    (if (listp url)
558        (propertize (concat " "
559                            (make-string (* 2 depth) ?\ )
560                            info
561                            " "
562                            (media/format-url (cdr url)) "\n")
563                    'url (car url)
564                    'title (cdr url))
565
566      (propertize (concat " "
567                          (make-string (* 2 depth) ?\ )
568                          info
569                          " "
570                          (media/format-url url) "\n")
571                  'url url
572                  'title nil))
573    ))
574
575 (defun media/string-from-size (size)
576   (if (< size 1024) (format "%5db" size)
577     (if (< size 1048576) (format "%5dk" (ash size -10))
578       (format "%5.01fM" (/ size 1048576.0))
579       )))
580
581 (defun media/insert-file (filename depth)
582   (media/insert-url (concat "file://" (file-truename filename))
583                     depth
584                     (concat (media/string-from-size (nth 7 (file-attributes filename))) " --")
585                     ))
586
587 (defun media/insert-dir (filename depth)
588   (media/separator)
589
590   (insert (propertize (concat "  "
591                               (make-string (* 2 depth) ?\ )
592                               filename
593                               "\n") 'face 'media/directory-face 'dir filename))
594
595   (media/separator)
596
597   (let ((dircontent (directory-files-and-attributes filename)))
598
599     (mapc (lambda (file)
600             (unless (string-match "^\\." (car file))
601               (let ((url (concat filename "/" (car file))))
602                 (when (file-regular-p url)
603                   (media/insert-file url depth)))))
604           dircontent)
605
606     (media/separator)
607
608     (mapc (lambda (file)
609             (unless (string-match "^\\." (car file))
610               (let ((url (concat filename "/" (car file))))
611                 (when (file-directory-p url)
612                   (media/insert-dir url (1+ depth))))))
613           dircontent)
614     )
615   )
616
617 (defun media/import (list)
618
619   (message "Importing the list of URLs")
620
621   (media/separator)
622
623   (mapc (lambda (c)
624           (let* ((url (or (and (consp c) (car c)) c))
625                  (title (or (and (consp c) (cdr c)) url)))
626             (if (string-match "^\\(http\\|mms\\)://" url)
627                 (media/insert-url (cons url title) 0)
628               (if (file-exists-p url)
629                   (if (file-regular-p url) (media/insert-file url 0)
630                     (if (file-directory-p url) (media/insert-dir url 0)
631                       (error "Unknown type `%s'" url))))
632               )))
633         list))
634
635 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
636 ;; Saving and loading the buffer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
637 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
638
639 (defun media/save-playlists () (interactive)
640
641   (let ((list '()))
642
643     (with-current-buffer media/buffer
644       (let ((pos (point-min))
645             (end (point-max)))
646
647         (while (< (setq pos
648                         (next-single-char-property-change pos 'url)
649                         ;; (min (next-single-char-property-change pos 'url)
650                         ;; (next-single-char-property-change pos 'time))
651                         ) end)
652
653           (let ((url (get-text-property pos 'url))
654                 (title (get-text-property pos 'title))
655                 (time (get-text-property pos 'time))
656                 (playlist (get-text-property pos 'playlist)))
657
658             ;; (message "url=%s title=%s time=%s playlist=%s"
659             ;; (prin1-to-string url)
660             ;; (prin1-to-string title)
661             ;; (prin1-to-string time)
662             ;; (prin1-to-string playlist))
663
664             (when (and playlist url)
665               (unless (assoc playlist list) (push (list playlist) list))
666               (push (cons url (cons title time)) (cdr (assoc playlist list)))
667               )))))
668
669     (with-current-buffer (find-file-noselect media/playlist-file)
670       (erase-buffer)
671       (mapc (lambda (x)
672               (insert "PLAYLIST:" (car x) "\n")
673               (mapc (lambda (y)
674                       (when (or media/do-not-remove-nonexisting-entries
675                                 (not (string-match "^file:" (car y)))
676                                 (file-exists-p (replace-regexp-in-string "^file://" "" (car y))))
677                         (when (car (cdr y)) (insert "TITLE:" (car (cdr y)) "\n"))
678                         (when (cdr (cdr y)) (insert "TIME:" (number-to-string (cdr (cdr y))) "\n"))
679                         (insert "URL:" (car y) "\n")))
680                     (reverse (cdr x)))
681               )
682             (reverse list))
683       (set-buffer-file-coding-system 'utf-8)
684       (save-buffer)
685       (kill-this-buffer)
686       ))
687
688   (set-buffer-modified-p nil))
689
690 (defun media/load-playlists () (interactive)
691   (if (file-exists-p media/playlist-file)
692       (with-temp-buffer
693         (insert-file media/playlist-file)
694         ;; (insert-file-contents-literally media/playlist-file)
695         (goto-char (point-min))
696         (let ((playlist nil)
697               (title nil)
698               (time nil))
699           (while (re-search-forward "^\\([A-Z]*\\):\\(.*\\)$" nil t)
700             (eval (cdr (assoc (match-string-no-properties 1)
701                               '(("PLAYLIST" . (setq playlist (match-string-no-properties 2)))
702                                 ("TITLE" . (setq title (match-string-no-properties 2)))
703                                 ("TIME" . (setq time (string-to-number (match-string-no-properties 2))))
704                                 ("URL" . (save-excursion
705                                            (media/add-song-to-playlist
706                                             playlist (match-string-no-properties 2) title time)
707                                            (setq title nil
708                                                  time nil)))))))
709             )))))
710
711 (defun media/select-active-playlist ()
712   (interactive)
713   (with-current-buffer media/buffer
714     (let ((playlists nil)
715           (pos (point-min))
716           (end (point-max)))
717
718       ;; Build the list of existing playlists
719       (while (< (setq pos (next-single-char-property-change pos 'playlist)) end)
720         (add-to-list 'playlists (list (get-text-property pos 'playlist))))
721
722       (setq media/active-playlist
723             (completing-read "Select playlist: " playlists))
724
725       (message "Using `%s' as active playlist" media/active-playlist)))
726   )
727
728 (defun media/create-playlist (name)
729   (interactive "MPlaylist to create: ")
730   (when (media/playlist-position name) (error "Playlist already existing"))
731   (save-excursion
732     (if media/playlist-at-top (media/goto-top)
733       (goto-char (point-max)))
734     (media/separator)
735     (insert (propertize (concat "  " name "\n") 'playlist name 'face 'media/playlist-face)
736             (propertize "\n" 'playlist name)
737             )
738     (setq media/active-playlist name)
739     (message "Playlist `%s' created" name)))
740
741 (defun media/playlist-position (name)
742   "Returns the position where the given playlist starts."
743   (let ((pos (point-min)))
744     (while (and (setq pos (next-single-char-property-change pos 'playlist))
745                 (not (string= name (get-text-property pos 'playlist)))
746                 (< pos (point-max))))
747     (and (< pos (point-max)) pos)))
748
749 ;; (defun media/playlist-position (name)
750 ;;   (text-property-any (point-min) (point-max) 'playlist name))
751
752 ;; (defun media/url-position (url &optional playlist)
753 ;;   (let ((pos (point-min)))
754 ;;     (while (and (setq pos (next-single-char-property-change pos 'playlist))
755 ;;                 (not (string= name (get-text-property pos 'playlist)))
756 ;;                 (< pos (point-max))))
757 ;;     (and (< pos (point-max)) pos)))
758
759 (defun media/playlist-content (playlist)
760   (let ((pos (point-min))
761         (urls ()))
762     (while (and (setq pos (next-single-char-property-change pos 'url))
763                 (string= playlist (get-text-property pos 'playlist))
764                 (< pos (point-max)))
765       (setq urls (cons (cons pos (get-text-property pos 'url)) urls)))
766     (nreverse urls)))
767
768 (defun media/put-in-history ()
769   (set-buffer media/buffer)
770   (when (> media/history-size 0)
771     (let* ((urls (media/playlist-content "History"))
772            (l (length urls))
773            (current-url (car media/current-information))
774            ;; For the title, if the URL we are actually playing is the
775            ;; one we intended to play, we use the accompagnying title
776            (current-title
777             (if (string= (car media/played-information) current-url)
778                 (cdr media/played-information))))
779
780       (media/add-song-to-playlist "History" current-url current-title)
781
782       (when (> (1+ l) media/history-size)
783         (delete-region (car (car urls))
784                        (car (nth (- l media/history-size) urls)))))))
785
786 (defun media/add-song-at-point-to-active-playlist () (interactive)
787   (if media/active-playlist
788       (let ((url (get-text-property (point) 'url))
789             (title (get-text-property (point) 'title))
790             (time (get-text-property (point) 'time)))
791         (if (not url) (error "No song at point")
792           (media/add-song-to-playlist media/active-playlist url title time)
793           (message "Added %s to playlist `%s'" (or title url) media/active-playlist)
794           (media/instant-highlight
795            (previous-single-char-property-change (1+ (point)) 'url)
796            (next-single-char-property-change (point) 'url))
797           (media/goto-next)))
798     (error "No current playlist")))
799
800 (defun media/add-current-song-to-active-playlist (&optional settime) (interactive "P")
801   (if media/active-playlist
802       (let ((url (get-text-property (overlay-start media/current-overlay) 'url))
803             (title (get-text-property (overlay-start media/current-overlay) 'title)))
804         (if (not url) (error "No current song")
805           (media/add-song-to-playlist media/active-playlist url title (and settime media/song-current-time))
806           (message "Added %s to playlist `%s'" (or title url) media/active-playlist)))
807     (error "No current playlist")))
808
809 (defun media/add-song-to-playlist (playlist url &optional title time)
810   (set-buffer media/buffer)
811   (let ((pos (or (media/playlist-position playlist)
812                  (progn (media/create-playlist playlist)
813                         (media/playlist-position playlist)))))
814     (unless pos (error (format "Weird: There is no playlist \"%s\" but can not create it") playlist))
815     (save-excursion
816       (goto-char (next-single-char-property-change pos 'playlist))
817       (prog1 (point)
818         (insert (propertize (concat
819                              "  "
820                              (or title (media/format-url url))
821                              (if time (propertize
822                                        (concat " @" (media/duration-to-string time))
823                                        'face 'media/timestamp-face
824                                        ))
825                              "\n"
826                              )
827                             'url url
828                             'title title
829                             'time time
830                             'playlist (get-text-property (1- (point)) 'playlist))))
831       )))
832
833 (defun media/pause () (interactive)
834   (message "Pause")
835   (media/api/pause))
836
837 (defun media/stop () (interactive)
838   (message "Stop")
839   (media/reset-current-information)
840   (media/api/stop))
841
842 (defun media/queue-song-at-point ()
843   "Switches to the 'continue' mode. If a song is currently playing and
844 not in the 'Queue' playlist, adds it. Then, adds the url at point to
845 the 'Queue' playlist, and plays it if no song is currently playing."
846   (interactive)
847
848   ;; If a song is playing and not in the the Queue list, put it
849
850   (when (and media/current-information
851              (not (string= (get-text-property (overlay-end media/current-overlay) 'playlist)
852                            "Queue")))
853
854     (let* ((url (nth 0 media/current-information))
855            (title (if (string= (car media/played-information) url) (cdr media/played-information)))
856            (pos (media/add-song-to-playlist "Queue" url title)))
857
858       (move-overlay media/current-overlay
859                     pos
860                     (next-single-char-property-change pos 'url))))
861
862   (let* ((position (point)))
863     (media/instant-highlight
864      (previous-single-char-property-change (1+ position) 'url)
865      (next-single-char-property-change position 'url))
866     )
867
868   (let* ((position (point))
869          (url (get-text-property position 'url))
870          (title (get-text-property position 'title))
871          (time (get-text-property position 'time))
872          (pos (and url (media/add-song-to-playlist "Queue" url title time))))
873
874     (when (and pos (not media/current-information)) (media/play-position pos))
875
876     (next-line 1)
877     (setq media/continue-mode t)
878     (force-mode-line-update)
879     )
880
881   )
882
883 (defun media/add-song (url) (interactive))
884
885 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
886
887 (defun media/player-error ()
888   (message "Player error")
889   (media/reset-current-information)
890   (media/remove-highlight))
891
892 (defun media/song-terminates ()
893   (with-current-buffer media/buffer
894     (if media/continue-mode (media/play-next t)
895       (media/reset-current-information)
896       (media/remove-highlight))))
897
898 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
899
900 (defun media/switch-continue-mode ()
901   "Switches between a mode which automatically chains files and a mode
902 which stops when the songs ends."
903   (interactive)
904   (setq media/continue-mode (not media/continue-mode))
905   (force-mode-line-update)
906   (if media/continue-mode (message "Continue mode switched on.")
907     (message "Continue mode switched off."))
908   )
909
910 (defun media/duration-to-string (duration)
911   (let ((sec (mod duration 60))
912         (min (/ duration 60)))
913     (if (zerop duration) "0s"
914       (concat (if (>= min 1) (format "%dm" min))
915               (if (>= sec 1) (format "%ds" sec)))
916       )))
917
918 (defun media/mode-string ()
919   (propertize
920    (concat
921     " "
922     media/player-id
923     (if media/continue-mode media/continue-mode-hint)
924     " "
925
926     (if media/current-information
927         (if media/song-current-time
928             (media/duration-to-string media/song-current-time)
929           "?"
930           ))
931
932     (if (and media/song-duration (> media/song-duration 0))
933         (concat "/"
934                 (media/duration-to-string media/song-duration)))
935     )
936
937    'face 'media/mode-string-face)
938   )
939
940 (defun media/show-current-information ()
941   "Print a message with informations about the song currently playing"
942   (interactive)
943   (if media/current-information
944       (message "Now playing %s %s(%dHz, %s, %dkbit/s)"
945                (or (and (string= (car media/played-information) (nth 0 media/current-information))
946                         (cdr media/played-information))
947                    (replace-regexp-in-string "^.*/\\([^/]*\\)$" "\\1" (nth 0 media/current-information)))
948                ;; (if media/current-song-in-stream (concat "[" media/current-song-in-stream "] ") "")
949                (if media/current-song-in-stream (concat "| " media/current-song-in-stream " ") "")
950                (nth 1 media/current-information)
951                (if (= 2 (nth 2 media/current-information)) "stereo" "mono")
952                (nth 3 media/current-information))
953     (message "No song playing")))
954
955 (defun media/save-and-kill-buffer ()
956   "Save the playlists and kill the media buffer"
957   (interactive)
958
959   (condition-case nil
960       (when media/add-current-song-to-interrupted-when-killing
961         (setq media/active-playlist "Interrupted")
962         (media/add-current-song-to-active-playlist t)
963         )
964     (error nil))
965
966   (unless (condition-case nil
967               (media/save-playlists)
968             (error (not (y-or-n-p "Can not save the playlists. Still kill the media buffer? "))))
969     (kill-buffer media/buffer))
970   )
971
972 (defun media/insert-keybindings (keymap)
973   (insert (format "media/insert-keybindings [%s]" (prin1-to-string keymap)))
974   (insert "\n---------------\n")
975   (if (listp keymap)
976       (if (eq (car keymap) 'keymap)
977           (mapc 'media/insert-keybindings (cdr keymap)))
978     (unless (eq (cdr keymap) 'undefined)
979       (insert (format "%s -> %s\n"
980                       (prin1-to-string (car keymap))
981                       (prin1-to-string (cdr keymap)))))
982     ))
983
984 (defun media/show-keys (&optional keymap) (interactive)
985   (set-buffer (get-buffer-create "*media help*"))
986   (media/insert-keybindings media/mode-map))
987
988 (defun media/quick-help () (interactive)
989   (message "<return> play <n> add to the queue <p> pause <m> continue mode <q> bury the buffer <k> kill it"))
990
991 (defun media/move-forward () (interactive) (media/api/jump-at-time 'relative 3))
992 (defun media/move-backward () (interactive) (media/api/jump-at-time 'relative -3))
993 (defun media/volume-reset () (interactive) (media/api/set-volume 'absolute 50))
994 (defun media/volume-increase ()  (interactive) (media/api/set-volume 'relative 1))
995 (defun media/volume-decrease () (interactive) (media/api/set-volume 'relative -1))
996
997 (defun media/mode () (interactive)
998   (if media/buffer (error "We already have a media buffer"))
999
1000   (kill-all-local-variables)
1001
1002   (unless (boundp 'media/mode-map)
1003
1004     (setq media/mode-map (make-sparse-keymap))
1005
1006     (suppress-keymap media/mode-map)
1007
1008     (mapc (lambda (x) (define-key media/mode-map (car x) (cdr x)))
1009           `(("p" . media/pause)
1010             ("\C-m" . media/play-or-active-at-point)
1011             ("\t" . media/goto-next-playlist-or-dir)
1012             ([(shift iso-lefttab)] . media/goto-previous-playlist-or-dir)
1013             (" " . media/goto-current)
1014             ("a" . media/add-song-at-point-to-active-playlist)
1015             ("A" . media/add-current-song-to-active-playlist)
1016             ("n" . media/queue-song-at-point)
1017             ("f" . media/show-id3-at-point)
1018             ("r" . media/rename-point)
1019             ("R" . media/rename-point-according-to-id3)
1020             ("K" . media/move-point-to-tmp)
1021             ("N" . media/play-next)
1022             ("P" . media/play-prev)
1023             ("q" . bury-buffer)
1024             ("k" . media/save-and-kill-buffer)
1025             ("s" . media/stop)
1026             ("m" . media/switch-continue-mode)
1027             ;; ("t" . media/switch-timing)
1028             ("g" . media/refresh-list)
1029             ("h" . media/quick-help)
1030             ("?" . media/quick-help)
1031             ("l" . media/select-active-playlist)
1032             ;;             ("L" . media/create-playlist)
1033             ("i" . media/show-current-information)
1034             ;; ("I" . media/edit-id3-at-point)
1035             ("j" . media/jump-at-percent)
1036             (">" . media/move-forward)
1037             ("<" . media/move-backward)
1038             ([(control >)] . (lambda () (interactive) (media/api/jump-at-time 'relative 30)))
1039             ([(control <)] . (lambda () (interactive) (media/api/jump-at-time 'relative -30)))
1040             ([(control x) (control s)] . media/save-playlists)
1041             ("=" . media/volume-reset)
1042             ("+" . media/volume-increase)
1043             ("-" . media/volume-decrease)
1044             )))
1045
1046   (setq major-mode 'media
1047         mode-name "Media"
1048         ;; buffer-read-only t
1049         truncate-lines t
1050         media/buffer (current-buffer)
1051         media/current-overlay (make-overlay 0 0)
1052         media/instant-highlight-overlay (make-overlay 0 0)
1053         media/song-current-time nil
1054         media/song-duration nil
1055         global-mode-string (append global-mode-string '((:eval (media/mode-string))))
1056         )
1057
1058   (overlay-put media/current-overlay 'face 'media/current-tune-face)
1059   (overlay-put media/instant-highlight-overlay 'face 'media/instant-highlight-face)
1060
1061   (use-local-map media/mode-map)
1062
1063   (add-hook 'kill-emacs-hook 'media/die-decently)
1064   (add-hook 'kill-buffer-hook 'media/kill-buffer-cleanup nil t)
1065   (add-hook 'write-contents-hooks 'media/save-buffer nil t)
1066   )
1067
1068 (defun media/die-decently ()
1069   (when media/add-current-song-to-interrupted-when-killing
1070     (condition-case nil
1071         (progn
1072           (setq media/active-playlist "Interrupted")
1073           (media/add-current-song-to-active-playlist t)
1074           (media/save-playlists))
1075       (error nil))
1076     )
1077   )
1078
1079 (defun media/kill-buffer-cleanup () (interactive)
1080   (media/api/cleanup)
1081   (setq media/buffer nil
1082         global-mode-string (remove '(:eval (media/mode-string)) global-mode-string))
1083   )
1084
1085 (defun media/full-refresh ()
1086
1087   (undo-boundary)
1088   (erase-buffer)
1089   (media/import media/url-list)
1090   (media/goto-top)
1091   (media/load-playlists)
1092
1093   (unless media/expert
1094     (insert (propertize "
1095   media.el
1096   Written and (C) Francois Fleuret
1097   Send comments and bug reports to francois@fleuret.org
1098
1099   Return   play or active the playlist for insertion
1100   Space    goto song playing
1101   p        pause
1102   g        refresh list
1103   a        insert song at point to the active playlist
1104   A        insert current song to the active playlist
1105            universal argument store the time too
1106   l        select active playlist
1107   C-x C-s  save playlists
1108   n        queue song for playing
1109   f        show ID3 of song
1110   r        rename song
1111   R        rename song according to ID3
1112   K        move song to /tmp
1113   N        play next
1114   P        play previous
1115   q        hide buffer
1116   k        stop song and kill buffer
1117   s        stop song
1118   m        switch the continuous mode
1119   i        show current song information
1120   j        jump at position
1121   >        fast forward
1122   <        fast backward
1123   Ctrl->   fast forward x10
1124   Ctrl-<   fast backward x10
1125   =        reset volume
1126   +        increase volume
1127   -        decrease volume
1128 " 'prologue t)))
1129
1130   (set-buffer-modified-p nil)
1131   (undo-boundary)
1132   )
1133
1134 (defun media/switch-to-buffer-or-window (buffer)
1135   (let ((w (get-buffer-window buffer)))
1136     (if w (select-window w)
1137       (switch-to-buffer buffer))))
1138
1139 (defun media ()
1140   "If a `media/buffer' exists, and we are not in it, switch to it, if
1141 we are already in it, bury it. If none exists, creates one and switch
1142 to it."
1143   (interactive)
1144
1145   (if media/buffer
1146       (if (eq (window-buffer (selected-window)) media/buffer)
1147           (bury-buffer)
1148         (media/switch-to-buffer-or-window media/buffer))
1149     (switch-to-buffer (get-buffer-create "*media*"))
1150     (buffer-disable-undo)
1151     (media/mode)
1152     (media/full-refresh)
1153     (buffer-enable-undo)
1154     (run-hooks 'media/starting-hook)
1155     )
1156   )
1157
1158 (load media/player-api)
1159
1160 (media/api/init)