Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion karaokay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""karaokay — synchronized lyrics in your terminal, powered by MPD."""

__version__ = "0.5.0"
__version__ = "0.5.1"

from .cli import main

Expand Down
36 changes: 35 additions & 1 deletion karaokay/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,42 @@
SYNCED_PROVIDERS = ["Musixmatch", "NetEase", "Megalobiz", "Genius"]


def _lrclib_first_synced(query: str) -> Optional[str]:
"""First synced LRC returned by Lrclib for `query`, or None.

Lrclib is the most reliable synced-lyrics source, so the autofetch tries it
before falling back to the syncedlyrics providers.
"""
if not REQUESTS_AVAILABLE:
return None
try:
r = requests.get(
LRCLIB_ENDPOINT,
params={"q": query},
headers={"User-Agent": USER_AGENT},
timeout=10,
)
r.raise_for_status()
tracks = r.json()
except Exception:
return None
for t in tracks:
synced = (t.get("syncedLyrics") or "").strip()
if synced:
return synced
return None


def simple_search(query: str) -> Optional[str]:
"""One best synced-only match, or None. Used by the automatic autofetch."""
"""First synced LRC found for `query`, or None. Used by the automatic
autofetch.

Tries Lrclib first (same source as the interactive `f` panel, and the most
reliable), then falls back to the syncedlyrics providers.
"""
result = _lrclib_first_synced(query)
if result:
return result
if not SYNCEDLYRICS_AVAILABLE:
return None
return syncedlyrics.search(query, synced_only=True)
Expand Down
28 changes: 24 additions & 4 deletions karaokay/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .lrc import LyricLine, parse_lrc, find_lrc, resolve_write_path
from .cover import CoverRenderer, find_cover
from .fetch import (FetchSession, LyricCandidate, simple_search,
SYNCEDLYRICS_AVAILABLE)
SYNCEDLYRICS_AVAILABLE, REQUESTS_AVAILABLE)
from .util import fmt_time


Expand Down Expand Up @@ -373,7 +373,9 @@ def _save_selected(self):
# ── Lyrics auto-fetch (silent, background) ────────────────────────────────

def _start_autofetch(self, song: dict):
if not SYNCEDLYRICS_AVAILABLE:
# Autofetch works through Lrclib (requests) and/or the syncedlyrics
# providers; only bail if neither source is available.
if not (REQUESTS_AVAILABLE or SYNCEDLYRICS_AVAILABLE):
with self.fetch_lock:
self.fetch_state = "no_module"
return
Expand Down Expand Up @@ -578,10 +580,28 @@ def _draw(self, status: dict, song: dict):

# ── Lyrics area ───────────────────────────────────────────────────────────

def _no_lyrics_msg(self) -> str:
"""The message shown in the lyrics area when the track has no .lrc.

Reflects the automatic autofetch state: while it runs, once it fails,
and — when it is disabled/unavailable — the plain "press f" hint.
"""
with self.fetch_lock:
fs = self.fetch_state
if fs == "fetching":
return "Searching lyrics online… (press f to pick one yourself)"
if fs == "not_found":
return ("Autofetch found no synchronized lyrics — "
"press f to search for them yourself")
if fs == "error":
return "Autofetch failed — press f to search for lyrics yourself"
# disabled / no_module / no_meta / none / cleared: the plain hint.
return "No lyrics — press f to fetch, or drop a .lrc next to the audio file"

def _draw_lyrics(self, row: int, h: int, w: int, elapsed_adj: float):
if not self.lyrics:
msg = "No lyrics — press f to fetch, or drop a .lrc next to the audio file"
self._addstr(row, 2, msg[:w - 2], curses.color_pair(5))
self._addstr(row, 2, self._no_lyrics_msg()[:w - 2],
curses.color_pair(5))
return
avail = max(1, h - row - 2) # available lines (- help at the bottom)
self._draw_lyric_lines(self.lyrics, row, avail, h, w, elapsed_adj,
Expand Down
Loading