feat: Lyrics-Funktion und Install-Script
- Lyrics-Ansicht mit Taste l (lrclib.net API) - Install-Script fuer systemweite Installation - README aktualisiert
This commit is contained in:
@@ -197,5 +197,43 @@ class SpotifyClient:
|
||||
return self._get("/me")
|
||||
|
||||
|
||||
class LyricsClient:
|
||||
LRCLIB_URL = "https://lrclib.net/api"
|
||||
|
||||
def get_lyrics(self, track_name, artist_name, album_name=None, duration_ms=None):
|
||||
import requests as req
|
||||
|
||||
params = {
|
||||
"track_name": track_name,
|
||||
"artist_name": artist_name,
|
||||
}
|
||||
if album_name:
|
||||
params["album_name"] = album_name
|
||||
if duration_ms:
|
||||
params["duration"] = duration_ms // 1000
|
||||
|
||||
try:
|
||||
resp = req.get(f"{self.LRCLIB_URL}/get", params=params, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
return data.get("syncedLyrics") or data.get("plainLyrics")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
resp = req.get(f"{self.LRCLIB_URL}/search", params={
|
||||
"q": f"{track_name} {artist_name}"
|
||||
}, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
results = resp.json()
|
||||
if results:
|
||||
data = results[0]
|
||||
return data.get("syncedLyrics") or data.get("plainLyrics")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class TokenExpired(Exception):
|
||||
pass
|
||||
|
||||
+104
-3
@@ -1,7 +1,7 @@
|
||||
import curses
|
||||
import time
|
||||
import threading
|
||||
from .client import SpotifyClient, TokenExpired
|
||||
from .client import SpotifyClient, LyricsClient, TokenExpired
|
||||
from .player import Player
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class View:
|
||||
ARTIST_DETAIL = "artist_detail"
|
||||
ARTIST_TOP = "artist_top"
|
||||
LIBRARY = "library"
|
||||
LYRICS = "lyrics"
|
||||
|
||||
|
||||
class UI:
|
||||
@@ -41,6 +42,7 @@ class UI:
|
||||
def __init__(self, stdscr, client: SpotifyClient):
|
||||
self.stdscr = stdscr
|
||||
self.client = client
|
||||
self.lyrics_client = LyricsClient()
|
||||
self.player = Player(client)
|
||||
self.current_view = View.HOME
|
||||
self.nav_index = 0
|
||||
@@ -68,6 +70,9 @@ class UI:
|
||||
self.context_uri = None
|
||||
self.focus = FOCUS_CONTENT
|
||||
self.force_refresh = False
|
||||
self.lyrics_data = []
|
||||
self.lyrics_scroll = 0
|
||||
self.lyrics_track_info = None
|
||||
|
||||
curses.curs_set(0)
|
||||
curses.use_default_colors()
|
||||
@@ -106,6 +111,11 @@ class UI:
|
||||
self.stdscr.refresh()
|
||||
return
|
||||
|
||||
if self.current_view == View.LYRICS:
|
||||
self._draw_lyrics(h, w)
|
||||
self.stdscr.refresh()
|
||||
return
|
||||
|
||||
nav_w = 18
|
||||
status_h = 1
|
||||
now_h = 3
|
||||
@@ -257,12 +267,46 @@ class UI:
|
||||
elif self.focus == FOCUS_NAV:
|
||||
keys = "j/k:Move Enter:Select Tab:Content q:Quit"
|
||||
else:
|
||||
keys = "j/k:Move Enter:Select Space:Play n/p:Next +/-:Vol Tab:Nav q:Quit"
|
||||
keys = "j/k:Move Enter:Select Space:Play n/p:Next +/-:Vol l:Lyrics Tab:Nav q:Quit"
|
||||
try:
|
||||
self.stdscr.addnstr(y, x, keys.ljust(w), w-1, curses.color_pair(7))
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
def _draw_lyrics(self, h, w):
|
||||
try:
|
||||
self.stdscr.addnstr(0, 0, " LYRICS".ljust(w), w-1, curses.color_pair(7) | curses.A_BOLD)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
if self.lyrics_track_info:
|
||||
info = self.lyrics_track_info
|
||||
header = f" {info['artists']} - {info['name']}"
|
||||
try:
|
||||
self.stdscr.addnstr(1, 0, header[:w-1], w-1, curses.color_pair(4) | curses.A_BOLD)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
visible = h - 3
|
||||
start = self.lyrics_scroll
|
||||
end = min(start + visible, len(self.lyrics_data))
|
||||
|
||||
for i in range(start, end):
|
||||
row = 2 + (i - start)
|
||||
if row >= h - 1:
|
||||
break
|
||||
line = self.lyrics_data[i]
|
||||
try:
|
||||
self.stdscr.addnstr(row, 0, line[:w-1], w-1, curses.color_pair(1))
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
bar = "q/Esc:Zurueck j/k:Scroll PgUp/PgDn:Page"
|
||||
try:
|
||||
self.stdscr.addnstr(h-1, 0, bar.ljust(w), w-1, curses.color_pair(7))
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
def _make_progress_bar(self, current, total, width):
|
||||
if total == 0:
|
||||
return ""
|
||||
@@ -670,6 +714,32 @@ class UI:
|
||||
self.show_message(f"Fehler: {e}")
|
||||
self.loading = False
|
||||
|
||||
def load_lyrics(self):
|
||||
self.loading = True
|
||||
self.draw()
|
||||
self.lyrics_data = []
|
||||
self.lyrics_scroll = 0
|
||||
self.lyrics_track_info = None
|
||||
|
||||
info = self.player.get_track_info()
|
||||
if not info:
|
||||
self.lyrics_data = ["Kein Track abgespielt."]
|
||||
self.loading = False
|
||||
return
|
||||
|
||||
self.lyrics_track_info = info
|
||||
track_name = info["name"]
|
||||
artist_name = info["artists"]
|
||||
album_name = info.get("album", "")
|
||||
duration_ms = info.get("duration_ms")
|
||||
|
||||
lyrics_text = self.lyrics_client.get_lyrics(track_name, artist_name, album_name, duration_ms)
|
||||
if lyrics_text:
|
||||
self.lyrics_data = lyrics_text.split("\n")
|
||||
else:
|
||||
self.lyrics_data = [f"Keine Lyrics gefunden fuer:", f"{artist_name} - {track_name}"]
|
||||
self.loading = False
|
||||
|
||||
def do_search(self, query):
|
||||
if not query:
|
||||
return
|
||||
@@ -783,7 +853,9 @@ class UI:
|
||||
self.navigate_to(View.ARTISTS)
|
||||
|
||||
def handle_back(self):
|
||||
if self.current_view in (View.PLAYLIST_DETAIL, View.LIBRARY):
|
||||
if self.current_view == View.LYRICS:
|
||||
self.navigate_to(View.HOME)
|
||||
elif self.current_view in (View.PLAYLIST_DETAIL, View.LIBRARY):
|
||||
self.navigate_to(View.PLAYLISTS if self.current_view == View.PLAYLIST_DETAIL else View.HOME)
|
||||
elif self.current_view in (View.ALBUM_DETAIL, View.ARTIST_DETAIL):
|
||||
self.navigate_to(View.HOME)
|
||||
@@ -827,6 +899,10 @@ class UI:
|
||||
self.search_query += chr(key)
|
||||
|
||||
def _handle_key(self, key):
|
||||
if self.current_view == View.LYRICS:
|
||||
self._handle_lyrics_key(key)
|
||||
return
|
||||
|
||||
content_count = len(self.content_items)
|
||||
nav_count = len(self.NAV_ITEMS)
|
||||
|
||||
@@ -846,6 +922,28 @@ class UI:
|
||||
else:
|
||||
self._handle_content_key(key, content_count)
|
||||
|
||||
def _handle_lyrics_key(self, key):
|
||||
lyrics_count = len(self.lyrics_data)
|
||||
h, w = self.stdscr.getmaxyx()
|
||||
visible = h - 3
|
||||
|
||||
if key == ord("q") or key == 27 or key == ord("l"):
|
||||
self.navigate_to(View.HOME)
|
||||
|
||||
elif key == ord("j") or key == curses.KEY_DOWN:
|
||||
if self.lyrics_scroll < max(0, lyrics_count - visible):
|
||||
self.lyrics_scroll += 1
|
||||
|
||||
elif key == ord("k") or key == curses.KEY_UP:
|
||||
if self.lyrics_scroll > 0:
|
||||
self.lyrics_scroll -= 1
|
||||
|
||||
elif key == curses.KEY_PPAGE:
|
||||
self.lyrics_scroll = max(0, self.lyrics_scroll - visible)
|
||||
|
||||
elif key == curses.KEY_NPAGE:
|
||||
self.lyrics_scroll = min(max(0, lyrics_count - visible), self.lyrics_scroll + visible)
|
||||
|
||||
def _handle_nav_key(self, key, nav_count):
|
||||
if key == ord("j") or key == curses.KEY_DOWN:
|
||||
if self.nav_index < nav_count - 1:
|
||||
@@ -993,6 +1091,9 @@ class UI:
|
||||
elif key == ord("h") or key == 27:
|
||||
self.handle_back()
|
||||
|
||||
elif key == ord("l"):
|
||||
self.navigate_to(View.LYRICS)
|
||||
|
||||
elif key == ord("L"):
|
||||
self.navigate_to(View.LIBRARY)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user