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:
@@ -21,14 +21,14 @@ Terminal-basierter Spotify Client im Stil von ncmpcpp.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd spotify-cli
|
cd spotify-cli
|
||||||
pip install -e .
|
sudo ./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Oder direkt:
|
Oder manuell:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
cd spotify-cli
|
||||||
python -m spotify_cli.main
|
sudo pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Einloggen
|
### 3. Einloggen
|
||||||
@@ -61,6 +61,7 @@ spotify-cli
|
|||||||
| `r` | Repeat togglen |
|
| `r` | Repeat togglen |
|
||||||
| `s` | Shuffle togglen |
|
| `s` | Shuffle togglen |
|
||||||
| `/` | Suche oeffnen |
|
| `/` | Suche oeffnen |
|
||||||
|
| `l` | Lyrics anzeigen |
|
||||||
| `h` oder `Esc` | Zurueck |
|
| `h` oder `Esc` | Zurueck |
|
||||||
| `1`-`8` | Schnellauswahl (Nav) |
|
| `1`-`8` | Schnellauswahl (Nav) |
|
||||||
| `L` | Library |
|
| `L` | Library |
|
||||||
|
|||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== spotify-cli Installation ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$EUID" -eq 0 ]; then
|
||||||
|
pip install "$SCRIPT_DIR"
|
||||||
|
else
|
||||||
|
echo "Installiere systemweit mit sudo..."
|
||||||
|
sudo pip install "$SCRIPT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Installation abgeschlossen!"
|
||||||
|
echo ""
|
||||||
|
echo "Benutzung:"
|
||||||
|
echo " spotify-cli --login # Einloggen"
|
||||||
|
echo " spotify-cli # Starten"
|
||||||
|
echo " spotify-cli --logout # Ausloggen"
|
||||||
@@ -197,5 +197,43 @@ class SpotifyClient:
|
|||||||
return self._get("/me")
|
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):
|
class TokenExpired(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
+104
-3
@@ -1,7 +1,7 @@
|
|||||||
import curses
|
import curses
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
from .client import SpotifyClient, TokenExpired
|
from .client import SpotifyClient, LyricsClient, TokenExpired
|
||||||
from .player import Player
|
from .player import Player
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ class View:
|
|||||||
ARTIST_DETAIL = "artist_detail"
|
ARTIST_DETAIL = "artist_detail"
|
||||||
ARTIST_TOP = "artist_top"
|
ARTIST_TOP = "artist_top"
|
||||||
LIBRARY = "library"
|
LIBRARY = "library"
|
||||||
|
LYRICS = "lyrics"
|
||||||
|
|
||||||
|
|
||||||
class UI:
|
class UI:
|
||||||
@@ -41,6 +42,7 @@ class UI:
|
|||||||
def __init__(self, stdscr, client: SpotifyClient):
|
def __init__(self, stdscr, client: SpotifyClient):
|
||||||
self.stdscr = stdscr
|
self.stdscr = stdscr
|
||||||
self.client = client
|
self.client = client
|
||||||
|
self.lyrics_client = LyricsClient()
|
||||||
self.player = Player(client)
|
self.player = Player(client)
|
||||||
self.current_view = View.HOME
|
self.current_view = View.HOME
|
||||||
self.nav_index = 0
|
self.nav_index = 0
|
||||||
@@ -68,6 +70,9 @@ class UI:
|
|||||||
self.context_uri = None
|
self.context_uri = None
|
||||||
self.focus = FOCUS_CONTENT
|
self.focus = FOCUS_CONTENT
|
||||||
self.force_refresh = False
|
self.force_refresh = False
|
||||||
|
self.lyrics_data = []
|
||||||
|
self.lyrics_scroll = 0
|
||||||
|
self.lyrics_track_info = None
|
||||||
|
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
curses.use_default_colors()
|
curses.use_default_colors()
|
||||||
@@ -106,6 +111,11 @@ class UI:
|
|||||||
self.stdscr.refresh()
|
self.stdscr.refresh()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.current_view == View.LYRICS:
|
||||||
|
self._draw_lyrics(h, w)
|
||||||
|
self.stdscr.refresh()
|
||||||
|
return
|
||||||
|
|
||||||
nav_w = 18
|
nav_w = 18
|
||||||
status_h = 1
|
status_h = 1
|
||||||
now_h = 3
|
now_h = 3
|
||||||
@@ -257,12 +267,46 @@ class UI:
|
|||||||
elif self.focus == FOCUS_NAV:
|
elif self.focus == FOCUS_NAV:
|
||||||
keys = "j/k:Move Enter:Select Tab:Content q:Quit"
|
keys = "j/k:Move Enter:Select Tab:Content q:Quit"
|
||||||
else:
|
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:
|
try:
|
||||||
self.stdscr.addnstr(y, x, keys.ljust(w), w-1, curses.color_pair(7))
|
self.stdscr.addnstr(y, x, keys.ljust(w), w-1, curses.color_pair(7))
|
||||||
except curses.error:
|
except curses.error:
|
||||||
pass
|
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):
|
def _make_progress_bar(self, current, total, width):
|
||||||
if total == 0:
|
if total == 0:
|
||||||
return ""
|
return ""
|
||||||
@@ -670,6 +714,32 @@ class UI:
|
|||||||
self.show_message(f"Fehler: {e}")
|
self.show_message(f"Fehler: {e}")
|
||||||
self.loading = False
|
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):
|
def do_search(self, query):
|
||||||
if not query:
|
if not query:
|
||||||
return
|
return
|
||||||
@@ -783,7 +853,9 @@ class UI:
|
|||||||
self.navigate_to(View.ARTISTS)
|
self.navigate_to(View.ARTISTS)
|
||||||
|
|
||||||
def handle_back(self):
|
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)
|
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):
|
elif self.current_view in (View.ALBUM_DETAIL, View.ARTIST_DETAIL):
|
||||||
self.navigate_to(View.HOME)
|
self.navigate_to(View.HOME)
|
||||||
@@ -827,6 +899,10 @@ class UI:
|
|||||||
self.search_query += chr(key)
|
self.search_query += chr(key)
|
||||||
|
|
||||||
def _handle_key(self, key):
|
def _handle_key(self, key):
|
||||||
|
if self.current_view == View.LYRICS:
|
||||||
|
self._handle_lyrics_key(key)
|
||||||
|
return
|
||||||
|
|
||||||
content_count = len(self.content_items)
|
content_count = len(self.content_items)
|
||||||
nav_count = len(self.NAV_ITEMS)
|
nav_count = len(self.NAV_ITEMS)
|
||||||
|
|
||||||
@@ -846,6 +922,28 @@ class UI:
|
|||||||
else:
|
else:
|
||||||
self._handle_content_key(key, content_count)
|
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):
|
def _handle_nav_key(self, key, nav_count):
|
||||||
if key == ord("j") or key == curses.KEY_DOWN:
|
if key == ord("j") or key == curses.KEY_DOWN:
|
||||||
if self.nav_index < nav_count - 1:
|
if self.nav_index < nav_count - 1:
|
||||||
@@ -993,6 +1091,9 @@ class UI:
|
|||||||
elif key == ord("h") or key == 27:
|
elif key == ord("h") or key == 27:
|
||||||
self.handle_back()
|
self.handle_back()
|
||||||
|
|
||||||
|
elif key == ord("l"):
|
||||||
|
self.navigate_to(View.LYRICS)
|
||||||
|
|
||||||
elif key == ord("L"):
|
elif key == ord("L"):
|
||||||
self.navigate_to(View.LIBRARY)
|
self.navigate_to(View.LIBRARY)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user