Files
spotify-cli/spotify_cli/ui.py
T
x3 c3f7d87e67 fix: Playlists Pagination + Player Fehlermeldungen
- Playlists: Alle werden jetzt geladen (Pagination)
- Player: Fehlermeldung wenn kein Geraet aktiv
- next/previous/repeat/shuffle zeigen Fehler statt stillschweigend zuversagen
2026-09-06 08:50:30 +00:00

1119 lines
39 KiB
Python

import curses
import time
import threading
from .client import SpotifyClient, LyricsClient, TokenExpired
from .player import Player
FOCUS_NAV = "nav"
FOCUS_CONTENT = "content"
class View:
HOME = "home"
PLAYLISTS = "playlists"
PLAYLIST_DETAIL = "playlist_detail"
LIKED_SONGS = "liked_songs"
RECENT = "recent"
ALBUMS = "albums"
ARTISTS = "artists"
SEARCH = "search"
QUEUE = "queue"
DEVICES = "devices"
ALBUM_DETAIL = "album_detail"
ARTIST_DETAIL = "artist_detail"
ARTIST_TOP = "artist_top"
LIBRARY = "library"
LYRICS = "lyrics"
class UI:
NAV_ITEMS = [
(View.HOME, "Home"),
(View.PLAYLISTS, "Playlists"),
(View.LIKED_SONGS, "Liked Songs"),
(View.RECENT, "Recently Played"),
(View.LIBRARY, "Library"),
(View.QUEUE, "Queue"),
(View.DEVICES, "Devices"),
(View.SEARCH, "Search"),
]
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
self.content_scroll = 0
self.content_items = []
self.selected_index = 0
self.running = True
self.message = ""
self.message_time = 0
self.search_query = ""
self.search_mode = False
self.loading = False
self.playlists_cache = []
self.liked_offset = 0
self.recent_data = []
self.devices_cache = []
self.albums_cache = []
self.artists_cache = []
self.search_results = {}
self.playlist_tracks = []
self.album_tracks = []
self.artist_data = None
self.artist_albums = []
self.artist_top_tracks = []
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()
curses.start_color()
curses.init_pair(1, curses.COLOR_WHITE, -1)
curses.init_pair(2, curses.COLOR_GREEN, -1)
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_GREEN)
curses.init_pair(4, curses.COLOR_CYAN, -1)
curses.init_pair(5, curses.COLOR_YELLOW, -1)
curses.init_pair(6, curses.COLOR_MAGENTA, -1)
curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(8, curses.COLOR_GREEN, curses.COLOR_BLUE)
curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_CYAN)
self.stdscr.timeout(250)
self.player.register_callback(self._on_player_update)
self.player.start_polling(interval=2)
def _on_player_update(self, playback, track):
self.force_refresh = True
def stop(self):
self.running = False
self.player.stop_polling()
def show_message(self, msg, duration=3):
self.message = msg
self.message_time = time.time() + duration
def draw(self):
self.stdscr.erase()
h, w = self.stdscr.getmaxyx()
if h < 10 or w < 40:
self.stdscr.addstr(0, 0, "Fenster zu klein! (min 40x10)")
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
bar_h = 1
self._draw_status_bar(0, 0, w, status_h)
self._draw_nav(status_h, 0, nav_w, h - status_h - now_h - bar_h)
self._draw_content(status_h, nav_w, w - nav_w, h - status_h - now_h - bar_h)
self._draw_now_playing(h - now_h - bar_h, 0, w, now_h)
self._draw_bar(h - bar_h, 0, w, bar_h)
self.stdscr.refresh()
def _draw_status_bar(self, y, x, w, h):
try:
state = self.player.get_playback_state()
if state:
play = " Playing" if state["is_playing"] else " Paused"
rep = f" R:{state['repeat_state']}"
shuf = f" S:{'on' if state['shuffle_state'] else 'off'}"
vol = f" Vol:{state['volume']}%"
dev = f" {state['device']}"
left = f"spotify-cli {play}{rep}{shuf}{vol}"
right = dev
pad = w - len(left) - len(right) - 2
line = left + " " * max(pad, 1) + right
else:
line = " spotify-cli -- No playback device"
self.stdscr.addnstr(y, x, line[:w-1], w-1, curses.color_pair(3) | curses.A_BOLD)
except Exception:
self.stdscr.addnstr(y, x, " spotify-cli", w-1, curses.color_pair(3) | curses.A_BOLD)
def _draw_nav(self, y, x, w, h):
try:
title_attr = curses.color_pair(7) | curses.A_BOLD
if self.focus == FOCUS_NAV:
title_attr = curses.color_pair(9) | curses.A_BOLD
self.stdscr.addnstr(y, x, " NAV".ljust(w), w-1, title_attr)
except curses.error:
pass
for i, (vid, label) in enumerate(self.NAV_ITEMS):
if i + 1 >= h:
break
try:
is_selected = i == self.nav_index and self.focus == FOCUS_NAV
prefix = "> " if is_selected else " "
if self.focus == FOCUS_NAV and i == self.nav_index:
attr = curses.color_pair(9) | curses.A_BOLD
elif i == self.nav_index:
attr = curses.color_pair(8) | curses.A_BOLD
else:
attr = curses.color_pair(1)
line = f"{prefix}{label}"
self.stdscr.addnstr(y + 1 + i, x, line.ljust(w), w-1, attr)
except curses.error:
pass
def _draw_content(self, y, x, w, h):
try:
title_attr = curses.color_pair(7) | curses.A_BOLD
if self.focus == FOCUS_CONTENT:
title_attr = curses.color_pair(9) | curses.A_BOLD
self.stdscr.addnstr(y, x, "".ljust(w), w-1, title_attr)
except curses.error:
pass
header = self._get_view_header()
try:
self.stdscr.addnstr(y, x + 1, header[:w-2], w-2, title_attr)
except curses.error:
pass
if self.loading:
try:
self.stdscr.addstr(y + 2, x + 2, "Laden...", curses.color_pair(5))
except curses.error:
pass
return
visible_items = h - 1
start = self.content_scroll
end = min(start + visible_items, len(self.content_items))
for i in range(start, end):
row = y + 1 + (i - start)
if row >= y + h:
break
item = self.content_items[i]
is_selected = i == self.selected_index and self.focus == FOCUS_CONTENT
try:
prefix = "> " if is_selected else " "
if isinstance(item, dict):
text = self._format_item(item)
else:
text = str(item)
line = f"{prefix}{text}"
if is_selected:
attr = curses.color_pair(2) | curses.A_BOLD
else:
attr = curses.color_pair(1)
self.stdscr.addnstr(row, x + 1, line[:w-2], w-2, attr)
except curses.error:
pass
if not self.content_items and not self.loading:
msg = self._get_empty_message()
try:
self.stdscr.addstr(y + 2, x + 2, msg, curses.color_pair(5))
except curses.error:
pass
def _draw_now_playing(self, y, x, w, h):
info = self.player.get_track_info()
try:
self.stdscr.addnstr(y, x, "".ljust(w), w-1, curses.color_pair(7) | curses.A_BOLD)
except curses.error:
pass
try:
self.stdscr.addnstr(y, x, " NOW PLAYING".ljust(w), w-1, curses.color_pair(7) | curses.A_BOLD)
except curses.error:
pass
if not info:
return
title = f"{info['artists']} - {info['name']}"
album = info['album']
progress = info['progress_ms']
duration = info['duration_ms']
prog_bar = self._make_progress_bar(progress, duration, w - 4)
line1 = f" {title}"
line2 = f" {album} [{prog_bar}]"
try:
self.stdscr.addnstr(y + 1, x, line1[:w-1], w-1, curses.color_pair(4))
except curses.error:
pass
try:
self.stdscr.addnstr(y + 2, x, line2[:w-1], w-1, curses.color_pair(1))
except curses.error:
pass
def _draw_bar(self, y, x, w, h):
if self.search_mode:
keys = f"Search: {self.search_query}_ Enter:Search Esc:Cancel"
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 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 ""
ratio = current / total
filled = int(width * ratio)
bar = "=" * filled + ">" + "-" * (width - filled - 1)
cur = self._format_ms(current)
dur = self._format_ms(total)
return f"{cur} [{bar}] {dur}"
def _format_ms(self, ms):
s = int(ms / 1000)
m = s // 60
s = s % 60
return f"{m}:{s:02d}"
def _get_view_header(self):
headers = {
View.HOME: "Home",
View.PLAYLISTS: "Playlists",
View.PLAYLIST_DETAIL: "Playlist",
View.LIKED_SONGS: "Liked Songs",
View.RECENT: "Recently Played",
View.LIBRARY: "Library",
View.QUEUE: "Queue",
View.DEVICES: "Devices",
View.SEARCH: "Search",
View.ALBUM_DETAIL: "Album",
View.ARTIST_DETAIL: "Artist",
View.ARTIST_TOP: "Top Tracks",
View.ALBUMS: "Albums",
View.ARTISTS: "Artists",
}
return headers.get(self.current_view, "")
def _get_empty_message(self):
msgs = {
View.PLAYLISTS: "Keine Playlists gefunden.",
View.LIKED_SONGS: "Keine Liked Songs.",
View.RECENT: "Keine kuerzlich gespielten Tracks.",
View.QUEUE: "Warteschlange ist leer.",
View.DEVICES: "Keine Geraete gefunden.",
View.SEARCH: "Druecke '/' zum Suchen.",
View.LIBRARY: "Keine Eintraege.",
}
return msgs.get(self.current_view, "Keine Eintraege.")
def _format_item(self, item):
kind = item.get("type", "")
if kind == "track":
artists = item.get("artists", "Unknown")
name = item.get("name", "Unknown")
album = item.get("album", "")
dur = item.get("duration", "")
return f"{artists} - {name} ({album}) {dur}"
elif kind == "playlist":
name = item.get("name", "Unknown")
tracks = item.get("tracks", "")
owner = item.get("owner", "")
return f"{name} ({tracks} tracks, by {owner})"
elif kind == "album":
name = item.get("name", "Unknown")
artist = item.get("artist", "")
year = item.get("year", "")
return f"{artist} - {name} ({year})"
elif kind == "artist":
name = item.get("name", "Unknown")
followers = item.get("followers", "")
return f"{name} ({followers} followers)"
elif kind == "device":
name = item.get("name", "Unknown")
dtype = item.get("dtype", "")
active = item.get("active", False)
marker = " *" if active else ""
return f"{name} ({dtype}){marker}"
elif kind == "search_track":
artists = item.get("artists", "Unknown")
name = item.get("name", "Unknown")
return f"[Track] {artists} - {name}"
elif kind == "search_album":
name = item.get("name", "Unknown")
artist = item.get("artist", "")
return f"[Album] {artist} - {name}"
elif kind == "search_artist":
name = item.get("name", "Unknown")
return f"[Artist] {name}"
elif kind == "search_playlist":
name = item.get("name", "Unknown")
return f"[Playlist] {name}"
elif kind == "section":
return item.get("label", "")
elif kind == "nav":
return item.get("label", "")
return str(item)
# --- Data Loading ---
def load_home(self):
self.loading = True
self.draw()
try:
recent = self.client.get_recently_played(limit=10)
items = []
seen = set()
for r in recent.get("items", []):
track = r.get("track", {})
if track and track.get("id") not in seen:
seen.add(track["id"])
items.append({
"type": "track",
"id": track["id"],
"name": track["name"],
"artists": ", ".join(a["name"] for a in track.get("artists", [])),
"album": track.get("album", {}).get("name", ""),
"uri": track["uri"],
"duration": self._format_ms(track.get("duration_ms", 0)),
"context_uri": track.get("album", {}).get("uri", ""),
})
self.content_items = items
self.recent_data = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_playlists(self):
self.loading = True
self.draw()
try:
all_items = []
offset = 0
limit = 50
while True:
data = self.client.get_playlists(limit=limit, offset=offset)
items = data.get("items", [])
for p in items:
all_items.append({
"type": "playlist",
"id": p["id"],
"name": p["name"],
"tracks": p.get("tracks", {}).get("total", 0),
"owner": p.get("owner", {}).get("display_name", ""),
"uri": p["uri"],
})
if len(items) < limit:
break
offset += limit
self.content_items = all_items
self.playlists_cache = all_items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_playlist_detail(self, playlist_id):
self.loading = True
self.draw()
try:
data = self.client.get_playlist_tracks(playlist_id, limit=100)
items = []
for t in data.get("items", []):
track = t.get("track", {})
if not track:
continue
items.append({
"type": "track",
"id": track["id"],
"name": track["name"],
"artists": ", ".join(a["name"] for a in track.get("artists", [])),
"album": track.get("album", {}).get("name", ""),
"uri": track["uri"],
"duration": self._format_ms(track.get("duration_ms", 0)),
"context_uri": f"spotify:playlist:{playlist_id}",
})
self.content_items = items
self.playlist_tracks = items
self.context_uri = f"spotify:playlist:{playlist_id}"
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_liked_songs(self):
self.loading = True
self.draw()
try:
data = self.client.get_saved_tracks(limit=50, offset=self.liked_offset)
items = []
for t in data.get("items", []):
track = t.get("track", {})
if not track:
continue
items.append({
"type": "track",
"id": track["id"],
"name": track["name"],
"artists": ", ".join(a["name"] for a in track.get("artists", [])),
"album": track.get("album", {}).get("name", ""),
"uri": track["uri"],
"duration": self._format_ms(track.get("duration_ms", 0)),
"context_uri": "spotify:library:tracks",
})
self.content_items = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_recent(self):
self.loading = True
self.draw()
try:
data = self.client.get_recently_played(limit=50)
items = []
seen = set()
for r in data.get("items", []):
track = r.get("track", {})
if track and track.get("id") not in seen:
seen.add(track["id"])
items.append({
"type": "track",
"id": track["id"],
"name": track["name"],
"artists": ", ".join(a["name"] for a in track.get("artists", [])),
"album": track.get("album", {}).get("name", ""),
"uri": track["uri"],
"duration": self._format_ms(track.get("duration_ms", 0)),
"context_uri": track.get("album", {}).get("uri", ""),
})
self.content_items = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_library(self):
self.loading = True
self.draw()
items = [
{"type": "nav", "label": "Liked Songs", "action": "liked"},
{"type": "nav", "label": "Albums", "action": "albums"},
{"type": "nav", "label": "Artists", "action": "artists"},
]
self.content_items = items
self.loading = False
def load_albums(self):
self.loading = True
self.draw()
try:
data = self.client.get_saved_albums(limit=50)
items = []
for a in data.get("items", []):
album = a.get("album", {})
items.append({
"type": "album",
"id": album["id"],
"name": album["name"],
"artist": ", ".join(ar["name"] for ar in album.get("artists", [])),
"year": album.get("release_year", ""),
"uri": album["uri"],
"total_tracks": album.get("total_tracks", 0),
})
self.content_items = items
self.albums_cache = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_artists(self):
self.loading = True
self.draw()
try:
data = self.client.get_saved_artists(limit=50)
items = []
for a in data.get("items", []):
items.append({
"type": "artist",
"id": a["id"],
"name": a["name"],
"followers": a.get("followers", {}).get("total", 0),
"uri": a["uri"],
})
self.content_items = items
self.artists_cache = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_queue(self):
self.loading = True
self.draw()
try:
data = self.client.get_queue()
items = []
current = data.get("currently_playing")
if current:
items.append({
"type": "track",
"id": current["id"],
"name": f">>> {current['name']}",
"artists": ", ".join(a["name"] for a in current.get("artists", [])),
"album": current.get("album", {}).get("name", ""),
"uri": current["uri"],
"duration": self._format_ms(current.get("duration_ms", 0)),
})
for t in data.get("queue", []):
items.append({
"type": "track",
"id": t["id"],
"name": t["name"],
"artists": ", ".join(a["name"] for a in t.get("artists", [])),
"album": t.get("album", {}).get("name", ""),
"uri": t["uri"],
"duration": self._format_ms(t.get("duration_ms", 0)),
})
self.content_items = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_devices(self):
self.loading = True
self.draw()
try:
self.player.refresh()
items = []
for d in self.player.devices:
items.append({
"type": "device",
"id": d["id"],
"name": d["name"],
"dtype": d.get("type", ""),
"active": d.get("is_active", False),
})
self.content_items = items
self.devices_cache = items
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_album_detail(self, album_id):
self.loading = True
self.draw()
try:
album = self.client.get_album(album_id)
tracks = self.client.get_album_tracks(album_id, limit=50)
items = []
for t in tracks.get("items", []):
items.append({
"type": "track",
"id": t["id"],
"name": t["name"],
"artists": ", ".join(a["name"] for a in t.get("artists", [])),
"album": album.get("name", ""),
"uri": t["uri"],
"duration": self._format_ms(t.get("duration_ms", 0)),
"context_uri": f"spotify:album:{album_id}",
})
self.content_items = items
self.album_tracks = items
self.context_uri = f"spotify:album:{album_id}"
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
def load_artist_detail(self, artist_id):
self.loading = True
self.draw()
try:
artist = self.client.get_artist(artist_id)
self.artist_data = artist
top = self.client.get_artist_top_tracks(artist_id)
self.artist_top_tracks = []
for t in top.get("tracks", []):
self.artist_top_tracks.append({
"type": "track",
"id": t["id"],
"name": t["name"],
"artists": ", ".join(a["name"] for a in t.get("artists", [])),
"album": t.get("album", {}).get("name", ""),
"uri": t["uri"],
"duration": self._format_ms(t.get("duration_ms", 0)),
"context_uri": f"spotify:artist:{artist_id}",
})
albums = self.client.get_artist_albums(artist_id, limit=20)
self.artist_albums = []
for a in albums.get("items", []):
self.artist_albums.append({
"type": "album",
"id": a["id"],
"name": a["name"],
"artist": artist["name"],
"year": a.get("release_date", "")[:4],
"uri": a["uri"],
"total_tracks": a.get("total_tracks", 0),
})
items = [{"type": "section", "label": f"--- {artist['name']} Top Tracks ---"}]
items.extend(self.artist_top_tracks)
items.append({"type": "section", "label": "--- Albums ---"})
items.extend(self.artist_albums)
self.content_items = items
except Exception as e:
self.content_items = []
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
self.loading = True
self.draw()
try:
data = self.client.search(query, types="track,album,artist,playlist", limit=10)
items = []
for t in data.get("tracks", {}).get("items", []):
items.append({
"type": "search_track",
"id": t["id"],
"name": t["name"],
"artists": ", ".join(a["name"] for a in t.get("artists", [])),
"album": t.get("album", {}).get("name", ""),
"uri": t["uri"],
"duration": self._format_ms(t.get("duration_ms", 0)),
"context_uri": t.get("album", {}).get("uri", ""),
})
for a in data.get("albums", {}).get("items", []):
items.append({
"type": "search_album",
"id": a["id"],
"name": a["name"],
"artist": ", ".join(ar["name"] for ar in a.get("artists", [])),
"year": a.get("release_date", "")[:4],
"uri": a["uri"],
})
for ar in data.get("artists", {}).get("items", []):
items.append({
"type": "search_artist",
"id": ar["id"],
"name": ar["name"],
"uri": ar["uri"],
})
for p in data.get("playlists", {}).get("items", []):
items.append({
"type": "search_playlist",
"id": p["id"],
"name": p["name"],
"uri": p["uri"],
})
self.content_items = items
self.search_results = data
except Exception as e:
self.content_items = []
self.show_message(f"Fehler: {e}")
self.loading = False
# --- Navigation ---
def navigate_to(self, view):
self.current_view = view
self.content_scroll = 0
self.selected_index = 0
self.content_items = []
loaders = {
View.HOME: self.load_home,
View.PLAYLISTS: self.load_playlists,
View.LIKED_SONGS: self.load_liked_songs,
View.RECENT: self.load_recent,
View.LIBRARY: self.load_library,
View.QUEUE: self.load_queue,
View.DEVICES: self.load_devices,
View.ALBUMS: self.load_albums,
View.ARTISTS: self.load_artists,
}
if view in loaders:
loaders[view]()
def handle_select(self):
if not self.content_items:
return
if self.selected_index >= len(self.content_items):
return
item = self.content_items[self.selected_index]
kind = item.get("type", "")
if kind == "playlist":
self.current_view = View.PLAYLIST_DETAIL
self.load_playlist_detail(item["id"])
elif kind == "album" or kind == "search_album":
self.current_view = View.ALBUM_DETAIL
self.load_album_detail(item["id"])
elif kind == "artist" or kind == "search_artist":
self.current_view = View.ARTIST_DETAIL
self.load_artist_detail(item["id"])
elif kind == "track" or kind == "search_track":
try:
self.player.play_tracks([item["uri"]])
self.show_message(f"Spiele: {item.get('name', '')}")
time.sleep(0.5)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif kind == "device":
try:
self.player.transfer_to(item["id"])
self.show_message(f"Uebertragen zu: {item['name']}")
time.sleep(0.5)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif kind == "nav":
action = item.get("action", "")
if action == "liked":
self.navigate_to(View.LIKED_SONGS)
elif action == "albums":
self.navigate_to(View.ALBUMS)
elif action == "artists":
self.navigate_to(View.ARTISTS)
def handle_back(self):
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)
else:
self.navigate_to(View.HOME)
def run(self):
self.navigate_to(View.HOME)
while self.running:
if self.force_refresh:
self.force_refresh = False
self.draw()
key = self.stdscr.getch()
if key == -1:
continue
if self.search_mode:
self._handle_search_input(key)
self.draw()
continue
self._handle_key(key)
self.draw()
def _handle_search_input(self, key):
if key == 27:
self.search_mode = False
self.search_query = ""
return
elif key in (curses.KEY_ENTER, 10, 13):
self.search_mode = False
self.current_view = View.SEARCH
self.do_search(self.search_query)
return
elif key in (curses.KEY_BACKSPACE, 127, 8):
self.search_query = self.search_query[:-1]
elif 32 <= key <= 126:
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)
if key == ord("q"):
self.running = False
elif key == 9:
if self.focus == FOCUS_NAV:
self.focus = FOCUS_CONTENT
self.selected_index = min(self.selected_index, max(0, content_count - 1))
else:
self.focus = FOCUS_NAV
self.nav_index = min(self.nav_index, nav_count - 1)
elif self.focus == FOCUS_NAV:
self._handle_nav_key(key, nav_count)
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:
self.nav_index += 1
elif key == ord("k") or key == curses.KEY_UP:
if self.nav_index > 0:
self.nav_index -= 1
elif key == 10 or key == 13:
self.navigate_to(self.NAV_ITEMS[self.nav_index][0])
elif key == ord("h") or key == 27:
pass
elif key == ord("/"):
self.search_mode = True
self.search_query = ""
elif key == ord("1"):
self.nav_index = 0
self.navigate_to(self.NAV_ITEMS[0][0])
elif key == ord("2"):
self.nav_index = 1
self.navigate_to(self.NAV_ITEMS[1][0])
elif key == ord("3"):
self.nav_index = 2
self.navigate_to(self.NAV_ITEMS[2][0])
elif key == ord("4"):
self.nav_index = 3
self.navigate_to(self.NAV_ITEMS[3][0])
elif key == ord("5"):
self.nav_index = 4
self.navigate_to(self.NAV_ITEMS[4][0])
elif key == ord("6"):
self.nav_index = 5
self.navigate_to(self.NAV_ITEMS[5][0])
elif key == ord("7"):
self.nav_index = 6
self.navigate_to(self.NAV_ITEMS[6][0])
elif key == ord("8"):
self.nav_index = 7
self.navigate_to(self.NAV_ITEMS[7][0])
elif key == ord("L"):
self.navigate_to(View.LIBRARY)
def _handle_content_key(self, key, content_count):
if key == ord("j") or key == curses.KEY_DOWN:
if self.selected_index < content_count - 1:
self.selected_index += 1
self._adjust_scroll()
elif key == ord("k") or key == curses.KEY_UP:
if self.selected_index > 0:
self.selected_index -= 1
self._adjust_scroll()
elif key == curses.KEY_PPAGE:
self.selected_index = max(0, self.selected_index - 10)
self._adjust_scroll()
elif key == curses.KEY_NPAGE:
self.selected_index = min(content_count - 1, self.selected_index + 10)
self._adjust_scroll()
elif key == curses.KEY_HOME:
self.selected_index = 0
self.content_scroll = 0
elif key == curses.KEY_END:
self.selected_index = max(0, content_count - 1)
self._adjust_scroll()
elif key == 10 or key == 13:
self.handle_select()
elif key == ord(" "):
try:
self.player.toggle_play()
self.show_message("Toggled play/pause")
time.sleep(0.3)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("n"):
try:
self.player.next()
self.show_message("Next track")
time.sleep(0.5)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("p"):
try:
self.player.previous()
self.show_message("Previous track")
time.sleep(0.5)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("+") or key == ord("="):
try:
self.player.volume_up()
self.show_message(f"Volume: {self.player.get_volume()}%")
time.sleep(0.3)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("-"):
try:
self.player.volume_down()
self.show_message(f"Volume: {self.player.get_volume()}%")
time.sleep(0.3)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("r"):
try:
self.player.toggle_repeat()
self.show_message("Toggle repeat")
time.sleep(0.3)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("s"):
try:
self.player.toggle_shuffle()
self.show_message("Toggle shuffle")
time.sleep(0.3)
self.player.refresh()
except Exception as e:
self.show_message(f"Fehler: {e}")
elif key == ord("/"):
self.search_mode = True
self.search_query = ""
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)
def _adjust_scroll(self):
h, w = self.stdscr.getmaxyx()
visible = h - 7
if visible < 1:
visible = 1
if self.selected_index < self.content_scroll:
self.content_scroll = self.selected_index
elif self.selected_index >= self.content_scroll + visible:
self.content_scroll = self.selected_index - visible + 1
def _add_to_playlist_prompt(self):
pass