From 27e4c13b7e1fa434b2c6f1629738deeea4ab31a3 Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Tue, 16 Dec 2025 07:58:43 +0000 Subject: [PATCH 01/10] change to an album based queuing system with history and display album cover, song and album name --- .gitignore | 3 +- app.py | 140 ++++++++++++++++++++++------- gapless_player.py | 223 +++++++++++++++++++++++++--------------------- 3 files changed, 228 insertions(+), 138 deletions(-) diff --git a/.gitignore b/.gitignore index 150bb7e..1230b0d 100644 --- a/.gitignore +++ b/.gitignore @@ -175,4 +175,5 @@ cython_debug/ .pypirc music -logs \ No newline at end of file +logs +data \ No newline at end of file diff --git a/app.py b/app.py index d25357f..a569769 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ import pyray as pr import math from ctypes import c_float -from gapless_player import GaplessPlayer, build_jellyfin_audio_url, server, client +from gapless_player import GaplessPlayer, song_data_to_Song, server, client # --- Configuration Constants --- INITIAL_SCREEN_WIDTH = 800 @@ -176,34 +176,53 @@ pr.set_target_fps(TARGET_FPS) player = GaplessPlayer() print("add queue") -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], , server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("66559c40d5904944a3f97198d0297894")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f37982227942d3df031381e653ec5790")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue('music/pink floyd/dark side of the moon/01 Speak to Me.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue('music/pink floyd/dark side of the moon/02 Breathe (In the Air).flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue('music/pink floyd/dark side of the moon/03 On the Run.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue('music/pink floyd/dark side of the moon/04 Time.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue('music/pink floyd/dark side of the moon/05 The Great Gig in the Sky.flac') -player.add_to_queue('music/pink floyd/dark side of the moon/06 Money.flac') -player.add_to_queue('music/pink floyd/dark side of the moon/07 Us and Them.flac') -player.add_to_queue('music/pink floyd/dark side of the moon/08 Any Colour You Like.flac') -player.add_to_queue('music/pink floyd/dark side of the moon/09 Brain Damage.flac') -player.add_to_queue('music/pink floyd/dark side of the moon/10 Eclipse.flac') +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f")["Id"], server["AccessToken"], server["UserId"])) + +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("66559c40d5904944a3f97198d0297894")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f37982227942d3df031381e653ec5790")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c")["Id"], server["AccessToken"], server["UserId"])) + +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("66559c40d5904944a3f97198d0297894"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("f37982227942d3df031381e653ec5790"), server)) +player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c"), server)) + +# player.add_to_queue('music/pink floyd/dark side of the moon/01 Speak to Me.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue('music/pink floyd/dark side of the moon/02 Breathe (In the Air).flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue('music/pink floyd/dark side of the moon/03 On the Run.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue('music/pink floyd/dark side of the moon/04 Time.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7")["Id"], server["AccessToken"], server["UserId"])) +# player.add_to_queue('music/pink floyd/dark side of the moon/05 The Great Gig in the Sky.flac') +# player.add_to_queue('music/pink floyd/dark side of the moon/06 Money.flac') +# player.add_to_queue('music/pink floyd/dark side of the moon/07 Us and Them.flac') +# player.add_to_queue('music/pink floyd/dark side of the moon/08 Any Colour You Like.flac') +# player.add_to_queue('music/pink floyd/dark side of the moon/09 Brain Damage.flac') +# player.add_to_queue('music/pink floyd/dark side of the moon/10 Eclipse.flac') print("add queue done") # Initial setup @@ -214,6 +233,23 @@ state["camera"] = setup_3d_environment(int(render_rect.width), int(render_rect.h # LOAD THE ASSETS state["album_texture"], state["album_model"] = load_album_assets() +current_path = None +texture = None + +def load_texture(path): + global texture, current_path + + if not path: + return + + if path == current_path: + return + + if texture is not None: + pr.unload_texture(texture) + + texture = pr.load_texture(path) + current_path = path # --- Main Game Loop --- while not pr.window_should_close(): # 1. Update @@ -226,8 +262,6 @@ while not pr.window_should_close(): render_rect = get_3d_render_area(current_width, current_height) pr.unload_render_texture(state["render_texture"]) state["render_texture"] = pr.load_render_texture(int(render_rect.width), int(render_rect.height)) - - delta_time = pr.get_frame_time() if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): if player.playing: @@ -270,15 +304,53 @@ while not pr.window_should_close(): pr.draw_texture_pro(state["render_texture"].texture, source_rect, render_rect, pr.Vector2(0, 0), 0.0, pr.WHITE) - pr.draw_rectangle_lines_ex(render_rect, 3, pr.LIME) + pr.draw_rectangle_lines_ex(render_rect, 3, pr.WHITE) - draw_progress_bar(progress_rect, player.position, player.playback_info_to_duration(player.playback_info)) - - pr.draw_text(f"Status: {'Playing' if player.playing else 'Paused'} (SPACE)", - int(current_width * 0.05), int(current_height * 0.9), int(current_height * 0.03), pr.LIME) + current_song = player.get_current_song() + load_texture(current_song and current_song.album_cover_path) + + max_size = int(current_height * 0.075) + pr.draw_text((current_song and f"{current_song.name} - {current_song.artist_name}") or "", + int(current_width * 0.05+max_size*1.1), int(current_height * 0.8), int(current_height * 0.03), pr.WHITE) + + draw_progress_bar(progress_rect, player.position, player.song_to_duration(player.get_current_song())) + + + pr.draw_text(f"Status: {'Playing' if player.playing else 'Paused'} (SPACE)", + int(current_width * 0.05), int(current_height * 0.9), int(current_height * 0.03), pr.WHITE) + + + if texture is not None: + + scale = min( + max_size / texture.width, + max_size / texture.height + ) + + dest_rect = pr.Rectangle( + int(current_width * 0.05), + int(current_height * 0.8), + texture.width * scale, + texture.height * scale + ) + + src_rect = pr.Rectangle(0, 0, texture.width, texture.height) + + pr.draw_texture_pro( + texture, + src_rect, + dest_rect, + pr.Vector2(0, 0), + 0.0, + pr.WHITE + ) pr.end_drawing() +# Cleanup +if texture is not None: + pr.unload_texture(texture) + # --- De-initialization --- pr.unload_texture(state["album_texture"]) # Unload the texture pr.unload_model(state["album_model"]) # Unload the model/mesh diff --git a/gapless_player.py b/gapless_player.py index 01d00e0..0b1c371 100644 --- a/gapless_player.py +++ b/gapless_player.py @@ -18,16 +18,20 @@ import sys import io import fcntl from dataclasses import dataclass +import requests +from pathlib import Path +import mimetypes os.makedirs("logs", exist_ok=True) +os.makedirs("data", exist_ok=True) +os.makedirs("data/images", exist_ok=True) @dataclass class Song: url: str - duration: float name: str album_name:str - album_cover:str + album_cover_path:str artist_name:str @@ -36,12 +40,6 @@ class GaplessPlayer: self.samplerate = samplerate self.channels = channels - self.proc = None - self.next_proc = None - - self.current_song: Song = None - self.next_file: Song = None - self.next_preload_state = 0 self.closed=False @@ -62,15 +60,18 @@ class GaplessPlayer: callback=self._callback ) self.stream.start() + + def get_current_song(self): + if self.current_song_in_list >= 0 and self.current_song_in_list= 0 and self.current_song_in_list +1 < len(self.song_list): + return self.song_list[self.current_song_in_list +1] + return None + def load_song(self, song:Song): + if song: + song.playback_info = self.get_stream_info(song.url) + song.ffmpeg = self._open_ffmpeg(song) + song.preload_state = 2 + + def preload_next_threaded(self): + next_song = self.get_next_song() + if not next_song or next_song.preload_state: return + next_song.preload_state = 1 + threading.Thread(target=self.load_song, args=(next_song,)).start() def get_stream_info(self, url): """Return duration in seconds for the track""" try: @@ -115,64 +164,21 @@ class GaplessPlayer: stderr=subprocess.PIPE, text=True ) - return json.loads(result.stdout) except Exception as e: print("ffprobe error:", e) return None - def add_to_queue(self, song:Song): - self.current_song_in_list.append(song) - - def play(self): - with self.lock: - if not self.playing: - if not self.proc and self.current_file: - self.proc = self._open_ffmpeg(self.current_file, self.position) - self.playing = True - - def pause(self): - with self.lock: - if self.proc: - self.proc.kill() - self.proc = None - self.playing = False - - def _start_next(self): - # Kill old pipeline - if self.proc: - self.proc.kill() - - # Move next pipeline into active - self.position = 0.0 - self.proc = self.next_proc - self.current_file = self.next_file - self.playback_info = self.next_playback_info - self.next_proc=None - self.next_playback_info = None - self.next_preload_state = 0 - - def preload_next(self): - self.next_file = self.song_queue.get() - self.next_playback_info = self.get_stream_info(self.next_file) - self.next_proc = self._open_ffmpeg(self.next_file) - self.next_preload_state = 2 - - def preload_next_threaded(self): - if self.next_preload_state: return - self.next_preload_state = 1 - threading.Thread(target=self.preload_next).start() - - def playback_info_to_duration(self, info): - if info is None: return 0.0 - if "streams" in info: - for s in info["streams"]: + def song_to_duration(self, song): + if not song or not song.playback_info: return 0.0 + if "streams" in song.playback_info: + for s in song.playback_info["streams"]: if "duration" in s: return float(s["duration"]) - if "format" in info and "duration" in info["format"]: - return float(info["format"]["duration"]) + if "format" in song.playback_info and "duration" in song.playback_info["format"]: + return float(song.playback_info["format"]["duration"]) return 0.0 @@ -181,67 +187,78 @@ class GaplessPlayer: needed = frames * self.channels * 2 data = b'' if self.playing: - if self.proc is None: - if self.next_preload_state==2: - self._start_next() - elif self.next_preload_state == 0: - self.preload_next_threaded() - else: + current_song = self.get_current_song() + if not current_song or current_song.ffmpeg is None: + next_song = self.get_next_song() + if next_song: + if next_song.preload_state==2: + self._start_next() + elif next_song.preload_state == 0: + self.preload_next_threaded() + elif current_song: try: - data = self.proc.stdout.read(needed) or b'' + data = current_song.ffmpeg.stdout.read(needed) or b'' except BlockingIOError: pass self.position += len(data) / (self.samplerate * self.channels * 2) - if self.position >= self.playback_info_to_duration(self.playback_info)-10: + if self.position >= self.song_to_duration(current_song)-10: self.preload_next_threaded() - if self.proc.poll() is not None and len(data)= self.playback_info_to_duration(self.playback_info)-0.1: + if current_song.ffmpeg.poll() is not None and len(data)= self.song_to_duration(current_song)-0.1: self._start_next() - if self.proc is not None and self.proc.poll() is None: + current_song = self.get_current_song() + if current_song and current_song.ffmpeg is not None and current_song.ffmpeg.poll() is None: try: - new_data = self.proc.stdout.read(needed-len(data)) or b'' + new_data = current_song.ffmpeg.stdout.read(needed-len(data)) or b'' except BlockingIOError: new_data = b'' self.position += len(new_data) / (self.samplerate * self.channels * 2) data += new_data else: - self.proc = self._open_ffmpeg(self.current_file, self.position) + current_song.ffmpeg = self._open_ffmpeg(current_song, self.position) outdata[:len(data)] = data outdata[len(data):] = b'\x00'*(needed-len(data)) -def build_jellyfin_audio_url( - base_url: str, - item_id: str, - api_key: str, - user_id: str, - container: str = "flac", - audio_codec: str = "flac", - bitrate: int | None = None, - media_source_id: str | None = None, -) -> str: - """ - Build a Jellyfin audio stream URL using urllib.parse. - """ +def song_data_to_Song( + data, + client_data +) -> Song: + # """ + # Build a Jellyfin audio stream URL using urllib.parse. + # """ + item_id = data["Id"] path = f"/Audio/{item_id}/universal" - params = { - "UserId": user_id, - "Container": container, - "AudioCodec": audio_codec, # <-- IMPORTANT - "api_key": api_key, + "UserId": client_data["UserId"], + "Container": "flac", + "AudioCodec": "flac", # <-- IMPORTANT + "api_key": client_data["AccessToken"], } - if bitrate is not None: - params["Bitrate"] = bitrate - - if media_source_id is not None: - params["MediaSourceId"] = media_source_id - query = urlencode(params) - return urljoin(base_url, path) + "?" + query + url = urljoin(client_data["address"], path) + "?" + query + + album_cover_url = urljoin(client_data["address"], f"/Items/{data["AlbumId"]}/Images/Primary") + + r = requests.get(album_cover_url) + r.raise_for_status() + + content_type = r.headers.get("Content-Type") # e.g. "image/jpeg" + + ext = mimetypes.guess_extension(content_type) # ".jpg" + + if ext is None: + ext = ".jpg" # safe fallback for album art + + saved_path = Path("data","images", data["AlbumId"] + ext).as_posix() + + with open(saved_path, "wb") as f: + f.write(r.content) + + return Song(url,data["Name"],data["Album"], saved_path, data["AlbumArtist"]) client = JellyfinClient() From d069150c54b024442e7acd70693ead7c3fff34ae Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Fri, 19 Dec 2025 04:23:28 +0000 Subject: [PATCH 02/10] add scrolling text and change ui layout --- app.py | 475 ++++++++++++++++++++++++---------------------- gapless_player.py | 190 ++++++++++--------- scrolling_text.py | 78 ++++++++ 3 files changed, 434 insertions(+), 309 deletions(-) create mode 100644 scrolling_text.py diff --git a/app.py b/app.py index a569769..6139a97 100644 --- a/app.py +++ b/app.py @@ -1,11 +1,12 @@ import pyray as pr import math from ctypes import c_float -from gapless_player import GaplessPlayer, song_data_to_Song, server, client +from gapless_player import GaplessPlayer, song_data_to_Song, server, client, Song +from scrolling_text import ScrollingText # --- Configuration Constants --- -INITIAL_SCREEN_WIDTH = 800 -INITIAL_SCREEN_HEIGHT = 600 +INITIAL_SCREEN_WIDTH = 240 +INITIAL_SCREEN_HEIGHT = 240 TARGET_FPS = 60 # --- State Variables --- @@ -16,15 +17,16 @@ state = { "total_time": 300.0, "is_playing": True, # 3D Camera State - "camera": None, + "camera": None, "render_texture": None, # Assets "album_texture": None, - "album_model": None + "album_model": None, } # --- Utility Functions --- + def format_time_mm_ss(seconds): """Converts a time in seconds to an 'MM:SS' string format.""" seconds = int(seconds) @@ -32,35 +34,15 @@ def format_time_mm_ss(seconds): seconds_remainder = seconds % 60 return f"{minutes:02d}:{seconds_remainder:02d}" -# --- Dynamic Layout Functions --- - -def get_3d_render_area(screen_width, screen_height): - ASPECT_WIDTH = 2.0 - ASPECT_HEIGHT = 1.0 - ASPECT_RATIO = ASPECT_WIDTH / ASPECT_HEIGHT - - max_available_width = screen_width * 0.7 - max_available_height = screen_height * 0.5 - - if (max_available_width / max_available_height) > ASPECT_RATIO: - height = max_available_height - width = height * ASPECT_RATIO - else: - width = max_available_width - height = width / ASPECT_RATIO - - x = (screen_width - width) / 2 - y = screen_height * 0.15 - - return pr.Rectangle(x, y, width, height) def get_progress_bar_rect(screen_width, screen_height): - width = screen_width * 0.7 - height = screen_height * 0.03 + width = screen_width + height = 5 x = (screen_width - width) / 2 - y = screen_height * 0.75 + y = screen_height - height return pr.Rectangle(x, y, width, height) + def draw_progress_bar(rect, current_time, total_time): if total_time > 0: progress_ratio = current_time / total_time @@ -69,107 +51,29 @@ def draw_progress_bar(rect, current_time, total_time): pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255)) progress_width = rect.width * progress_ratio - pr.draw_rectangle(int(rect.x), int(rect.y), int(progress_width), int(rect.height), pr.Color(200, 50, 50, 255)) - pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) - - time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" + pr.draw_rectangle( + int(rect.x), + int(rect.y), + int(progress_width), + int(rect.height), + pr.Color(200, 50, 50, 255), + ) + # pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) + + time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" text_width = pr.measure_text(time_text, int(rect.height * 0.7)) - pr.draw_text(time_text, - int(rect.x + rect.width / 2 - text_width / 2), - int(rect.y + rect.height * 0.15), - int(rect.height * 0.7), - pr.WHITE) + # pr.draw_text( + # time_text, + # int(rect.x + rect.width / 2 - text_width / 2), + # int(rect.y + rect.height * 0.15), + # int(rect.height * 0.7), + # pr.WHITE, + # ) -# --- ASSET MANAGEMENT --- - -def load_album_assets(): - """Loads the texture, creates the 3D model, and applies the flipped texture.""" - - # 1. Load Image - try: - image = pr.load_image("music/albumcover.png") - except: - print("WARNING: 'music/albumcover.png' not found. Using placeholder.") - image = pr.gen_image_checked(512, 512, 32, 32, pr.DARKGRAY, pr.WHITE) - - # --- THE FIX: FLIP THE IMAGE VERTICALLY --- - pr.image_flip_vertical(image) - - # 2. Create Texture - texture = pr.load_texture_from_image(image) - pr.unload_image(image) - - # 3. Generate Mesh (CD Case) - mesh = pr.gen_mesh_cube(1.5, 1.5, 0.0) - - # 4. Load Model - model = pr.load_model_from_mesh(mesh) - - # 5. Apply Texture - # We use index 0 for the Albedo/Diffuse map - map_index = 0 - # Use MATERIAL_MAP_ALBEDO if the binding is modern enough - if hasattr(pr.MaterialMapIndex, 'MATERIAL_MAP_ALBEDO'): - map_index = pr.MaterialMapIndex.MATERIAL_MAP_ALBEDO - - model.materials[0].maps[map_index].texture = texture - - return texture, model - -# --- CORE 3D RENDERING --- - -def setup_3d_environment(render_width, render_height): - camera = pr.Camera3D() - camera.position = pr.Vector3(0.0, -0.35, 4.0) # Moved back slightly to fit the new models - camera.target = pr.Vector3(0.0, 0.0, 0.0) - camera.up = pr.Vector3(0.0, 1.0, 0.0) - camera.fovy = 45.0 - camera.projection = pr.CameraProjection.CAMERA_PERSPECTIVE - return camera - -def draw_3d_cover_flow(camera, model): - """ - Draws the textured model using the existing Matrix logic. - """ - pr.begin_mode_3d(camera) - - # We use pr.WHITE as the tint so the texture shows its original colors. - # If you use pr.RED, the album cover will look red-tinted. - - # -------------------------------------------------------- - # 2. CURRENT ALBUM (Center) - # -------------------------------------------------------- - # Draw model at (0,0,0) with 1.0 scale - pr.rl_push_matrix() - pr.rl_translatef(0.0, -0.0, 1.5) # Spaced out slightly more - pr.rl_rotatef(5.0, 1.0, 0.0, 0.0) # Sharper angle - pr.draw_model(model, pr.Vector3(0.0, 0.0, 0.0), 1.0, pr.WHITE) - pr.rl_pop_matrix() - - - for i in range(-5, 0): - pr.rl_push_matrix() - pr.rl_translatef(-1.5+0.15*i, 0.0, 0.5) # Added slight Z offset for depth - pr.rl_rotatef(50.0, 0.0, 1.0, 0.0) - pr.draw_model(model, pr.Vector3(0.0, 0.0, 0.0), 1.0, pr.WHITE) - pr.rl_pop_matrix() - - - - for i in range(1,6): - pr.rl_push_matrix() - pr.rl_translatef(1.5+0.15*i, 0.0, 0.5) - pr.rl_rotatef(-50.0, 0.0, 1.0, 0.0) - pr.draw_model(model, pr.Vector3(0.0, 0.0, 0.0), 1.0, pr.WHITE) - pr.rl_pop_matrix() - - pr.end_mode_3d() - -# --- Main Setup and Loop --- # Initialization pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) -pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) +# pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) pr.init_window(state["screen_width"], state["screen_height"], "UgPod") pr.set_target_fps(TARGET_FPS) @@ -196,22 +100,118 @@ print("add queue") # player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f37982227942d3df031381e653ec5790")["Id"], server["AccessToken"], server["UserId"])) # player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c")["Id"], server["AccessToken"], server["UserId"])) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("66559c40d5904944a3f97198d0297894"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("f37982227942d3df031381e653ec5790"), server)) -player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c"), server)) +print(client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376")) +# player.add_to_queue( +# Song( +# "music/pink floyd/dark side of the moon/01 Speak to Me.flac", +# "Speak to Me", +# "The Dark Side Of The Moon", +# "music/albumcover.png", +# "Pink Floyd", +# ) +# ) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7"), server + ) +) + + +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("66559c40d5904944a3f97198d0297894"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("f37982227942d3df031381e653ec5790"), server + ) +) +player.add_to_queue( + song_data_to_Song( + client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c"), server + ) +) # player.add_to_queue('music/pink floyd/dark side of the moon/01 Speak to Me.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f")["Id"], server["AccessToken"], server["UserId"])) # player.add_to_queue('music/pink floyd/dark side of the moon/02 Breathe (In the Air).flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60")["Id"], server["AccessToken"], server["UserId"])) @@ -225,17 +225,10 @@ player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("0e8fc5fcf119de04 # player.add_to_queue('music/pink floyd/dark side of the moon/10 Eclipse.flac') print("add queue done") -# Initial setup -render_rect = get_3d_render_area(state["screen_width"], state["screen_height"]) -state["render_texture"] = pr.load_render_texture(int(render_rect.width), int(render_rect.height)) -state["camera"] = setup_3d_environment(int(render_rect.width), int(render_rect.height)) - -# LOAD THE ASSETS -state["album_texture"], state["album_model"] = load_album_assets() - current_path = None texture = None + def load_texture(path): global texture, current_path @@ -250,109 +243,145 @@ def load_texture(path): texture = pr.load_texture(path) current_path = path + + +def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool: + clicked = False + + rect = pr.Rectangle(pos.x, pos.y, size.x, size.y) + + # Optional hover background + if pr.check_collision_point_rec(pr.get_mouse_position(), rect): + pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) + if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): + clicked = True + + cx = pos.x + size.x / 2 + cy = pos.y + size.y / 2 + + icon_padding = size.x * 0.25 + icon_size = size.x - icon_padding * 2 + + if is_playing: + # PAUSE (two bars centered, same visual weight as play) + bar_width = icon_size * 0.25 + bar_height = icon_size + + left_x = cx - bar_width - bar_width * 0.4 + right_x = cx + bar_width * 0.4 + top_y = cy - bar_height / 2 + + pr.draw_rectangle( + int(left_x), + int(top_y), + int(bar_width), + int(bar_height), + pr.WHITE, + ) + pr.draw_rectangle( + int(right_x), + int(top_y), + int(bar_width), + int(bar_height), + pr.WHITE, + ) + else: + # PLAY (centered triangle) + p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2) + p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2) + p3 = pr.Vector2(cx + icon_size / 2, cy) + + pr.draw_triangle(p1, p2, p3, pr.WHITE) + + return clicked + + +title = ScrollingText( + text="Aphex Twin - Xtal", + font_size=15, + speed=35, + pause_time=1.2, +) + # --- Main Game Loop --- while not pr.window_should_close(): # 1. Update current_width = pr.get_screen_width() current_height = pr.get_screen_height() - - if pr.is_window_resized(): - state["screen_width"] = current_width - state["screen_height"] = current_height - render_rect = get_3d_render_area(current_width, current_height) - pr.unload_render_texture(state["render_texture"]) - state["render_texture"] = pr.load_render_texture(int(render_rect.width), int(render_rect.height)) - + if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): if player.playing: player.pause() else: player.play() - if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT): - player.seek(player.position-5) + player.seek(player.position - 5) if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT): - player.seek(player.position+5) - - - # ---------------------------------------------------- - # 2. DRAW 3D SCENE - # ---------------------------------------------------- - render_rect = get_3d_render_area(current_width, current_height) - - pr.begin_texture_mode(state["render_texture"]) - pr.clear_background(pr.Color(20, 20, 20, 255)) - - # Pass the loaded model to the draw function - draw_3d_cover_flow(state["camera"], state["album_model"]) + player.seek(player.position + 5) - pr.end_texture_mode() - - # ---------------------------------------------------- - # 3. DRAW 2D GUI - # ---------------------------------------------------- pr.begin_drawing() pr.clear_background(pr.Color(40, 40, 40, 255)) + dt = pr.get_frame_time() progress_rect = get_progress_bar_rect(current_width, current_height) - - title_size = int(current_height * 0.05) - pr.draw_text("UgPod", int(current_width * 0.05), int(current_height * 0.05), title_size, pr.SKYBLUE) - - source_rect = pr.Rectangle(0, 0, state["render_texture"].texture.width, -state["render_texture"].texture.height) - - pr.draw_texture_pro(state["render_texture"].texture, - source_rect, render_rect, pr.Vector2(0, 0), 0.0, pr.WHITE) - pr.draw_rectangle_lines_ex(render_rect, 3, pr.WHITE) - + pr.draw_text( + "UgPod", + int(current_width * 0.05), + int(current_height * 0.05), + int(current_height * 0.05), + pr.SKYBLUE, + ) + current_song = player.get_current_song() - load_texture(current_song and current_song.album_cover_path) - max_size = int(current_height * 0.075) - pr.draw_text((current_song and f"{current_song.name} - {current_song.artist_name}") or "", - int(current_width * 0.05+max_size*1.1), int(current_height * 0.8), int(current_height * 0.03), pr.WHITE) - draw_progress_bar(progress_rect, player.position, player.song_to_duration(player.get_current_song())) - - - pr.draw_text(f"Status: {'Playing' if player.playing else 'Paused'} (SPACE)", - int(current_width * 0.05), int(current_height * 0.9), int(current_height * 0.03), pr.WHITE) - - - if texture is not None: - - scale = min( - max_size / texture.width, - max_size / texture.height + if current_song: + load_texture(current_song.album_cover_path) + title_size = pr.Vector2(current_width-int(current_width * 0.05 + max_size * 1.1), 30) + title.update(dt,title_size) + title.set_text(f"{current_song.name} - {current_song.artist_name}") + title.draw(pr.Vector2(int(current_width * 0.05 + max_size * 1.1),int(current_height * 0.8)),title_size) + # pr.draw_text( + # , + # , + # int(current_height * 0.03), + # pr.WHITE, + # ) + draw_progress_bar( + progress_rect, + player.position, + current_song.duration, ) + if texture is not None: + scale = min(max_size / texture.width, max_size / texture.height) - dest_rect = pr.Rectangle( - int(current_width * 0.05), - int(current_height * 0.8), - texture.width * scale, - texture.height * scale - ) + dest_rect = pr.Rectangle( + int(current_width * 0.05), + int(current_height * 0.8), + texture.width * scale, + texture.height * scale, + ) - src_rect = pr.Rectangle(0, 0, texture.width, texture.height) + src_rect = pr.Rectangle(0, 0, texture.width, texture.height) - pr.draw_texture_pro( - texture, - src_rect, - dest_rect, - pr.Vector2(0, 0), - 0.0, - pr.WHITE - ) + pr.draw_texture_pro( + texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE + ) + + pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) + size = pr.Vector2(current_height * 0.1, current_height * 0.1) + + if draw_play_pause_button(pos, size, player.playing): + if player.playing: + player.pause() + else: + player.play() pr.end_drawing() # Cleanup if texture is not None: pr.unload_texture(texture) -# --- De-initialization --- -pr.unload_texture(state["album_texture"]) # Unload the texture -pr.unload_model(state["album_model"]) # Unload the model/mesh -pr.unload_render_texture(state["render_texture"]) -pr.close_window() \ No newline at end of file +pr.close_window() diff --git a/gapless_player.py b/gapless_player.py index 0b1c371..3143d40 100644 --- a/gapless_player.py +++ b/gapless_player.py @@ -26,23 +26,25 @@ os.makedirs("logs", exist_ok=True) os.makedirs("data", exist_ok=True) os.makedirs("data/images", exist_ok=True) + @dataclass class Song: url: str name: str - album_name:str - album_cover_path:str - artist_name:str + duration: float + album_name: str + album_cover_path: str + artist_name: str class GaplessPlayer: - def __init__(self, samplerate:int=44100, channels:int=2): + def __init__(self, samplerate: int = 44100, channels: int = 2): self.samplerate = samplerate self.channels = channels self.next_preload_state = 0 - self.closed=False + self.closed = False self.playing = False self.position = 0.0 @@ -57,12 +59,14 @@ class GaplessPlayer: samplerate=self.samplerate, channels=self.channels, dtype="int16", - callback=self._callback + callback=self._callback, ) self.stream.start() - + def get_current_song(self): - if self.current_song_in_list >= 0 and self.current_song_in_list= 0 and self.current_song_in_list < len( + self.song_list + ): return self.song_list[self.current_song_in_list] def _open_ffmpeg(self, song, seek=0): @@ -70,16 +74,22 @@ class GaplessPlayer: [ "ffmpeg", # "-re", - "-ss", str(seek), - "-i", song.url, - "-f", "s16le", - "-ac", str(self.channels), - "-ar", str(self.samplerate), - "-loglevel", "verbose", - "-" + "-ss", + str(seek), + "-i", + song.url, + "-f", + "s16le", + "-ac", + str(self.channels), + "-ar", + str(self.samplerate), + "-loglevel", + "verbose", + "-", ], stdout=subprocess.PIPE, - stderr=subprocess.PIPE + stderr=subprocess.PIPE, ) # --- make stdout non-blocking --- @@ -88,23 +98,25 @@ class GaplessPlayer: fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) return proc - + def seek(self, pos): with self.lock: song = self.get_current_song() if song: - pos = min(max(0,pos), self.song_to_duration(song)) + pos = min(max(0, pos), song.duration) if song.ffmpeg: song.ffmpeg.kill() - song.ffmpeg = self._open_ffmpeg(song, pos) + song.ffmpeg = None + if self.playing: + song.ffmpeg = self._open_ffmpeg(song, pos) self.position = pos - + def close(self): - self.closed=True + self.closed = True self.stream.close() - def add_to_queue(self, song:Song): + + def add_to_queue(self, song: Song): song.ffmpeg = None - song.playback_info = None song.preload_state = 0 self.song_list.append(song) @@ -129,103 +141,101 @@ class GaplessPlayer: current_song = self.get_current_song() if current_song and current_song.ffmpeg: current_song.ffmpeg.kill() - + current_song.ffmpeg = None + # Move next pipeline into active self.position = 0.0 - self.current_song_in_list+=1 + self.current_song_in_list += 1 + def get_next_song(self): - if self.current_song_in_list+1 >= 0 and self.current_song_in_list +1 < len(self.song_list): - return self.song_list[self.current_song_in_list +1] + if self.current_song_in_list + 1 >= 0 and self.current_song_in_list + 1 < len( + self.song_list + ): + return self.song_list[self.current_song_in_list + 1] return None - def load_song(self, song:Song): + + def forward_song(self): + current_song = self.get_current_song() + if current_song and current_song.ffmpeg: + current_song.ffmpeg.kill() + current_song.ffmpeg = None + if self.current_song_in_list < len( + self.song_list + ): + self.current_song_in_list += 1 + + + def load_song(self, song: Song): if song: - song.playback_info = self.get_stream_info(song.url) song.ffmpeg = self._open_ffmpeg(song) song.preload_state = 2 def preload_next_threaded(self): next_song = self.get_next_song() - if not next_song or next_song.preload_state: return + if not next_song or next_song.preload_state: + return next_song.preload_state = 1 threading.Thread(target=self.load_song, args=(next_song,)).start() - def get_stream_info(self, url): - """Return duration in seconds for the track""" - try: - result = subprocess.run( - [ - "ffprobe", - "-v", "quiet", - "-print_format", "json", - "-show_format", - "-show_streams", - url - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - return json.loads(result.stdout) - - except Exception as e: - print("ffprobe error:", e) return None - def song_to_duration(self, song): - if not song or not song.playback_info: return 0.0 - if "streams" in song.playback_info: - for s in song.playback_info["streams"]: - if "duration" in s: - return float(s["duration"]) - - if "format" in song.playback_info and "duration" in song.playback_info["format"]: - return float(song.playback_info["format"]["duration"]) - - return 0.0 def _callback(self, outdata, frames, t, status): with self.lock: needed = frames * self.channels * 2 - data = b'' + data = b"" if self.playing: current_song = self.get_current_song() if not current_song or current_song.ffmpeg is None: next_song = self.get_next_song() if next_song: - if next_song.preload_state==2: + if next_song.preload_state == 2: self._start_next() elif next_song.preload_state == 0: self.preload_next_threaded() elif current_song: try: - data = current_song.ffmpeg.stdout.read(needed) or b'' + data = current_song.ffmpeg.stdout.read(needed) or b"" except BlockingIOError: pass self.position += len(data) / (self.samplerate * self.channels * 2) - if self.position >= self.song_to_duration(current_song)-10: + if self.position >= current_song.duration - 10: self.preload_next_threaded() - if current_song.ffmpeg.poll() is not None and len(data)= self.song_to_duration(current_song)-0.1: + if current_song.ffmpeg.poll() is not None and len(data) < needed: + if round(self.position, 2) >= current_song.duration - 0.1: self._start_next() current_song = self.get_current_song() - if current_song and current_song.ffmpeg is not None and current_song.ffmpeg.poll() is None: + if ( + current_song + and current_song.ffmpeg is not None + and current_song.ffmpeg.poll() is None + ): try: - new_data = current_song.ffmpeg.stdout.read(needed-len(data)) or b'' + new_data = ( + current_song.ffmpeg.stdout.read( + needed - len(data) + ) + or b"" + ) except BlockingIOError: - new_data = b'' - self.position += len(new_data) / (self.samplerate * self.channels * 2) + new_data = b"" + self.position += len(new_data) / ( + self.samplerate * self.channels * 2 + ) data += new_data else: - current_song.ffmpeg = self._open_ffmpeg(current_song, self.position) + current_song.ffmpeg = self._open_ffmpeg( + current_song, self.position + ) - outdata[:len(data)] = data + outdata[: len(data)] = data - outdata[len(data):] = b'\x00'*(needed-len(data)) + outdata[len(data) :] = b"\x00" * (needed - len(data)) -def song_data_to_Song( - data, - client_data -) -> Song: +album_covers = [] + + +def song_data_to_Song(data, client_data) -> Song: # """ # Build a Jellyfin audio stream URL using urllib.parse. # """ @@ -234,14 +244,16 @@ def song_data_to_Song( params = { "UserId": client_data["UserId"], "Container": "flac", - "AudioCodec": "flac", # <-- IMPORTANT + "AudioCodec": "flac", # <-- IMPORTANT "api_key": client_data["AccessToken"], } query = urlencode(params) url = urljoin(client_data["address"], path) + "?" + query - - album_cover_url = urljoin(client_data["address"], f"/Items/{data["AlbumId"]}/Images/Primary") + + album_cover_url = urljoin( + client_data["address"], f"/Items/{data["AlbumId"]}/Images/Primary" + ) r = requests.get(album_cover_url) r.raise_for_status() @@ -253,18 +265,25 @@ def song_data_to_Song( if ext is None: ext = ".jpg" # safe fallback for album art - saved_path = Path("data","images", data["AlbumId"] + ext).as_posix() + saved_path = Path("data", "images", data["AlbumId"] + ext).as_posix() with open(saved_path, "wb") as f: f.write(r.content) - return Song(url,data["Name"],data["Album"], saved_path, data["AlbumArtist"]) + return Song( + url, + data["Name"], + data["RunTimeTicks"] / 10_000_000, + data["Album"], + saved_path, + data["AlbumArtist"], + ) client = JellyfinClient() load_dotenv() -client.config.app('UgPod', '0.0.1', 'UgPod prototype', 'UgPod_prototype_1') +client.config.app("UgPod", "0.0.1", "UgPod prototype", "UgPod_prototype_1") client.config.data["auth.ssl"] = True client.auth.connect_to_address(os.getenv("host")) @@ -275,8 +294,7 @@ server = credentials["Servers"][0] print(json.dumps(server)) - # while True: # duration = player.playback_info_to_duration(player.playback_info) # print("pos:", str(round((player.position*100)/(duration or 1.0)))+"%", player.position, '/', duration) -# time.sleep(1) \ No newline at end of file +# time.sleep(1) diff --git a/scrolling_text.py b/scrolling_text.py new file mode 100644 index 0000000..81e94e1 --- /dev/null +++ b/scrolling_text.py @@ -0,0 +1,78 @@ +import pyray as pr + + +class ScrollingText: + def __init__( + self, + text: str, + font_size: int, + speed: float = 40.0, # pixels per second + pause_time: float = 1.0, # seconds before scrolling + color=pr.WHITE, + ): + self.font_size = font_size + self.speed = speed + self.pause_time = pause_time + self.color = color + self.text = None + self.set_text(text) + + + def set_text(self, text: str): + if text == self.text: + return + self.text = text + self.text_width = pr.measure_text(self.text, self.font_size) + self.reset() + + def reset(self): + self.offset = 0.0 + self.timer = 0.0 + self.scrolling = False + + def update(self, dt: float, size: pr.Vector2): + if self.text_width <= size.x: + return + + self.timer += dt + + if not self.scrolling: + if self.timer >= self.pause_time: + self.scrolling = True + self.timer = 0.0 + else: + self.offset += self.speed * dt + + if self.offset >= self.text_width + 20: + self.reset() + + def draw(self, pos: pr.Vector2, size: pr.Vector2): + clip = pr.Rectangle(pos.x, pos.y, size.x, size.y) + pr.begin_scissor_mode( + int(clip.x), + int(clip.y), + int(clip.width), + int(clip.height), + ) + + y = pos.y + (size.y - self.font_size) / 2 + + pr.draw_text( + self.text, + int(pos.x - self.offset), + int(y), + self.font_size, + self.color, + ) + + # Second copy for seamless loop + if self.text_width > size.x: + pr.draw_text( + self.text, + int(pos.x - self.offset + self.text_width + 20), + int(y), + self.font_size, + self.color, + ) + + pr.end_scissor_mode() From d86587e526549ad723fb1078b7448d5449cf315c Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:47:30 +0000 Subject: [PATCH 03/10] change the layout and fix f string bug on raspberry pi python 3 --- app.py | 392 ++++++++++++++++++++++++++-------------------- gapless_player.py | 171 +++++++++++--------- jelly.py | 41 +++++ requirements.txt | 4 +- run-fb.sh | 2 + scrolling_text.py | 14 +- 6 files changed, 371 insertions(+), 253 deletions(-) create mode 100644 jelly.py create mode 100755 run-fb.sh diff --git a/app.py b/app.py index 6139a97..cfbfeb0 100644 --- a/app.py +++ b/app.py @@ -1,27 +1,23 @@ import pyray as pr import math from ctypes import c_float -from gapless_player import GaplessPlayer, song_data_to_Song, server, client, Song +from gapless_player import GaplessPlayer, Song, song_data_to_Song from scrolling_text import ScrollingText +import numpy as np +import threading +import time +import os +from jelly import server, client # --- Configuration Constants --- INITIAL_SCREEN_WIDTH = 240 INITIAL_SCREEN_HEIGHT = 240 -TARGET_FPS = 60 +TARGET_FPS =60 # --- State Variables --- state = { "screen_width": INITIAL_SCREEN_WIDTH, "screen_height": INITIAL_SCREEN_HEIGHT, - "current_time": 120.0, - "total_time": 300.0, - "is_playing": True, - # 3D Camera State - "camera": None, - "render_texture": None, - # Assets - "album_texture": None, - "album_model": None, } # --- Utility Functions --- @@ -37,7 +33,7 @@ def format_time_mm_ss(seconds): def get_progress_bar_rect(screen_width, screen_height): width = screen_width - height = 5 + height = screen_height*0.021 x = (screen_width - width) / 2 y = screen_height - height return pr.Rectangle(x, y, width, height) @@ -53,7 +49,7 @@ def draw_progress_bar(rect, current_time, total_time): progress_width = rect.width * progress_ratio pr.draw_rectangle( int(rect.x), - int(rect.y), + int(rect.y)+1, int(progress_width), int(rect.height), pr.Color(200, 50, 50, 255), @@ -70,10 +66,9 @@ def draw_progress_bar(rect, current_time, total_time): # pr.WHITE, # ) - -# Initialization pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) -# pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) +pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) +#pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) pr.init_window(state["screen_width"], state["screen_height"], "UgPod") pr.set_target_fps(TARGET_FPS) @@ -81,149 +76,54 @@ player = GaplessPlayer() print("add queue") -# player.add_to_queue(build_jellyfin_audio_url(server["address"], , server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8")["Id"], server["AccessToken"], server["UserId"])) - -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f")["Id"], server["AccessToken"], server["UserId"])) - -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("66559c40d5904944a3f97198d0297894")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f37982227942d3df031381e653ec5790")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c")["Id"], server["AccessToken"], server["UserId"])) - -print(client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376")) # player.add_to_queue( # Song( -# "music/pink floyd/dark side of the moon/01 Speak to Me.flac", -# "Speak to Me", -# "The Dark Side Of The Moon", -# "music/albumcover.png", -# "Pink Floyd", +# "/mnt/HDD/Downloads/72 Pantera.wav", +# "Bullseye", +# 1, +# "KDrew", +# "", +# "KDrew", # ) # ) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7"), server - ) + +albums = client.jellyfin.user_items( + params={ + "IncludeItemTypes": "MusicAlbum", + "SearchTerm": "Dawn FM", # album name + "Recursive": True, + }, ) +album = albums["Items"][0] # pick the album you want +album_id = album["Id"] -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("66559c40d5904944a3f97198d0297894"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("f37982227942d3df031381e653ec5790"), server - ) -) -player.add_to_queue( - song_data_to_Song( - client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c"), server - ) + +tracks = client.jellyfin.user_items( + params={ + "ParentId": album_id, + "IncludeItemTypes": "Audio", + "SortBy": "IndexNumber", + "SortOrder": "Ascending", + }, ) -# player.add_to_queue('music/pink floyd/dark side of the moon/01 Speak to Me.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue('music/pink floyd/dark side of the moon/02 Breathe (In the Air).flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue('music/pink floyd/dark side of the moon/03 On the Run.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue('music/pink floyd/dark side of the moon/04 Time.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7")["Id"], server["AccessToken"], server["UserId"])) -# player.add_to_queue('music/pink floyd/dark side of the moon/05 The Great Gig in the Sky.flac') -# player.add_to_queue('music/pink floyd/dark side of the moon/06 Money.flac') -# player.add_to_queue('music/pink floyd/dark side of the moon/07 Us and Them.flac') -# player.add_to_queue('music/pink floyd/dark side of the moon/08 Any Colour You Like.flac') -# player.add_to_queue('music/pink floyd/dark side of the moon/09 Brain Damage.flac') -# player.add_to_queue('music/pink floyd/dark side of the moon/10 Eclipse.flac') +for track in tracks["Items"]: + player.add_to_queue( + song_data_to_Song( + track, server + ) + ) + print("add queue done") +player.load_state("data/player.json") +close_event = threading.Event() +def save_state_loop(): + while not close_event.wait(10): + player.save_state("data/player.lock.json") + os.rename("data/player.lock.json", "data/player.json") +# save_state_thread = threading.Thread(target=save_state_loop) +# save_state_thread.start() current_path = None texture = None @@ -255,7 +155,6 @@ def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): clicked = True - cx = pos.x + size.x / 2 cy = pos.y + size.y / 2 @@ -269,7 +168,7 @@ def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) left_x = cx - bar_width - bar_width * 0.4 right_x = cx + bar_width * 0.4 - top_y = cy - bar_height / 2 + top_y = 1+cy - bar_height / 2 pr.draw_rectangle( int(left_x), @@ -297,10 +196,8 @@ def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) title = ScrollingText( - text="Aphex Twin - Xtal", - font_size=15, - speed=35, - pause_time=1.2, + "", + 15 ) # --- Main Game Loop --- @@ -309,6 +206,8 @@ while not pr.window_should_close(): current_width = pr.get_screen_width() current_height = pr.get_screen_height() + if pr.is_key_pressed(pr.KEY_F11): + pr.toggle_fullscreen() if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): if player.playing: player.pause() @@ -325,41 +224,44 @@ while not pr.window_should_close(): progress_rect = get_progress_bar_rect(current_width, current_height) - pr.draw_text( - "UgPod", - int(current_width * 0.05), - int(current_height * 0.05), - int(current_height * 0.05), - pr.SKYBLUE, - ) + # pr.draw_text( + # "UgPod", + # int(current_width * 0.05), + # int(current_height * 0.05), + # int(current_height * 0.05), + # pr.SKYBLUE, + # ) current_song = player.get_current_song() - max_size = int(current_height * 0.075) + draw_progress_bar( + progress_rect, + player.position, + (current_song and current_song.duration) or 0.0, + ) if current_song: load_texture(current_song.album_cover_path) - title_size = pr.Vector2(current_width-int(current_width * 0.05 + max_size * 1.1), 30) + title_font_size = int(current_height*0.05) + album_cover_size = int(min(current_width, current_height*0.7)) + title.speed = title_font_size*2.5 + title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size) title.update(dt,title_size) - title.set_text(f"{current_song.name} - {current_song.artist_name}") - title.draw(pr.Vector2(int(current_width * 0.05 + max_size * 1.1),int(current_height * 0.8)),title_size) + title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size) + title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size) # pr.draw_text( # , # , # int(current_height * 0.03), # pr.WHITE, # ) - draw_progress_bar( - progress_rect, - player.position, - current_song.duration, - ) + points = player.oscilloscope_data_points if texture is not None: - scale = min(max_size / texture.width, max_size / texture.height) + scale = min(album_cover_size / texture.width, album_cover_size / texture.height) dest_rect = pr.Rectangle( - int(current_width * 0.05), - int(current_height * 0.8), + current_width//2 - album_cover_size//2, + (current_height*0.8)//2 - album_cover_size//2, texture.width * scale, texture.height * scale, ) @@ -369,6 +271,146 @@ while not pr.window_should_close(): pr.draw_texture_pro( texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE ) + else: + clip = pr.Rectangle(int(current_width//2 - album_cover_size//2), + int((current_height*0.8)//2 - album_cover_size//2), + int(album_cover_size), + int(album_cover_size)) + pr.begin_scissor_mode( + int(clip.x), + int(clip.y), + int(clip.width), + int(clip.height), + ) + pr.draw_rectangle( + int(clip.x), + int(clip.y), + int(clip.width), + int(clip.height), pr.BLACK) + + # cx = current_width * 0.5+1 + # cy = current_height * 0.4+1 + + # MAX_LEN = album_cover_size * 0.25 # tune this + # MIN_ALPHA = 10 + # MAX_ALPHA = 255 + + # for i in range(len(points) - 1): + # x1 = cx + points[i][0] * album_cover_size * 0.5 + # y1 = cy + -points[i][1] * album_cover_size * 0.5 + # x2 = cx + points[i+1][0] * album_cover_size * 0.5 + # y2 = cy + -points[i+1][1] * album_cover_size * 0.5 + + # dx = x2 - x1 + # dy = y2 - y1 + # length = (dx * dx + dy * dy) ** 0.5 + + # # 1.0 = short line, 0.0 = long line + # t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2) + + # alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA)) + + # color = pr.Color(255, 255, 255, alpha) + + # pr.draw_line(int(x1), int(y1), int(x2), int(y2), color) + # draw background square + if len(points) >= 2: + samples = np.fromiter( + ((p[0] + p[1]) * 0.5 for p in points), + dtype=np.float32 + ) + + # Guard: FFT must have meaningful size + if samples.size > 128: + + + rect_x = int(current_width // 2 - album_cover_size // 2) + rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2) + + # ---- FFT ---- + + FFT_SIZE = min(samples.size, 2048) + window = np.hanning(FFT_SIZE) + + fft = np.fft.rfft(samples[:FFT_SIZE] * window) + magnitudes = np.abs(fft) + + # remove DC component (important for visuals) + magnitudes[0] = 0.0 + + # ---- LOG BINNING ---- + + num_bars = album_cover_size//10 + num_bins = magnitudes.size + + # logarithmic bin edges (low end stretched) + log_min = 1 + log_max = math.log10(num_bins) + + log_edges = np.logspace( + math.log10(log_min), + log_max, + num_bars + 1 + ).astype(int) + + bar_values = np.zeros(num_bars, dtype=np.float32) + + for i in range(num_bars): + start = log_edges[i] + end = log_edges[i + 1] + + if end <= start: + continue + + bar_values[i] = np.mean(magnitudes[start:end]) + +# ---- STATIC SCALING ---- + + # Instead of normalizing to the max of the frame, we scale by the FFT size. + # For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps + # maximum possible volume roughly to 1.0. + bar_values = bar_values / (FFT_SIZE / 4.0) + + # ---- DRAW ---- + + def map_to_screen(val): + return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size + + spacing = 0 + + for i in range(num_bars): + # 1. Calculate integer pixel boundaries first + # This ensures the right edge of one bar is exactly the left edge of the next + x_start_int = int(map_to_screen(log_edges[i])) + x_end_int = int(map_to_screen(log_edges[i+1])) + + # 2. Width is the difference between these fixed integer points + w = (x_end_int - x_start_int) - spacing + + value = bar_values[i] + h = int(min(1.0, value) * album_cover_size) + + # 3. Anchor to bottom + y = (rect_y + album_cover_size) - h + + alpha = min(1.0, ((value+1)**2)-1) + r = 255 + g = 0 + b = 0 + + # Keep alpha at 255 (fully opaque) + color = pr.Color(r, g, b, int(255 * alpha)) + + # 4. Draw the bar + # Use max(1, w) to ensure high-frequency bars don't disappear on small screens + pr.draw_rectangle( + x_start_int, + int(y), + max(1, int(w)), + h, + color + ) + pr.end_scissor_mode() pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) size = pr.Vector2(current_height * 0.1, current_height * 0.1) @@ -385,3 +427,5 @@ if texture is not None: pr.unload_texture(texture) pr.close_window() +close_event.set() +# save_state_thread.join() \ No newline at end of file diff --git a/gapless_player.py b/gapless_player.py index 3143d40..065badb 100644 --- a/gapless_player.py +++ b/gapless_player.py @@ -1,14 +1,11 @@ -from jellyfin_apiclient_python import JellyfinClient import json import uuid import subprocess -from dotenv import load_dotenv import os import time import ffmpeg import requests import threading -from urllib.parse import urlencode, urljoin import subprocess import numpy as np import sounddevice as sd @@ -18,17 +15,67 @@ import sys import io import fcntl from dataclasses import dataclass +import numpy as np +from collections import deque +from jelly import server, client +from urllib.parse import urlencode, urljoin import requests from pathlib import Path import mimetypes -os.makedirs("logs", exist_ok=True) +def song_data_to_Song(data, client_data) -> Song: + # """ + # Build a Jellyfin audio stream URL using urllib.parse. + # """ + item_id = data["Id"] + path = f"/Audio/{item_id}/universal" + params = { + "UserId": client_data["UserId"], + "Container": "flac", + "AudioCodec": "flac", # <-- IMPORTANT + "api_key": client_data["AccessToken"], + } + + query = urlencode(params) + url = urljoin(client_data["address"], path) + "?" + query + + album_cover_url = urljoin( + client_data["address"], f"/Items/{data['AlbumId']}/Images/Primary" + ) + + r = requests.get(album_cover_url) + r.raise_for_status() + + content_type = r.headers.get("Content-Type") # e.g. "image/jpeg" + + ext = mimetypes.guess_extension(content_type) # ".jpg" + + if ext is None: + ext = ".jpg" # safe fallback for album art + + saved_path = Path("data", "images", data["AlbumId"] + ext).as_posix() + + with open(saved_path, "wb") as f: + f.write(r.content) + + return Song( + item_id, + url, + data["Name"], + data["RunTimeTicks"] / 10_000_000, + data["Album"], + saved_path, + data["AlbumArtist"], + ) + +#os.makedirs("logs", exist_ok=True) os.makedirs("data", exist_ok=True) os.makedirs("data/images", exist_ok=True) @dataclass class Song: + id: str url: str name: str duration: float @@ -38,18 +85,16 @@ class Song: class GaplessPlayer: - def __init__(self, samplerate: int = 44100, channels: int = 2): + def __init__(self, samplerate: int = 96000, channels: int = 2): self.samplerate = samplerate self.channels = channels - self.next_preload_state = 0 - self.closed = False self.playing = False self.position = 0.0 - self.song_list = [] + self.song_list: list[Song] = [] self.current_song_in_list = -1 @@ -62,6 +107,7 @@ class GaplessPlayer: callback=self._callback, ) self.stream.start() + self.oscilloscope_data_points = deque(maxlen=samplerate//60) def get_current_song(self): if self.current_song_in_list >= 0 and self.current_song_in_list < len( @@ -92,6 +138,8 @@ class GaplessPlayer: stderr=subprocess.PIPE, ) + print("yo") + # --- make stdout non-blocking --- fd = proc.stdout.fileno() flags = fcntl.fcntl(fd, fcntl.F_GETFL) @@ -169,7 +217,7 @@ class GaplessPlayer: if song: song.ffmpeg = self._open_ffmpeg(song) song.preload_state = 2 - + def preload_next_threaded(self): next_song = self.get_next_song() if not next_song or next_song.preload_state: @@ -200,6 +248,13 @@ class GaplessPlayer: self.position += len(data) / (self.samplerate * self.channels * 2) if self.position >= current_song.duration - 10: self.preload_next_threaded() + else: + next_song = self.get_next_song() + if next_song and next_song.ffmpeg: + if next_song.ffmpeg.poll() is None: + next_song.ffmpeg.kill() + next_song.ffmpeg = None + next_song.preload_state = 0 if current_song.ffmpeg.poll() is not None and len(data) < needed: if round(self.position, 2) >= current_song.duration - 0.1: self._start_next() @@ -223,76 +278,52 @@ class GaplessPlayer: ) data += new_data else: + # if current_song.ffmpeg and current_song.ffmpeg.poll() is not None: + # current_song.ffmpeg.kill() + # current_song.ffmpeg = None current_song.ffmpeg = self._open_ffmpeg( current_song, self.position ) + samples = np.frombuffer(data, dtype=np.int16) + left = samples[0::2] + right = samples[1::2] + norm = 32769.0 + x = left / norm + y = right / norm + + points = list(zip(x, y)) + # step = max(1, len(points) // 1000) + # points = points[::step] + self.oscilloscope_data_points.extend(points) outdata[: len(data)] = data outdata[len(data) :] = b"\x00" * (needed - len(data)) + def save_state(self, path): + with open(path,"w") as f: + data = { + "queue": [song.id for song in self.song_list], + "current_song": self.current_song_in_list, + "position": self.position + } + json.dump(data, f) + def load_state(self, path): + try: + with open(path,"r") as f: + data = json.load(f) + self.song_list = [] + for song in data["queue"]: + songOBJ = song_data_to_Song(client.jellyfin.get_item(song), server) + songOBJ.ffmpeg = None + songOBJ.preload_state = 0 + self.song_list.append(songOBJ) + + self.current_song_in_list = data['current_song'] + self.seek(data['position']) + except: + return -album_covers = [] - - -def song_data_to_Song(data, client_data) -> Song: - # """ - # Build a Jellyfin audio stream URL using urllib.parse. - # """ - item_id = data["Id"] - path = f"/Audio/{item_id}/universal" - params = { - "UserId": client_data["UserId"], - "Container": "flac", - "AudioCodec": "flac", # <-- IMPORTANT - "api_key": client_data["AccessToken"], - } - - query = urlencode(params) - url = urljoin(client_data["address"], path) + "?" + query - - album_cover_url = urljoin( - client_data["address"], f"/Items/{data["AlbumId"]}/Images/Primary" - ) - - r = requests.get(album_cover_url) - r.raise_for_status() - - content_type = r.headers.get("Content-Type") # e.g. "image/jpeg" - - ext = mimetypes.guess_extension(content_type) # ".jpg" - - if ext is None: - ext = ".jpg" # safe fallback for album art - - saved_path = Path("data", "images", data["AlbumId"] + ext).as_posix() - - with open(saved_path, "wb") as f: - f.write(r.content) - - return Song( - url, - data["Name"], - data["RunTimeTicks"] / 10_000_000, - data["Album"], - saved_path, - data["AlbumArtist"], - ) - - -client = JellyfinClient() -load_dotenv() - -client.config.app("UgPod", "0.0.1", "UgPod prototype", "UgPod_prototype_1") -client.config.data["auth.ssl"] = True - -client.auth.connect_to_address(os.getenv("host")) -client.auth.login(os.getenv("URL"), os.getenv("username"), os.getenv("password")) - -credentials = client.auth.credentials.get_credentials() -server = credentials["Servers"][0] -print(json.dumps(server)) - # while True: # duration = player.playback_info_to_duration(player.playback_info) diff --git a/jelly.py b/jelly.py new file mode 100644 index 0000000..7ef87c1 --- /dev/null +++ b/jelly.py @@ -0,0 +1,41 @@ +from jellyfin_apiclient_python import JellyfinClient +import os +from dotenv import load_dotenv +import json + +load_dotenv() + + +album_covers = {} + +client = JellyfinClient() +client.config.app("UgPod", "0.0.1", "UgPod prototype", "UgPod_prototype_1") +client.config.data["auth.ssl"] = True + +try: + with open("data/auth.json", "r") as f: + credentials = json.load(f) + + client.authenticate(credentials, discover=False) + + # 🔴 THIS IS THE MISSING STEP + server = credentials["Servers"][0] + client.config.data["auth.server"] = server["Id"] + client.config.data["auth.servers"] = credentials["Servers"] + + client.start() + + server = credentials["Servers"][0] + + assert server["Address"].startswith("http") + print("Server address:", server["Address"]) + print("Server ID:", server["Id"]) +except: + print("authenticating") + client.auth.connect_to_address(os.getenv("host")) + client.auth.login(os.getenv("URL"), os.getenv("username"), os.getenv("password")) + + credentials = client.auth.credentials.get_credentials() +# with open("data/auth.json", 'w') as f: +# json.dump(credentials, f) +server = credentials["Servers"][0] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 371124d..3394a1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ ffmpeg-python==0.2.0 frozenlist==1.8.0 future==1.0.0 idna==3.11 +inflection==0.5.1 jellyfin-apiclient-python==1.11.0 multidict==6.7.0 numpy==2.3.5 @@ -18,8 +19,7 @@ propcache==0.4.1 pycparser==2.23 pyee==13.0.0 python-dotenv==1.2.1 -python-vlc==3.0.21203 -raylib==5.5.0.3 +raylib_drm==5.5.0.4 requests==2.32.5 sounddevice==0.5.3 typing_extensions==4.15.0 diff --git a/run-fb.sh b/run-fb.sh new file mode 100755 index 0000000..68679ef --- /dev/null +++ b/run-fb.sh @@ -0,0 +1,2 @@ +export LIBGL_ALWAYS_SOFTWARE=1 +python3 app.py \ No newline at end of file diff --git a/scrolling_text.py b/scrolling_text.py index 81e94e1..5ce6d0f 100644 --- a/scrolling_text.py +++ b/scrolling_text.py @@ -15,13 +15,13 @@ class ScrollingText: self.pause_time = pause_time self.color = color self.text = None - self.set_text(text) - + self.set_text(text, font_size) - def set_text(self, text: str): - if text == self.text: + def set_text(self, text: str, font_size: int): + if text == self.text and font_size == self.font_size: return self.text = text + self.font_size = font_size self.text_width = pr.measure_text(self.text, self.font_size) self.reset() @@ -32,7 +32,7 @@ class ScrollingText: def update(self, dt: float, size: pr.Vector2): if self.text_width <= size.x: - return + return self.reset() self.timer += dt @@ -43,7 +43,7 @@ class ScrollingText: else: self.offset += self.speed * dt - if self.offset >= self.text_width + 20: + if self.offset >= self.text_width + self.font_size*2.5: self.reset() def draw(self, pos: pr.Vector2, size: pr.Vector2): @@ -69,7 +69,7 @@ class ScrollingText: if self.text_width > size.x: pr.draw_text( self.text, - int(pos.x - self.offset + self.text_width + 20), + int(pos.x - self.offset + self.text_width + self.font_size*2.5), int(y), self.font_size, self.color, From 275f39458068eceda74da703d3ce1961ee9b3ca3 Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Fri, 26 Dec 2025 20:04:39 +0000 Subject: [PATCH 04/10] fix for older python runtime --- gapless_player.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/gapless_player.py b/gapless_player.py index 065badb..bfbc88a 100644 --- a/gapless_player.py +++ b/gapless_player.py @@ -23,6 +23,16 @@ import requests from pathlib import Path import mimetypes +@dataclass +class Song: + id: str + url: str + name: str + duration: float + album_name: str + album_cover_path: str + artist_name: str + def song_data_to_Song(data, client_data) -> Song: # """ # Build a Jellyfin audio stream URL using urllib.parse. @@ -73,17 +83,6 @@ os.makedirs("data", exist_ok=True) os.makedirs("data/images", exist_ok=True) -@dataclass -class Song: - id: str - url: str - name: str - duration: float - album_name: str - album_cover_path: str - artist_name: str - - class GaplessPlayer: def __init__(self, samplerate: int = 96000, channels: int = 2): self.samplerate = samplerate From faf15f6069b092ab55cf29fe86b68af83596583a Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Fri, 26 Dec 2025 20:58:37 +0000 Subject: [PATCH 05/10] remove MSAA --- app.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index cfbfeb0..5e6d18e 100644 --- a/app.py +++ b/app.py @@ -67,7 +67,7 @@ def draw_progress_bar(rect, current_time, total_time): # ) pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) -pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) +# pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) #pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) pr.init_window(state["screen_width"], state["screen_height"], "UgPod") pr.set_target_fps(TARGET_FPS) diff --git a/requirements.txt b/requirements.txt index 3394a1a..fd76f89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ propcache==0.4.1 pycparser==2.23 pyee==13.0.0 python-dotenv==1.2.1 -raylib_drm==5.5.0.4 +raylib==5.5.0.4 requests==2.32.5 sounddevice==0.5.3 typing_extensions==4.15.0 From cfd83089c989cba863928cde4eed6a2d35cbac09 Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:30:06 +0000 Subject: [PATCH 06/10] disable graphics (the raspberry pi doesnt support drm, only a direct frame buffer) --- app.py | 597 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 302 insertions(+), 295 deletions(-) diff --git a/app.py b/app.py index 5e6d18e..661b905 100644 --- a/app.py +++ b/app.py @@ -9,68 +9,68 @@ import time import os from jelly import server, client -# --- Configuration Constants --- -INITIAL_SCREEN_WIDTH = 240 -INITIAL_SCREEN_HEIGHT = 240 -TARGET_FPS =60 +# # --- Configuration Constants --- +# INITIAL_SCREEN_WIDTH = 240 +# INITIAL_SCREEN_HEIGHT = 240 +# TARGET_FPS =60 -# --- State Variables --- -state = { - "screen_width": INITIAL_SCREEN_WIDTH, - "screen_height": INITIAL_SCREEN_HEIGHT, -} +# # --- State Variables --- +# state = { +# "screen_width": INITIAL_SCREEN_WIDTH, +# "screen_height": INITIAL_SCREEN_HEIGHT, +# } -# --- Utility Functions --- +# # --- Utility Functions --- -def format_time_mm_ss(seconds): - """Converts a time in seconds to an 'MM:SS' string format.""" - seconds = int(seconds) - minutes = seconds // 60 - seconds_remainder = seconds % 60 - return f"{minutes:02d}:{seconds_remainder:02d}" +# def format_time_mm_ss(seconds): +# """Converts a time in seconds to an 'MM:SS' string format.""" +# seconds = int(seconds) +# minutes = seconds // 60 +# seconds_remainder = seconds % 60 +# return f"{minutes:02d}:{seconds_remainder:02d}" -def get_progress_bar_rect(screen_width, screen_height): - width = screen_width - height = screen_height*0.021 - x = (screen_width - width) / 2 - y = screen_height - height - return pr.Rectangle(x, y, width, height) +# def get_progress_bar_rect(screen_width, screen_height): +# width = screen_width +# height = screen_height*0.021 +# x = (screen_width - width) / 2 +# y = screen_height - height +# return pr.Rectangle(x, y, width, height) -def draw_progress_bar(rect, current_time, total_time): - if total_time > 0: - progress_ratio = current_time / total_time - else: - progress_ratio = 0.0 +# def draw_progress_bar(rect, current_time, total_time): +# if total_time > 0: +# progress_ratio = current_time / total_time +# else: +# progress_ratio = 0.0 - pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255)) - progress_width = rect.width * progress_ratio - pr.draw_rectangle( - int(rect.x), - int(rect.y)+1, - int(progress_width), - int(rect.height), - pr.Color(200, 50, 50, 255), - ) - # pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) +# pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255)) +# progress_width = rect.width * progress_ratio +# pr.draw_rectangle( +# int(rect.x), +# int(rect.y)+1, +# int(progress_width), +# int(rect.height), +# pr.Color(200, 50, 50, 255), +# ) +# # pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) - time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" - text_width = pr.measure_text(time_text, int(rect.height * 0.7)) - # pr.draw_text( - # time_text, - # int(rect.x + rect.width / 2 - text_width / 2), - # int(rect.y + rect.height * 0.15), - # int(rect.height * 0.7), - # pr.WHITE, - # ) +# time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" +# text_width = pr.measure_text(time_text, int(rect.height * 0.7)) +# # pr.draw_text( +# # time_text, +# # int(rect.x + rect.width / 2 - text_width / 2), +# # int(rect.y + rect.height * 0.15), +# # int(rect.height * 0.7), +# # pr.WHITE, +# # ) -pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) -# pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) -#pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) -pr.init_window(state["screen_width"], state["screen_height"], "UgPod") -pr.set_target_fps(TARGET_FPS) +# pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) +# # pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) +# #pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) +# pr.init_window(state["screen_width"], state["screen_height"], "UgPod") +# pr.set_target_fps(TARGET_FPS) player = GaplessPlayer() @@ -116,316 +116,323 @@ for track in tracks["Items"]: ) print("add queue done") -player.load_state("data/player.json") -close_event = threading.Event() -def save_state_loop(): - while not close_event.wait(10): - player.save_state("data/player.lock.json") - os.rename("data/player.lock.json", "data/player.json") -# save_state_thread = threading.Thread(target=save_state_loop) -# save_state_thread.start() +# player.load_state("data/player.json") +# close_event = threading.Event() +# def save_state_loop(): +# while not close_event.wait(10): +# player.save_state("data/player.lock.json") +# os.rename("data/player.lock.json", "data/player.json") +# # save_state_thread = threading.Thread(target=save_state_loop) +# # save_state_thread.start() -current_path = None -texture = None +# current_path = None +# texture = None -def load_texture(path): - global texture, current_path +# def load_texture(path): +# global texture, current_path - if not path: - return +# if not path: +# return - if path == current_path: - return +# if path == current_path: +# return - if texture is not None: - pr.unload_texture(texture) +# if texture is not None: +# pr.unload_texture(texture) - texture = pr.load_texture(path) - current_path = path +# texture = pr.load_texture(path) +# current_path = path -def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool: - clicked = False +# def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool: +# clicked = False - rect = pr.Rectangle(pos.x, pos.y, size.x, size.y) +# rect = pr.Rectangle(pos.x, pos.y, size.x, size.y) - # Optional hover background - if pr.check_collision_point_rec(pr.get_mouse_position(), rect): - pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) - if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): - clicked = True - cx = pos.x + size.x / 2 - cy = pos.y + size.y / 2 +# # Optional hover background +# if pr.check_collision_point_rec(pr.get_mouse_position(), rect): +# pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) +# if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): +# clicked = True +# cx = pos.x + size.x / 2 +# cy = pos.y + size.y / 2 - icon_padding = size.x * 0.25 - icon_size = size.x - icon_padding * 2 +# icon_padding = size.x * 0.25 +# icon_size = size.x - icon_padding * 2 - if is_playing: - # PAUSE (two bars centered, same visual weight as play) - bar_width = icon_size * 0.25 - bar_height = icon_size +# if is_playing: +# # PAUSE (two bars centered, same visual weight as play) +# bar_width = icon_size * 0.25 +# bar_height = icon_size - left_x = cx - bar_width - bar_width * 0.4 - right_x = cx + bar_width * 0.4 - top_y = 1+cy - bar_height / 2 +# left_x = cx - bar_width - bar_width * 0.4 +# right_x = cx + bar_width * 0.4 +# top_y = 1+cy - bar_height / 2 - pr.draw_rectangle( - int(left_x), - int(top_y), - int(bar_width), - int(bar_height), - pr.WHITE, - ) - pr.draw_rectangle( - int(right_x), - int(top_y), - int(bar_width), - int(bar_height), - pr.WHITE, - ) - else: - # PLAY (centered triangle) - p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2) - p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2) - p3 = pr.Vector2(cx + icon_size / 2, cy) +# pr.draw_rectangle( +# int(left_x), +# int(top_y), +# int(bar_width), +# int(bar_height), +# pr.WHITE, +# ) +# pr.draw_rectangle( +# int(right_x), +# int(top_y), +# int(bar_width), +# int(bar_height), +# pr.WHITE, +# ) +# else: +# # PLAY (centered triangle) +# p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2) +# p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2) +# p3 = pr.Vector2(cx + icon_size / 2, cy) - pr.draw_triangle(p1, p2, p3, pr.WHITE) +# pr.draw_triangle(p1, p2, p3, pr.WHITE) - return clicked +# return clicked -title = ScrollingText( - "", - 15 -) +# title = ScrollingText( +# "", +# 15 +# ) -# --- Main Game Loop --- -while not pr.window_should_close(): - # 1. Update - current_width = pr.get_screen_width() - current_height = pr.get_screen_height() +# # --- Main Game Loop --- +# while not pr.window_should_close(): +# # 1. Update +# current_width = pr.get_screen_width() +# current_height = pr.get_screen_height() - if pr.is_key_pressed(pr.KEY_F11): - pr.toggle_fullscreen() - if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): - if player.playing: - player.pause() - else: - player.play() - if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT): - player.seek(player.position - 5) - if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT): - player.seek(player.position + 5) +# if pr.is_key_pressed(pr.KEY_F11): +# pr.toggle_fullscreen() +# if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): +# if player.playing: +# player.pause() +# else: +# player.play() +# if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT): +# player.seek(player.position - 5) +# if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT): +# player.seek(player.position + 5) - pr.begin_drawing() - pr.clear_background(pr.Color(40, 40, 40, 255)) - dt = pr.get_frame_time() +# pr.begin_drawing() +# pr.clear_background(pr.Color(40, 40, 40, 255)) +# dt = pr.get_frame_time() - progress_rect = get_progress_bar_rect(current_width, current_height) +# progress_rect = get_progress_bar_rect(current_width, current_height) - # pr.draw_text( - # "UgPod", - # int(current_width * 0.05), - # int(current_height * 0.05), - # int(current_height * 0.05), - # pr.SKYBLUE, - # ) +# # pr.draw_text( +# # "UgPod", +# # int(current_width * 0.05), +# # int(current_height * 0.05), +# # int(current_height * 0.05), +# # pr.SKYBLUE, +# # ) - current_song = player.get_current_song() +# current_song = player.get_current_song() - draw_progress_bar( - progress_rect, - player.position, - (current_song and current_song.duration) or 0.0, - ) - if current_song: - load_texture(current_song.album_cover_path) - title_font_size = int(current_height*0.05) - album_cover_size = int(min(current_width, current_height*0.7)) - title.speed = title_font_size*2.5 - title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size) - title.update(dt,title_size) - title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size) - title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size) - # pr.draw_text( - # , - # , - # int(current_height * 0.03), - # pr.WHITE, - # ) - points = player.oscilloscope_data_points - if texture is not None: - scale = min(album_cover_size / texture.width, album_cover_size / texture.height) +# draw_progress_bar( +# progress_rect, +# player.position, +# (current_song and current_song.duration) or 0.0, +# ) +# if current_song: +# load_texture(current_song.album_cover_path) +# title_font_size = int(current_height*0.05) +# album_cover_size = int(min(current_width, current_height*0.7)) +# title.speed = title_font_size*2.5 +# title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size) +# title.update(dt,title_size) +# title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size) +# title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size) +# # pr.draw_text( +# # , +# # , +# # int(current_height * 0.03), +# # pr.WHITE, +# # ) +# points = player.oscilloscope_data_points +# if texture is not None: +# scale = min(album_cover_size / texture.width, album_cover_size / texture.height) - dest_rect = pr.Rectangle( - current_width//2 - album_cover_size//2, - (current_height*0.8)//2 - album_cover_size//2, - texture.width * scale, - texture.height * scale, - ) +# dest_rect = pr.Rectangle( +# current_width//2 - album_cover_size//2, +# (current_height*0.8)//2 - album_cover_size//2, +# texture.width * scale, +# texture.height * scale, +# ) - src_rect = pr.Rectangle(0, 0, texture.width, texture.height) +# src_rect = pr.Rectangle(0, 0, texture.width, texture.height) - pr.draw_texture_pro( - texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE - ) - else: - clip = pr.Rectangle(int(current_width//2 - album_cover_size//2), - int((current_height*0.8)//2 - album_cover_size//2), - int(album_cover_size), - int(album_cover_size)) - pr.begin_scissor_mode( - int(clip.x), - int(clip.y), - int(clip.width), - int(clip.height), - ) - pr.draw_rectangle( - int(clip.x), - int(clip.y), - int(clip.width), - int(clip.height), pr.BLACK) +# pr.draw_texture_pro( +# texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE +# ) +# else: +# clip = pr.Rectangle(int(current_width//2 - album_cover_size//2), +# int((current_height*0.8)//2 - album_cover_size//2), +# int(album_cover_size), +# int(album_cover_size)) +# pr.begin_scissor_mode( +# int(clip.x), +# int(clip.y), +# int(clip.width), +# int(clip.height), +# ) +# pr.draw_rectangle( +# int(clip.x), +# int(clip.y), +# int(clip.width), +# int(clip.height), pr.BLACK) - # cx = current_width * 0.5+1 - # cy = current_height * 0.4+1 +# # cx = current_width * 0.5+1 +# # cy = current_height * 0.4+1 - # MAX_LEN = album_cover_size * 0.25 # tune this - # MIN_ALPHA = 10 - # MAX_ALPHA = 255 +# # MAX_LEN = album_cover_size * 0.25 # tune this +# # MIN_ALPHA = 10 +# # MAX_ALPHA = 255 - # for i in range(len(points) - 1): - # x1 = cx + points[i][0] * album_cover_size * 0.5 - # y1 = cy + -points[i][1] * album_cover_size * 0.5 - # x2 = cx + points[i+1][0] * album_cover_size * 0.5 - # y2 = cy + -points[i+1][1] * album_cover_size * 0.5 +# # for i in range(len(points) - 1): +# # x1 = cx + points[i][0] * album_cover_size * 0.5 +# # y1 = cy + -points[i][1] * album_cover_size * 0.5 +# # x2 = cx + points[i+1][0] * album_cover_size * 0.5 +# # y2 = cy + -points[i+1][1] * album_cover_size * 0.5 - # dx = x2 - x1 - # dy = y2 - y1 - # length = (dx * dx + dy * dy) ** 0.5 +# # dx = x2 - x1 +# # dy = y2 - y1 +# # length = (dx * dx + dy * dy) ** 0.5 - # # 1.0 = short line, 0.0 = long line - # t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2) +# # # 1.0 = short line, 0.0 = long line +# # t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2) - # alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA)) +# # alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA)) - # color = pr.Color(255, 255, 255, alpha) +# # color = pr.Color(255, 255, 255, alpha) - # pr.draw_line(int(x1), int(y1), int(x2), int(y2), color) - # draw background square - if len(points) >= 2: - samples = np.fromiter( - ((p[0] + p[1]) * 0.5 for p in points), - dtype=np.float32 - ) +# # pr.draw_line(int(x1), int(y1), int(x2), int(y2), color) +# # draw background square +# if len(points) >= 2: +# samples = np.fromiter( +# ((p[0] + p[1]) * 0.5 for p in points), +# dtype=np.float32 +# ) - # Guard: FFT must have meaningful size - if samples.size > 128: +# # Guard: FFT must have meaningful size +# if samples.size > 128: - rect_x = int(current_width // 2 - album_cover_size // 2) - rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2) +# rect_x = int(current_width // 2 - album_cover_size // 2) +# rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2) - # ---- FFT ---- +# # ---- FFT ---- - FFT_SIZE = min(samples.size, 2048) - window = np.hanning(FFT_SIZE) +# FFT_SIZE = min(samples.size, 2048) +# window = np.hanning(FFT_SIZE) - fft = np.fft.rfft(samples[:FFT_SIZE] * window) - magnitudes = np.abs(fft) +# fft = np.fft.rfft(samples[:FFT_SIZE] * window) +# magnitudes = np.abs(fft) - # remove DC component (important for visuals) - magnitudes[0] = 0.0 +# # remove DC component (important for visuals) +# magnitudes[0] = 0.0 - # ---- LOG BINNING ---- +# # ---- LOG BINNING ---- - num_bars = album_cover_size//10 - num_bins = magnitudes.size +# num_bars = album_cover_size//10 +# num_bins = magnitudes.size - # logarithmic bin edges (low end stretched) - log_min = 1 - log_max = math.log10(num_bins) +# # logarithmic bin edges (low end stretched) +# log_min = 1 +# log_max = math.log10(num_bins) - log_edges = np.logspace( - math.log10(log_min), - log_max, - num_bars + 1 - ).astype(int) +# log_edges = np.logspace( +# math.log10(log_min), +# log_max, +# num_bars + 1 +# ).astype(int) - bar_values = np.zeros(num_bars, dtype=np.float32) +# bar_values = np.zeros(num_bars, dtype=np.float32) - for i in range(num_bars): - start = log_edges[i] - end = log_edges[i + 1] +# for i in range(num_bars): +# start = log_edges[i] +# end = log_edges[i + 1] - if end <= start: - continue +# if end <= start: +# continue - bar_values[i] = np.mean(magnitudes[start:end]) +# bar_values[i] = np.mean(magnitudes[start:end]) -# ---- STATIC SCALING ---- +# # ---- STATIC SCALING ---- - # Instead of normalizing to the max of the frame, we scale by the FFT size. - # For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps - # maximum possible volume roughly to 1.0. - bar_values = bar_values / (FFT_SIZE / 4.0) +# # Instead of normalizing to the max of the frame, we scale by the FFT size. +# # For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps +# # maximum possible volume roughly to 1.0. +# bar_values = bar_values / (FFT_SIZE / 4.0) - # ---- DRAW ---- +# # ---- DRAW ---- - def map_to_screen(val): - return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size +# def map_to_screen(val): +# return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size - spacing = 0 +# spacing = 0 - for i in range(num_bars): - # 1. Calculate integer pixel boundaries first - # This ensures the right edge of one bar is exactly the left edge of the next - x_start_int = int(map_to_screen(log_edges[i])) - x_end_int = int(map_to_screen(log_edges[i+1])) +# for i in range(num_bars): +# # 1. Calculate integer pixel boundaries first +# # This ensures the right edge of one bar is exactly the left edge of the next +# x_start_int = int(map_to_screen(log_edges[i])) +# x_end_int = int(map_to_screen(log_edges[i+1])) - # 2. Width is the difference between these fixed integer points - w = (x_end_int - x_start_int) - spacing +# # 2. Width is the difference between these fixed integer points +# w = (x_end_int - x_start_int) - spacing - value = bar_values[i] - h = int(min(1.0, value) * album_cover_size) +# value = bar_values[i] +# h = int(min(1.0, value) * album_cover_size) - # 3. Anchor to bottom - y = (rect_y + album_cover_size) - h +# # 3. Anchor to bottom +# y = (rect_y + album_cover_size) - h - alpha = min(1.0, ((value+1)**2)-1) - r = 255 - g = 0 - b = 0 +# alpha = min(1.0, ((value+1)**2)-1) +# r = 255 +# g = 0 +# b = 0 - # Keep alpha at 255 (fully opaque) - color = pr.Color(r, g, b, int(255 * alpha)) +# # Keep alpha at 255 (fully opaque) +# color = pr.Color(r, g, b, int(255 * alpha)) - # 4. Draw the bar - # Use max(1, w) to ensure high-frequency bars don't disappear on small screens - pr.draw_rectangle( - x_start_int, - int(y), - max(1, int(w)), - h, - color - ) - pr.end_scissor_mode() +# # 4. Draw the bar +# # Use max(1, w) to ensure high-frequency bars don't disappear on small screens +# pr.draw_rectangle( +# x_start_int, +# int(y), +# max(1, int(w)), +# h, +# color +# ) +# pr.end_scissor_mode() - pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) - size = pr.Vector2(current_height * 0.1, current_height * 0.1) +# pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) +# size = pr.Vector2(current_height * 0.1, current_height * 0.1) - if draw_play_pause_button(pos, size, player.playing): - if player.playing: - player.pause() - else: - player.play() - pr.end_drawing() +# if draw_play_pause_button(pos, size, player.playing): +# if player.playing: +# player.pause() +# else: +# player.play() +# pr.end_drawing() -# Cleanup -if texture is not None: - pr.unload_texture(texture) +# # Cleanup +# if texture is not None: +# pr.unload_texture(texture) -pr.close_window() -close_event.set() -# save_state_thread.join() \ No newline at end of file +# pr.close_window() +# close_event.set() +# # save_state_thread.join() + + + + +player.play() +while True: + time.sleep(1) \ No newline at end of file From db728d684bc3a437d794259a50c9a6ef881fb2fd Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:46:11 +0000 Subject: [PATCH 07/10] change to not download the album photo --- app.py | 68 +++++++++++++++++++++++------------------------ gapless_player.py | 10 ++++--- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/app.py b/app.py index 661b905..6d1cbdf 100644 --- a/app.py +++ b/app.py @@ -76,46 +76,46 @@ player = GaplessPlayer() print("add queue") -# player.add_to_queue( -# Song( -# "/mnt/HDD/Downloads/72 Pantera.wav", -# "Bullseye", -# 1, -# "KDrew", -# "", -# "KDrew", -# ) +player.add_to_queue( + Song( + "/mnt/HDD/Downloads/72 Pantera.wav", + "Bullseye", + 1, + "KDrew", + "", + "KDrew", + ) +) + +# albums = client.jellyfin.user_items( +# params={ +# "IncludeItemTypes": "MusicAlbum", +# "SearchTerm": "Dawn FM", # album name +# "Recursive": True, +# }, # ) -albums = client.jellyfin.user_items( - params={ - "IncludeItemTypes": "MusicAlbum", - "SearchTerm": "Dawn FM", # album name - "Recursive": True, - }, -) - -album = albums["Items"][0] # pick the album you want -album_id = album["Id"] +# album = albums["Items"][0] # pick the album you want +# album_id = album["Id"] -tracks = client.jellyfin.user_items( - params={ - "ParentId": album_id, - "IncludeItemTypes": "Audio", - "SortBy": "IndexNumber", - "SortOrder": "Ascending", - }, -) +# tracks = client.jellyfin.user_items( +# params={ +# "ParentId": album_id, +# "IncludeItemTypes": "Audio", +# "SortBy": "IndexNumber", +# "SortOrder": "Ascending", +# }, +# ) -for track in tracks["Items"]: - player.add_to_queue( - song_data_to_Song( - track, server - ) - ) +# for track in tracks["Items"]: +# player.add_to_queue( +# song_data_to_Song( +# track, server +# ) +# ) -print("add queue done") +# print("add queue done") # player.load_state("data/player.json") # close_event = threading.Event() # def save_state_loop(): diff --git a/gapless_player.py b/gapless_player.py index bfbc88a..7278309 100644 --- a/gapless_player.py +++ b/gapless_player.py @@ -53,12 +53,14 @@ def song_data_to_Song(data, client_data) -> Song: client_data["address"], f"/Items/{data['AlbumId']}/Images/Primary" ) - r = requests.get(album_cover_url) - r.raise_for_status() + # r = requests.get(album_cover_url) + # r.raise_for_status() - content_type = r.headers.get("Content-Type") # e.g. "image/jpeg" + # content_type = r.headers.get("Content-Type") # e.g. "image/jpeg" - ext = mimetypes.guess_extension(content_type) # ".jpg" + # ext = mimetypes.guess_extension(content_type) # ".jpg" + + ext = None if ext is None: ext = ".jpg" # safe fallback for album art From ca5ab41677e2f518a77cc425e489d62543034128 Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:14:03 +0000 Subject: [PATCH 08/10] change to not load from the internet rn --- app.py | 21 +++++++++++++++++---- jelly.py | 53 ++++++++++++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/app.py b/app.py index 6d1cbdf..f03b852 100644 --- a/app.py +++ b/app.py @@ -78,12 +78,25 @@ print("add queue") player.add_to_queue( Song( - "/mnt/HDD/Downloads/72 Pantera.wav", - "Bullseye", + "bruhh", + "music/pink floyd/dark side of the moon/06 Money.flac", + "Money", 1, - "KDrew", + "The Dark Side Of The Moon", "", - "KDrew", + "Pink Floyd", + ) +) + +player.add_to_queue( + Song( + "bruhh", + "music/pink floyd/dark side of the moon/07 Us and Them.flac", + "Us and Them", + 1, + "The Dark Side Of The Moon", + "", + "Pink Floyd", ) ) diff --git a/jelly.py b/jelly.py index 7ef87c1..8c89673 100644 --- a/jelly.py +++ b/jelly.py @@ -6,36 +6,39 @@ import json load_dotenv() -album_covers = {} +# album_covers = {} -client = JellyfinClient() -client.config.app("UgPod", "0.0.1", "UgPod prototype", "UgPod_prototype_1") -client.config.data["auth.ssl"] = True +# client = JellyfinClient() +# client.config.app("UgPod", "0.0.1", "UgPod prototype", "UgPod_prototype_1") +# client.config.data["auth.ssl"] = True -try: - with open("data/auth.json", "r") as f: - credentials = json.load(f) +# try: +# with open("data/auth.json", "r") as f: +# credentials = json.load(f) - client.authenticate(credentials, discover=False) +# client.authenticate(credentials, discover=False) - # 🔴 THIS IS THE MISSING STEP - server = credentials["Servers"][0] - client.config.data["auth.server"] = server["Id"] - client.config.data["auth.servers"] = credentials["Servers"] +# # 🔴 THIS IS THE MISSING STEP +# server = credentials["Servers"][0] +# client.config.data["auth.server"] = server["Id"] +# client.config.data["auth.servers"] = credentials["Servers"] - client.start() +# client.start() - server = credentials["Servers"][0] +# server = credentials["Servers"][0] - assert server["Address"].startswith("http") - print("Server address:", server["Address"]) - print("Server ID:", server["Id"]) -except: - print("authenticating") - client.auth.connect_to_address(os.getenv("host")) - client.auth.login(os.getenv("URL"), os.getenv("username"), os.getenv("password")) +# assert server["Address"].startswith("http") +# print("Server address:", server["Address"]) +# print("Server ID:", server["Id"]) +# except: +# print("authenticating") +# client.auth.connect_to_address(os.getenv("host")) +# client.auth.login(os.getenv("URL"), os.getenv("username"), os.getenv("password")) - credentials = client.auth.credentials.get_credentials() -# with open("data/auth.json", 'w') as f: -# json.dump(credentials, f) -server = credentials["Servers"][0] \ No newline at end of file +# credentials = client.auth.credentials.get_credentials() +# # with open("data/auth.json", 'w') as f: +# # json.dump(credentials, f) +# server = credentials["Servers"][0] + +client = None +server = None \ No newline at end of file From 56e29a57e21477f7971ea384e9d465a46339f2d3 Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:34:42 +0000 Subject: [PATCH 09/10] test with new gui code --- app.py | 569 +++++++++++++---------------------------------------- old_app.py | 451 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 592 insertions(+), 428 deletions(-) create mode 100644 old_app.py diff --git a/app.py b/app.py index f03b852..b522688 100644 --- a/app.py +++ b/app.py @@ -1,451 +1,164 @@ -import pyray as pr -import math -from ctypes import c_float -from gapless_player import GaplessPlayer, Song, song_data_to_Song -from scrolling_text import ScrollingText -import numpy as np -import threading +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Copyright (c) 2017 Adafruit Industries +# Author: James DeVito +# Ported to RGB Display by Melissa LeBlanc-Williams +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This example is for use on (Linux) computers that are using CPython with +# Adafruit Blinka to support CircuitPython libraries. CircuitPython does +# not support PIL/pillow (python imaging library)! +""" +This example is for use on (Linux) computers that are using CPython with +Adafruit Blinka to support CircuitPython libraries. CircuitPython does +not support PIL/pillow (python imaging library)! +""" + +import random import time -import os -from jelly import server, client +from colorsys import hsv_to_rgb -# # --- Configuration Constants --- -# INITIAL_SCREEN_WIDTH = 240 -# INITIAL_SCREEN_HEIGHT = 240 -# TARGET_FPS =60 +import board +from digitalio import DigitalInOut, Direction +from PIL import Image, ImageDraw, ImageFont -# # --- State Variables --- -# state = { -# "screen_width": INITIAL_SCREEN_WIDTH, -# "screen_height": INITIAL_SCREEN_HEIGHT, -# } +from adafruit_rgb_display import st7789 +import old_app -# # --- Utility Functions --- +# Create the display +cs_pin = DigitalInOut(board.CE0) +dc_pin = DigitalInOut(board.D25) +reset_pin = DigitalInOut(board.D24) +BAUDRATE = 24000000 - -# def format_time_mm_ss(seconds): -# """Converts a time in seconds to an 'MM:SS' string format.""" -# seconds = int(seconds) -# minutes = seconds // 60 -# seconds_remainder = seconds % 60 -# return f"{minutes:02d}:{seconds_remainder:02d}" - - -# def get_progress_bar_rect(screen_width, screen_height): -# width = screen_width -# height = screen_height*0.021 -# x = (screen_width - width) / 2 -# y = screen_height - height -# return pr.Rectangle(x, y, width, height) - - -# def draw_progress_bar(rect, current_time, total_time): -# if total_time > 0: -# progress_ratio = current_time / total_time -# else: -# progress_ratio = 0.0 - -# pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255)) -# progress_width = rect.width * progress_ratio -# pr.draw_rectangle( -# int(rect.x), -# int(rect.y)+1, -# int(progress_width), -# int(rect.height), -# pr.Color(200, 50, 50, 255), -# ) -# # pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) - -# time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" -# text_width = pr.measure_text(time_text, int(rect.height * 0.7)) -# # pr.draw_text( -# # time_text, -# # int(rect.x + rect.width / 2 - text_width / 2), -# # int(rect.y + rect.height * 0.15), -# # int(rect.height * 0.7), -# # pr.WHITE, -# # ) - -# pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) -# # pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) -# #pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) -# pr.init_window(state["screen_width"], state["screen_height"], "UgPod") -# pr.set_target_fps(TARGET_FPS) - -player = GaplessPlayer() - -print("add queue") - -player.add_to_queue( - Song( - "bruhh", - "music/pink floyd/dark side of the moon/06 Money.flac", - "Money", - 1, - "The Dark Side Of The Moon", - "", - "Pink Floyd", - ) +spi = board.SPI() +disp = st7789.ST7789( + spi, + height=240, + y_offset=80, + rotation=180, + cs=cs_pin, + dc=dc_pin, + rst=reset_pin, + baudrate=BAUDRATE, ) -player.add_to_queue( - Song( - "bruhh", - "music/pink floyd/dark side of the moon/07 Us and Them.flac", - "Us and Them", - 1, - "The Dark Side Of The Moon", - "", - "Pink Floyd", - ) -) +# Input pins: +button_A = DigitalInOut(board.D5) +button_A.direction = Direction.INPUT -# albums = client.jellyfin.user_items( -# params={ -# "IncludeItemTypes": "MusicAlbum", -# "SearchTerm": "Dawn FM", # album name -# "Recursive": True, -# }, -# ) +button_B = DigitalInOut(board.D6) +button_B.direction = Direction.INPUT -# album = albums["Items"][0] # pick the album you want -# album_id = album["Id"] +button_L = DigitalInOut(board.D27) +button_L.direction = Direction.INPUT +button_R = DigitalInOut(board.D23) +button_R.direction = Direction.INPUT -# tracks = client.jellyfin.user_items( -# params={ -# "ParentId": album_id, -# "IncludeItemTypes": "Audio", -# "SortBy": "IndexNumber", -# "SortOrder": "Ascending", -# }, -# ) +button_U = DigitalInOut(board.D17) +button_U.direction = Direction.INPUT -# for track in tracks["Items"]: -# player.add_to_queue( -# song_data_to_Song( -# track, server -# ) -# ) +button_D = DigitalInOut(board.D22) +button_D.direction = Direction.INPUT -# print("add queue done") -# player.load_state("data/player.json") -# close_event = threading.Event() -# def save_state_loop(): -# while not close_event.wait(10): -# player.save_state("data/player.lock.json") -# os.rename("data/player.lock.json", "data/player.json") -# # save_state_thread = threading.Thread(target=save_state_loop) -# # save_state_thread.start() +button_C = DigitalInOut(board.D4) +button_C.direction = Direction.INPUT -# current_path = None -# texture = None +# Turn on the Backlight +backlight = DigitalInOut(board.D26) +backlight.switch_to_output() +backlight.value = True +# Create blank image for drawing. +# Make sure to create image with mode 'RGB' for color. +width = disp.width +height = disp.height +image = Image.new("RGB", (width, height)) -# def load_texture(path): -# global texture, current_path +# Get drawing object to draw on image. +draw = ImageDraw.Draw(image) -# if not path: -# return +# Clear display. +draw.rectangle((0, 0, width, height), outline=0, fill=(255, 0, 0)) +disp.image(image) -# if path == current_path: -# return +# Get drawing object to draw on image. +draw = ImageDraw.Draw(image) -# if texture is not None: -# pr.unload_texture(texture) +# Draw a black filled box to clear the image. +draw.rectangle((0, 0, width, height), outline=0, fill=0) -# texture = pr.load_texture(path) -# current_path = path +udlr_fill = "#00FF00" +udlr_outline = "#00FFFF" +button_fill = "#FF00FF" +button_outline = "#FFFFFF" +fnt = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 30) -# def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool: -# clicked = False - -# rect = pr.Rectangle(pos.x, pos.y, size.x, size.y) - -# # Optional hover background -# if pr.check_collision_point_rec(pr.get_mouse_position(), rect): -# pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) -# if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): -# clicked = True -# cx = pos.x + size.x / 2 -# cy = pos.y + size.y / 2 - -# icon_padding = size.x * 0.25 -# icon_size = size.x - icon_padding * 2 - -# if is_playing: -# # PAUSE (two bars centered, same visual weight as play) -# bar_width = icon_size * 0.25 -# bar_height = icon_size - -# left_x = cx - bar_width - bar_width * 0.4 -# right_x = cx + bar_width * 0.4 -# top_y = 1+cy - bar_height / 2 - -# pr.draw_rectangle( -# int(left_x), -# int(top_y), -# int(bar_width), -# int(bar_height), -# pr.WHITE, -# ) -# pr.draw_rectangle( -# int(right_x), -# int(top_y), -# int(bar_width), -# int(bar_height), -# pr.WHITE, -# ) -# else: -# # PLAY (centered triangle) -# p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2) -# p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2) -# p3 = pr.Vector2(cx + icon_size / 2, cy) - -# pr.draw_triangle(p1, p2, p3, pr.WHITE) - -# return clicked - - -# title = ScrollingText( -# "", -# 15 -# ) - -# # --- Main Game Loop --- -# while not pr.window_should_close(): -# # 1. Update -# current_width = pr.get_screen_width() -# current_height = pr.get_screen_height() - -# if pr.is_key_pressed(pr.KEY_F11): -# pr.toggle_fullscreen() -# if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): -# if player.playing: -# player.pause() -# else: -# player.play() -# if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT): -# player.seek(player.position - 5) -# if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT): -# player.seek(player.position + 5) - -# pr.begin_drawing() -# pr.clear_background(pr.Color(40, 40, 40, 255)) -# dt = pr.get_frame_time() - -# progress_rect = get_progress_bar_rect(current_width, current_height) - -# # pr.draw_text( -# # "UgPod", -# # int(current_width * 0.05), -# # int(current_height * 0.05), -# # int(current_height * 0.05), -# # pr.SKYBLUE, -# # ) - -# current_song = player.get_current_song() - - -# draw_progress_bar( -# progress_rect, -# player.position, -# (current_song and current_song.duration) or 0.0, -# ) -# if current_song: -# load_texture(current_song.album_cover_path) -# title_font_size = int(current_height*0.05) -# album_cover_size = int(min(current_width, current_height*0.7)) -# title.speed = title_font_size*2.5 -# title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size) -# title.update(dt,title_size) -# title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size) -# title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size) -# # pr.draw_text( -# # , -# # , -# # int(current_height * 0.03), -# # pr.WHITE, -# # ) -# points = player.oscilloscope_data_points -# if texture is not None: -# scale = min(album_cover_size / texture.width, album_cover_size / texture.height) - -# dest_rect = pr.Rectangle( -# current_width//2 - album_cover_size//2, -# (current_height*0.8)//2 - album_cover_size//2, -# texture.width * scale, -# texture.height * scale, -# ) - -# src_rect = pr.Rectangle(0, 0, texture.width, texture.height) - -# pr.draw_texture_pro( -# texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE -# ) -# else: -# clip = pr.Rectangle(int(current_width//2 - album_cover_size//2), -# int((current_height*0.8)//2 - album_cover_size//2), -# int(album_cover_size), -# int(album_cover_size)) -# pr.begin_scissor_mode( -# int(clip.x), -# int(clip.y), -# int(clip.width), -# int(clip.height), -# ) -# pr.draw_rectangle( -# int(clip.x), -# int(clip.y), -# int(clip.width), -# int(clip.height), pr.BLACK) - -# # cx = current_width * 0.5+1 -# # cy = current_height * 0.4+1 - -# # MAX_LEN = album_cover_size * 0.25 # tune this -# # MIN_ALPHA = 10 -# # MAX_ALPHA = 255 - -# # for i in range(len(points) - 1): -# # x1 = cx + points[i][0] * album_cover_size * 0.5 -# # y1 = cy + -points[i][1] * album_cover_size * 0.5 -# # x2 = cx + points[i+1][0] * album_cover_size * 0.5 -# # y2 = cy + -points[i+1][1] * album_cover_size * 0.5 - -# # dx = x2 - x1 -# # dy = y2 - y1 -# # length = (dx * dx + dy * dy) ** 0.5 - -# # # 1.0 = short line, 0.0 = long line -# # t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2) - -# # alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA)) - -# # color = pr.Color(255, 255, 255, alpha) - -# # pr.draw_line(int(x1), int(y1), int(x2), int(y2), color) -# # draw background square -# if len(points) >= 2: -# samples = np.fromiter( -# ((p[0] + p[1]) * 0.5 for p in points), -# dtype=np.float32 -# ) - -# # Guard: FFT must have meaningful size -# if samples.size > 128: - - -# rect_x = int(current_width // 2 - album_cover_size // 2) -# rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2) - -# # ---- FFT ---- - -# FFT_SIZE = min(samples.size, 2048) -# window = np.hanning(FFT_SIZE) - -# fft = np.fft.rfft(samples[:FFT_SIZE] * window) -# magnitudes = np.abs(fft) - -# # remove DC component (important for visuals) -# magnitudes[0] = 0.0 - -# # ---- LOG BINNING ---- - -# num_bars = album_cover_size//10 -# num_bins = magnitudes.size - -# # logarithmic bin edges (low end stretched) -# log_min = 1 -# log_max = math.log10(num_bins) - -# log_edges = np.logspace( -# math.log10(log_min), -# log_max, -# num_bars + 1 -# ).astype(int) - -# bar_values = np.zeros(num_bars, dtype=np.float32) - -# for i in range(num_bars): -# start = log_edges[i] -# end = log_edges[i + 1] - -# if end <= start: -# continue - -# bar_values[i] = np.mean(magnitudes[start:end]) - -# # ---- STATIC SCALING ---- - -# # Instead of normalizing to the max of the frame, we scale by the FFT size. -# # For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps -# # maximum possible volume roughly to 1.0. -# bar_values = bar_values / (FFT_SIZE / 4.0) - -# # ---- DRAW ---- - -# def map_to_screen(val): -# return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size - -# spacing = 0 - -# for i in range(num_bars): -# # 1. Calculate integer pixel boundaries first -# # This ensures the right edge of one bar is exactly the left edge of the next -# x_start_int = int(map_to_screen(log_edges[i])) -# x_end_int = int(map_to_screen(log_edges[i+1])) - -# # 2. Width is the difference between these fixed integer points -# w = (x_end_int - x_start_int) - spacing - -# value = bar_values[i] -# h = int(min(1.0, value) * album_cover_size) - -# # 3. Anchor to bottom -# y = (rect_y + album_cover_size) - h - -# alpha = min(1.0, ((value+1)**2)-1) -# r = 255 -# g = 0 -# b = 0 - -# # Keep alpha at 255 (fully opaque) -# color = pr.Color(r, g, b, int(255 * alpha)) - -# # 4. Draw the bar -# # Use max(1, w) to ensure high-frequency bars don't disappear on small screens -# pr.draw_rectangle( -# x_start_int, -# int(y), -# max(1, int(w)), -# h, -# color -# ) -# pr.end_scissor_mode() - -# pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) -# size = pr.Vector2(current_height * 0.1, current_height * 0.1) - -# if draw_play_pause_button(pos, size, player.playing): -# if player.playing: -# player.pause() -# else: -# player.play() -# pr.end_drawing() - -# # Cleanup -# if texture is not None: -# pr.unload_texture(texture) - -# pr.close_window() -# close_event.set() -# # save_state_thread.join() - - - - -player.play() while True: - time.sleep(1) \ No newline at end of file + up_fill = 0 + if not button_U.value: # up pressed + up_fill = udlr_fill + draw.polygon([(40, 40), (60, 4), (80, 40)], outline=udlr_outline, fill=up_fill) # Up + + down_fill = 0 + if not button_D.value: # down pressed + down_fill = udlr_fill + draw.polygon([(60, 120), (80, 84), (40, 84)], outline=udlr_outline, fill=down_fill) # down + + left_fill = 0 + if not button_L.value: # left pressed + left_fill = udlr_fill + draw.polygon([(0, 60), (36, 42), (36, 81)], outline=udlr_outline, fill=left_fill) # left + + right_fill = 0 + if not button_R.value: # right pressed + right_fill = udlr_fill + draw.polygon([(120, 60), (84, 42), (84, 82)], outline=udlr_outline, fill=right_fill) # right + + center_fill = 0 + if not button_C.value: # center pressed + center_fill = button_fill + draw.rectangle((40, 44, 80, 80), outline=button_outline, fill=center_fill) # center + + A_fill = 0 + if not button_A.value: # left pressed + A_fill = button_fill + draw.ellipse((140, 80, 180, 120), outline=button_outline, fill=A_fill) # A button + + B_fill = 0 + if not button_B.value: # left pressed + B_fill = button_fill + draw.ellipse((190, 40, 230, 80), outline=button_outline, fill=B_fill) # B button + + # make a random color and print text + rcolor = tuple(int(x * 255) for x in hsv_to_rgb(random.random(), 1, 1)) + draw.text((20, 150), "Hello World", font=fnt, fill=rcolor) + rcolor = tuple(int(x * 255) for x in hsv_to_rgb(random.random(), 1, 1)) + draw.text((20, 180), "Hello World", font=fnt, fill=rcolor) + rcolor = tuple(int(x * 255) for x in hsv_to_rgb(random.random(), 1, 1)) + draw.text((20, 210), "Hello World", font=fnt, fill=rcolor) + + # Display the Image + disp.image(image) + + time.sleep(0.1) \ No newline at end of file diff --git a/old_app.py b/old_app.py new file mode 100644 index 0000000..453b9df --- /dev/null +++ b/old_app.py @@ -0,0 +1,451 @@ +import pyray as pr +import math +from ctypes import c_float +from gapless_player import GaplessPlayer, Song, song_data_to_Song +from scrolling_text import ScrollingText +import numpy as np +import threading +import time +import os +from jelly import server, client + +# # --- Configuration Constants --- +# INITIAL_SCREEN_WIDTH = 240 +# INITIAL_SCREEN_HEIGHT = 240 +# TARGET_FPS =60 + +# # --- State Variables --- +# state = { +# "screen_width": INITIAL_SCREEN_WIDTH, +# "screen_height": INITIAL_SCREEN_HEIGHT, +# } + +# # --- Utility Functions --- + + +# def format_time_mm_ss(seconds): +# """Converts a time in seconds to an 'MM:SS' string format.""" +# seconds = int(seconds) +# minutes = seconds // 60 +# seconds_remainder = seconds % 60 +# return f"{minutes:02d}:{seconds_remainder:02d}" + + +# def get_progress_bar_rect(screen_width, screen_height): +# width = screen_width +# height = screen_height*0.021 +# x = (screen_width - width) / 2 +# y = screen_height - height +# return pr.Rectangle(x, y, width, height) + + +# def draw_progress_bar(rect, current_time, total_time): +# if total_time > 0: +# progress_ratio = current_time / total_time +# else: +# progress_ratio = 0.0 + +# pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255)) +# progress_width = rect.width * progress_ratio +# pr.draw_rectangle( +# int(rect.x), +# int(rect.y)+1, +# int(progress_width), +# int(rect.height), +# pr.Color(200, 50, 50, 255), +# ) +# # pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) + +# time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" +# text_width = pr.measure_text(time_text, int(rect.height * 0.7)) +# # pr.draw_text( +# # time_text, +# # int(rect.x + rect.width / 2 - text_width / 2), +# # int(rect.y + rect.height * 0.15), +# # int(rect.height * 0.7), +# # pr.WHITE, +# # ) + +# pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) +# # pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) +# #pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) +# pr.init_window(state["screen_width"], state["screen_height"], "UgPod") +# pr.set_target_fps(TARGET_FPS) + +player = GaplessPlayer() + +print("add queue") + +player.add_to_queue( + Song( + "bruhh", + "music/pink floyd/dark side of the moon/06 Money.flac", + "Money", + 1, + "The Dark Side Of The Moon", + "", + "Pink Floyd", + ) +) + +player.add_to_queue( + Song( + "bruhh", + "music/pink floyd/dark side of the moon/07 Us and Them.flac", + "Us and Them", + 1, + "The Dark Side Of The Moon", + "", + "Pink Floyd", + ) +) + +# albums = client.jellyfin.user_items( +# params={ +# "IncludeItemTypes": "MusicAlbum", +# "SearchTerm": "Dawn FM", # album name +# "Recursive": True, +# }, +# ) + +# album = albums["Items"][0] # pick the album you want +# album_id = album["Id"] + + +# tracks = client.jellyfin.user_items( +# params={ +# "ParentId": album_id, +# "IncludeItemTypes": "Audio", +# "SortBy": "IndexNumber", +# "SortOrder": "Ascending", +# }, +# ) + +# for track in tracks["Items"]: +# player.add_to_queue( +# song_data_to_Song( +# track, server +# ) +# ) + +# print("add queue done") +# player.load_state("data/player.json") +# close_event = threading.Event() +# def save_state_loop(): +# while not close_event.wait(10): +# player.save_state("data/player.lock.json") +# os.rename("data/player.lock.json", "data/player.json") +# # save_state_thread = threading.Thread(target=save_state_loop) +# # save_state_thread.start() + +# current_path = None +# texture = None + + +# def load_texture(path): +# global texture, current_path + +# if not path: +# return + +# if path == current_path: +# return + +# if texture is not None: +# pr.unload_texture(texture) + +# texture = pr.load_texture(path) +# current_path = path + + +# def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool: +# clicked = False + +# rect = pr.Rectangle(pos.x, pos.y, size.x, size.y) + +# # Optional hover background +# if pr.check_collision_point_rec(pr.get_mouse_position(), rect): +# pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) +# if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): +# clicked = True +# cx = pos.x + size.x / 2 +# cy = pos.y + size.y / 2 + +# icon_padding = size.x * 0.25 +# icon_size = size.x - icon_padding * 2 + +# if is_playing: +# # PAUSE (two bars centered, same visual weight as play) +# bar_width = icon_size * 0.25 +# bar_height = icon_size + +# left_x = cx - bar_width - bar_width * 0.4 +# right_x = cx + bar_width * 0.4 +# top_y = 1+cy - bar_height / 2 + +# pr.draw_rectangle( +# int(left_x), +# int(top_y), +# int(bar_width), +# int(bar_height), +# pr.WHITE, +# ) +# pr.draw_rectangle( +# int(right_x), +# int(top_y), +# int(bar_width), +# int(bar_height), +# pr.WHITE, +# ) +# else: +# # PLAY (centered triangle) +# p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2) +# p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2) +# p3 = pr.Vector2(cx + icon_size / 2, cy) + +# pr.draw_triangle(p1, p2, p3, pr.WHITE) + +# return clicked + + +# title = ScrollingText( +# "", +# 15 +# ) + +# # --- Main Game Loop --- +# while not pr.window_should_close(): +# # 1. Update +# current_width = pr.get_screen_width() +# current_height = pr.get_screen_height() + +# if pr.is_key_pressed(pr.KEY_F11): +# pr.toggle_fullscreen() +# if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): +# if player.playing: +# player.pause() +# else: +# player.play() +# if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT): +# player.seek(player.position - 5) +# if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT): +# player.seek(player.position + 5) + +# pr.begin_drawing() +# pr.clear_background(pr.Color(40, 40, 40, 255)) +# dt = pr.get_frame_time() + +# progress_rect = get_progress_bar_rect(current_width, current_height) + +# # pr.draw_text( +# # "UgPod", +# # int(current_width * 0.05), +# # int(current_height * 0.05), +# # int(current_height * 0.05), +# # pr.SKYBLUE, +# # ) + +# current_song = player.get_current_song() + + +# draw_progress_bar( +# progress_rect, +# player.position, +# (current_song and current_song.duration) or 0.0, +# ) +# if current_song: +# load_texture(current_song.album_cover_path) +# title_font_size = int(current_height*0.05) +# album_cover_size = int(min(current_width, current_height*0.7)) +# title.speed = title_font_size*2.5 +# title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size) +# title.update(dt,title_size) +# title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size) +# title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size) +# # pr.draw_text( +# # , +# # , +# # int(current_height * 0.03), +# # pr.WHITE, +# # ) +# points = player.oscilloscope_data_points +# if texture is not None: +# scale = min(album_cover_size / texture.width, album_cover_size / texture.height) + +# dest_rect = pr.Rectangle( +# current_width//2 - album_cover_size//2, +# (current_height*0.8)//2 - album_cover_size//2, +# texture.width * scale, +# texture.height * scale, +# ) + +# src_rect = pr.Rectangle(0, 0, texture.width, texture.height) + +# pr.draw_texture_pro( +# texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE +# ) +# else: +# clip = pr.Rectangle(int(current_width//2 - album_cover_size//2), +# int((current_height*0.8)//2 - album_cover_size//2), +# int(album_cover_size), +# int(album_cover_size)) +# pr.begin_scissor_mode( +# int(clip.x), +# int(clip.y), +# int(clip.width), +# int(clip.height), +# ) +# pr.draw_rectangle( +# int(clip.x), +# int(clip.y), +# int(clip.width), +# int(clip.height), pr.BLACK) + +# # cx = current_width * 0.5+1 +# # cy = current_height * 0.4+1 + +# # MAX_LEN = album_cover_size * 0.25 # tune this +# # MIN_ALPHA = 10 +# # MAX_ALPHA = 255 + +# # for i in range(len(points) - 1): +# # x1 = cx + points[i][0] * album_cover_size * 0.5 +# # y1 = cy + -points[i][1] * album_cover_size * 0.5 +# # x2 = cx + points[i+1][0] * album_cover_size * 0.5 +# # y2 = cy + -points[i+1][1] * album_cover_size * 0.5 + +# # dx = x2 - x1 +# # dy = y2 - y1 +# # length = (dx * dx + dy * dy) ** 0.5 + +# # # 1.0 = short line, 0.0 = long line +# # t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2) + +# # alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA)) + +# # color = pr.Color(255, 255, 255, alpha) + +# # pr.draw_line(int(x1), int(y1), int(x2), int(y2), color) +# # draw background square +# if len(points) >= 2: +# samples = np.fromiter( +# ((p[0] + p[1]) * 0.5 for p in points), +# dtype=np.float32 +# ) + +# # Guard: FFT must have meaningful size +# if samples.size > 128: + + +# rect_x = int(current_width // 2 - album_cover_size // 2) +# rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2) + +# # ---- FFT ---- + +# FFT_SIZE = min(samples.size, 2048) +# window = np.hanning(FFT_SIZE) + +# fft = np.fft.rfft(samples[:FFT_SIZE] * window) +# magnitudes = np.abs(fft) + +# # remove DC component (important for visuals) +# magnitudes[0] = 0.0 + +# # ---- LOG BINNING ---- + +# num_bars = album_cover_size//10 +# num_bins = magnitudes.size + +# # logarithmic bin edges (low end stretched) +# log_min = 1 +# log_max = math.log10(num_bins) + +# log_edges = np.logspace( +# math.log10(log_min), +# log_max, +# num_bars + 1 +# ).astype(int) + +# bar_values = np.zeros(num_bars, dtype=np.float32) + +# for i in range(num_bars): +# start = log_edges[i] +# end = log_edges[i + 1] + +# if end <= start: +# continue + +# bar_values[i] = np.mean(magnitudes[start:end]) + +# # ---- STATIC SCALING ---- + +# # Instead of normalizing to the max of the frame, we scale by the FFT size. +# # For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps +# # maximum possible volume roughly to 1.0. +# bar_values = bar_values / (FFT_SIZE / 4.0) + +# # ---- DRAW ---- + +# def map_to_screen(val): +# return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size + +# spacing = 0 + +# for i in range(num_bars): +# # 1. Calculate integer pixel boundaries first +# # This ensures the right edge of one bar is exactly the left edge of the next +# x_start_int = int(map_to_screen(log_edges[i])) +# x_end_int = int(map_to_screen(log_edges[i+1])) + +# # 2. Width is the difference between these fixed integer points +# w = (x_end_int - x_start_int) - spacing + +# value = bar_values[i] +# h = int(min(1.0, value) * album_cover_size) + +# # 3. Anchor to bottom +# y = (rect_y + album_cover_size) - h + +# alpha = min(1.0, ((value+1)**2)-1) +# r = 255 +# g = 0 +# b = 0 + +# # Keep alpha at 255 (fully opaque) +# color = pr.Color(r, g, b, int(255 * alpha)) + +# # 4. Draw the bar +# # Use max(1, w) to ensure high-frequency bars don't disappear on small screens +# pr.draw_rectangle( +# x_start_int, +# int(y), +# max(1, int(w)), +# h, +# color +# ) +# pr.end_scissor_mode() + +# pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) +# size = pr.Vector2(current_height * 0.1, current_height * 0.1) + +# if draw_play_pause_button(pos, size, player.playing): +# if player.playing: +# player.pause() +# else: +# player.play() +# pr.end_drawing() + +# # Cleanup +# if texture is not None: +# pr.unload_texture(texture) + +# pr.close_window() +# close_event.set() +# # save_state_thread.join() + + + + +player.play() +# while True: +# time.sleep(1) \ No newline at end of file From b753b340501e1e2d97c0f4a78504333e4fe44039 Mon Sep 17 00:00:00 2001 From: William Bell <62452284+Ugric@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:02:31 +0000 Subject: [PATCH 10/10] fix duration into the code --- old_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/old_app.py b/old_app.py index 453b9df..2537827 100644 --- a/old_app.py +++ b/old_app.py @@ -81,7 +81,7 @@ player.add_to_queue( "bruhh", "music/pink floyd/dark side of the moon/06 Money.flac", "Money", - 1, + 382.200000, "The Dark Side Of The Moon", "", "Pink Floyd", @@ -93,7 +93,7 @@ player.add_to_queue( "bruhh", "music/pink floyd/dark side of the moon/07 Us and Them.flac", "Us and Them", - 1, + 470.333333, "The Dark Side Of The Moon", "", "Pink Floyd",