change the layout and fix f string bug on raspberry pi python 3
This commit is contained in:
392
app.py
392
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()
|
||||
@@ -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)
|
||||
|
||||
41
jelly.py
Normal file
41
jelly.py
Normal file
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user