change the layout and fix f string bug on raspberry pi python 3

This commit is contained in:
William Bell
2025-12-26 19:47:30 +00:00
parent d069150c54
commit d86587e526
6 changed files with 371 additions and 253 deletions

392
app.py
View File

@@ -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()

View File

@@ -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
View 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]

View File

@@ -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

2
run-fb.sh Executable file
View File

@@ -0,0 +1,2 @@
export LIBGL_ALWAYS_SOFTWARE=1
python3 app.py

View File

@@ -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,