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:
Arthur K. 2025-04-02 01:37:19 +03:00 committed by GitHub
parent d6b36886ba
commit bad4a355e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 103 additions and 19 deletions

View file

@ -1,7 +1,7 @@
[providers]
order = ["musixmatch", "kugou"]
order = ["musixmatch", "kugou", "lrclib", "genius"]
delay = 10
prepend_header = true
[providers.musixmatch]
token = "123456"
token = ""

View file

@ -1,10 +1,10 @@
import time
from typing import Optional
from pathlib import Path
import traceback
from pathlib import Path
from typing import Optional
# Initialize classes from lrc_dl/providers
import lrc_dl.providers
import lrc_dl.providers as _
from lrc_dl.core import Song
from lrc_dl.registry import Registry
from lrc_dl.config import LyricsDlConfig
@ -23,11 +23,12 @@ class LyricsDl:
self.providers = []
for name in config.order:
Provider = providers_classes[name]
provider_config = config.providers_configs.get(name)
Provider = providers_classes.get(name)
if not provider_config:
provider_config = {}
if not Provider:
continue
provider_config = config.providers_configs.get(name, {})
try:
provider = Provider(**provider_config)
@ -57,7 +58,7 @@ class LyricsDl:
return lyrics
self.logger.info(f"[{provider.name}] No lyrics was found!")
self.logger.info(f"[{provider.name}] No lyrics were found!")
return None
@ -78,7 +79,7 @@ class LyricsDl:
lyrics = self.fetch_lyrics(song)
if not lyrics:
self.logger.error("[lrc-dl] No lyrics was found!")
self.logger.error("[lrc-dl] No lyrics were found!")
return True
with open(lyrics_path, "w") as f:

View file

@ -21,7 +21,7 @@ CONFIG_PATH = _get_config_file()
@dataclass
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
prepend_header: bool = True
providers_configs: dict[str, dict] = field(default_factory=lambda: {})

View file

@ -1,3 +1,5 @@
from lrc_dl.providers import musixmatch
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

View 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)

View 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)

View file

@ -4,14 +4,14 @@ from lrc_dl.core import AbstractProvider
class Registry:
providers: dict[str, type[AbstractProvider]] = {}
@staticmethod
def get_synced_providers() -> dict[str, type[AbstractProvider]]:
@classmethod
def get_synced_providers(cls) -> dict[str, type[AbstractProvider]]:
# TODO: stub
return dict(Registry.providers)
return dict(cls.providers)
@staticmethod
def register_provider(provider_class: type[AbstractProvider]) -> None:
Registry.providers[provider_class.name] = provider_class
@classmethod
def register_provider(cls, provider_class: type[AbstractProvider]) -> None:
cls.providers[provider_class.name] = provider_class
def lyrics_provider(cls: type[AbstractProvider]) -> type[AbstractProvider]:

View file

@ -15,6 +15,7 @@ setup(
},
install_requires=[
"httpx>=0.24.1",
"beautifulsoup4>=4.13.3",
"mutagen>=1.46.0",
"yt-dlp>=2023.10.13",
]