Compare commits

...

37 commits

Author SHA1 Message Date
2686f97830 lyrics-fetcher: fix genius.com 2024-12-22 19:20:05 +03:00
bcde34a7ae lyrics-fetcher: fix Published-At (x2) 2023-12-26 02:50:39 +03:00
3707e8c53f lyrics-fetcher: fix Published-At 2023-12-26 01:42:41 +03:00
f7f82ed997 lyrics-fetcher: add Published-At header & update copyright 2023-12-26 01:33:55 +03:00
0359ad482c ci: fix 2023-12-26 01:32:18 +03:00
4cf7f34212 fix: broken link in README 2023-12-19 01:28:34 +03:00
a3be34b015 fix: lint 2022-07-17 20:16:48 +03:00
42a348658b feat: add lyrics-fetcher-analyze 2022-07-17 20:13:14 +03:00
5e36d35148 chore: update copyright 2022-07-17 15:21:35 +03:00
06bd0293df chore: update version 2022-02-07 16:26:57 +03:00
8edf2447f0 fix: native-comp warning 2022-02-07 16:11:58 +03:00
f543862a06 docs: update README 2022-02-07 16:10:14 +03:00
3aa5847206 fix: docstring 2022-02-07 16:08:12 +03:00
3455a44bfa feat: switch backends 2022-02-07 16:04:01 +03:00
fc0c4731b6 fix: recursive load 2022-02-07 15:02:48 +03:00
27f12fc8c1 fix: byte-compilation and checkdoc 2022-02-07 14:51:09 +03:00
f49d4dc054 ci: add melpazoid 2022-02-07 14:44:52 +03:00
f5b2f93763
Merge pull request #6 from Elilif/neteasecloud
fix: require neteasecloud backend
2022-02-07 14:43:36 +03:00
eli
d0f458b0b6 fix: require neteasecloud backend 2022-02-07 13:38:29 +08:00
f6948259b3
Merge pull request #5 from Elilif/neteasecloud
Add neteasecloud backend
2022-02-04 12:16:17 +03:00
eli
1ac35c95b0 chore: remove FSF Copyright 2022-02-04 16:59:23 +08:00
eli
48b6d8e8ac chore: remove trailing parentheses 2022-02-04 16:58:51 +08:00
eli
3dfe79f34c chore: update docstring 2022-02-04 16:35:21 +08:00
eli
3613646387 chore: remove unused packages 2022-02-04 16:34:09 +08:00
eli
e5d636e01e feat: add neteasecloud backend
Emms supports real-time lyrics(.lrc format) which can be showed in
minibuffer or modeline. It would be better for lyrics-fetcher to fetch
`.lrc` format lyrics.

Luckily, I find `music.163.com` offers an API to return such format
lyrics, for instance:
https://music.163.com/api/song/lyric?id=35804609&lv=1&kv=1&tv=-1,
in which '35804609' is the song id you query.

So I writes a new backend for 'lyrics-fetcher' to get lrc format lyrics,
and the following are my config
```
(setq lyrics-fetcher-fetch-method #'lyrics-fetcher-neteasecloud-do-search)
(setq lyrics-fetcher-format-file-name-method #'lyrics-fetcher-neteasecloud-format-file-name)
(setq lyrics-fetcher-format-song-name-method #'lyrics-fetcher-neteasecloud-format-song-name)
(setq lyrics-fetcher-lyrics-file-extension ".lrc")
```
P.S. This is my first package writing and first pr, so if I did something
wrong, please tell me.
2022-02-04 14:31:55 +08:00
eli
61d4d25f71 fix: pass correct query
the original method always passes unmodified query regardless of the
value of 'edit'
2022-02-04 13:37:20 +08:00
f0212bea83
docs: add MELPA badge 2021-09-12 11:42:30 +03:00
4545f5c560 chore: new version 2021-08-28 13:13:31 +05:00
f14ab86af9 docs: update the package docstring 2021-08-28 12:58:04 +05:00
a933d4201a refactor: shell-command-to-string > call-process 2021-08-28 12:51:15 +05:00
ce7858a602 docs: add comments for genius behavior 2021-08-14 11:38:23 +03:00
12175e6965 fix: add fallback DOM queries for lyrics-fetcher-genius 2021-08-14 11:27:54 +03:00
87a99f328b fix: dom-print 2021-08-13 22:38:10 +03:00
b7e381a704 chore: add doc and FSF copyright for the copied function 2021-08-13 12:11:01 +03:00
eb3e9e94a7
chore: update the copyright list 2021-08-13 11:55:54 +03:00
1765073d1c
Merge pull request #3 from syohex/older-emacs
Copy dom-print from Emacs 28.1 for older Emacs
2021-08-13 11:48:22 +03:00
Shohei YOSHIDA
462b1d45d1 Copy dom-print from Emacs 28.1 for older Emacs
dom-print was introduced at Emacs 28.1 which is still
developing version.
2021-08-13 17:10:09 +09:00
6 changed files with 688 additions and 29 deletions

