diff --git a/.gitignore b/.gitignore index ed8ebf5..677c3ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__ \ No newline at end of file +__pycache__ +art/* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c6e8c42..9895f89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ requests==2.31.0 textual==1.0.0 -pygame==2.6.0 \ No newline at end of file +pygame==2.6.0 +Pillow==10.0.0 +rich-pixels==3.0.1 \ No newline at end of file diff --git a/rich-pixel.py b/rich-pixel.py index 65a31eb..d17566b 100644 --- a/rich-pixel.py +++ b/rich-pixel.py @@ -4,7 +4,9 @@ from PIL import Image console = Console() +size = 70 + with Image.open("cover.png") as image: - pixels = Pixels.from_image(image, resize=(60, 60)) + pixels = Pixels.from_image(image, resize=(size, size)) console.print(pixels) \ No newline at end of file diff --git a/screens-test.py b/screens-test.py index bd2df94..ab5d4d2 100644 --- a/screens-test.py +++ b/screens-test.py @@ -16,12 +16,15 @@ import SubSonic os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" import pygame -from textual import on, events -from textual.app import App, ComposeResult +from textual import on, events, work +from textual.app import App, ComposeResult, RenderResult from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label -from textual.containers import Horizontal, Vertical +from textual.containers import Horizontal, Vertical, Container from textual.screen import Screen from textual.widgets.option_list import Option +from textual.timer import Timer + +from rich_pixels import Pixels u = 'pseudo' p = 'telegram9dock-MICHEL' @@ -241,7 +244,13 @@ class PlaylistSongs(Screen): return i class Favourites(Screen): + BINDINGS = [ + ('p', 'play()', 'Play Song'), + ('q', 'queue()', 'Queue Song') + ] + songs: list[SubSonic.Song] | None = None + selected_song_id: str | None = None def compose(self) -> ComposeResult: yield Header(icon="") @@ -259,13 +268,52 @@ class Favourites(Screen): for song in self.songs: songTable.add_row(song.title, song.album, song.artist, song.playCount, song.duration, song.bitRate, key=song.id) + @on(DataTable.RowHighlighted) + def row_highlighted(self, event: DataTable.RowHighlighted) -> None: + self.selected_song_id = event.row_key.value + + def action_play(self) -> None: + song_index = self._find_selected_song() + app.play_song(self.songs[song_index]) + app.song_queue = self.songs[song_index + 1:] + + def action_queue(self) -> None: + app.queue_song(self._find_selected_song()) + + # Returns index of selected song + def _find_selected_song(self) -> int: + for i, song in enumerate(self.songs): + if song.id == self.selected_song_id: + return i + +# TODO : Maybe should just take bytes instead of saving it as a file +class CoverArtPixels(Container): + + def __init__(self, cover_art_id: str): + super().__init__() + + self.cover_art_id = cover_art_id + conn.getCoverArt(cover_art_id, path=f'art/{cover_art_id}.png') + + def render(self) -> RenderResult: + + # Make the size square + width, height = self.size + + if width > height: + width = height + elif height > width: + height = width + + return Pixels().from_image_path(f'art/{self.cover_art_id}.png', resize=(width, height)) + class MainView(Screen): CSS_PATH = 'main.tcss' def compose(self) -> ComposeResult: yield Header(icon="") with Horizontal(): - with Vertical(classes="info"): + with Vertical(classes="info", id='info'): yield Label("Playing") yield DataTable(id="currently_playing") yield ProgressBar() @@ -284,12 +332,8 @@ class MainView(Screen): currentlyPlaying.cursor_type = "row" currentlyPlaying.add_columns("", "") - @on(events.ScreenSuspend) - def sus(self, event: events.ScreenSuspend) -> None: - logging.debug('sus') - @on(events.ScreenResume) - def screen_resumed(self, event: events.ScreenResume) -> None: + async def screen_resumed(self, event: events.ScreenResume) -> None: app.sub_title = "Overview" # Update Queue Table @@ -315,6 +359,8 @@ class MainView(Screen): currentlyPlaying.add_row("Track Number", song.trackNumber) currentlyPlaying.add_row("Bit Rate", song.bitRate) + await self.query_one('#info').mount(CoverArtPixels(app.currently_playing.id)) + def _clear_table(self, table_id: str) -> None: table: DataTable = self.query_one(table_id) @@ -347,9 +393,10 @@ class TextualSubpyPlayer(App): currently_playing: SubSonic.Song | None = None song_queue: list[SubSonic.Song] = [] user_queue: list[SubSonic.Song] = [] - isPlaying: bool = False + is_paused: bool = False place: int = 0 + check_playing_timer: Timer | None = None def on_mount(self) -> None: self.title = "SubPy" @@ -359,9 +406,10 @@ class TextualSubpyPlayer(App): # logging.debug(f'Key Pressed {event.name}') pass - def play_song(self, song: SubSonic.Song) -> None: + @work(exclusive=True) + async def play_song(self, song: SubSonic.Song) -> None: self.currently_playing = song - self.isPlaying = True + self.is_paused = False logging.debug(f"Playing song {song.title} by {song.artist}.") self.notify(f"Playing \"{song.title}\" by {song.artist}.") @@ -372,17 +420,31 @@ class TextualSubpyPlayer(App): pygame.mixer.music.load(audio_data) pygame.mixer.music.play() + self.check_playing_timer = self.set_interval(1 / 5, self.check_done_playing, pause=False) + # playingLabel: Label = self.query_one(Label) # playingLabel. # Set callback loop for progress bar # self.progress_timer = self.set_interval(1 / 10, self.update_progress, pause=False) + def check_done_playing(self) -> None: + if not pygame.mixer.music.get_busy() and not self.is_paused: + queue = self.user_queue + queue.extend(self.song_queue) + + next_song = queue.pop(0) + + self.play_song(next_song) + + self.check_playing_timer.stop() + # def update_progress(self) -> None: # self.query_one(ProgressBar).advance(1) def queue_song(self, song: SubSonic.Song) -> None: logging.debug(f'Queue\'d song "{song.title}" by {song.artist}.') + self.notify(f'Queue\'d song "{song.title}" by {song.artist}.') self.user_queue.append(song) def action_play_pause_song(self) -> None: @@ -392,13 +454,13 @@ class TextualSubpyPlayer(App): if not self.currently_playing: return None - if self.isPlaying: + if not self.is_paused: self.place = pygame.mixer.music.get_pos() pygame.mixer.music.pause() - self.isPlaying = False + self.is_paused = True else: pygame.mixer.music.unpause() - self.isPlaying = True + self.is_paused = False if __name__ == '__main__': app = TextualSubpyPlayer() diff --git a/textual.log b/textual.log index c105bfb..c41e81a 100644 --- a/textual.log +++ b/textual.log @@ -1,9 +1,11 @@ -[2025-01-24 14:38:51,125] DEBUG in selector_events: Using selector: EpollSelector -[2025-01-24 14:38:51,902] DEBUG in screens-test: sus -[2025-01-24 14:38:51,930] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 -[2025-01-24 14:38:52,091] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/getPlaylists?u=pseudo&t=3b3e07bd0a56711c9db81acfb74b5560&s=2Q2MV419&v=1.16.1&c=testing&f=json HTTP/1.1" 200 705 -[2025-01-24 14:38:52,947] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 -[2025-01-24 14:38:53,121] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/getPlaylist?u=pseudo&t=174305d05353398cf608a65562db736f&s=GQLIDGEJ&v=1.16.1&c=testing&f=json&id=e6e8ff27-2a60-4e33-8aa1-09586191166a HTTP/1.1" 200 None -[2025-01-24 14:38:54,045] DEBUG in screens-test: Playing song Theia by King Gizzard & The Lizard Wizard. -[2025-01-24 14:38:54,046] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 -[2025-01-24 14:38:54,249] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/stream?u=pseudo&t=61c596042c3be872619f0f39ddcc645d&s=LAPQRCLD&v=1.16.1&c=testing&id=9e28af41e8bfcf423a5a958cd7aca5cc HTTP/1.1" 200 8314893 +[2025-01-24 17:00:13,155] DEBUG in selector_events: Using selector: EpollSelector +[2025-01-24 17:00:15,224] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-24 17:00:15,621] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/getStarred?u=pseudo&t=df47a647004f9967d54a717354d758bd&s=OEYQY6IP&v=1.16.1&c=testing&f=json HTTP/1.1" 200 None +[2025-01-24 17:00:18,614] DEBUG in screens-test: Playing song rap music by Lil Darkie. +[2025-01-24 17:00:18,615] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-24 17:00:18,843] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/stream?u=pseudo&t=17ea6e0ce1111a102699f10662381805&s=UUJ177KV&v=1.16.1&c=testing&id=62196e9d0d531adff738bf47349cbfc6 HTTP/1.1" 200 8464191 +[2025-01-24 17:00:20,392] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-24 17:00:20,628] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/getCoverArt?u=pseudo&t=36a4b74c419b83821131781619811886&s=I4SC818D&v=1.16.1&c=testing&id=62196e9d0d531adff738bf47349cbfc6 HTTP/1.1" 200 None +[2025-01-24 17:03:36,276] DEBUG in screens-test: Playing song Pegasus Pro Ultra Fusion by Dan Terminus. +[2025-01-24 17:03:36,278] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-24 17:03:36,763] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/stream?u=pseudo&t=3b4bd01e32919d4f7868d0f89e702b93&s=94XX1BQN&v=1.16.1&c=testing&id=49f56133cf2f7bc6462be7620113ee79 HTTP/1.1" 200 None