change to an album based queuing system with history and display album cover, song and album name
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -175,4 +175,5 @@ cython_debug/
|
|||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
music
|
music
|
||||||
logs
|
logs
|
||||||
|
data
|
||||||
140
app.py
140
app.py
@@ -1,7 +1,7 @@
|
|||||||
import pyray as pr
|
import pyray as pr
|
||||||
import math
|
import math
|
||||||
from ctypes import c_float
|
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 ---
|
# --- Configuration Constants ---
|
||||||
INITIAL_SCREEN_WIDTH = 800
|
INITIAL_SCREEN_WIDTH = 800
|
||||||
@@ -176,34 +176,53 @@ pr.set_target_fps(TARGET_FPS)
|
|||||||
player = GaplessPlayer()
|
player = GaplessPlayer()
|
||||||
|
|
||||||
print("add queue")
|
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"], , 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("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("38a6c21561f54d284a6acad89a3ea8b0")["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("631aeddb0557fef65f49463abb20ad7f")["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("3d611c8664c5b2072edbf46da2a76c89")["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("66559c40d5904944a3f97198d0297894")["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(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b")["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(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4")["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(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f37982227942d3df031381e653ec5790")["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(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/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(song_data_to_Song(client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"), server))
|
||||||
player.add_to_queue('music/pink floyd/dark side of the moon/06 Money.flac')
|
player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3"), server))
|
||||||
player.add_to_queue('music/pink floyd/dark side of the moon/07 Us and Them.flac')
|
player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6"), server))
|
||||||
player.add_to_queue('music/pink floyd/dark side of the moon/08 Any Colour You Like.flac')
|
player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6"), server))
|
||||||
player.add_to_queue('music/pink floyd/dark side of the moon/09 Brain Damage.flac')
|
player.add_to_queue(song_data_to_Song(client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0"), server))
|
||||||
player.add_to_queue('music/pink floyd/dark side of the moon/10 Eclipse.flac')
|
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")
|
print("add queue done")
|
||||||
|
|
||||||
# Initial setup
|
# Initial setup
|
||||||
@@ -214,6 +233,23 @@ state["camera"] = setup_3d_environment(int(render_rect.width), int(render_rect.h
|
|||||||
# LOAD THE ASSETS
|
# LOAD THE ASSETS
|
||||||
state["album_texture"], state["album_model"] = load_album_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 ---
|
# --- Main Game Loop ---
|
||||||
while not pr.window_should_close():
|
while not pr.window_should_close():
|
||||||
# 1. Update
|
# 1. Update
|
||||||
@@ -226,8 +262,6 @@ while not pr.window_should_close():
|
|||||||
render_rect = get_3d_render_area(current_width, current_height)
|
render_rect = get_3d_render_area(current_width, current_height)
|
||||||
pr.unload_render_texture(state["render_texture"])
|
pr.unload_render_texture(state["render_texture"])
|
||||||
state["render_texture"] = pr.load_render_texture(int(render_rect.width), int(render_rect.height))
|
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 pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE):
|
||||||
if player.playing:
|
if player.playing:
|
||||||
@@ -270,15 +304,53 @@ while not pr.window_should_close():
|
|||||||
pr.draw_texture_pro(state["render_texture"].texture,
|
pr.draw_texture_pro(state["render_texture"].texture,
|
||||||
source_rect, render_rect, pr.Vector2(0, 0), 0.0, pr.WHITE)
|
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))
|
current_song = 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.LIME)
|
|
||||||
|
|
||||||
|
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()
|
pr.end_drawing()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
if texture is not None:
|
||||||
|
pr.unload_texture(texture)
|
||||||
|
|
||||||
# --- De-initialization ---
|
# --- De-initialization ---
|
||||||
pr.unload_texture(state["album_texture"]) # Unload the texture
|
pr.unload_texture(state["album_texture"]) # Unload the texture
|
||||||
pr.unload_model(state["album_model"]) # Unload the model/mesh
|
pr.unload_model(state["album_model"]) # Unload the model/mesh
|
||||||
|
|||||||
@@ -18,16 +18,20 @@ import sys
|
|||||||
import io
|
import io
|
||||||
import fcntl
|
import fcntl
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
os.makedirs("logs", exist_ok=True)
|
os.makedirs("logs", exist_ok=True)
|
||||||
|
os.makedirs("data", exist_ok=True)
|
||||||
|
os.makedirs("data/images", exist_ok=True)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Song:
|
class Song:
|
||||||
url: str
|
url: str
|
||||||
duration: float
|
|
||||||
name: str
|
name: str
|
||||||
album_name:str
|
album_name:str
|
||||||
album_cover:str
|
album_cover_path:str
|
||||||
artist_name:str
|
artist_name:str
|
||||||
|
|
||||||
|
|
||||||
@@ -36,12 +40,6 @@ class GaplessPlayer:
|
|||||||
self.samplerate = samplerate
|
self.samplerate = samplerate
|
||||||
self.channels = channels
|
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.next_preload_state = 0
|
||||||
|
|
||||||
self.closed=False
|
self.closed=False
|
||||||
@@ -62,15 +60,18 @@ class GaplessPlayer:
|
|||||||
callback=self._callback
|
callback=self._callback
|
||||||
)
|
)
|
||||||
self.stream.start()
|
self.stream.start()
|
||||||
|
|
||||||
|
def get_current_song(self):
|
||||||
|
if 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, url, seek=0):
|
def _open_ffmpeg(self, song, seek=0):
|
||||||
self.song+=1
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
# "-re",
|
# "-re",
|
||||||
"-ss", str(seek),
|
"-ss", str(seek),
|
||||||
"-i", url,
|
"-i", song.url,
|
||||||
"-f", "s16le",
|
"-f", "s16le",
|
||||||
"-ac", str(self.channels),
|
"-ac", str(self.channels),
|
||||||
"-ar", str(self.samplerate),
|
"-ar", str(self.samplerate),
|
||||||
@@ -90,15 +91,63 @@ class GaplessPlayer:
|
|||||||
|
|
||||||
def seek(self, pos):
|
def seek(self, pos):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
pos = min(max(0,pos), self.playback_info_to_duration(self.playback_info))
|
song = self.get_current_song()
|
||||||
if self.proc:
|
if song:
|
||||||
self.proc.kill()
|
pos = min(max(0,pos), self.song_to_duration(song))
|
||||||
self.proc = self._open_ffmpeg(self.current_file, pos)
|
if song.ffmpeg:
|
||||||
self.position = pos
|
song.ffmpeg.kill()
|
||||||
|
song.ffmpeg = self._open_ffmpeg(song, pos)
|
||||||
|
self.position = pos
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.closed=True
|
self.closed=True
|
||||||
self.stream.close()
|
self.stream.close()
|
||||||
|
def add_to_queue(self, song:Song):
|
||||||
|
song.ffmpeg = None
|
||||||
|
song.playback_info = None
|
||||||
|
song.preload_state = 0
|
||||||
|
self.song_list.append(song)
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
with self.lock:
|
||||||
|
if not self.playing:
|
||||||
|
current_song = self.get_current_song()
|
||||||
|
if current_song and not current_song.ffmpeg:
|
||||||
|
current_song.ffmpeg = self._open_ffmpeg(current_song, self.position)
|
||||||
|
self.playing = True
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
with self.lock:
|
||||||
|
# current_song = self.get_current_song()
|
||||||
|
# if current_song and current_song.ffmpeg:
|
||||||
|
# current_song.ffmpeg.kill()
|
||||||
|
# current_song.ffmpeg = None
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
|
def _start_next(self):
|
||||||
|
# Kill old pipeline
|
||||||
|
current_song = self.get_current_song()
|
||||||
|
if current_song and current_song.ffmpeg:
|
||||||
|
current_song.ffmpeg.kill()
|
||||||
|
|
||||||
|
# Move next pipeline into active
|
||||||
|
self.position = 0.0
|
||||||
|
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]
|
||||||
|
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):
|
def get_stream_info(self, url):
|
||||||
"""Return duration in seconds for the track"""
|
"""Return duration in seconds for the track"""
|
||||||
try:
|
try:
|
||||||
@@ -115,64 +164,21 @@ class GaplessPlayer:
|
|||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True
|
text=True
|
||||||
)
|
)
|
||||||
|
|
||||||
return json.loads(result.stdout)
|
return json.loads(result.stdout)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("ffprobe error:", e)
|
print("ffprobe error:", e)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
def add_to_queue(self, song:Song):
|
def song_to_duration(self, song):
|
||||||
self.current_song_in_list.append(song)
|
if not song or not song.playback_info: return 0.0
|
||||||
|
if "streams" in song.playback_info:
|
||||||
def play(self):
|
for s in song.playback_info["streams"]:
|
||||||
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"]:
|
|
||||||
if "duration" in s:
|
if "duration" in s:
|
||||||
return float(s["duration"])
|
return float(s["duration"])
|
||||||
|
|
||||||
if "format" in info and "duration" in info["format"]:
|
if "format" in song.playback_info and "duration" in song.playback_info["format"]:
|
||||||
return float(info["format"]["duration"])
|
return float(song.playback_info["format"]["duration"])
|
||||||
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
@@ -181,67 +187,78 @@ class GaplessPlayer:
|
|||||||
needed = frames * self.channels * 2
|
needed = frames * self.channels * 2
|
||||||
data = b''
|
data = b''
|
||||||
if self.playing:
|
if self.playing:
|
||||||
if self.proc is None:
|
current_song = self.get_current_song()
|
||||||
if self.next_preload_state==2:
|
if not current_song or current_song.ffmpeg is None:
|
||||||
self._start_next()
|
next_song = self.get_next_song()
|
||||||
elif self.next_preload_state == 0:
|
if next_song:
|
||||||
self.preload_next_threaded()
|
if next_song.preload_state==2:
|
||||||
else:
|
self._start_next()
|
||||||
|
elif next_song.preload_state == 0:
|
||||||
|
self.preload_next_threaded()
|
||||||
|
elif current_song:
|
||||||
try:
|
try:
|
||||||
data = self.proc.stdout.read(needed) or b''
|
data = current_song.ffmpeg.stdout.read(needed) or b''
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
pass
|
pass
|
||||||
self.position += len(data) / (self.samplerate * self.channels * 2)
|
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()
|
self.preload_next_threaded()
|
||||||
if self.proc.poll() is not None and len(data)<needed:
|
if current_song.ffmpeg.poll() is not None and len(data)<needed:
|
||||||
if round(self.position, 2) >= self.playback_info_to_duration(self.playback_info)-0.1:
|
if round(self.position, 2) >= self.song_to_duration(current_song)-0.1:
|
||||||
self._start_next()
|
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:
|
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:
|
except BlockingIOError:
|
||||||
new_data = b''
|
new_data = b''
|
||||||
self.position += len(new_data) / (self.samplerate * self.channels * 2)
|
self.position += len(new_data) / (self.samplerate * self.channels * 2)
|
||||||
data += new_data
|
data += new_data
|
||||||
else:
|
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)] = data
|
||||||
|
|
||||||
outdata[len(data):] = b'\x00'*(needed-len(data))
|
outdata[len(data):] = b'\x00'*(needed-len(data))
|
||||||
|
|
||||||
|
|
||||||
def build_jellyfin_audio_url(
|
def song_data_to_Song(
|
||||||
base_url: str,
|
data,
|
||||||
item_id: str,
|
client_data
|
||||||
api_key: str,
|
) -> Song:
|
||||||
user_id: str,
|
# """
|
||||||
container: str = "flac",
|
# Build a Jellyfin audio stream URL using urllib.parse.
|
||||||
audio_codec: str = "flac",
|
# """
|
||||||
bitrate: int | None = None,
|
item_id = data["Id"]
|
||||||
media_source_id: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Build a Jellyfin audio stream URL using urllib.parse.
|
|
||||||
"""
|
|
||||||
path = f"/Audio/{item_id}/universal"
|
path = f"/Audio/{item_id}/universal"
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"UserId": user_id,
|
"UserId": client_data["UserId"],
|
||||||
"Container": container,
|
"Container": "flac",
|
||||||
"AudioCodec": audio_codec, # <-- IMPORTANT
|
"AudioCodec": "flac", # <-- IMPORTANT
|
||||||
"api_key": api_key,
|
"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)
|
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()
|
client = JellyfinClient()
|
||||||
|
|||||||
Reference in New Issue
Block a user