fordon_greeman / voidfiles

Branch: main
Switch branches/tags
main
theidioticdev add emacs 2079ec5 1d ago
576 lines · 20 KB Raw Blame History
1 ;;; emms-lyrics.el --- Display lyrics synchronically -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2005-2021 Free Software Foundation, Inc.
4
5 ;; Author: William Xu <william.xwl@gmail.com>
6 ;; Keywords: emms music lyrics
7
8 ;; This file is part of EMMS.
9
10 ;; EMMS is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 3, or (at your option)
13 ;; any later version.
14 ;;
15 ;; EMMS is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
19 ;;
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with EMMS; if not, write to the Free Software Foundation,
22 ;; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
23
24 ;;; Commentary:
25
26 ;; This package enables you to play music files and display lyrics
27 ;; synchronically! :-) Plus, it provides a `emms-lyrics-mode' for
28 ;; making lyric files.
29
30 ;; Put this file into your load-path and the following into your
31 ;; ~/.emacs:
32 ;; (require 'emms-lyrics)
33 ;;
34 ;; Then either `M-x emms-lyrics-enable' or add (emms-lyrics 1) in
35 ;; your .emacs to enable.
36
37 ;;; TODO:
38
39 ;; 1. Maybe the lyric setup should run before `emms-start'.
40 ;; 2. Give a user a chance to choose when finding out multiple lyrics.
41 ;; 3. Search .lrc format lyrics from internet ?
42
43 ;;; Code:
44
45 (require 'emms)
46 (require 'emms-player-simple)
47 (require 'emms-source-file)
48 (require 'time-date)
49 (require 'emms-url)
50 (require 'emms-compat)
51
52 ;;; User Customization
53
54 (defgroup emms-lyrics nil
55 "Lyrics module for EMMS."
56 :group 'emms)
57
58 (defcustom emms-lyrics-display-on-modeline t
59 "If non-nil, display lyrics on mode line."
60 :type 'boolean)
61
62 (defcustom emms-lyrics-display-on-minibuffer nil
63 "If non-nil, display lyrics on minibuffer."
64 :type 'boolean)
65
66 (defcustom emms-lyrics-display-buffer nil
67 "Non-nil will create deciated `emms-lyrics-buffer' to display lyrics."
68 :type 'boolean)
69
70 (defcustom emms-lyrics-dir "~/music/lyrics"
71 "Local lyrics repository.
72 `emms-lyrics-find-lyric' will look for lyrics in current directory(i.e.,
73 same as the music file) and this directory."
74 :type 'string)
75
76 (defcustom emms-lyrics-display-format " %s "
77 "Format for displaying lyrics."
78 :type 'string)
79
80 (defcustom emms-lyrics-coding-system nil
81 "Coding system for reading lyrics files.
82
83 If all your lyrics use the same coding system, you can set this
84 variable to that value; else you'd better leave it to nil, and
85 rely on `prefer-coding-system', `file-coding-system-alist' or
86 \(info \"(emacs)File Variables\"), sorted by priority
87 increasingly."
88 :type 'coding-system)
89
90 (defcustom emms-lyrics-mode-hook nil
91 "Normal hook run after entering Emms Lyric mode."
92 :type 'hook)
93
94 (defcustom emms-lyrics-find-lyric-function 'emms-lyrics-find-lyric
95 "Function for finding lyric files."
96 :type 'symbol)
97
98 (defcustom emms-lyrics-scroll-p t
99 "Non-nil value will enable lyrics scrolling on mode line.
100
101 Note: Even if this is set to t, it also depends on
102 `emms-lyrics-display-on-modeline' to be t."
103 :type 'boolean)
104
105 (defcustom emms-lyrics-scroll-timer-interval 0.4
106 "Interval between scroller timers. The shorter, the faster."
107 :type 'number)
108
109
110 ;;; User Interfaces
111
112 (defvar emms-lyrics-display-p t
113 "If non-nil, will diplay lyrics.")
114
115 (defvar emms-lyrics-mode-line-string ""
116 "Current lyric.")
117
118 (defvar emms-lyrics-buffer nil
119 "Buffer to show lyrics.")
120
121 (defvar emms-lyrics-chinese-url "http://mp3.baidu.com/m?f=ms&rn=10&tn=baidump3lyric&ct=150994944&word=%s&lm=-1"
122 "URL used to find Chinese lyrics.
123 Should contain one %s which is replaced with the filename.")
124
125 (defvar emms-lyrics-latin-url "http://lyrics.wikia.com/%s%s"
126 "URL used to find Latin lyrics.
127 Should contain two %s-expressions. The first is replaced with
128 the artist and second with the title.")
129
130 ;;;###autoload
131 (defun emms-lyrics-enable ()
132 "Enable displaying emms lyrics."
133 (interactive)
134 (emms-lyrics 1)
135 (message "emms lyrics enabled."))
136
137 ;;;###autoload
138 (defun emms-lyrics-disable ()
139 "Disable displaying emms lyrics."
140 (interactive)
141 (emms-lyrics -1)
142 (message "EMMS lyrics disabled"))
143
144 ;;;###autoload
145 (defun emms-lyrics-toggle ()
146 "Toggle displaying emms lyrics."
147 (interactive)
148 (if emms-lyrics-display-p
149 (emms-lyrics-disable)
150 (emms-lyrics-enable)))
151
152 (defun emms-lyrics-toggle-display-on-minibuffer ()
153 "Toggle display lyrics on minibbufer."
154 (interactive)
155 (if emms-lyrics-display-on-minibuffer
156 (progn
157 (setq emms-lyrics-display-on-minibuffer nil)
158 (message "Disable lyrics on minibufer"))
159 (setq emms-lyrics-display-on-minibuffer t)
160 (message "Enable lyrics on minibufer")))
161
162 (defun emms-lyrics-toggle-display-on-modeline ()
163 "Toggle display lyrics on mode line."
164 (interactive)
165 (if emms-lyrics-display-on-modeline
166 (progn
167 (setq emms-lyrics-display-on-modeline nil
168 emms-lyrics-mode-line-string "")
169 (message "Disable lyrics on mode line"))
170 (setq emms-lyrics-display-on-modeline t)
171 (message "Enable lyrics on mode line")))
172
173 (defun emms-lyrics-toggle-display-buffer ()
174 "Toggle showing/hiding `emms-lyrics-buffer'."
175 (interactive)
176 (let ((w (get-buffer-window emms-lyrics-buffer)))
177 (if w
178 (delete-window w)
179 (save-selected-window
180 (pop-to-buffer emms-lyrics-buffer)
181 (set-window-dedicated-p w t)))))
182
183 (defun emms-lyrics (arg)
184 "Turn on emms lyrics display if ARG is positive, off otherwise."
185 (interactive "p")
186 (if (and arg (> arg 0))
187 (progn
188 (setq emms-lyrics-display-p t)
189 (add-hook 'emms-player-started-hook 'emms-lyrics-start)
190 (add-hook 'emms-player-stopped-hook 'emms-lyrics-stop)
191 (add-hook 'emms-player-finished-hook 'emms-lyrics-stop)
192 (add-hook 'emms-player-paused-hook 'emms-lyrics-pause)
193 (add-hook 'emms-player-seeked-functions 'emms-lyrics-seek)
194 (add-hook 'emms-player-time-set-functions 'emms-lyrics-sync))
195 (emms-lyrics-stop)
196 (setq emms-lyrics-display-p nil)
197 (emms-lyrics-restore-mode-line)
198 (remove-hook 'emms-player-started-hook 'emms-lyrics-start)
199 (remove-hook 'emms-player-stopped-hook 'emms-lyrics-stop)
200 (remove-hook 'emms-player-finished-hook 'emms-lyrics-stop)
201 (remove-hook 'emms-player-paused-hook 'emms-lyrics-pause)
202 (remove-hook 'emms-player-seeked-functions 'emms-lyrics-seek)
203 (remove-hook 'emms-player-time-set-functions 'emms-lyrics-sync)))
204
205 (defun emms-lyrics-visit-lyric ()
206 "Visit playing track's lyric file.
207 If we can't find it from local disk, then search it from internet."
208 (interactive)
209 (let* ((track (emms-playlist-current-selected-track))
210 (name (emms-track-get track 'name))
211 (lrc (funcall emms-lyrics-find-lyric-function
212 (emms-replace-regexp-in-string
213 (concat "\\." (file-name-extension name) "\\'")
214 ".lrc"
215 (file-name-nondirectory name)))))
216 (if (and lrc (file-exists-p lrc) (not (string= lrc "")))
217 (find-file lrc)
218 (message "Lyric file does not exist on file-system. Searching online...")
219 (let* ((title (or (emms-track-get track 'info-title)
220 (file-name-sans-extension
221 (file-name-nondirectory name))))
222 (artist (when (emms-track-get track 'info-title)
223 (emms-track-get track 'info-artist)))
224 (url
225 (cond ((string-match "\\cc" title) ; Chinese lyrics.
226 ;; Since tag info might be encoded using various coding
227 ;; systems, we'd better fall back on filename.
228 (format emms-lyrics-chinese-url
229 (emms-url-quote-plus
230 (encode-coding-string name 'gb2312))))
231 (t ; English lyrics.g
232 (format emms-lyrics-latin-url
233 (if artist (concat (emms-url-quote-underscore artist) ":") "")
234 (emms-url-quote-underscore title))))))
235 (if (fboundp 'eww)
236 (progn (require 'eww)
237 (let ((readable-hook (when (fboundp 'eww-readable)
238 (add-hook 'eww-after-render-hook #'eww-readable)
239 #'eww-readable)))
240 (eww url)
241 (when readable-hook
242 (remove-hook 'eww-after-render-hook readable-hook))))
243 (browse-url url))
244 (message "Lyric file does not exist on file-system. Searching online...")))))
245
246
247 ;;; EMMS Lyrics
248
249 (defvar emms-lyrics-alist nil
250 "a list of the form: \\='((time0 . lyric0) (time1 . lyric1)...)). In
251 short, at time-i, display lyric-i.")
252
253 (defvar emms-lyrics-timers nil
254 "timers for displaying lyric.")
255
256 (defvar emms-lyrics-start-time nil
257 "emms lyric start time.")
258
259 (defvar emms-lyrics-pause-time nil
260 "emms lyric pause time.")
261
262 (defvar emms-lyrics-elapsed-time 0
263 "How long time has emms lyric played.")
264
265 (defvar emms-lyrics-scroll-timers nil
266 "Lyrics scroller timers.")
267
268 (defun emms-lyrics-read-file (file &optional catchup)
269 "Read a lyric file(LRC format).
270 Optional CATCHUP is for recognizing `emms-lyrics-catchup\\='.
271 FILE should end up with \".lrc\", its content looks like one of the
272 following:
273
274 [1:39]I love you, Emacs!
275 [00:39]I love you, Emacs!
276 [00:39.67]I love you, Emacs!
277
278 FILE should be under the same directory as the music file, or under
279 `emms-lyrics-dir\\='."
280 (or catchup
281 (setq file (funcall emms-lyrics-find-lyric-function file)))
282 (when (and file (file-exists-p file))
283 (with-temp-buffer
284 (let ((coding-system-for-read emms-lyrics-coding-system))
285 (emms-insert-file-contents file)
286 (while (search-forward-regexp "\\[[0-9:.]+\\].*" nil t)
287 (let ((lyric-string (match-string 0))
288 (time 0)
289 (lyric ""))
290 (setq lyric
291 (emms-replace-regexp-in-string ".*\\]" "" lyric-string))
292 (while (string-match "\\[[0-9:.]+\\]" lyric-string)
293 (let* ((time-string (match-string 0 lyric-string))
294 (semi-pos (string-match ":" time-string)))
295 (setq time
296 (+ (* (string-to-number
297 (substring time-string 1 semi-pos))
298 60)
299 (string-to-number
300 (substring time-string
301 (1+ semi-pos)
302 (1- (length time-string))))))
303 (setq lyric-string
304 (substring lyric-string (length time-string)))
305 (setq emms-lyrics-alist
306 (append emms-lyrics-alist `((,time . ,lyric))))
307 (setq time 0)))))
308 (setq emms-lyrics-alist
309 (sort emms-lyrics-alist (lambda (a b) (< (car a) (car b))))))
310 t)))
311
312 (defun emms-lyrics-create-buffer ()
313 "Create `emms-lyrics-buffer\\=' dedicated to lyrics. "
314 ;; leading white space in buffer name to hide the buffer
315 (setq emms-lyrics-buffer (get-buffer-create " *EMMS Lyrics*"))
316 (set-buffer emms-lyrics-buffer)
317 (setq buffer-read-only nil
318 cursor-type nil)
319 (erase-buffer)
320 (mapc (lambda (time-lyric) (insert (cdr time-lyric) "\n"))
321 emms-lyrics-alist)
322 (goto-char (point-min))
323 (emms-activate-highlighting-mode)
324 (setq buffer-read-only t))
325
326 (defun emms-lyrics-start ()
327 "Start displaying lryics."
328 (setq emms-lyrics-start-time (current-time)
329 emms-lyrics-pause-time nil
330 emms-lyrics-elapsed-time 0)
331 (when (let ((file
332 (emms-track-get
333 (emms-playlist-current-selected-track)
334 'name)))
335 (emms-lyrics-read-file
336 (emms-replace-regexp-in-string
337 (concat "\\." (file-name-extension file) "\\'")
338 ".lrc"
339 (file-name-nondirectory file))))
340 (when emms-lyrics-display-buffer
341 (emms-lyrics-create-buffer))
342 (emms-lyrics-set-timer)))
343
344 (defun emms-lyrics-catchup (lrc)
345 "Catchup with later downloaded LRC file(full path).
346 If you write some lyrics crawler, which is running asynchronically,
347 then this function would be useful to call when the crawler finishes its
348 job."
349 (let ((old-start emms-lyrics-start-time))
350 (setq emms-lyrics-start-time (current-time)
351 emms-lyrics-pause-time nil
352 emms-lyrics-elapsed-time 0)
353 (emms-lyrics-read-file lrc t)
354 (emms-lyrics-set-timer)
355 (emms-lyrics-seek (float-time (time-since old-start)))))
356
357 (defun emms-lyrics-stop ()
358 "Stop displaying lyrics."
359 (interactive)
360 (when emms-lyrics-alist
361 (mapc #'emms-cancel-timer emms-lyrics-timers)
362 (if (or (not emms-player-paused-p)
363 emms-player-stopped-p)
364 (setq emms-lyrics-alist nil
365 emms-lyrics-timers nil
366 emms-lyrics-mode-line-string ""))))
367
368 (defun emms-lyrics-pause ()
369 "Pause displaying lyrics."
370 (if emms-player-paused-p
371 (setq emms-lyrics-pause-time (current-time))
372 (when emms-lyrics-pause-time
373 (setq emms-lyrics-elapsed-time
374 (+ (float-time
375 (time-subtract emms-lyrics-pause-time
376 emms-lyrics-start-time))
377 emms-lyrics-elapsed-time)))
378 (setq emms-lyrics-start-time (current-time)))
379 (when emms-lyrics-alist
380 (if emms-player-paused-p
381 (emms-lyrics-stop)
382 (emms-lyrics-set-timer))))
383
384 (defun emms-lyrics-seek (sec)
385 "Seek forward or backward SEC seconds lyrics."
386 (setq emms-lyrics-elapsed-time
387 (+ emms-lyrics-elapsed-time
388 (float-time (time-since emms-lyrics-start-time))
389 sec))
390 (when (< emms-lyrics-elapsed-time 0) ; back to start point
391 (setq emms-lyrics-elapsed-time 0))
392 (setq emms-lyrics-start-time (current-time))
393 (when emms-lyrics-alist
394 (let ((paused-orig emms-player-paused-p))
395 (setq emms-player-paused-p t)
396 (emms-lyrics-stop)
397 (setq emms-player-paused-p paused-orig))
398 (emms-lyrics-set-timer)))
399
400 (defun emms-lyrics-sync (sec)
401 "Synchronize the lyric display at SEC seconds."
402 (setq emms-lyrics-start-time (current-time)
403 emms-lyrics-elapsed-time 0)
404 (emms-lyrics-seek sec))
405
406 (defun emms-lyrics-set-timer ()
407 "Set timers for displaying lyrics."
408 (setq emms-lyrics-timers '())
409 (let ((lyrics-alist emms-lyrics-alist)
410 (line 0))
411 (while lyrics-alist
412 (let ((time (- (caar lyrics-alist) emms-lyrics-elapsed-time))
413 (lyric (cdar lyrics-alist))
414 (next-time (and (cdr lyrics-alist)
415 (- (car (cadr lyrics-alist))
416 emms-lyrics-elapsed-time)))
417 (next-lyric (and (cdr lyrics-alist)
418 (cdr (cadr lyrics-alist)))))
419 (setq line (1+ line))
420 (when (> time 0)
421 (setq emms-lyrics-timers
422 (append emms-lyrics-timers
423 (list
424 (run-at-time (format "%d sec" time)
425 nil
426 #'emms-lyrics-display-handler
427 lyric
428 next-lyric
429 line
430 (and next-time (- next-time time)))))))
431 (setq lyrics-alist (cdr lyrics-alist))))))
432
433 (defun emms-lyrics-mode-line ()
434 "Add lyric to the mode line."
435 (or global-mode-string (setq global-mode-string '("")))
436 (unless (member 'emms-lyrics-mode-line-string
437 global-mode-string)
438 (setq global-mode-string
439 (append global-mode-string
440 '(emms-lyrics-mode-line-string)))))
441
442 (defun emms-lyrics-restore-mode-line ()
443 "Restore the mode line."
444 (setq global-mode-string
445 (remove 'emms-lyrics-mode-line-string global-mode-string))
446 (force-mode-line-update))
447
448 (defun emms-lyrics-display-handler (lyric next-lyric line diff)
449 "DIFF is the timestamp differences between current LYRIC and
450 NEXT-LYRIC; LINE corresponds line number for LYRIC in `emms-lyrics-buffer\\='."
451 (emms-lyrics-display (format emms-lyrics-display-format lyric) line)
452 (when (and emms-lyrics-display-on-modeline emms-lyrics-scroll-p)
453 (emms-lyrics-scroll lyric next-lyric diff)))
454
455 (defun emms-lyrics-display (lyric line)
456 "Display LYRIC now.
457 See `emms-lyrics-display-on-modeline\\=' and
458 `emms-lyrics-display-on-minibuffer\\=' on how to config where to
459 display."
460 (when emms-lyrics-alist
461 (when emms-lyrics-display-on-modeline
462 (emms-lyrics-mode-line)
463 (setq emms-lyrics-mode-line-string lyric)
464 ;; (setq emms-lyrics-mode-line-string ; make it fit scroller width
465 ;; (concat emms-lyrics-mode-line-string
466 ;; (make-string
467 ;; (abs (- emms-lyrics-scroll-width (length lyric)))
468 ;; (string-to-char " "))))
469 (force-mode-line-update))
470
471 (when emms-lyrics-display-on-minibuffer
472 (unless (minibuffer-window-active-p (selected-window))
473 (message lyric)))
474
475 (when emms-lyrics-display-buffer
476 (with-current-buffer emms-lyrics-buffer
477 (when line
478 (goto-char (point-min))
479 (forward-line (1- line))
480 (emms-line-highlight))))))
481
482 (defun emms-lyrics-find-lyric (file)
483 "Return full path of found lrc FILE, or nil if not found.
484 Use `emms-source-file-directory-tree-function\\=' to find lrc FILE under
485 current directory and `emms-lyrics-dir\\='.
486 e.g., (emms-lyrics-find-lyric \"abc.lrc\")"
487 (let* ((track (emms-playlist-current-selected-track))
488 (lyric-under-curr-dir
489 (concat (file-name-directory (emms-track-get track 'name))
490 file)))
491 (or (and (eq (emms-track-type track) 'file)
492 (file-exists-p lyric-under-curr-dir)
493 lyric-under-curr-dir)
494 (car (funcall emms-source-file-directory-tree-function
495 emms-lyrics-dir
496 file)))))
497
498 ;; (setq emms-lyrics-scroll-width 20)
499
500 (defun emms-lyrics-scroll (lyric next-lyric diff)
501 "Scroll LYRIC to left smoothly in DIFF seconds.
502 DIFF is the timestamp differences between current LYRIC and
503 NEXT-LYRIC."
504 (setq diff (floor diff))
505 (setq emms-lyrics-scroll-timers '())
506 (let ((scrolled-lyric (concat lyric " " next-lyric))
507 (time 0)
508 (pos 0))
509 (catch 'return
510 (while (< time diff)
511 (setq emms-lyrics-scroll-timers
512 (append emms-lyrics-scroll-timers
513 (list
514 (run-at-time time
515 nil
516 #'emms-lyrics-display
517 (if (>= (length lyric) pos)
518 (substring scrolled-lyric pos)
519 (throw 'return t))
520 nil))))
521 (setq time (+ time emms-lyrics-scroll-timer-interval))
522 (setq pos (1+ pos))))))
523
524
525 ;;; emms-lyrics-mode
526
527 (defvar emms-lyrics-mode-map
528 (let ((map (make-sparse-keymap)))
529 (define-key map "p" #'emms-lyrics-previous-line)
530 (define-key map "n" #'emms-lyrics-next-line)
531 (define-key map "i" #'emms-lyrics-insert-time)
532 map)
533 "Keymap for `emms-lyrics-mode\\='.")
534
535 (defun emms-lyrics-rem* (x y)
536 "The remainder of X divided by Y, with the same sign as X."
537 (let* ((q (floor x y))
538 (rem (- x (* y q))))
539 (if (= rem 0)
540 0
541 (if (eq (>= x 0) (>= y 0))
542 rem
543 (- rem y)))))
544
545 (defun emms-lyrics-insert-time ()
546 "Insert lyric time in the form: [01:23.21], then goto the
547 beginning of next line."
548 (interactive)
549 (let* ((total (+ (float-time
550 (time-subtract (current-time)
551 emms-lyrics-start-time))
552 emms-lyrics-elapsed-time))
553 (min (/ (* (floor (/ total 60)) 100) 100))
554 (sec (/ (floor (* (emms-lyrics-rem* total 60) 100)) 100.0)))
555 (insert (emms-replace-regexp-in-string
556 " " "0" (format "[%2d:%2d]" min sec))))
557 (emms-lyrics-next-line))
558
559 (defun emms-lyrics-next-line ()
560 "Goto the beginning of next line."
561 (interactive)
562 (forward-line 1))
563
564 (defun emms-lyrics-previous-line ()
565 "Goto the beginning of previous line."
566 (interactive)
567 (forward-line -1))
568
569 (define-derived-mode emms-lyrics-mode nil "Emms Lyric"
570 "Major mode for creating lyric files.
571 \\{emms-lyrics-mode-map}"
572 (run-hooks 'emms-lyrics-mode-hook))
573
574 (provide 'emms-lyrics)
575
576 ;;; emms-lyrics.el ends here