fixed playing stuff, added cover art, added pause button
This commit is contained in:
1 parent
00d66f7258
commit
4c1f5bfe82
2 files changed
+84
-17
No files matched your search
+71
-17
@@ -22,7 +22,7 @@ from rich.table import Table, Column
|
|||||||
from textual import on, events, work
|
from textual import on, events, work
|
||||||
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
|
from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Static
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.containers import Horizontal, Vertical, Container, VerticalScroll
|
from textual.containers import Horizontal, Vertical, Container, VerticalScroll
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
@@ -38,19 +38,24 @@ conn = SubSonic.SubpyConn(u, p, "https://music.0x01fe.net")
|
|||||||
|
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
|
|
||||||
|
class CoverArtPixels(Container):
|
||||||
|
|
||||||
class NowPlaying(Widget):
|
|
||||||
song = reactive[SubSonic.Song | None](None, layout=True)
|
|
||||||
cover = reactive[Image.Image | None](None)
|
cover = reactive[Image.Image | None](None)
|
||||||
|
|
||||||
def render(self) -> RenderResult:
|
def render(self) -> RenderResult:
|
||||||
if not self.song:
|
|
||||||
return "Nothing playing"
|
|
||||||
|
|
||||||
if self.cover:
|
if self.cover:
|
||||||
return Pixels.from_image(self.cover)
|
width, height = self.size
|
||||||
else:
|
width -= 4
|
||||||
return f"Playing: {self.song.title} by {self.song.artist}"
|
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)
|
@work(exclusive=True, thread=True)
|
||||||
def get_cover_art(self, cover_art_id: str) -> None:
|
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}")
|
logging.error(f"Failed to get cover art: {e}")
|
||||||
self.cover = None
|
self.cover = None
|
||||||
|
|
||||||
def watch_song(self, song: SubSonic.Song | None) -> None:
|
class NowPlayingInfo(Container):
|
||||||
if song and song.albumId:
|
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)
|
album = conn.getAlbumSongs(song.albumId)
|
||||||
if album:
|
if album:
|
||||||
cover_id = album[0].coverArtId
|
cover_id: str | None = album[0].coverArtId
|
||||||
if cover_id:
|
if cover_id:
|
||||||
self.get_cover_art(cover_id)
|
cover_art.get_cover_art(cover_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MusicApp(App):
|
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:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header()
|
yield Header()
|
||||||
yield Horizontal(
|
yield Horizontal(
|
||||||
VerticalScroll(OptionList(id="playlists"), id="sidebar"),
|
VerticalScroll(OptionList(id="playlists"), id="sidebar"),
|
||||||
Vertical(
|
Vertical(
|
||||||
|
DataTable(id="songs", cursor_type='row'),
|
||||||
NowPlaying(id="now-playing"),
|
NowPlaying(id="now-playing"),
|
||||||
DataTable(id="songs"),
|
|
||||||
id="main-panel"
|
id="main-panel"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -109,6 +146,7 @@ class MusicApp(App):
|
|||||||
table.clear()
|
table.clear()
|
||||||
logging.debug([song.title for song in songs])
|
logging.debug([song.title for song in songs])
|
||||||
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)
|
table.add_row(song.trackNumber, song.title, song.artist, song.album, key=song.id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to get playlist songs: {e}")
|
logging.error(f"Failed to get playlist songs: {e}")
|
||||||
@@ -119,19 +157,35 @@ class MusicApp(App):
|
|||||||
if song_id:
|
if song_id:
|
||||||
self.play_song(song_id)
|
self.play_song(song_id)
|
||||||
|
|
||||||
@work(exclusive=True, thread=True)
|
@work(exclusive=True)
|
||||||
def play_song(self, song_id: str) -> None:
|
async def play_song(self, song_id: str) -> None:
|
||||||
try:
|
try:
|
||||||
song = conn.getSong(song_id)
|
song = conn.getSong(song_id)
|
||||||
now_playing = self.query_one(NowPlaying)
|
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)
|
stream_data = conn.stream(song_id)
|
||||||
pygame.mixer.music.load(io.BytesIO(stream_data.content))
|
pygame.mixer.music.load(io.BytesIO(stream_data.content))
|
||||||
pygame.mixer.music.play()
|
pygame.mixer.music.play()
|
||||||
|
self.currently_playing = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to play song {song_id}: {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__":
|
if __name__ == "__main__":
|
||||||
app = MusicApp()
|
app = MusicApp()
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#songs {
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#now-playing {
|
||||||
|
height: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#coverart {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user