mirror of
https://github.com/SqrtMinusOne/lyrics-fetcher.el.git
synced 2025-12-10 17:03:03 +03:00
Compare commits
37 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2686f97830 | |||
| bcde34a7ae | |||
| 3707e8c53f | |||
| f7f82ed997 | |||
| 0359ad482c | |||
| 4cf7f34212 | |||
| a3be34b015 | |||
| 42a348658b | |||
| 5e36d35148 | |||
| 06bd0293df | |||
| 8edf2447f0 | |||
| f543862a06 | |||
| 3aa5847206 | |||
| 3455a44bfa | |||
| fc0c4731b6 | |||
| 27f12fc8c1 | |||
| f49d4dc054 | |||
| f5b2f93763 | |||
|
|
d0f458b0b6 | ||
| f6948259b3 | |||
|
|
1ac35c95b0 | ||
|
|
48b6d8e8ac | ||
|
|
3dfe79f34c | ||
|
|
3613646387 | ||
|
|
e5d636e01e | ||
|
|
61d4d25f71 | ||
| f0212bea83 | |||
| 4545f5c560 | |||
| f14ab86af9 | |||
| a933d4201a | |||
| ce7858a602 | |||
| 12175e6965 | |||
| 87a99f328b | |||
| b7e381a704 | |||
| eb3e9e94a7 | |||
| 1765073d1c | |||
|
|
462b1d45d1 |
6 changed files with 688 additions and 29 deletions
30
.github/workflows/melpazoid.yml
vendored
Normal file
30
.github/workflows/melpazoid.yml
vendored
Normal 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
|
||||
36
README.org
36
README.org
|
|
@ -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
275
lyrics-fetcher-analyze.el
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
216
lyrics-fetcher-neteasecloud.el
Normal file
216
lyrics-fetcher-neteasecloud.el
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue