diff --git a/textual-take2/app.py b/textual-take2/app.py index d4d7942..b8d679e 100644 --- a/textual-take2/app.py +++ b/textual-take2/app.py @@ -22,7 +22,7 @@ from rich.table import Table, Column from textual import on, events, work from textual.reactive import reactive from textual.app import App, ComposeResult, RenderResult -from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label +from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Static from textual.widget import Widget from textual.containers import Horizontal, Vertical, Container, VerticalScroll from textual.screen import Screen @@ -38,19 +38,24 @@ conn = SubSonic.SubpyConn(u, p, "https://music.0x01fe.net") pygame.mixer.init() +class CoverArtPixels(Container): -class NowPlaying(Widget): - song = reactive[SubSonic.Song | None](None, layout=True) cover = reactive[Image.Image | None](None) def render(self) -> RenderResult: - if not self.song: - return "Nothing playing" - if self.cover: - return Pixels.from_image(self.cover) - else: - return f"Playing: {self.song.title} by {self.song.artist}" + width, height = self.size + width -= 4 + height -= 4 + + if width > height: + width = height + elif height > width: + height = width + + return Pixels.from_image(self.cover, resize=(width, height)) + + return '' @work(exclusive=True, thread=True) def get_cover_art(self, cover_art_id: str) -> None: @@ -62,23 +67,55 @@ class NowPlaying(Widget): logging.error(f"Failed to get cover art: {e}") self.cover = None - def watch_song(self, song: SubSonic.Song | None) -> None: - if song and song.albumId: +class NowPlayingInfo(Container): + song = reactive[SubSonic.Song | None](None, layout=True) + + def render(self) -> RenderResult: + if self.song: + return f"Playing: {self.song.title} by {self.song.artist}" + else: + return "Nothing Playing" + +class NowPlaying(Widget): + def compose(self) -> ComposeResult: + yield NowPlayingInfo() + yield CoverArtPixels(id="coverart") + + def update_song(self, song: SubSonic.Song) -> None: + info = self.query_one(NowPlayingInfo) + info.song = song + + if song.albumId: + cover_art = self.query_one(CoverArtPixels) + album = conn.getAlbumSongs(song.albumId) if album: - cover_id = album[0].coverArtId + cover_id: str | None = album[0].coverArtId if cover_id: - self.get_cover_art(cover_id) + cover_art.get_cover_art(cover_id) + class MusicApp(App): + CSS_PATH = 'app.tcss' + + BINDINGS = [ + # ("r", "rewind()", "⏪"), + ("space", "play_pause_song()", "⏯") + # ("s", "skip()", "⏭") + ] + + currently_playing: bool = False + is_paused: bool = False + place: int = 0 + def compose(self) -> ComposeResult: yield Header() yield Horizontal( VerticalScroll(OptionList(id="playlists"), id="sidebar"), Vertical( + DataTable(id="songs", cursor_type='row'), NowPlaying(id="now-playing"), - DataTable(id="songs"), id="main-panel" ) ) @@ -109,6 +146,7 @@ class MusicApp(App): table.clear() logging.debug([song.title for song in songs]) for song in songs: + # Doesn't work with duplicated songs table.add_row(song.trackNumber, song.title, song.artist, song.album, key=song.id) except Exception as e: logging.error(f"Failed to get playlist songs: {e}") @@ -119,19 +157,35 @@ class MusicApp(App): if song_id: self.play_song(song_id) - @work(exclusive=True, thread=True) - def play_song(self, song_id: str) -> None: + @work(exclusive=True) + async def play_song(self, song_id: str) -> None: try: song = conn.getSong(song_id) now_playing = self.query_one(NowPlaying) - self.call_from_thread(setattr, now_playing, "song", song) + # self.call_from_thread(NowPlaying.update_song, now_playing, song) + now_playing.update_song(song) stream_data = conn.stream(song_id) pygame.mixer.music.load(io.BytesIO(stream_data.content)) pygame.mixer.music.play() + self.currently_playing = True except Exception as e: logging.error(f"Failed to play song {song_id}: {e}") + def action_play_pause_song(self) -> None: + if not self.currently_playing: + return None + + if not self.is_paused: + self.notify("Playback paused") + self.place = pygame.mixer.music.get_pos() + pygame.mixer.music.pause() + self.is_paused = True + else: + self.notify("Resuming Playback") + pygame.mixer.music.unpause() + self.is_paused = False + if __name__ == "__main__": app = MusicApp() app.run() diff --git a/textual-take2/app.tcss b/textual-take2/app.tcss new file mode 100644 index 0000000..93546c7 --- /dev/null +++ b/textual-take2/app.tcss @@ -0,0 +1,13 @@ +#songs { + height: 50%; +} + +#now-playing { + height: 90%; +} + +#coverart { + height: 100%; + width: 100%; +} +