Skip to content

Commit

Permalink
Merge pull request #26 from deedy5/dev
Browse files Browse the repository at this point in the history
Migrate from requests to curl_cffi + enable Deezer
  • Loading branch information
moehmeni authored Mar 8, 2024
2 parents 6ed79cb + aa7f51b commit 7341137
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 349 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ syncedlyrics.search("...", lang="de")
- [Lrclib](https://github.com/tranxuanthang/lrcget/issues/2#issuecomment-1326925928)
- [NetEase](https://music.163.com/)
- [Megalobiz](https://www.megalobiz.com/)
- [Deezer](https://deezer.com/)
- ~~[Lyricsify](https://www.lyricsify.com/)~~ (Broken duo to Cloudflare protection)
- ~~[Deezer](https://deezer.com/)~~ (Broken)

Feel free to suggest more providers or make PRs to fix the broken ones.

Expand Down
524 changes: 242 additions & 282 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ classifiers = [

[tool.poetry.dependencies]
python = ">=3.8"
requests = "^2.31.0"
rapidfuzz = "^3.5.2"
beautifulsoup4 = "^4.12.2"
curl_cffi = "^0.6.2"
rapidfuzz = "^3.6.2"
beautifulsoup4 = "^4.12.3"

[tool.poetry.scripts]
syncedlyrics = "syncedlyrics.cli:cli_handler"

[tool.poetry.group.dev.dependencies]
black = "^23.11.0"
pytest = "^7.4.3"
black = "^24.2.0"
pytest = "^8.0.2"

[build-system]
requires = ["poetry-core"]
Expand Down
17 changes: 9 additions & 8 deletions syncedlyrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
```
"""

from typing import Optional, List
import logging
from .providers import NetEase, Megalobiz, Musixmatch, Lrclib
from typing import List, Optional

from .providers import Deezer, Lrclib, Megalobiz, Musixmatch, NetEase
from .utils import is_lrc_valid, save_lrc_file

logger = logging.getLogger(__name__)
Expand All @@ -18,9 +19,9 @@
def search(
search_term: str,
allow_plain_format: bool = False,
save_path: str = None,
providers: List[str] = None,
lang: str = None,
save_path: Optional[str] = None,
providers: Optional[List[str]] = None,
lang: Optional[str] = None,
) -> Optional[str]:
"""
Returns the synced lyrics of the song in [LRC](https://en.wikipedia.org/wiki/LRC_(file_format)) format if found.
Expand All @@ -31,7 +32,7 @@ def search(
- `providers`: A list of provider names to include in searching; loops over all the providers as soon as an LRC is found
- `lang`: Language of the translation along with the lyrics. **Only supported by Musixmatch**
"""
_providers = [Musixmatch(lang=lang), Lrclib(), NetEase(), Megalobiz()]
_providers = [Musixmatch(lang=lang), Lrclib(), NetEase(), Megalobiz(), Deezer()]
if providers and any(providers):
# Filtering the providers
_providers = [
Expand All @@ -43,7 +44,7 @@ def search(
logger.error(
f"Providers {providers} not found in the list of available providers."
)
return
return None
lrc = None
for provider in _providers:
logger.debug(f"Looking for an LRC on {provider.__class__.__name__}")
Expand All @@ -55,7 +56,7 @@ def search(
break
if not lrc:
logger.info(f'No synced-lyrics found for "{search_term}" :(')
return
return None
if save_path:
save_path = save_path.format(search_term=search_term)
save_lrc_file(save_path, lrc)
Expand Down
9 changes: 6 additions & 3 deletions syncedlyrics/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ def cli_handler():
"-p",
help="Comma-separated list of providers to include in searching",
default="",
choices=["deezer", "lrclib", "megalobiz", "musixmatch", "netease"],
nargs="+",
)
parser.add_argument(
"-l", "--lang", help="Language of the translation along with the lyrics"
)
parser.add_argument("-l", "--lang", help="Language of the translation along with the lyrics")
parser.add_argument(
"-o", "--output", help="Path to save '.lrc' lyrics", default="{search_term}.lrc"
)
Expand All @@ -31,7 +35,6 @@ def cli_handler():
)
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
p = args.p.split(",") if args.p else None
lrc = search(args.search_term, args.allow_plain, args.output, p, lang=args.lang)
lrc = search(args.search_term, args.allow_plain, args.output, args.p, lang=args.lang)
if lrc:
print(lrc)
10 changes: 2 additions & 8 deletions syncedlyrics/providers/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import requests
from curl_cffi import requests
from typing import Optional


Expand All @@ -7,14 +7,8 @@ class LRCProvider:
Base class for all of the synced (LRC format) lyrics providers.
"""

session = requests.Session()

def __init__(self) -> None:
self.session.headers.update(
{
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
}
)
self.session = requests.Session(impersonate="chrome")

def get_lrc_by_id(self, track_id: str) -> Optional[str]:
"""
Expand Down
5 changes: 3 additions & 2 deletions syncedlyrics/providers/deezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ def get_lrc_by_id(self, track_id: str) -> Optional[str]:
return lrc or None

def get_lrc(self, search_term: str) -> Optional[str]:
search_results = self.session.get(self.SEARCH_ENDPOINT + search_term).json()
url = self.SEARCH_ENDPOINT + search_term.replace(" ", "+")
search_results = self.session.get(url).json()
cmp_key = lambda t: f"{t.get('title')} {t.get('artist').get('name')}"
track = get_best_match(search_results.get("data", []), search_term, cmp_key)
if not track:
return
return None
return self.get_lrc_by_id(track["id"])
8 changes: 4 additions & 4 deletions syncedlyrics/providers/lrclib.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ def get_lrc_by_id(self, track_id: str) -> Optional[str]:
url = self.LRC_ENDPOINT + track_id
r = self.session.get(url)
if not r.ok:
return
return None
track = r.json()
return track.get("syncedLyrics", track.get("plainLyrics"))

def get_lrc(self, search_term: str) -> Optional[str]:
url = self.SEARCH_ENDPOINT
r = self.session.get(url, params={"q": search_term})
if not r.ok:
return
return None
tracks = r.json()
if not tracks:
return
return None
tracks = sort_results(
tracks, search_term, lambda t: f'{t["artistName"]} - {t["trackName"]}'
)
Expand All @@ -43,5 +43,5 @@ def get_lrc(self, search_term: str) -> Optional[str]:
_id = str(track["id"])
break
if not _id:
return
return None
return self.get_lrc_by_id(_id)
2 changes: 1 addition & 1 deletion syncedlyrics/providers/megalobiz.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Megalobiz(LRCProvider):
SEARCH_ENDPOINT = ROOT_URL + "/search/all?qry={q}&searchButton.x=0&searchButton.y=0"

def get_lrc(self, search_term: str) -> Optional[str]:
url = self.SEARCH_ENDPOINT.format(q=search_term)
url = self.SEARCH_ENDPOINT.format(q=search_term.replace(" ", "+"))

def href_match(h: Optional[str]):
if h and h.startswith("/lrc/maker/"):
Expand Down
14 changes: 4 additions & 10 deletions syncedlyrics/providers/musixmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,10 @@ class Musixmatch(LRCProvider):

ROOT_URL = "https://apic-desktop.musixmatch.com/ws/1.1/"

def __init__(self, lang : str = None) -> None:
def __init__(self, lang : Optional[str] = None) -> None:
super().__init__()
self.lang = lang
self.token = None
self.session.headers.update(
{
"authority": "apic-desktop.musixmatch.com",
"cookie": "AWSELBCORS=0; AWSELB=0",
}
)

def _get(self, action: str, query: List[tuple]):
if action != "token.get" and self.token is None:
Expand Down Expand Up @@ -82,10 +76,10 @@ def get_lrc_by_id(self, track_id: str) -> Optional[str]:
)
body_tr = r_tr.json()["message"]["body"]
if not r.ok:
return
return None
body = r.json()["message"]["body"]
if not body:
return
return None
lrc = body["subtitle"]["subtitle_body"]
if self.lang is not None and body_tr:
for i in body_tr["translations_list"]:
Expand All @@ -109,5 +103,5 @@ def get_lrc(self, search_term: str) -> Optional[str]:
cmp_key = lambda t: f"{t['track']['track_name']} {t['track']['artist_name']}"
track = get_best_match(tracks, search_term, cmp_key)
if not track:
return
return None
return self.get_lrc_by_id(track["track"]["track_id"])
25 changes: 6 additions & 19 deletions syncedlyrics/providers/netease.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,6 @@
from .base import LRCProvider
from ..utils import get_best_match

headers = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "en-US,en;q=0.9,fa;q=0.8",
"cache-control": "max-age=0",
"sec-ch-ua": '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"cookie": "NMTID=00OAVK3xqDG726ITU6jopU6jF2yMk0AAAGCO8l1BA; JSESSIONID-WYYY=8KQo11YK2GZP45RMlz8Kn80vHZ9%2FGvwzRKQXXy0iQoFKycWdBlQjbfT0MJrFa6hwRfmpfBYKeHliUPH287JC3hNW99WQjrh9b9RmKT%2Fg1Exc2VwHZcsqi7ITxQgfEiee50po28x5xTTZXKoP%2FRMctN2jpDeg57kdZrXz%2FD%2FWghb%5C4DuZ%3A1659124633932; _iuqxldmzr_=32; _ntes_nnid=0db6667097883aa9596ecfe7f188c3ec,1659122833973; _ntes_nuid=0db6667097883aa9596ecfe7f188c3ec; WNMCID=xygast.1659122837568.01.0; WEVNSM=1.0.0; WM_NI=CwbjWAFbcIzPX3dsLP%2F52VB%2Bxr572gmqAYwvN9KU5X5f1nRzBYl0SNf%2BV9FTmmYZy%2FoJLADaZS0Q8TrKfNSBNOt0HLB8rRJh9DsvMOT7%2BCGCQLbvlWAcJBJeXb1P8yZ3RHA%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6ee90c65b85ae87b9aa5483ef8ab3d14a939e9a83c459959caeadce47e991fbaee82af0fea7c3b92a81a9ae8bd64b86beadaaf95c9cedac94cf5cedebfeb7c121bcaefbd8b16dafaf8fbaf67e8ee785b6b854f7baff8fd1728287a4d1d246a6f59adac560afb397bbfc25ad9684a2c76b9a8d00b2bb60b295aaafd24a8e91bcd1cb4882e8beb3c964fb9cbd97d04598e9e5a4c6499394ae97ef5d83bd86a3c96f9cbeffb1bb739aed9ea9c437e2a3; WM_TID=AAkRFnl03RdABEBEQFOBWHCPOeMra4IL; playerid=94262567",
}


class NetEase(LRCProvider):
"""NetEase provider class"""
Expand All @@ -28,15 +13,17 @@ class NetEase(LRCProvider):

def __init__(self) -> None:
super().__init__()
self.session.headers.update(headers)
self.session.headers["cookie"] = (
"NMTID=00OAVK3xqDG726ITU6jopU6jF2yMk0AAAGCO8l1BA; JSESSIONID-WYYY=8KQo11YK2GZP45RMlz8Kn80vHZ9%2FGvwzRKQXXy0iQoFKycWdBlQjbfT0MJrFa6hwRfmpfBYKeHliUPH287JC3hNW99WQjrh9b9RmKT%2Fg1Exc2VwHZcsqi7ITxQgfEiee50po28x5xTTZXKoP%2FRMctN2jpDeg57kdZrXz%2FD%2FWghb%5C4DuZ%3A1659124633932; _iuqxldmzr_=32; _ntes_nnid=0db6667097883aa9596ecfe7f188c3ec,1659122833973; _ntes_nuid=0db6667097883aa9596ecfe7f188c3ec; WNMCID=xygast.1659122837568.01.0; WEVNSM=1.0.0; WM_NI=CwbjWAFbcIzPX3dsLP%2F52VB%2Bxr572gmqAYwvN9KU5X5f1nRzBYl0SNf%2BV9FTmmYZy%2FoJLADaZS0Q8TrKfNSBNOt0HLB8rRJh9DsvMOT7%2BCGCQLbvlWAcJBJeXb1P8yZ3RHA%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6ee90c65b85ae87b9aa5483ef8ab3d14a939e9a83c459959caeadce47e991fbaee82af0fea7c3b92a81a9ae8bd64b86beadaaf95c9cedac94cf5cedebfeb7c121bcaefbd8b16dafaf8fbaf67e8ee785b6b854f7baff8fd1728287a4d1d246a6f59adac560afb397bbfc25ad9684a2c76b9a8d00b2bb60b295aaafd24a8e91bcd1cb4882e8beb3c964fb9cbd97d04598e9e5a4c6499394ae97ef5d83bd86a3c96f9cbeffb1bb739aed9ea9c437e2a3; WM_TID=AAkRFnl03RdABEBEQFOBWHCPOeMra4IL; playerid=94262567"
)

def search_track(self, search_term: str) -> Optional[dict]:
"""Returns a `dict` containing some metadata for the found track."""
params = {"limit": 10, "type": 1, "offset": 0, "s": search_term}
response = self.session.get(self.API_ENDPOINT_METADATA, params=params)
results = response.json().get("result", {}).get("songs")
if not results:
return
return None
cmp_key = lambda t: f"{t.get('name')} {t.get('artists')[0].get('name')}"
track = get_best_match(results, search_term, cmp_key)
# Update the session cookies from the new sent cookies for the next request.
Expand All @@ -49,11 +36,11 @@ def get_lrc_by_id(self, track_id: str) -> Optional[str]:
response = self.session.get(self.API_ENDPOINT_LYRICS, params=params)
lrc = response.json().get("lrc", {}).get("lyric")
if not lrc:
return
return None
return lrc

def get_lrc(self, search_term: str) -> Optional[str]:
track = self.search_track(search_term)
if not track:
return
return None
return self.get_lrc_by_id(track["id"])
2 changes: 1 addition & 1 deletion syncedlyrics/providers/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def __init__(self) -> None:
super().__init__()

@classmethod
def get_track_id(search_term: str) -> Optional[str]:
def get_track_id(cls, search_term: str) -> Optional[str]:
"""Returns a Spotify track ID for given `search_term`"""
# TODO: self.client.search(search_term) and processing the results
raise NotImplementedError
Expand Down
10 changes: 6 additions & 4 deletions syncedlyrics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
R_FEAT = re.compile(r"\((feat.+)\)", re.IGNORECASE)


def is_lrc_valid(lrc: str, allow_plain_format: bool = False) -> bool:
def is_lrc_valid(lrc: Optional[str], allow_plain_format: bool = False) -> bool:
"""Checks whether a given LRC string is valid or not."""
if not lrc:
return False
Expand Down Expand Up @@ -84,9 +84,11 @@ def get_best_match(
with the `search_term`.
"""
if not results:
return
return None
results = sort_results(results, search_term, compare_key=compare_key)
best_match = results[0]
if not str_same(compare_key(best_match), search_term, n=min_score):
return

value_to_compare = best_match[compare_key] if isinstance(compare_key, str) else compare_key(best_match)
if not str_same(value_to_compare, search_term, n=min_score):
return None
return best_match
10 changes: 9 additions & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@


def _test_provider(provider: str, **kwargs):
lrc = syncedlyrics.search(q, allow_plain_format=True, providers=[provider], **kwargs)
lrc = syncedlyrics.search(
search_term=q, allow_plain_format=True, providers=[provider], **kwargs
)
logging.debug(lrc)
assert isinstance(lrc, str)

Expand All @@ -26,8 +28,14 @@ def test_megalobiz():
def test_musixmatch():
_test_provider("Musixmatch")


def test_musixmatch_translation():
_test_provider("Musixmatch", lang="es")


def test_lrclib():
_test_provider("Lrclib")


def test_deezer():
_test_provider("Deezer")

0 comments on commit 7341137

Please sign in to comment.