This document describes the FastAPI backend (see main.py) and how to integrate it from a browser-based frontend. It includes:
- Quick start (run the server)
- Full API reference (endpoints, bodies, responses)
- Server-Sent Events (SSE) usage
- JavaScript and React examples (fetch + EventSource)
- Notes and troubleshooting
- JukeboxV2 — API Reference & Frontend Integration
Requirements: Python 3.8+, system VLC available (libVLC), and the Python requirements in requirements.txt.
Install dependencies (from the repository root):
python -m pip install -r requirements.txtStart the FastAPI server (example using uvicorn):
# from the JukeboxV2 folder
uvicorn main:app --host 0.0.0.0 --port 8000 --reload- Inputs: REST requests (JSON) to control queue, playback, volume, and audio device.
- Real-time updates: Server-Sent Events on
/events(EventSource in the browser). - Outputs: JSON responses for REST calls; event stream messages for state changes.
Success criteria: able to queue songs (YouTube URLs or search), receive updates via SSE, and control playback (pause/resume/skip/set volume).
Edge cases covered: invalid URLs, extraction failures, no-song-playing errors, and client SSE disconnects.
Base URL for examples: http://localhost:8000
All endpoints return JSON on success (unless streaming SSE).
- Description: Search YouTube for a prompt (first result) and queue the found video for extraction and playback.
- Request body (JSON): { "prompt": "search terms" }
- Success response: 200 { "status": "queued_for_extraction" }
- Errors: 404 if nothing found, 500 on server error
Example (fetch):
await fetch('http://localhost:8000/search_and_request_song', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ prompt: 'Daft Punk around the world' })
});- Description: Submit a direct YouTube URL (or other supported URL) to be extracted and queued.
- Request body: { "url": "https://www.youtube.com/watch?v=..." }
- Success: 200 { "status": "queued_for_extraction" }
Example:
await fetch('http://localhost:8000/request_song_url', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' })
});- Description: Return the queued songs.
- Response: { "queue": [ { /* song info objects queued */ } ] }
Example:
const resp = await fetch('http://localhost:8000/get_queue');
const { queue } = await resp.json();-
Description: Return the currently playing song with detailed metadata and playback progress (or null if nothing is playing).
-
Response:
{ "current_song": { "title": "Song Title", "channel": "Artist/Channel Name", "duration": 180, "current_progress": 45, "thumbnail": "https://i.ytimg.com/vi/.../maxresdefault.jpg", "url": "https://www.youtube.com/watch?v=...", "is_playing": true } }or
{ "current_song": null }if no song is playing. -
Fields:
title: Song/video titlechannel: Uploader/artist nameduration: Total length in secondscurrent_progress: Current playback position in secondsthumbnail: Video thumbnail URL (highest quality available)url: Original YouTube URLis_playing: Boolean indicating if playback is active
Example:
const resp = await fetch('http://localhost:8000/current_song');
const { current_song } = await resp.json();
if (current_song) {
console.log(`Playing: ${current_song.title} (${current_song.current_progress}/${current_song.duration}s)`);
}- Description: Get current volume. Response: { "volume": 0-100 }
- Description: Set playback volume.
- Body: { "volume": <0-100> }
- Success: { "status": "volume_set", "volume": }
- Errors: 400 if out-of-range or no_song_playing
- Pause: POST /pause_playback — returns { "status": "paused" }
- Resume: POST /resume_playback — returns { "status": "resumed" }
- Description: Skip the currently playing song.
- Response: { "status": "skipped" } or 400 if no song playing.
- Description: Get the currently selected audio device.
- Response:
{ "device_id": "device_identifier_or_null", "description": "Device Name or System Default" } - Returns
nulldevice_id and "System Default" description if no device is explicitly set.
- Description: Get list of all available audio output devices.
- Response: { "devices": [ { device_id, description }, ... ] }
- Includes "System Default" as the first option with
device_id: null.
- Description: Set the audio output device for playback.
- Request body: { "device_id": "" | null }
- Response: { "status": "device_changed", "device_id": "" } if currently playing
- Response: { "status": "device_set_for_next_song", "device_id": "" } if no song playing
- Use
device_id: nullto set system default.
- Description: Server-Sent Events endpoint. Connect from the browser using EventSource.
- Content-Type: text/event-stream
Initial events on connect:
- event: connected — { status: 'connected' }
- event: song_started — current song if any
- event: queue_updated — current queue
- event: volume_changed — current volume
Runtime events broadcasted by the server (useful list):
- song_started — { title, audio_url, duration, thumbnail, channel, url }
- playback_progress — { current_progress, duration, is_playing } (sent every second during playback)
- song_ended — { title }
- queue_updated — { queue: [ { title }, ... ] }
- volume_changed — { volume }
- playback_paused / playback_resumed — { status }
- song_skipped — { status: 'skipped' }
Example SSE connect (vanilla JS):
const es = new EventSource('http://localhost:8000/events');
es.addEventListener('connected', (e) => console.log('SSE connected'));
es.addEventListener('song_started', (e) => {
const d = JSON.parse(e.data);
console.log('Now playing', d.title, d.thumbnail);
});
es.addEventListener('playback_progress', (e) => {
const d = JSON.parse(e.data);
console.log(`Progress: ${d.current_progress}/${d.duration}s`);
});
es.addEventListener('queue_updated', (e) => {
const d = JSON.parse(e.data);
console.log('Queue updated', d.queue);
});
es.onerror = (err) => console.error('SSE error', err);This small hook connects to the SSE stream and keeps local state up to date.
import { useEffect, useState } from 'react';
interface ProgressData {
current_progress: number;
duration: number;
is_playing: boolean;
}
export function useJukeboxEvents(url = 'http://localhost:8000/events') {
const [currentSong, setCurrentSong] = useState<CurrentSong | null>(null);
const [queue, setQueue] = useState<Song[]>([]);
const [volume, setVolume] = useState(100);
const [progress, setProgress] = useState<ProgressData>({
current_progress: 0,
duration: 0,
is_playing: false
});
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const es = new EventSource(url);
es.addEventListener('connected', () => {
console.log('SSE connected');
setIsConnected(true);
});
es.addEventListener('song_started', (e) => {
const data = JSON.parse(e.data);
setCurrentSong({
...data,
current_progress: 0,
is_playing: true
});
setProgress({
current_progress: 0,
duration: data.duration || 0,
is_playing: true
});
});
es.addEventListener('playback_progress', (e) => {
const data = JSON.parse(e.data);
setProgress(data);
// Update current song with progress
setCurrentSong(prev => prev ? {
...prev,
current_progress: data.current_progress,
is_playing: data.is_playing,
duration: data.duration
} : null);
});
es.addEventListener('song_ended', () => {
setCurrentSong(null);
setProgress({ current_progress: 0, duration: 0, is_playing: false });
});
es.addEventListener('song_skipped', () => {
setCurrentSong(null);
setProgress({ current_progress: 0, duration: 0, is_playing: false });
});
es.addEventListener('queue_updated', (e) => {
const data = JSON.parse(e.data);
setQueue(data.queue || []);
});
es.addEventListener('volume_changed', (e) => {
const data = JSON.parse(e.data);
setVolume(data.volume);
});
es.addEventListener('playback_paused', () => {
setProgress(prev => ({ ...prev, is_playing: false }));
setCurrentSong(prev => prev ? { ...prev, is_playing: false } : null);
});
es.addEventListener('playback_resumed', () => {
setProgress(prev => ({ ...prev, is_playing: true }));
setCurrentSong(prev => prev ? { ...prev, is_playing: true } : null);
});
es.onerror = (error) => {
console.error('SSE error', error);
setIsConnected(false);
};
return () => es.close();
}, [url]);
return { currentSong, queue, volume, progress, isConnected };
}const API_BASE_URL = 'http://localhost:8000';
// Search and request song
async function searchAndRequest(prompt: string): Promise<void> {
await fetch(`${API_BASE_URL}/search_and_request_song`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ prompt })
});
}
// Request by URL
async function requestSongByUrl(url: string): Promise<void> {
await fetch(`${API_BASE_URL}/request_song_url`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ url })
});
}
// Get queue
async function getQueue(): Promise<Song[]> {
const response = await fetch(`${API_BASE_URL}/get_queue`);
const data = await response.json();
return data.queue || [];
}
// Get current song
async function getCurrentSong(): Promise<CurrentSong | null> {
const response = await fetch(`${API_BASE_URL}/current_song`);
const data = await response.json();
return data.current_song;
}
// Playback controls
async function pause(): Promise<void> {
await fetch(`${API_BASE_URL}/pause_playback`, { method: 'POST' });
}
async function resume(): Promise<void> {
await fetch(`${API_BASE_URL}/resume_playback`, { method: 'POST' });
}
async function skip(): Promise<void> {
await fetch(`${API_BASE_URL}/skip`, { method: 'POST' });
}
// Volume control
async function getVolume(): Promise<number> {
const response = await fetch(`${API_BASE_URL}/get_volume`);
const data = await response.json();
return data.volume;
}
async function setVolume(value: number): Promise<void> {
await fetch(`${API_BASE_URL}/set_volume`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ volume: value })
});
}
// Audio device management
async function getAudioDevices(): Promise<AudioDevice[]> {
const response = await fetch(`${API_BASE_URL}/get_audio_devices`);
const data = await response.json();
return data.devices || [];
}
async function getCurrentAudioDevice(): Promise<AudioDevice> {
const response = await fetch(`${API_BASE_URL}/get_current_audio_device`);
return await response.json();
}
async function setAudioDevice(deviceId: string | null): Promise<void> {
await fetch(`${API_BASE_URL}/set_audio_device`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ device_id: deviceId })
});
}
// TypeScript interfaces
interface Song {
title: string;
channel?: string;
duration?: number;
thumbnail?: string;
url?: string;
}
interface CurrentSong extends Song {
current_progress: number;
is_playing: boolean;
}
interface AudioDevice {
device_id: string | null;
description: string;
}- VLC: The server uses libVLC via python-vlc. Ensure system VLC/libvlc is installed and accessible.
- yt_dlp: The server uses
yt_dlpto extract direct audio URLs; some videos or regions may be blocked or removed — handle errors accordingly. - Audio devices: Not all platforms expose device IDs in the same format;
get_audio_devicesenumerates available outputs via libVLC. Switching devices while playing attempts to set the device on the active player. - Running headless: The server controls audio on the host machine. If you run on a server without an audio system, expected playback won't be audible.
- SSE reconnection: The frontend implements automatic reconnection with a 3-second delay if the SSE connection drops.
- CORS: The backend is configured to accept requests from
http://localhost:3000(Next.js default port).
A complete Next.js/React frontend is available in the jukebox_frontend repository with:
- Full TypeScript implementation
- Real-time SSE updates via
useJukeboxEventshook - API wrapper functions in
src/lib/api.ts - React components for playback control and queue management
End of documentation.