committing here incase navidrome adds an api endpoint for artist bio

This commit is contained in:
JISAUAY committed 2025-09-17 13:48:50 -05:00
1 parent 7b6a1ed30c
commit 3a07071284
2 files changed
+259 -5

No files matched your search

+211 -5
View File
@@ -25,6 +25,7 @@ BITRATE = int(BITRATE) if BITRATE else BITRATE
os.makedirs(".cache/art/", exist_ok=True) os.makedirs(".cache/art/", exist_ok=True)
os.makedirs(".cache/audio/", exist_ok=True) os.makedirs(".cache/audio/", exist_ok=True)
os.makedirs(".cache/art/artists/", exist_ok=True)
logging.basicConfig(filename=LOG_FILE, level=LOGGING_LEVEL, format=LOG_FORMAT) logging.basicConfig(filename=LOG_FILE, level=LOGGING_LEVEL, format=LOG_FORMAT)
@@ -39,7 +40,7 @@ from textual import on, events, work
from textual.binding import Binding from textual.binding import Binding
from textual.reactive import reactive from textual.reactive import reactive
from textual.app import App, ComposeResult, RenderResult from textual.app import App, ComposeResult, RenderResult
from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Input from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Input, Placeholder, Static, Rule
from textual.widget import Widget from textual.widget import Widget
from textual.containers import Horizontal, Vertical, Container from textual.containers import Horizontal, Vertical, Container
from textual.screen import Screen from textual.screen import Screen
@@ -47,7 +48,9 @@ from textual.widgets.option_list import Option
from textual.timer import Timer from textual.timer import Timer
from rich_pixels import Pixels from rich_pixels import Pixels
from PIL import Image from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
u, p = [s.strip() for s in open('auth', 'r').read().split(',')] u, p = [s.strip() for s in open('auth', 'r').read().split(',')]
@@ -149,6 +152,90 @@ class ArtistAlbums(Screen):
app.pop_screen() app.pop_screen()
app.uninstall_screen("artistAlbums") app.uninstall_screen("artistAlbums")
class ArtistInfoPanel(Widget):
artist: SubSonic.Artist
def compose(self) -> ComposeResult:
with Horizontal():
yield ArtistArt(classes="artistInfoArt")
yield Placeholder(classes="artistInfoTextbox")
yield Rule()
def set_artist(self, artist: SubSonic.Artist) -> None:
self.artist = artist
artist_art = self.query_one(ArtistArt)
artist_art.get_artist_image(self.artist.id)
class AlbumPanel(Widget, can_focus=True, can_focus_children=True):
album: SubSonic.Album
def compose(self) -> ComposeResult:
yield CoverArtPixels(classes="albumArt")
yield Label(classes="albumName")
def set_album(self, album: SubSonic.Album) -> None:
self.album = album
coverArt = self.query_one(CoverArtPixels)
coverArt.get_cover_art(self.album.id)
label = self.query_one(Label)
label.update(self.album.album)
class ArtistAlbums2(Screen):
BINDINGS = [
('escape', 'die()', "Back")
]
artist: SubSonic.Artist
albums: list[SubSonic.Album]
def __init__(self, artist: SubSonic.Artist):
self.artist = artist
super().__init__()
def compose(self) -> ComposeResult:
with Horizontal(id="headerbs"):
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
yield ArtistInfoPanel(classes="artistInfo")
with Horizontal():
yield AlbumPanel(classes="albumPanel")
yield AlbumPanel(classes="albumPanel")
yield AlbumPanel(classes="albumPanel")
yield AlbumPanel(classes="albumPanel")
with Horizontal():
yield AlbumPanel(classes="albumPanel")
yield AlbumPanel(classes="albumPanel")
yield AlbumPanel(classes="albumPanel")
yield AlbumPanel(classes="albumPanel")
yield Footer()
def on_mount(self) -> None:
self.albums = conn.getArtistAlbums(self.artist.id)
artist_info_panel = self.query_one(ArtistInfoPanel)
artist_info_panel.set_artist(self.artist)
albums_displayed = 0
album_panels = self.query(AlbumPanel)
for album_panel in album_panels:
album_panel.set_album(self.albums[albums_displayed])
albums_displayed += 1
if albums_displayed >= len(self.albums) - 1:
return
def action_die(self) -> None:
app.pop_screen()
app.uninstall_screen("artistAlbums")
class Artists(Screen): class Artists(Screen):
artists: list[SubSonic.Artist] artists: list[SubSonic.Artist]
@@ -190,6 +277,126 @@ class Artists(Screen):
if search_value in artist.name.lower(): if search_value in artist.name.lower():
artist_list.add_option(Option(artist.name, artist.id)) artist_list.add_option(Option(artist.name, artist.id))
class ArtistLabel(Label, can_focus=True):
def render(self) -> RenderResult:
if self.has_focus:
return Text(str(self.content), style="white on blue")
else:
return self.content
class ArtistArt(Static):
art = reactive[Image.Image | None](None)
def render(self) -> RenderResult:
if self.art:
width, height = self.size
logging.debug(f'{width=}, {height=}')
height *= 2
if width > height:
width = height
elif height > width:
height = width
return Pixels.from_image(self.art, resize=(width, height))
return ''
@work(exclusive=True, thread=True)
def get_artist_image(self, artist_id: str) -> None:
artist_id = 'ar-' + artist_id
image = None
cache_path = f".cache/art/artists/{artist_id}.png"
if CACHE and os.path.exists(cache_path):
try:
self.art = Image.open(cache_path)
except Exception as e:
logging.error(f"Failed to load cached artist art {cache_path}: {e}")
if image is None:
try:
artist_art_data = conn.getCoverArtRaw(artist_id, size=300)
image = Image.open(io.BytesIO(artist_art_data))
if CACHE:
try:
image.save(cache_path, 'PNG')
except Exception as e:
logging.error(f"Failed to save artist art to cache {cache_path}: {e}")
self.art = image
except Exception as e:
logging.error(f"Failed to get artist art: {e}")
self.art = None
class ArtistPanel(Widget, can_focus=True, can_focus_children=True):
artist: SubSonic.Artist
def compose(self) -> ComposeResult:
yield ArtistArt(classes="artistArt")
yield ArtistLabel(classes="artistName")
def set_artist(self, artist: SubSonic.Artist) -> None:
self.artist = artist
art: ArtistArt = self.query_one(ArtistArt)
art.get_artist_image(self.artist.id)
label = self.query_one(Label)
label.update(self.artist.name)
@on(events.Click)
def clicked(self, event: events.Click) -> None:
logging.debug(f'ArtistPanel {self.artist.name} was clicked.')
app.install_screen(ArtistAlbums2(self.artist), 'artistAlbums')
app.push_screen('artistAlbums')
class Artists2(Screen):
artists: list[SubSonic.Artist]
def compose(self) -> ComposeResult:
yield Header(icon='')
with Horizontal(id="headerbs"):
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
with Horizontal():
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
with Horizontal():
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
with Horizontal():
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield ArtistPanel(classes="artistPanel")
yield Footer()
def on_mount(self) -> None:
app.sub_title = 'Artists2'
self.artists = conn.getArtists()
artists_displayed = 0
artist_panels = self.query(ArtistPanel)
for artist_panel in artist_panels:
artist_panel.set_artist(self.artists[artists_displayed])
artists_displayed += 1
class Albums(Screen): class Albums(Screen):
albums: list[SubSonic.Album] albums: list[SubSonic.Album]
@@ -405,7 +612,7 @@ class CoverArtPixels(Container):
if self.cover: if self.cover:
width, height = self.size width, height = self.size
height += 100 height *= 2
if width > height: if width > height:
width = height width = height
@@ -440,7 +647,6 @@ class CoverArtPixels(Container):
except Exception as e: except Exception as e:
logging.error(f"Failed to get cover art: {e}") logging.error(f"Failed to get cover art: {e}")
self.cover = None self.cover = None
return
class QueueTable(Widget): class QueueTable(Widget):
@@ -610,7 +816,7 @@ class TextualSubpyPlayer(App):
MODES = { MODES = {
"main" : MainView, "main" : MainView,
"artists" : Artists, "artists" : Artists2,
"albums" : Albums, "albums" : Albums,
"favourites" : Favourites, "favourites" : Favourites,
"playlists" : Playlists "playlists" : Playlists
+48
View File
@@ -34,3 +34,51 @@
#headerbs { #headerbs {
height: 1; height: 1;
} }
.artistPanel {
width: 20%;
height: 100%;
padding: 0 1;
}
.artistArt {
height: 1fr;
content-align: center middle;
}
.artistName {
width: 100%;
text-align: center;
}
.artistInfo {
width: 1fr;
height: 1.25fr;
}
.artistInfoArt {
width: 1fr;
height: 100%;
content-align: center middle;
}
.artistInfoTextbox {
width: 4fr;
}
.albumPanel {
width: 1fr;
height: 1fr;
padding: 0 1;
}
.albumArt {
height: 1fr;
content-align: center middle;
}
.albumName {
text-wrap: wrap;
text-align: center;
width: 100%;
}