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 import threading import queue import sys import io class FFQueuePlayer: def __init__(self, samplerate=44100, channels=2): self.samplerate = samplerate self.channels = channels self.proc = None self.next_proc = None self.current_file = None self.next_file = None self.next_preloaded = False self.closed=False self.playing = False self.position = 0.0 self.duration = 1.0 self.next_duration=1.0 self.song = 0 self.song_queue = queue.Queue() self.swap_pending = False self.lock = threading.Lock() self.stream = sd.RawOutputStream( samplerate=self.samplerate, channels=self.channels, dtype="int16", callback=self._callback ) self.stream.start() def _open_ffmpeg(self, url, seek=0): self.song+=1 return subprocess.Popen( [ "ffmpeg", "-ss", str(seek), "-i", url, "-f", "s16le", "-ac", str(self.channels), "-ar", str(self.samplerate), "-loglevel", "verbose", "-" ], stdout=subprocess.PIPE, stderr=open(str(self.song)+".txt", "wb") ) def seek(self, pos): with self.lock: self.proc = self._open_ffmpeg(self.current_file, pos) self.position = pos def close(self): self.closed=True self.stream.close() def get_duration(self, url): """Return duration in seconds for the track""" try: result = subprocess.run( [ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", url ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) info = json.loads(result.stdout) # Prefer stream duration → fallback to format duration if "streams" in info: for s in info["streams"]: if "duration" in s: return float(s["duration"]) if "format" in info and "duration" in info["format"]: return float(info["format"]["duration"]) except Exception as e: print("ffprobe error:", e) return None def add_to_queue(self, url): self.song_queue.put_nowait(url) def play(self): with self.lock: if not self.playing: self.playing = True def pause(self): with self.lock: 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.duration = self.next_duration self.next_proc=None self.next_preloaded = False def preload_next(self): self.next_file = self.song_queue.get() self.next_duration = self.get_duration(self.next_file) self.next_proc = self._open_ffmpeg(self.next_file) self.next_preloaded = True def preload_next_threaded(self): if self.next_preloaded: return self.next_preloaded = True threading.Thread(target=self.preload_next).start() def _callback(self, outdata, frames, t, status): with self.lock: needed = frames * self.channels * 2 data = b'' if self.proc is None: self.preload_next() self._start_next() else: data = self.proc.stdout.read(needed) or b'' self.position += len(data) / (self.samplerate * self.channels * 2) if self.position >= self.duration-10: self.preload_next_threaded() if self.proc.poll() is not None and len(data) str: """ Build a Jellyfin audio stream URL using urllib.parse. """ path = f"/Items/{item_id}/Download" params = { "UserId": user_id, "Container": container, "AudioCodec": audio_codec, # <-- IMPORTANT "api_key": api_key, } if bitrate is not None: params["Bitrate"] = bitrate if media_source_id is not None: params["MediaSourceId"] = media_source_id query = urlencode(params) return urljoin(base_url, path) + "?" + query client = JellyfinClient() load_dotenv() client.config.app('FinPod', '0.0.1', 'FinPod prototype', 'FinPod_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)) player = FFQueuePlayer() # Build Jellyfin URLs # Add to queue print("add queue") player.add_to_queue(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("916eda422f48efd8705f29e0600a3e60")["Id"], server["AccessToken"], server["UserId"])) player.add_to_queue(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("8bcf8240d12aa5c3b14dc3b57f32fef7")["Id"], server["AccessToken"], server["UserId"])) print("add queue done") player.play() while True: print("pos:", str(round((player.position*100)/player.duration))+"%", player.position, '/', player.duration) time.sleep(1) player.close()