30
.github/workflows/melpazoid.yml vendored Normal file
View file

@ -0,0 +1,30 @@
# melpazoid <https://github.com/riscy/melpazoid> build checks.
# If your package is on GitHub, enable melpazoid's checks by copying this file
# to .github/workflows/melpazoid.yml and modifying RECIPE and EXIST_OK below.
name: melpazoid
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install
run: |
python -m pip install --upgrade pip
sudo apt-get install emacs && emacs --version
git clone https://github.com/riscy/melpazoid.git ~/melpazoid
pip install ~/melpazoid
- name: Run
env:
LOCAL_REPO: ${{ github.workspace }}
# RECIPE is your recipe as written for MELPA:
RECIPE: (lyrics-fetcher :repo "SqrtMinusOne/lyrics-fetcher.el" :fetcher github)
# set this to false (or remove it) if the package isn't on MELPA:
EXIST_OK: false
run: echo $GITHUB_REF && make -C ~/melpazoid

View file

@ -1,27 +1,24 @@
#+TITLE: lyrics-fetcher.el
[[https://melpa.org/#/lyrics-fetcher][file:https://melpa.org/packages/lyrics-fetcher-badge.svg]]
A package to fetch song lyrics and album covers. Integrates with EMMS.
[[./img/screenshot.png]]
As of now, the only available backend is [[https://genius.com/][genius.com]], but in principle, new ones can be added.
The available backends are [[https://genius.com][genius.com]] and [[https://music.163.com/][music.163.com]].
* Installation
As the package isn't yet available anywhere but in this repository, you can clone the repository, add it to the =load-path= and =require= the package:
#+begin_src emacs-lisp
(require 'lyrics-fetcher)
#+end_src
My preferred way is to use =use-package= with =straight=:
The package is available on MELPA. Install it however you normally install packages, I prefer =use-package= with =straight=:
#+begin_src emacs-lisp
(use-package lyrics-fetcher
:straight (:host github :repo "SqrtMinusOne/lyrics-fetcher.el")
:straight t
:after (emms))
#+end_src
Install [[https://imagemagick.org/index.php][imagemagick]] if you want to download covers.
Set [[https://docs.genius.com/][genius.com]] client access token. To do that, [[https://genius.com/api-clients/new][create a new client,]] click "Generate Access Token" and put the result to the =lyrics-fetcher-genius-access-token= variable. I do this with password-store:
If you want to use the genius backend, you have to set [[https://docs.genius.com/][genius.com]] client access token. To do that, [[https://genius.com/api-clients/new][create a new client,]] click "Generate Access Token" and put the result to the =lyrics-fetcher-genius-access-token= variable. I do this with password-store:
#+begin_src emacs-lisp
(setq lyrics-fetcher-genius-access-token
(password-store-get "My_Online/APIs/genius.com"))
@ -46,6 +43,8 @@ Available commands:
Modified by =C-u= the same way as ~lyrics-fetcher-show-lyrics~.
- ~M-x lyrics-fetcher-use-backend~ - select a backend to use.
EMMS integration:
- ~M-x lyrics-fetcher-emms-browser-show-at-point~ - fetch data for the current point in EMMS browser.
@ -64,16 +63,31 @@ EMMS integration:
Modified by =C-u= the same way as ~lyrics-fetcher-show-lyrics~.
- ~M-x lyrics-fetcher-emms-browser-open-large-cover-at-point~ - open large_cover for the current point in EMMS browser.
- ~M-x lyrics-fetcher-lyrics-catchup~ - feed the LRC file for the current track to EMMS.
Lyric view mode keybindings:
- =q= - close the lyrics buffer
- =r= - refetch the lyrics in the buffer
* Available backends
As of now, the available backends are =genius= and =neteasecloud= (thanks [[https://github.com/Elilif][@Elilif]]). Backends can be switched with ~M-x lyrics-fetcher-use-backend~, or from the Lisp code:
#+begin_src emacs-lisp
(lyrics-fetcher-use-backend 'neteasecloud)
#+end_src
The =genius= backend fetches lyrics in a simple text format.
=neteasecloud= fetches in the [[https://en.wikipedia.org/wiki/LRC_(file_format)][LRC]] format, which contains timestamps for each line of the lyrics text.
LRC files can also be read by =emms-lyrics=. ~lyrics-fetcher-use-backend~ sets up =lyrics-fetcher= and EMMS variables so that EMMS could see the lyrics, downloaded by =lyrics-fetcher=. Running ~M-x emms-lyrics~ then should enable lyric display for newly played tracks, or you can run ~M-x lyrics-fetcher-lyrics-catchup~ to manually feed the current LRC file to EMMS.
* Customization and extension
** Lyrics file naming and location
As was outlined above, lyrics files are saved to ~lyrics-fetcher-lyrics-folder~ and have an extension set in ~lyrics-fetcher-lyrics-file-extension~.
Take a look at the ~lyrics-fetcher-format-song-name-method~ and ~lyrics-fetcher-format-file-name-method~ variables if you want to customize the lyrics buffer and file naming.
** Using another player than EMMS
Also note that integration with =emms-lyrics= requires these variables to be set with =lyrics-fetcher-use-backend=
** Using other player than EMMS
To use another player, customize ~lyrics-fetcher-current-track-method~.
This variable contains a function that returns the current playing track. The return format has to be either a string or (recommended) an EMMS-like alist, which has to have the following fields:
@ -94,3 +108,5 @@ The album cover fetching is similar. The corresponding function is set in ~lyric
- =sync= - as above.
The first argument is =track= because in EMMS all the required information is stored in tracks, and album data is deduced from tracks. So this package just takes a sample track in the album.
* Troubleshooting
I've noticed that Genius can give pages with different DOMs to different people. If you have an empty buffer instead of lyrics, please attach the =curl-cookie-jar= file to the issue. It usually resides in =.emacs.d/request=.

275
lyrics-fetcher-analyze.el Normal file
View file

@ -0,0 +1,275 @@
;;; lyrics-fetcher-analyze.el --- Fetch lyrics from music.163.com -*- lexical-binding: t -*-
;; Copyright (C) 2022, 2023 Korytov Pavel
;; Copyright (C) 2022 Eli Qian
;; Copyright (C) 2021 Syohei YOSHIDA
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; Homepage: https://github.com/SqrtMinusOne/lyrics-fetcher.el
;; This file is NOT part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Perform various interesting computations on lyrics.
;;; Code:
(require 'lyrics-fetcher)
(require 'emms-browser)
(require 'f)
;; XXX This is not `defcustom' because the customize UI doesn't look
;; good with such a large list
(defvar lyrics-fetcher-analyze-stop-words
'("me" "my" "myself" "we" "our" "ours" "ourselves" "you" "your"
"yours" "yourself" "yourselves" "he" "him" "his" "himself" "she"
"her" "hers" "herself" "it" "its" "itself" "they" "them" "their"
"theirs" "themselves" "what" "which" "who" "whom" "this" "that"
"these" "those" "am" "is" "are" "was" "were" "be" "been" "being"
"have" "has" "had" "having" "do" "does" "did" "doing" "a" "an"
"the" "and" "but" "if" "or" "because" "as" "until" "while" "of"
"at" "by" "for" "with" "about" "against" "between" "into"
"through" "during" "before" "after" "above" "below" "to" "from"
"up" "down" "in" "out" "on" "off" "over" "under" "again" "further"
"then" "once" "here" "there" "when" "where" "why" "how" "all"
"any" "both" "each" "few" "more" "most" "other" "some" "such" "no"
"nor" "not" "only" "own" "same" "so" "than" "too" "very" "s" "t"
"can" "will" "just" "don" "should" "now" "ve" "chorus" "verse"
"pre-chorus" "bridge" "instrumental" "interlude" "intro" "outro"
"every" "ll")
"List of words to ignore when doing a word count.
Taken from NLTK + some additions of mine.")
(defcustom lyrics-fetcher-analyze-top-n 10
"Number of top words to display in the analysis."
:type 'integer
:group 'lyrics-fetcher)
(defvar lyrics-fetcher-analyze-lyrics-count-buffer-name
"*Lyrics Fetcher Lyrics Counts*"
"Name of the buffer to display lyrics counts.")
(defun lyrics-fetcher-analyze--get-bdata ()
"Get bdata from EMMS browser.
When the region is not active, return the entry at point. Otherwise,
return all the entries in the region."
(let ((count
(if (use-region-p)
(-
(line-number-at-pos (region-end))
(line-number-at-pos (region-beginning)))
1)))
(save-excursion
(when (use-region-p)
(goto-char (region-beginning)))
(cl-loop for i from 1 to count
collect (prog1
(emms-browser-bdata-at-point)
(forward-line))))))
(defun lyrics-fetcher-analyze--get-lyrics-paths (bdata &optional hash)
"Recursively get paths to existing files with lyrics.
BDATA is a list of EMMS bdata, such as returned by
`emms-browser-bdata-at-point'. HASH is a table to store the result,
where the key is the path and the value is the path to the file."
(unless hash
(setq hash (make-hash-table :test 'equal)))
(dolist (bdatum bdata)
(let ((maybe-track (car-safe (alist-get 'data bdatum))))
(if (emms-track-p maybe-track)
(let ((file-name (funcall lyrics-fetcher-format-file-name-method maybe-track)))
(if (lyrics-fetcher--lyrics-saved-p file-name)
(puthash (emms-track-name maybe-track)
(lyrics-fetcher--process-filename file-name) hash)
(message "No lyrics fetched for %s"
(funcall lyrics-fetcher-format-song-name-method maybe-track))))
(lyrics-fetcher-analyze--get-lyrics-paths (alist-get 'data bdatum) hash))))
hash)
(defun lyrics-fetcher-analyze--get-entiries-recursive (bdata level lyrics-paths)
"Get data for analysis recursively.
BDATA is a list of EMMS bdata. LEVEL is the level of the recursion.
LYRICS-PATHS is the output of
`lyrics-fetcher-analyze--get-lyrics-paths'."
(cl-loop for bdatum in bdata
for maybe-track = (car-safe (alist-get 'data bdatum))
;; If the level is 1 or higher and the current item is a
;; track, the group is the track and its lyrics
if (and (>= level 1)
(emms-track-p maybe-track)
(gethash (emms-track-name maybe-track) lyrics-paths))
collect `((kind . track)
(track . ,maybe-track)
(lyrics . (,(f-read (gethash (emms-track-name maybe-track) lyrics-paths)))))
;; If the level is 1 and the current item is not a track,
;; the group is the item and lyrics of all its tracks
else if (and (= level 1)
(not (emms-track-p maybe-track)))
collect `((kind . last-item)
(item . ,bdatum)
(lyrics . ,(lyrics-fetcher-analyze--get-entiries-recursive
(alist-get 'data bdatum)
(1- level)
lyrics-paths)))
;; If the level is less than 1, just return the lyrics for
;; each track
else if (and (< level 1)
(emms-track-p maybe-track)
(gethash (emms-track-name maybe-track) lyrics-paths))
collect (f-read (gethash (emms-track-name maybe-track) lyrics-paths))
else if (and (< level 1)
(not (emms-track-p maybe-track)))
append (lyrics-fetcher-analyze--get-entiries-recursive
(alist-get 'data bdatum)
(1- level)
lyrics-paths)
;; If the level is higher than 1 and the current item is
;; not a track, recursively process groups below
else if (and (> level 1)
(not (emms-track-p maybe-track)))
collect `((kind . item)
(item . ,bdatum)
(groups . ,(lyrics-fetcher-analyze--get-entiries-recursive
(alist-get 'data bdatum)
(1- level)
lyrics-paths)))))
(defun lyrics-fetcher-analyze--get-entities ()
"Get data for the analysis."
(let* ((tracks (lyrics-fetcher-analyze--get-bdata))
(level (if current-prefix-arg
(truncate (sqrt (prefix-numeric-value current-prefix-arg)))
1))
(lyrics-paths (lyrics-fetcher-analyze--get-lyrics-paths tracks)))
(lyrics-fetcher-analyze--get-entiries-recursive tracks level lyrics-paths)))
(defun lyrics-fetcher-analyze--get-top-n (texts)
"Get the top N words from TEXTS. Return a list of (word . count).
TEXTS is a list of strings."
(let ((counts (make-hash-table :test 'equal))
(stop-words (make-hash-table :test 'equal)))
(cl-loop for word in lyrics-fetcher-analyze-stop-words
do (puthash word t stop-words))
(dolist (text texts)
(dolist (word (split-string text "[^[:word:]]+"))
(let ((word (downcase word)))
(when (and (> (length word) 1)
(not (gethash word stop-words)))
(puthash word (1+ (gethash word counts 0)) counts)))))
(setq counts (cl-loop for word being the hash-keys of counts
collect (cons word (gethash word counts 0))))
(seq-take
(cl-sort counts '> :key 'cdr)
lyrics-fetcher-analyze-top-n)))
(defun lyrics-fetcher-analyze--format-top-n (counts)
"Format the top N words list for display.
COUNTS is a list of (word . count)."
(mapconcat
(lambda (count)
;; (format "%s: %d" (car count) (cdr count))
(car count))
counts
"; "))
(defun lyrics-fetcher-analyze--lyrics-count-display (data &optional level)
"Display the lyrics count for DATA.
DATA is a list of entities as returned by
`lyrics-fetcher-analyze--get-entities'. LEVEL is the level of the
recursion."
(unless level
(setq level 0))
(dolist (datum data)
(pcase (alist-get 'kind datum)
('track
(let ((name (funcall lyrics-fetcher-format-song-name-method
(alist-get 'track datum)))
(counts (lyrics-fetcher-analyze--get-top-n
(alist-get 'lyrics datum))))
(insert (make-string (* level 2) ?\s))
(insert (propertize
(format "Track: %s\n" name)
'face 'emms-browser-track-face))
(insert (make-string (* level 2) ?\s))
(insert (lyrics-fetcher-analyze--format-top-n counts))
(insert "\n")))
('last-item
(let ((name (alist-get 'name (alist-get 'item datum)))
(counts (lyrics-fetcher-analyze--get-top-n
(alist-get 'lyrics datum))))
(insert (make-string (* level 2) ?\s))
(insert (propertize
(format "%s\n" name)
'face 'emms-browser-album-face))
(insert (make-string level ?\s))
(insert (lyrics-fetcher-analyze--format-top-n counts))
(insert "\n")))
('item
(let ((name (alist-get 'name (alist-get 'item datum))))
(insert (make-string (* level 2) ?\s))
(insert (propertize
(format "%s\n" name)
'face 'emms-browser-album-face))
(lyrics-fetcher-analyze--lyrics-count-display (alist-get 'groups datum)
(1+ level)))))))
(defvar lyrics-fetcher-analyze-mode-map
(let ((keymap (make-sparse-keymap)))
(define-key keymap (kbd "q") 'lyrics-fetcher-view-close-lyrics)
(when (fboundp 'evil-define-key*)
(evil-define-key* 'normal keymap
"q" 'lyrics-fetcher-view-close-lyrics))
keymap)
"Keymap for `lyrics-fetcher-analyze-mode'.")
(define-derived-mode lyrics-fetcher-analyze-mode special-mode "Lyrics Fetcher Analysis"
"Major mode for viewing lyrics analysis data.
\\{lyrics-fetcher-analyze-mode-map}")
;;;###autoload
(defun lyrics-fetcher-analyze-lyrics-count ()
"Count top-N words for EMMS tracks at point or region.
\\[universal-argument] sets the level of detalization. I.e. in
`emms-browse-by-artist', running this function on an artist results in
per-artist summary, running with one argument creates a summary on
each of the artist's albums, and running with two arguments creates a
summary for each track.
Change the `lyrics-fetcher-analyze-top-n' variable to control the
number of words to count. You may also want to extend
`lyrics-fetcher-analyze-stop-words' if you're running this with a
language other than English."
(interactive)
(let ((data (lyrics-fetcher-analyze--get-entities)))
(with-current-buffer (get-buffer-create lyrics-fetcher-analyze-lyrics-count-buffer-name)
(lyrics-fetcher-analyze-mode)
(let ((inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(lyrics-fetcher-analyze--lyrics-count-display data))))
(switch-to-buffer-other-window lyrics-fetcher-analyze-lyrics-count-buffer-name)))
(provide 'lyrics-fetcher-analyze)
;;; lyrics-fetcher-analyze.el ends here

View file

@ -1,6 +1,8 @@
;;; lyrics-fetcher-genius.el --- Fetch lyrics from genius.com -*- lexical-binding: t -*-
;; Copyright (C) 2021 Korytov Pavel
;; Copyright (C) 2022, 2023 Korytov Pavel
;; Copyright (C) 2021 Syohei YOSHIDA
;; Copyright (C) 2014-2021 Free Software Foundation, Inc.
;; Author: Korytov Pavel <thexcloud@gmail.com>
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
@ -32,6 +34,7 @@
(require 'seq)
(require 'shr)
(require 'f)
(require 'dom)
(defcustom lyrics-fetcher-genius-access-token nil
"Genius access token. Get one at https://genius.com."
@ -110,9 +113,9 @@ When EDIT is non-nil, edit the query in minibuffer before search."
(defun lyrics-fetcher-genius--maybe-edit-query (query edit)
"If EDIT is non-nil, edit QUERY in minibuffer."
(when edit
(read-from-minibuffer "Query: " query))
query)
(if edit
(read-from-minibuffer "Query: " query)
query))
(defun lyrics-fetcher-genius--format-query (track)
"Format track to genius.com query.
@ -176,6 +179,50 @@ first song."
results-songs-for-select)))
(assoc key (assoc 'result (car results-songs)))))))
(defun lyrics-fetcher-genius--dom-print (dom &optional pretty xml)
"Print DOM at point as HTML/XML.
If PRETTY, indent the HTML/XML logically. If XML, generate XML
instead of HTML.
This function was copied from Emacs 28.1 to ensure the backward
compatibilty with Emacs 27."
(let ((column (current-column)))
(insert (format "<%s" (dom-tag dom)))
(let ((attr (dom-attributes dom)))
(dolist (elem attr)
(if (and (memq (car elem)
'(async autofocus autoplay checked
contenteditable controls default
defer disabled formNoValidate frameborder
hidden ismap itemscope loop
multiple muted nomodule novalidate open
readonly required reversed
scoped selected typemustmatch))
(cdr elem)
(not xml))
(insert (format " %s" (car elem)))
(insert (format " %s=%S" (car elem) (cdr elem))))))
(let* ((children (dom-children dom))
(non-text nil))
(if (null children)
(insert " />")
(insert ">")
(dolist (child children)
(if (stringp child)
(insert child)
(setq non-text t)
(when pretty
(insert "\n" (make-string (+ column 2) ? )))
(lyrics-fetcher-genius--dom-print child pretty xml)))
(when (and pretty
(or (bolp)
non-text))
(unless (bolp)
(insert "\n"))
(insert (make-string column ? )))
(insert (format "</%s>" (dom-tag dom)))))))
(defun lyrics-fetcher-genius--fetch-lyrics (url callback &optional sync)
"Fetch lyrics from genius.com page at URL and call CALLBACK with the result.
@ -189,14 +236,23 @@ If SYNC is non-nil, the request will be performed synchronously."
(let* ((html (with-temp-buffer
(insert data)
(libxml-parse-html-region (point-min) (point-max))))
(lyrics-div (dom-by-class html (rx bos "lyrics" eos))))
(with-temp-buffer
(dom-print lyrics-div)
(shr-render-region (point-min) (point-max))
(funcall callback
(buffer-substring-no-properties
(point-min)
(point-max)))))))
;; Apparently, Genius can give different
;; responses to different people based on
;; cookies.
(lyrics-divs (or (dom-by-class html (rx bos "lyrics" eos))
(dom-by-class html (rx bos "Lyrics__Container" (* nonl)))
(dom-by-class html (rx bos "Lyrics-sc" (* nonl))) )))
(funcall callback
(mapconcat
(lambda (lyrics-div)
(with-temp-buffer
(lyrics-fetcher-genius--dom-print lyrics-div)
(shr-render-region (point-min) (point-max))
(buffer-substring-no-properties
(point-min)
(point-max))))
lyrics-divs
"\n")))))
:error
(cl-function
(lambda (&key error-thrown &allow-other-keys)

View file

@ -0,0 +1,216 @@
;;; lyrics-fetcher-neteasecloud.el --- Fetch lyrics from music.163.com -*- lexical-binding: t -*-
;; Copyright (C) 2022, 2023 Korytov Pavel
;; Copyright (C) 2022 Eli Qian
;; Copyright (C) 2021 Syohei YOSHIDA
;; Author: Eli Qian <eli.q.qian@gmail.com>
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; Homepage: https://github.com/SqrtMinusOne/lyrics-fetcher.el
;; This file is NOT part of GNU Emacs.
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Fetch song lyrics from genius.com.
;;; Code:
(require 'emms)
(require 'request)
(require 'cl-lib)
(require 'json)
(require 'seq)
(declare-function lyrics-fetcher--prepare-string "lyrics-fetcher")
(defcustom lyrics-fetcher-neteasecloud-strip-parens-from-query t
"Strip parens from the query.
I've noticed that these often break the search, e.g. when
searching \"Song (feat. Artist)\""
:type 'boolean
:group 'lyrics-fetcher)
(defun lyrics-fetcher-neteasecloud-do-search (track callback &optional sync edit)
"Perform a lyrics search on 'music.163.com'.
The flow is as follows:
1. Send a POST /search request with a text query
2. Pick the first result (or prompt user if SYNC is non-nil)
3. Fetch lyrics
4. Call CALLBACK with the resulting lyrics string
TRACK should be EMMS-compatible alist or string, take a look at
`lyrics-fetcher-neteasecloud--format-query'. If the search is
successful, CALLBACK will be called with the result.
If SYNC is non-nil, perform request synchronously and ask the
user to pick the matching search result.
When EDIT is non-nil, edit the query in minibuffer before search.
Genius usually struggles to find song if there is extra
information in the title."
(lyrics-fetcher-neteasecloud--do-query
track
(lambda (data)
(lyrics-fetcher-neteasecloud--fetch-lyrics
(lyrics-fetcher-neteasecloud--get-song-id data sync)
callback
sync))
sync
edit))
(defun lyrics-fetcher-neteasecloud--fetch-lyrics (song-id callback &optional sync)
"Fetch lyrics from 'music.163.com' page at URL.
CALLBACK is called with the result.
SONG-ID is a sequence of number which indicates a song, it can be
returned by 'lyrics-fetcher-neteasecloud--get-song-id' If SYNC is
non-nil, the request will be performed synchronously."
(message "Getting lyrics from NeteaseCloud API...")
(request
(format "http://music.163.com/api/song/lyric?id=%s&lv=1&kv=1&tv=-1" song-id)
:parser 'json-read
:sync sync
:success (cl-function
(lambda (&key data &allow-other-keys)
(funcall callback (alist-get 'lyric (alist-get 'lrc data)))))
:error
(cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Error!: %S" error-thrown)))))
(defun lyrics-fetcher-neteasecloud--do-query (track callback &optional sync edit)
"Perform a song search on 'music.163.com'.
TRACK should be EMMS-compatible alist or string, take a look at
`lyrics-fetcher-neteasecloud--format-query'. If the search is
successful, CALLBACK will be called with the result.
SYNC determines whether the request is synchronous. The parameter
is useful when it is necessary to ask the user for something right
after the request.
When EDIT is non-nil, edit the query in minibuffer before search."
(message "Sending a query to NeteaseCloud API...")
(request "http://music.163.com/api/search/get/"
:type "POST"
:data `(("s" . ,(lyrics-fetcher-neteasecloud--maybe-edit-query
(lyrics-fetcher-neteasecloud--format-query track)
edit))
("limit" . "10")
("type" . "1")
("offset" . "0"))
:parser 'json-read
:sync sync
:success (cl-function
(lambda (&key data &allow-other-keys)
(funcall callback data)))
:error (cl-function
(lambda (&key error-thrown &allow-other-keys)
(message "Error!: %S" error-thrown)))))
(defun lyrics-fetcher-neteasecloud--maybe-edit-query (query edit)
"If EDIT is non-nil, edit QUERY in minibuffer."
(if edit
(read-from-minibuffer "Query: " query)
query))
(defun lyrics-fetcher-neteasecloud--format-query (track)
"Format track to 'music.163.com' query.
When `lyrics-fetcher-neteasecloud-strip-parens-from-query' is non-nil,
remove all the text in parens from the query,
for instance (feat. someone).
TRACK should either be a string or an EMMS-compatible alist, which
contains `info-artist' or `info-title'"
(if (stringp track)
track
(let ((query (concat
(cdr (assoc 'info-title track))
" "
(cdr (assoc 'info-artist track)))))
(when lyrics-fetcher-neteasecloud-strip-parens-from-query
(setq query (replace-regexp-in-string
(rx (or (: "(" (* nonl) ")")
(: "[" (* nonl) "]")))
"" query)))
query)))
(defun lyrics-fetcher-neteasecloud--get-song-id (data &optional ask)
"Retrieve a song id from the 'music.163.com' response DATA.
If ASK is non-nil, prompt the user for a choice, otherwise select the
first song."
(if (/= 200 (alist-get 'code data))
(error "ERROR: %s" (alist-get 'code data))
(let* ((results (alist-get 'songs (alist-get 'result data))))
(if (seq-empty-p results)
(error "ERROR: no results!")
(cdr
(if ask
(let ((results-songs-for-select
(mapcar
(lambda (entry)
(cons (lyrics-fetcher-neteasecloud--format-song-title entry)
(assoc 'id entry)))
results)))
(cdr
(assoc
(completing-read
"Pick a result: "
results-songs-for-select
nil t)
results-songs-for-select)))
(assoc 'id (aref results 0))))))))
(defun lyrics-fetcher-neteasecloud--format-song-title (entry)
"Convert a 'music.163.com' search ENTRY to a string.
That entry can then be used in selection"
(format "%s by %s"
(cdr (assoc 'name entry))
(cdr (assoc 'name (aref (alist-get 'artists entry) 0)))))
(defun lyrics-fetcher-neteasecloud-format-file-name (track)
"TRACK should be either a string or EMMS alist.
'Emms' requires lyrics files' name should be the same as their
tracks' name except extensions."
(if (stringp track)
(substring
(lyrics-fetcher--prepare-string track)
0
(min (length track) 250))
(let ((full-name (emms-track-get track 'name)))
(emms-replace-regexp-in-string
(concat "\\." (file-name-extension full-name) "\\'")
""
(file-name-nondirectory full-name)))))
(defun lyrics-fetcher-neteasecloud-format-song-name (track)
"Format TRACK to a human-readable form.
TRACK should be either a string or EMMS alist."
(if (stringp track)
track
(format "%s %s"
(cdr (assoc 'info-title track))
(cdr (assoc 'info-artist track)))))
(provide 'lyrics-fetcher-neteasecloud)
;;; lyrics-fetcher-neteasecloud.el ends here

View file

@ -1,12 +1,14 @@
;;; lyrics-fetcher.el --- Fetch song lyrics and album covers -*- lexical-binding: t -*-
;; Copyright (C) 2021 Korytov Pavel
;; Copyright (C) 2022, 2023 Korytov Pavel
;; Copyright (C) 2021 Syohei YOSHIDA
;; Author: Korytov Pavel <thexcloud@gmail.com>
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; Version: 0.1.2
;; Version: 0.2.0
;; Package-Requires: ((emacs "27") (emms "7.5") (f "0.20.0") (request "0.3.2"))
;; Homepage: https://github.com/SqrtMinusOne/lyrics-fetcher.el
;; Published-At: 2021-08-14
;; This file is NOT part of GNU Emacs.
@ -26,13 +28,30 @@
;;; Commentary:
;; A package to fetch song lyrics and album covers, mainly to use with
;; EMMS. Take a look at the package README.org for more information.
;; EMMS.
;;
;; Main commands:
;; - `lyrics-fetcher-show-lyrics' - show lyrics for the current
;; playing track.
;; - `lyrics-fetcher-show-lyrics-query' - show lyrics by a
;; text query.
;; - `lyrics-fetcher-emms-browser-show-at-point' - show lyrics
;; for the current point in EMMS browser.
;; - `lyrics-fetcher-emms-browser-fetch-covers-at-point' - fetch
;; album covers for the current point in the EMMS browser.
;; - `lyrics-fetcher-use-backend' - select a backend.
;;
;; Take a look at the package README.org at
;; <https://github.com/SqrtMinusOne/lyrics-fetcher.el> for more
;; information.
;;; Code:
(require 'lyrics-fetcher-genius)
(require 'lyrics-fetcher-neteasecloud)
(require 'f)
(require 'emms)
(require 'emms-browser)
(require 'emms-lyrics)
(defgroup lyrics-fetcher ()
"Fetch song and album covers."
@ -239,6 +258,52 @@ See `lyrics-fetcher-show-lyrics' for behavior."
(interactive "sEnter query: ")
(lyrics-fetcher-show-lyrics query))
;;;###autoload
(defun lyrics-fetcher-use-backend (backend)
"Select a backend to use with lyrics-fetcher.
BACKEND is a symbol that corresponds to the required backend. As of
now, the supported ones are:
- `genius'
- `neteasecloud'
`genius' fetches lyrics in a text format, more suitable for
viewing in a text buffer.
`neteasecloud' fetches in the LRC format, which contains
timestamps for every line in the lyric text. This function also
sets up EMMS variables so that the default
`emms-lyrics-find-lyric-function' could find lyrics."
(interactive
(list (intern
(completing-read "Select backend: " '("genius" "neteasecloud")
nil t))))
(pcase backend
('genius
(setq lyrics-fetcher-fetch-method #'lyrics-fetcher-genius-do-search
lyrics-fetcher-format-file-name-method #'lyrics-fetcher-format-file-name
lyrics-fetcher-format-song-name-method #'lyrics-fetcher-format-song-name
lyrics-fetcher-lyrics-file-extension ".txt"))
('neteasecloud
(setq lyrics-fetcher-fetch-method #'lyrics-fetcher-neteasecloud-do-search
lyrics-fetcher-format-file-name-method #'lyrics-fetcher-neteasecloud-format-file-name
lyrics-fetcher-format-song-name-method #'lyrics-fetcher-neteasecloud-format-song-name
lyrics-fetcher-lyrics-file-extension ".lrc"
emms-lyrics-dir lyrics-fetcher-lyrics-folder))
(_ (user-error "Backend %s not found" (symbol-name backend)))))
(defun lyrics-fetcher-lyrics-catchup ()
"Feed the LRC file for the current track to EMMS."
(interactive)
(let* ((track (emms-playlist-current-selected-track))
(name (emms-track-get track 'name))
(lrc (funcall emms-lyrics-find-lyric-function
(emms-replace-regexp-in-string
(concat "\\." (file-name-extension name) "\\'")
".lrc"
(file-name-nondirectory name)))))
(emms-lyrics-catchup lrc)))
(cl-defun lyrics-fetcher--fetch-many (tracks &optional &key start force-fetch sync edit)
"Fetch lyrics for every track in the TRACKS list.
@ -505,10 +570,11 @@ If EDIT is non-nil, edit the query in minibuffer before search."
Requires imagemagick installed."
(dolist (size `((,lyrics-fetcher-small-cover-size . "cover_small.")
(,lyrics-fetcher-medium-cover-size . "cover_med.")))
(shell-command-to-string
(format "convert \"%s\" -resize %s^ -gravity Center -extent %s \"%s\""
filename (car size) (car size)
(f-join (f-dirname filename) (concat (cdr size) (f-ext filename)))))))
(call-process
"convert" nil nil nil
filename "-resize" (concat (car size) "^")
"-gravity" "Center" "-extent" (car size)
(f-join (f-dirname filename) (concat (cdr size) (f-ext filename))))))
(provide 'lyrics-fetcher)
;;; lyrics-fetcher.el ends here