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:
x3
2026-09-06 08:40:59 +00:00
parent 68f6e0ef84
commit e06542cb1d
4 changed files with 169 additions and 7 deletions
+104 -3
View File
@@ -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)