feat: add lrclib and genius providers (#5)
* feat: lrclib and genius providers * revert `prepend_header` to true * change providers order --------- Co-authored-by: mrsobakin <68982655+mrsobakin@users.noreply.github.com>
This commit is contained in:
parent
d6b36886ba
commit
bad4a355e8
8 changed files with 103 additions and 19 deletions
|
@ -1,7 +1,7 @@
|
||||||
[providers]
|
[providers]
|
||||||
order = ["musixmatch", "kugou"]
|
order = ["musixmatch", "kugou", "lrclib", "genius"]
|
||||||
delay = 10
|
delay = 10
|
||||||
prepend_header = true
|
prepend_header = true
|
||||||
|
|
||||||
[providers.musixmatch]
|
[providers.musixmatch]
|
||||||
token = "123456"
|
token = ""
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
|
||||||
from pathlib import Path
|
|
||||||
import traceback
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
# Initialize classes from lrc_dl/providers
|
# Initialize classes from lrc_dl/providers
|
||||||
import lrc_dl.providers
|
import lrc_dl.providers as _
|
||||||
from lrc_dl.core import Song
|
from lrc_dl.core import Song
|
||||||
from lrc_dl.registry import Registry
|
from lrc_dl.registry import Registry
|
||||||
from lrc_dl.config import LyricsDlConfig
|
from lrc_dl.config import LyricsDlConfig
|
||||||
|
@ -23,11 +23,12 @@ class LyricsDl:
|
||||||
self.providers = []
|
self.providers = []
|
||||||
|
|
||||||
for name in config.order:
|
for name in config.order:
|
||||||
Provider = providers_classes[name]
|
Provider = providers_classes.get(name)
|
||||||
provider_config = config.providers_configs.get(name)
|
|
||||||
|
|
||||||
if not provider_config:
|
if not Provider:
|
||||||
provider_config = {}
|
continue
|
||||||
|
|
||||||
|
provider_config = config.providers_configs.get(name, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider = Provider(**provider_config)
|
provider = Provider(**provider_config)
|
||||||
|
@ -57,7 +58,7 @@ class LyricsDl:
|
||||||
|
|
||||||
return lyrics
|
return lyrics
|
||||||
|
|
||||||
self.logger.info(f"[{provider.name}] No lyrics was found!")
|
self.logger.info(f"[{provider.name}] No lyrics were found!")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ class LyricsDl:
|
||||||
lyrics = self.fetch_lyrics(song)
|
lyrics = self.fetch_lyrics(song)
|
||||||
|
|
||||||
if not lyrics:
|
if not lyrics:
|
||||||
self.logger.error("[lrc-dl] No lyrics was found!")
|
self.logger.error("[lrc-dl] No lyrics were found!")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
with open(lyrics_path, "w") as f:
|
with open(lyrics_path, "w") as f:
|
||||||
|
|
|
@ -21,7 +21,7 @@ CONFIG_PATH = _get_config_file()
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LyricsDlConfig:
|
class LyricsDlConfig:
|
||||||
order: list[str] = field(default_factory=lambda: ["kugou", "youtube"])
|
order: list[str] = field(default_factory=lambda: ["kugou", "lrclib", "genius"])
|
||||||
delay: float | None = 10
|
delay: float | None = 10
|
||||||
prepend_header: bool = True
|
prepend_header: bool = True
|
||||||
providers_configs: dict[str, dict] = field(default_factory=lambda: {})
|
providers_configs: dict[str, dict] = field(default_factory=lambda: {})
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
from lrc_dl.providers import musixmatch
|
|
||||||
from lrc_dl.providers import kugou
|
from lrc_dl.providers import kugou
|
||||||
|
from lrc_dl.providers import lrclib
|
||||||
|
from lrc_dl.providers import musixmatch
|
||||||
|
from lrc_dl.providers import genius
|
||||||
from lrc_dl.providers import youtube
|
from lrc_dl.providers import youtube
|
||||||
|
|
62
lrc_dl/providers/genius.py
Normal file
62
lrc_dl/providers/genius.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# https://github.com/jeffvli/feishin/blob/development/src/main/features/core/lyrics/genius.ts
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
import httpx
|
||||||
|
import bs4
|
||||||
|
|
||||||
|
from lrc_dl.core import Song, AbstractProvider
|
||||||
|
from lrc_dl.registry import lyrics_provider
|
||||||
|
|
||||||
|
|
||||||
|
def _format_div(div: bs4.Tag):
|
||||||
|
for br in div.find_all('br'):
|
||||||
|
br.replace_with('\n') # type: ignore
|
||||||
|
text = div.get_text().strip()
|
||||||
|
text = re.sub(r"\[.*\]\n", '', text).strip()
|
||||||
|
|
||||||
|
# remove extra newlines
|
||||||
|
return '\n\n'.join(['\n'.join(x.split('\n')) for x in text.split('\n\n')])
|
||||||
|
|
||||||
|
|
||||||
|
@lyrics_provider
|
||||||
|
class Genius(AbstractProvider):
|
||||||
|
name = "genius"
|
||||||
|
|
||||||
|
def fetch_lyrics(self, song: Song) -> Optional[str]:
|
||||||
|
r = httpx.get('https://genius.com/api/search/song', params={
|
||||||
|
'per_page': 1,
|
||||||
|
'q': f'{song.artist} {song.title}'
|
||||||
|
})
|
||||||
|
|
||||||
|
if r.status_code != 200 or 'application/json' not in r.headers.get('content-type', ''):
|
||||||
|
return
|
||||||
|
|
||||||
|
hits = r.json().get('response', {}).get('sections', [{}])[0].get('hits')
|
||||||
|
|
||||||
|
if not hits:
|
||||||
|
return
|
||||||
|
|
||||||
|
url: str = hits[0].get('result', {}).get('url')
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return
|
||||||
|
|
||||||
|
r = httpx.get(url, headers={
|
||||||
|
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0'
|
||||||
|
})
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
soup = bs4.BeautifulSoup(r.text, features='html.parser')
|
||||||
|
div = soup.select_one('div.lyrics')
|
||||||
|
|
||||||
|
if div:
|
||||||
|
return _format_div(div)
|
||||||
|
|
||||||
|
div = soup.select_one('div[class^=Lyrics__Container]')
|
||||||
|
|
||||||
|
if not div:
|
||||||
|
return
|
||||||
|
|
||||||
|
return _format_div(div)
|
18
lrc_dl/providers/lrclib.py
Normal file
18
lrc_dl/providers/lrclib.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from typing import Optional
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from lrc_dl.core import Song, AbstractProvider
|
||||||
|
from lrc_dl.registry import lyrics_provider
|
||||||
|
|
||||||
|
@lyrics_provider
|
||||||
|
class LrcLib(AbstractProvider):
|
||||||
|
name = "lrclib"
|
||||||
|
|
||||||
|
def fetch_lyrics(self, song: Song, with_album = True) -> Optional[str]:
|
||||||
|
r = httpx.get("https://lrclib.net/api/get", params={
|
||||||
|
'track_name': song.title,
|
||||||
|
'artist_name': song.artist,
|
||||||
|
'album_name': song.album if with_album else None,
|
||||||
|
}).json()
|
||||||
|
|
||||||
|
return r.get('syncedLyrics') or r.get('plainLyrics') or (self.fetch_lyrics(song, False) if with_album else None)
|
|
@ -4,14 +4,14 @@ from lrc_dl.core import AbstractProvider
|
||||||
class Registry:
|
class Registry:
|
||||||
providers: dict[str, type[AbstractProvider]] = {}
|
providers: dict[str, type[AbstractProvider]] = {}
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def get_synced_providers() -> dict[str, type[AbstractProvider]]:
|
def get_synced_providers(cls) -> dict[str, type[AbstractProvider]]:
|
||||||
# TODO: stub
|
# TODO: stub
|
||||||
return dict(Registry.providers)
|
return dict(cls.providers)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def register_provider(provider_class: type[AbstractProvider]) -> None:
|
def register_provider(cls, provider_class: type[AbstractProvider]) -> None:
|
||||||
Registry.providers[provider_class.name] = provider_class
|
cls.providers[provider_class.name] = provider_class
|
||||||
|
|
||||||
|
|
||||||
def lyrics_provider(cls: type[AbstractProvider]) -> type[AbstractProvider]:
|
def lyrics_provider(cls: type[AbstractProvider]) -> type[AbstractProvider]:
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -15,6 +15,7 @@ setup(
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"httpx>=0.24.1",
|
"httpx>=0.24.1",
|
||||||
|
"beautifulsoup4>=4.13.3",
|
||||||
"mutagen>=1.46.0",
|
"mutagen>=1.46.0",
|
||||||
"yt-dlp>=2023.10.13",
|
"yt-dlp>=2023.10.13",
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue