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