diff --git a/config.toml b/config.toml index 810bd9c..1580f95 100644 --- a/config.toml +++ b/config.toml @@ -1,7 +1,7 @@ [providers] -order = ["musixmatch", "kugou"] +order = ["musixmatch", "kugou", "lrclib", "genius"] delay = 10 prepend_header = true [providers.musixmatch] -token = "123456" +token = "" diff --git a/lrc_dl/__init__.py b/lrc_dl/__init__.py index e31fc68..a69ae8a 100644 --- a/lrc_dl/__init__.py +++ b/lrc_dl/__init__.py @@ -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: diff --git a/lrc_dl/config.py b/lrc_dl/config.py index eda4c10..b0df2c8 100644 --- a/lrc_dl/config.py +++ b/lrc_dl/config.py @@ -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: {}) diff --git a/lrc_dl/providers/__init__.py b/lrc_dl/providers/__init__.py index 274898e..e796069 100644 --- a/lrc_dl/providers/__init__.py +++ b/lrc_dl/providers/__init__.py @@ -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 diff --git a/lrc_dl/providers/genius.py b/lrc_dl/providers/genius.py new file mode 100644 index 0000000..10ec8bc --- /dev/null +++ b/lrc_dl/providers/genius.py @@ -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) diff --git a/lrc_dl/providers/lrclib.py b/lrc_dl/providers/lrclib.py new file mode 100644 index 0000000..042a16c --- /dev/null +++ b/lrc_dl/providers/lrclib.py @@ -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) diff --git a/lrc_dl/registry.py b/lrc_dl/registry.py index afb04bd..907e6ab 100644 --- a/lrc_dl/registry.py +++ b/lrc_dl/registry.py @@ -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]: diff --git a/setup.py b/setup.py index 9f7abc0..0bd77bc 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ setup( }, install_requires=[ "httpx>=0.24.1", + "beautifulsoup4>=4.13.3", "mutagen>=1.46.0", "yt-dlp>=2023.10.13", ]