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)