| 1 | ;;; emms-lyrics-lrclib.el --- Fetch synchronized lyrics through LRCLIB -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;; Copyright (C) 2024 Free Software Foundation, Inc. |
| 4 | |
| 5 | ;; Author: Daniel Semyonov <daniel@dsemy.com> |
| 6 | |
| 7 | ;; This file is part of EMMS. |
| 8 | |
| 9 | ;; EMMS is free software; you can redistribute it and/or modify it |
| 10 | ;; under the terms of the GNU General Public License as published by |
| 11 | ;; the Free Software Foundation; either version 3, or (at your option) |
| 12 | ;; any later version. |
| 13 | |
| 14 | ;; EMMS is distributed in the hope that it will be useful, but WITHOUT |
| 15 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| 16 | ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public |
| 17 | ;; License for more details. |
| 18 | |
| 19 | ;; You should have received a copy of the GNU General Public License |
| 20 | ;; along with EMMS; see the file COPYING. If not, write to the Free |
| 21 | ;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |
| 22 | ;; MA 02110-1301, USA. |
| 23 | |
| 24 | ;;; Commentary: |
| 25 | |
| 26 | ;; This file provides a command/track initialization function which |
| 27 | ;; automatically fetches synchronized lyrics for tracks (the current |
| 28 | ;; track interactively) through an LRCLIB server. |
| 29 | |
| 30 | ;;; Code: |
| 31 | |
| 32 | (eval-when-compile |
| 33 | (declare-function json-parse-buffer "json.c")) |
| 34 | |
| 35 | (require 'emms-lyrics) |
| 36 | (require 'emms-later-do) |
| 37 | |
| 38 | (defgroup emms-lyrics-lrclib nil |
| 39 | "EMMS module for fetching synchronized lyrics through LRCLIB servers." |
| 40 | :group 'emms-lyrics |
| 41 | :prefix "emms-lyrics-lrclib-") |
| 42 | |
| 43 | (defcustom emms-lyrics-lrclib-url "https://lrclib.net/api/" |
| 44 | "Base URL for LRCLIB API requests." |
| 45 | :type 'string) |
| 46 | |
| 47 | (defconst emms-lyrics-lrclib-max-requests 250 |
| 48 | "Maximum number of concurrent requests to LRCLIB.") |
| 49 | |
| 50 | (defvar emms-lyrics-lrclib-requests 0 |
| 51 | "Current number of concurrent requests to LRCLIB.") |
| 52 | |
| 53 | (defun emms-lyrics-lrclib-encode-name (name) |
| 54 | "Encode (artist/album/track) NAME for an LRCLIB search." |
| 55 | (and (stringp name) (string-replace " " "+" name))) |
| 56 | |
| 57 | (defun emms-lyrics-lrclib-parse (_ file track interactive) |
| 58 | "Parse and save synced lyrics in FILE. |
| 59 | If TRACK is the selected track in the current playlist, catch up. |
| 60 | When INTERACTIVE is non-nil, display messages and confirm overwrite." |
| 61 | (unwind-protect |
| 62 | (let (lyrics) |
| 63 | (search-forward "\n\n") |
| 64 | (if-let* (((functionp 'json-available-p)) |
| 65 | ((json-available-p)) |
| 66 | (p (json-parse-buffer :null-object nil))) |
| 67 | (and (hash-table-p p) (setq lyrics (gethash "syncedLyrics" p))) |
| 68 | (when-let* ((beg (search-forward "\"syncedLyrics\":\"" nil t)) |
| 69 | (end (1- (search-forward-regexp "[^\\]\"" nil t)))) |
| 70 | (replace-string-in-region "\\n" "\n" beg end) |
| 71 | (setq lyrics (buffer-substring-no-properties |
| 72 | beg (1- (point)))))) |
| 73 | (and lyrics interactive (file-exists-p file) |
| 74 | (not (y-or-n-p (format "Overwrite existing file (\"%s\")?" file))) |
| 75 | (setq lyrics nil)) |
| 76 | (when lyrics |
| 77 | (with-temp-file file (insert lyrics)) |
| 78 | (when interactive (message "Saved synced lyrics at \"%s\"" file)) |
| 79 | (and (boundp 'emms-lyrics-display-p) |
| 80 | emms-lyrics-display-p emms-player-playing-p |
| 81 | (equal track (emms-playlist-current-selected-track)) |
| 82 | (emms-lyrics-catchup file)))) |
| 83 | (setq emms-lyrics-lrclib-requests (1- emms-lyrics-lrclib-requests)))) |
| 84 | |
| 85 | ;;;###autoload |
| 86 | (defun emms-lyrics-lrclib-get (&optional track force interactive) |
| 87 | "Search for synchronized lyrics for TRACK through LRCLIB's API. |
| 88 | If TRACK is omitted or nil, use the selected track in the current playlist. |
| 89 | The lyrics are saved in an \".lrc\" file alongside the track, unless the |
| 90 | file already exists (in which case the search isn't performed). |
| 91 | When called interactively (non-nil INTERACTIVE), display informative |
| 92 | messages, and with prefix argument FORCE, ask to overwrite existing |
| 93 | \".lrc\" files." |
| 94 | (interactive (list nil current-prefix-arg t)) |
| 95 | (if (> emms-lyrics-lrclib-requests emms-lyrics-lrclib-max-requests) |
| 96 | (emms-later-do #'emms-lyrics-lrclib-get track force interactive) |
| 97 | (when-let* ((track (or track (emms-playlist-current-selected-track))) |
| 98 | ((eq (emms-track-type track) 'file)) |
| 99 | (file (emms-track-name track)) |
| 100 | (lrc (replace-regexp-in-string "\\.[^.]+\\'" ".lrc" file)) |
| 101 | ((or force (not (file-exists-p lrc)))) |
| 102 | ((file-writable-p lrc)) |
| 103 | (title (emms-lyrics-lrclib-encode-name |
| 104 | (emms-track-get track 'info-title))) |
| 105 | (artist (emms-lyrics-lrclib-encode-name |
| 106 | (emms-track-get track 'info-artist))) |
| 107 | (album (emms-lyrics-lrclib-encode-name |
| 108 | (emms-track-get track 'info-album))) |
| 109 | (time (emms-track-get track 'info-playing-time))) |
| 110 | (setq emms-lyrics-lrclib-requests (1+ emms-lyrics-lrclib-requests)) |
| 111 | (when interactive (message "Searching for lyrics...")) |
| 112 | (url-retrieve |
| 113 | (url-encode-url |
| 114 | (format "%sget?artist_name=%s&track_name=%s&album_name=%s&duration=%d" |
| 115 | emms-lyrics-lrclib-url artist title album time)) |
| 116 | #'emms-lyrics-lrclib-parse (list lrc track interactive))))) |
| 117 | |
| 118 | (provide 'emms-lyrics-lrclib) |
| 119 | |
| 120 | ;;; emms-lyrics-lrclib.el ends here |