Merge pull request #71 from ChocoMeow/Added-Musixmatch-lyrics
Added musixmatch lyrics
This commit is contained in:
147
addons/lyrics.py
147
addons/lyrics.py
@@ -21,9 +21,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import aiohttp, random, bs4, re
|
||||
import aiohttp
|
||||
import random
|
||||
import re
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
import urllib.parse
|
||||
import function as func
|
||||
|
||||
from datetime import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from urllib.parse import quote
|
||||
from math import floor
|
||||
@@ -71,9 +79,6 @@ Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.9 (KHTM
|
||||
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/ Safari/530.6
|
||||
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5'''
|
||||
|
||||
LYRIST_ENDPOINT = "https://lyrist.vercel.app/api/"
|
||||
LRCLIB_ENDPOINT = "https://lrclib.net/api/"
|
||||
|
||||
class LyricsPlatform(ABC):
|
||||
@abstractmethod
|
||||
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
|
||||
@@ -207,9 +212,12 @@ class Genius(LyricsPlatform):
|
||||
return {"default": song.lyrics}
|
||||
|
||||
class Lyrist(LyricsPlatform):
|
||||
def __init__(self):
|
||||
self.base_url: str = "https://lyrist.vercel.app/api/"
|
||||
|
||||
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
|
||||
try:
|
||||
request_url = LYRIST_ENDPOINT + title + "/" + artist
|
||||
request_url = self.base_url + title + "/" + artist
|
||||
async with aiohttp.ClientSession() as session:
|
||||
resp = await session.get(url=request_url, headers={'User-Agent': random.choice(userAgents)})
|
||||
if resp.status != 200:
|
||||
@@ -221,6 +229,9 @@ class Lyrist(LyricsPlatform):
|
||||
return None
|
||||
|
||||
class Lrclib(LyricsPlatform):
|
||||
def __init__(self):
|
||||
self.base_url: str = "https://lrclib.net/api/"
|
||||
|
||||
async def get(self, url, params: dict = None) -> list[dict]:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -231,15 +242,135 @@ class Lrclib(LyricsPlatform):
|
||||
except:
|
||||
return []
|
||||
|
||||
async def get_lyrics(self, title, artist):
|
||||
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
|
||||
params = {"q": title}
|
||||
result = await self.get(LRCLIB_ENDPOINT + "search", params)
|
||||
result = await self.get(self.base_url + "search", params)
|
||||
if result:
|
||||
return {"default": result[0].get("plainLyrics", "")}
|
||||
|
||||
"""
|
||||
Strvm/musicxmatch-api: a reverse engineered API wrapper for MusicXMatch
|
||||
Copyright (c) 2025 Strvm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
class MusixMatch(LyricsPlatform):
|
||||
def __init__(self):
|
||||
self.base_url = "https://www.musixmatch.com/ws/1.1/"
|
||||
self.headers = {'User-Agent': random.choice(userAgents)}
|
||||
self.secret: Optional[str] = None
|
||||
|
||||
async def search_tracks(self, track_query: str, page: int = 1) -> dict:
|
||||
url = f"track.search?app_id=web-desktop-app-v1.0&format=json&q={urllib.parse.quote(track_query)}&f_has_lyrics=true&page_size=5&page={page}"
|
||||
return await self.make_request(url)
|
||||
|
||||
async def get_track_lyrics(self, track_id: Optional[str] = None, track_isrc: Optional[str] = None) -> dict:
|
||||
if not (track_id or track_isrc):
|
||||
raise ValueError("Either track_id or track_isrc must be provided.")
|
||||
param = f"track_id={track_id}" if track_id else f"track_isrc={track_isrc}"
|
||||
url = f"track.lyrics.get?app_id=web-desktop-app-v1.0&format=json&{param}"
|
||||
return await self.make_request(url)
|
||||
|
||||
async def get_latest_app(self):
|
||||
url = "https://www.musixmatch.com/search"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers={**self.headers, "Cookie": "mxm_bab=AB"}) as response:
|
||||
html_content = await response.text()
|
||||
pattern = r'src="([^"]*/_next/static/chunks/pages/_app-[^"]+\.js)"'
|
||||
matches = re.findall(pattern, html_content)
|
||||
|
||||
if not matches:
|
||||
raise Exception("_app URL not found in the HTML content.")
|
||||
|
||||
return matches[-1]
|
||||
|
||||
async def get_secret(self) -> str:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(await self.get_latest_app(), headers=self.headers, timeout=5) as response:
|
||||
javascript_code = await response.text()
|
||||
|
||||
pattern = r'from\(\s*"(.*?)"\s*\.split'
|
||||
match = re.search(pattern, javascript_code)
|
||||
|
||||
if match:
|
||||
encoded_string = match.group(1)
|
||||
reversed_string = encoded_string[::-1]
|
||||
|
||||
decoded_bytes = base64.b64decode(reversed_string)
|
||||
return decoded_bytes.decode("utf-8")
|
||||
else:
|
||||
raise Exception("Encoded string not found in the JavaScript code.")
|
||||
|
||||
async def generate_signature(self, url: str) -> str:
|
||||
current_date = datetime.now()
|
||||
date_str = f"{current_date.year}{str(current_date.month).zfill(2)}{str(current_date.day).zfill(2)}"
|
||||
message = (url + date_str).encode()
|
||||
|
||||
if not self.secret:
|
||||
self.secret = await self.get_secret()
|
||||
|
||||
key = self.secret.encode()
|
||||
hash_output = hmac.new(key, message, hashlib.sha256).digest()
|
||||
signature = (
|
||||
"&signature="
|
||||
+ urllib.parse.quote(base64.b64encode(hash_output).decode())
|
||||
+ "&signature_protocol=sha256"
|
||||
)
|
||||
return signature
|
||||
|
||||
async def make_request(self, url: str) -> dict:
|
||||
url = url.replace("%20", "+").replace(" ", "+")
|
||||
url = self.base_url + url
|
||||
signed_url = url + await self.generate_signature(url)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(signed_url, headers=self.headers, timeout=5) as response:
|
||||
if response.status != 200:
|
||||
raise Exception(f"HTTP Error: {response.status} for URL: {signed_url}")
|
||||
|
||||
try:
|
||||
text_data = await response.text()
|
||||
return json.loads(text_data)
|
||||
except json.JSONDecodeError:
|
||||
raise Exception(f"Failed to parse JSON. Response: {text_data}")
|
||||
|
||||
async def get_lyrics(self, title: str, artist: str) -> Optional[dict[str, str]]:
|
||||
results = await self.search_tracks(track_query=f"{artist} {title}" if artist else title)
|
||||
|
||||
track_list = results.get("message", {}).get("body", {}).get("track_list")
|
||||
if not track_list:
|
||||
return None
|
||||
|
||||
track_id = track_list[0]["track"]["track_id"]
|
||||
lyric = await self.get_track_lyrics(track_id=track_id)
|
||||
|
||||
lyrics_body = lyric.get("message", {}).get("body", {}).get("lyrics", {}).get("lyrics_body", "")
|
||||
if not lyrics_body:
|
||||
return None
|
||||
|
||||
return {"default": lyrics_body}
|
||||
|
||||
LYRICS_PLATFORMS: dict[str, Type[LyricsPlatform]] = {
|
||||
"a_zlyrics": A_ZLyrics,
|
||||
"genius": Genius,
|
||||
"lyrist": Lyrist,
|
||||
"lrclib": Lrclib
|
||||
"lrclib": Lrclib,
|
||||
"musixmatch": MusixMatch
|
||||
}
|
||||
Reference in New Issue
Block a user