import pyray as pr import math from ctypes import c_float from player import FFQueuePlayer, build_jellyfin_audio_url, server, client # --- Configuration Constants --- INITIAL_SCREEN_WIDTH = 800 INITIAL_SCREEN_HEIGHT = 600 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 --- 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}" # --- 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 x = (screen_width - width) / 2 y = screen_height * 0.75 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), 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) # --- 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.init_window(state["screen_width"], state["screen_height"], "UgPod") pr.set_target_fps(TARGET_FPS) player = FFQueuePlayer() 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"], 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') 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() # --- 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)) delta_time = pr.get_frame_time() 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) # ---------------------------------------------------- # 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"]) pr.end_texture_mode() # ---------------------------------------------------- # 3. DRAW 2D GUI # ---------------------------------------------------- pr.begin_drawing() pr.clear_background(pr.Color(40, 40, 40, 255)) 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.LIME) 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) pr.end_drawing() # --- 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()