adding shazam, and LRClib support

This commit is contained in:
jadssss
2026-10-24 06:34:12 +03:00
parent 8ffb778241
commit f702cc87ef
7 changed files with 822 additions and 213 deletions

115
core/lyrics.py Normal file
View File

@@ -0,0 +1,115 @@
"""Lyrics lookup via LRCLIB (https://lrclib.net) — free, no API key.
Rate limit: LRCLIB allows ~90 req/min with a proper User-Agent,
~45 without. We throttle to 0.4s between requests (150/min theoretical,
well within limits) and always send a UA.
"""
from __future__ import annotations
import threading
import time
from dataclasses import dataclass
from typing import Optional
import requests
DEFAULT_USER_AGENT = "audio-transcriber/1.0"
_API_BASE = "https://lrclib.net/api"
_throttle_lock = threading.Lock()
_last_request_at = 0.0
_MIN_INTERVAL = 0.4 # seconds
@dataclass
class LyricsMatch:
title: str
artist: str
lyrics: str
duration: Optional[float] = None
def _throttle() -> None:
global _last_request_at
with _throttle_lock:
now = time.monotonic()
wait = _last_request_at + _MIN_INTERVAL - now
if wait > 0:
time.sleep(wait)
_last_request_at = time.monotonic()
def _get(url: str, params: dict, user_agent: str, timeout: float = 10.0) -> Optional[dict]:
_throttle()
try:
resp = requests.get(
url, params=params,
headers={"User-Agent": user_agent},
timeout=timeout,
)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
except requests.RequestException:
return None
def _make_match(item: dict) -> Optional[LyricsMatch]:
lyrics = item.get("plainLyrics") or item.get("syncedLyrics")
if not lyrics:
return None
return LyricsMatch(
title=item.get("trackName", ""),
artist=item.get("artistName", ""),
lyrics=lyrics.strip(),
duration=item.get("duration"),
)
def search_by_identity(
title: Optional[str],
artist: Optional[str],
album: Optional[str] = None,
duration: Optional[float] = None,
user_agent: str = DEFAULT_USER_AGENT,
timeout: float = 10.0,
) -> Optional[LyricsMatch]:
"""Search LRCLIB by track metadata. Works even with messy tags."""
if not title and not artist:
return None
query = " ".join(filter(None, [artist or "", title or ""])).strip()
if not query:
return None
data = _get(f"{_API_BASE}/search", {"q": query}, user_agent, timeout)
if not isinstance(data, list) or not data:
return None
for item in data[:5]:
match = _make_match(item)
if match:
return match
return None
def search_by_text(
query: str,
limit: int = 5,
user_agent: str = DEFAULT_USER_AGENT,
timeout: float = 10.0,
) -> list[LyricsMatch]:
"""Search LRCLIB by plain text (e.g. first words of recognized speech)."""
if not query or not query.strip():
return []
data = _get(f"{_API_BASE}/search", {"q": query.strip()}, user_agent, timeout)
if not isinstance(data, list):
return []
matches: list[LyricsMatch] = []
for item in data[:limit]:
match = _make_match(item)
if match:
matches.append(match)
return matches