Skip to content

Self hosted alternative for a Music Party like Apple Music SharePlay or like a discord music bot in real life. Never fight over AUX again.

Notifications You must be signed in to change notification settings

sloorjuice/JukeboxV2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JukeboxV2 — API Reference & Frontend Integration

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

Table of Contents

Quick start

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.txt

Start the FastAPI server (example using uvicorn):

# from the JukeboxV2 folder
uvicorn main:app --host 0.0.0.0 --port 8000 --reload

High-level contract

  • 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.

API Reference

Base URL for examples: http://localhost:8000

All endpoints return JSON on success (unless streaming SSE).

POST /search_and_request_song

  • 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' })
});

POST /request_song_url

  • 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' })
});

GET /get_queue

  • 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();

GET /current_song

  • 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 title
    • channel: Uploader/artist name
    • duration: Total length in seconds
    • current_progress: Current playback position in seconds
    • thumbnail: Video thumbnail URL (highest quality available)
    • url: Original YouTube URL
    • is_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)`);
}

GET /get_volume

  • Description: Get current volume. Response: { "volume": 0-100 }

POST /set_volume

  • Description: Set playback volume.
  • Body: { "volume": <0-100> }
  • Success: { "status": "volume_set", "volume": }
  • Errors: 400 if out-of-range or no_song_playing

POST /pause_playback and POST /resume_playback

  • Pause: POST /pause_playback — returns { "status": "paused" }
  • Resume: POST /resume_playback — returns { "status": "resumed" }

POST /skip

  • Description: Skip the currently playing song.
  • Response: { "status": "skipped" } or 400 if no song playing.

GET /get_current_audio_device

  • Description: Get the currently selected audio device.
  • Response:
    {
      "device_id": "device_identifier_or_null",
      "description": "Device Name or System Default"
    }
  • Returns null device_id and "System Default" description if no device is explicitly set.

GET /get_audio_devices

  • 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.

POST /set_audio_device

  • 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: null to set system default.

SSE: GET /events

  • 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);

Example React Hook: SSE + state

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 };
}

Example frontend actions (fetch wrappers)

JavaScript/TypeScript

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;
}

Troubleshooting & Notes

  • VLC: The server uses libVLC via python-vlc. Ensure system VLC/libvlc is installed and accessible.
  • yt_dlp: The server uses yt_dlp to 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_devices enumerates 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).

Frontend Project

A complete Next.js/React frontend is available in the jukebox_frontend repository with:

  • Full TypeScript implementation
  • Real-time SSE updates via useJukeboxEvents hook
  • API wrapper functions in src/lib/api.ts
  • React components for playback control and queue management

End of documentation.

About

Self hosted alternative for a Music Party like Apple Music SharePlay or like a discord music bot in real life. Never fight over AUX again.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages