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 os.makedirs("logs", exist_ok=True) 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_preload_state = 0 self.last_sample = 0.0 self.samples_since_last_sample = 0 self.closed=False self.playing = False self.position = 0.0 self.playback_info = None self.next_playback_info=None 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('logs/'+str(self.song)+".txt", "wb") ) def seek(self, pos): with self.lock: pos = min(max(0,pos), self.playback_info_to_duration(self.playback_info)) if self.proc: self.proc.kill() self.proc = self._open_ffmpeg(self.current_file, pos) self.position = pos def close(self): self.closed=True self.stream.close() def get_stream_info(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 ) return json.loads(result.stdout) 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: 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: return float(s["duration"]) if "format" in info and "duration" in info["format"]: return float(info["format"]["duration"]) return 0.0 def _callback(self, outdata, frames, t, status): with self.lock: needed = frames * self.channels * 2 data = b'' if self.playing: if self.proc is None: if self.next_preload_state==2: self._start_next() elif self.next_preload_state == 0: self.preload_next_threaded() else: data = self.proc.stdout.read(needed) or b'' self.position += len(data) / (self.samplerate * self.channels * 2) if self.position >= self.playback_info_to_duration(self.playback_info)-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)) # while True: # duration = player.playback_info_to_duration(player.playback_info) # print("pos:", str(round((player.position*100)/(duration or 1.0)))+"%", player.position, '/', duration) # time.sleep(1)