asteroid/docs/PLAYLIST-SYSTEM.org

10 KiB
Raw Blame History

Playlist System - Complete (MVP)

Overview

Implemented user playlist system with creation, storage, and playback functionality. Core features complete with database update limitations noted for PostgreSQL migration.

What Was Completed

Playlist Creation

  • Create empty playlists with name and description
  • Save queue as playlist (captures current queue state)
  • User-specific playlists (tied to user ID)
  • Automatic timestamp tracking

Playlist Management

  • View all user playlists
  • Display playlist metadata (name, track count, date)
  • Load playlists into play queue
  • Automatic playback on load

Playlist Playback

  • Load playlist tracks into queue
  • Start playing first track automatically
  • Queue displays remaining tracks
  • Full playback controls available

Features Implemented

User Interface

Playlist Creation Form

<div class="playlist-controls">
  <input type="text" id="new-playlist-name" placeholder="New playlist name...">
  <button id="create-playlist"> Create Playlist</button>
</div>

Playlist Display

  • Shows all user playlists
  • Displays track count
  • Load button for each playlist
  • Clean card-based layout

Queue Integration

  • "Save as Playlist" button in queue
  • Prompts for playlist name
  • Saves all queued tracks
  • Immediate feedback

API Endpoints

GET /api/playlists

Get all playlists for current user

{
  "status": "success",
  "playlists": [
    {
      "id": 12,
      "name": "My Favorites",
      "description": "Created from queue with 3 tracks",
      "track-count": 3,
      "created-date": 1759559112
    }
  ]
}

POST /api/playlists/create

Create a new playlist

POST /asteroid/api/playlists/create
Content-Type: application/x-www-form-urlencoded

name=My Playlist&description=Optional description

GET api/playlists:id

Get playlist details with tracks

{
  "status": "success",
  "playlist": {
    "id": 12,
    "name": "My Favorites",
    "tracks": [
      {
        "id": 1298,
        "title": ["City Lights From A Train"],
        "artist": ["Vector Lovers"],
        "album": ["Capsule For One"]
      }
    ]
  }
}

POST /api/playlists/add-track

Add track to playlist (limited by database backend)

POST /asteroid/api/playlists/add-track
Content-Type: application/x-www-form-urlencoded

playlist-id=12&track-id=1298

Technical Implementation

Database Schema

Playlists Collection

(db:create "playlists" 
  '((name :text)
    (description :text)
    (user-id :integer)
    (tracks :text)  ; List of track IDs
    (created-date :integer)
    (modified-date :integer)))

Backend Functions (playlist-management.lisp)

Create Playlist

(defun create-playlist (user-id name &optional description)
  "Create a new playlist for a user"
  (let ((playlist-data `(("user-id" ,user-id)
                         ("name" ,name)
                         ("description" ,(or description ""))
                         ("tracks" ())
                         ("created-date" ,(local-time:timestamp-to-unix (local-time:now)))
                         ("modified-date" ,(local-time:timestamp-to-unix (local-time:now))))))
    (db:insert "playlists" playlist-data)
    t))

Get User Playlists

(defun get-user-playlists (user-id)
  "Get all playlists for a user"
  ;; Manual filtering due to database ID type mismatch
  (let ((all-playlists (db:select "playlists" (db:query :all))))
    (remove-if-not (lambda (playlist)
                     (let ((stored-user-id (gethash "user-id" playlist)))
                       (or (equal stored-user-id user-id)
                           (and (listp stored-user-id) 
                                (equal (first stored-user-id) user-id)))))
                   all-playlists)))

Get Playlist by ID

(defun get-playlist-by-id (playlist-id)
  "Get a specific playlist by ID"
  ;; Manual search to handle ID type variations
  (let ((all-playlists (db:select "playlists" (db:query :all))))
    (find-if (lambda (playlist)
               (let ((stored-id (gethash "_id" playlist)))
                 (or (equal stored-id playlist-id)
                     (and (listp stored-id) 
                          (equal (first stored-id) playlist-id)))))
             all-playlists)))

Frontend Implementation

Save Queue as Playlist

async function saveQueueAsPlaylist() {
  const name = prompt('Enter playlist name:');
  if (!name) return;
  
  // Create playlist
  const formData = new FormData();
  formData.append('name', name);
  formData.append('description', `Created from queue with ${playQueue.length} tracks`);
  
  const response = await fetch('/asteroid/api/playlists/create', {
    method: 'POST',
    body: formData
  });
  
  // Add tracks to playlist
  for (const track of playQueue) {
    const addFormData = new FormData();
    addFormData.append('playlist-id', newPlaylist.id);
    addFormData.append('track-id', track.id);
    
    await fetch('/asteroid/api/playlists/add-track', {
      method: 'POST',
      body: addFormData
    });
  }
  
  alert(`Playlist "${name}" created with ${playQueue.length} tracks!`);
  loadPlaylists();
}

Load Playlist

async function loadPlaylist(playlistId) {
  const response = await fetch(`/asteroid/api/playlists/${playlistId}`);
  const result = await response.json();
  
  if (result.status === 'success' && result.playlist) {
    const playlist = result.playlist;
    
    // Clear current queue
    playQueue = [];
    
    // Add all playlist tracks to queue
    playlist.tracks.forEach(track => {
      const fullTrack = tracks.find(t => t.id === track.id);
      if (fullTrack) {
        playQueue.push(fullTrack);
      }
    });
    
    updateQueueDisplay();
    
    // Start playing first track
    if (playQueue.length > 0) {
      const firstTrack = playQueue.shift();
      const trackIndex = tracks.findIndex(t => t.id === firstTrack.id);
      if (trackIndex >= 0) {
        playTrack(trackIndex);
      }
    }
  }
}

Known Limitations (Requires PostgreSQL)

Database Update Issues

The current Radiance database backend has limitations:

Problem: Updates Don't Persist

;; This doesn't work reliably with current backend
(db:update "playlists"
           (db:query (:= "_id" playlist-id))
           `(("tracks" ,new-tracks)))

Impact

  • Cannot add tracks to existing playlists after creation
  • Cannot modify playlist metadata after creation
  • Workaround: Create playlist with all tracks at once (save queue as playlist)

Solution

Migration to PostgreSQL will resolve this:

  • Proper UPDATE query support
  • Consistent data types
  • Better query matching
  • Full CRUD operations

Type Handling Issues

Database stores some values as lists when they should be scalars:

  • user-id stored as (2) instead of 2
  • _id sometimes wrapped in list
  • Requires manual type checking in queries

Current Workaround

;; Handle both scalar and list values
(let ((stored-id (gethash "_id" playlist)))
  (or (equal stored-id playlist-id)
      (and (listp stored-id) 
           (equal (first stored-id) playlist-id))))

Working Features (MVP)

Core Workflow

  1. User adds tracks to queue
  2. User saves queue as playlist
  3. Playlist created with all tracks
  4. User can view playlists
  5. User can load and play playlists

Tested Scenarios

  • Create empty playlist
  • Save 3-track queue as playlist
  • Load playlist into queue
  • Play playlist tracks
  • Multiple playlists per user
  • Playlist persistence across sessions

Files Created/Modified

New Files

  • playlist-management.lisp - Core playlist functions
  • docs/PLAYLIST-SYSTEM.org - This documentation

Modified Files

  • asteroid.asd - Added playlist-management.lisp
  • asteroid.lisp - Added playlist API endpoints
  • template/player.chtml - Added playlist UI and functions
  • database.lisp - Playlists collection schema

Future Enhancements (Post-PostgreSQL)

Playlist Editing

  • Add tracks to existing playlists
  • Remove tracks from playlists
  • Reorder tracks
  • Update playlist metadata

Advanced Features

  • Playlist sharing
  • Collaborative playlists
  • Playlist import/export
  • Smart playlists (auto-generated)
  • Playlist statistics

Liquidsoap Integration

  • Stream user playlists
  • Scheduled playlist playback
  • Multiple mount points per user
  • Real-time playlist updates

Status: ⚠️ PARTIAL - Core Features Working, Playlist Playback Limited

Core functionality working. Users can browse and play tracks from library. Audio playback functional after adding get-track-by-id function with type mismatch handling. Playlist system has significant limitations due to database backend issues.

What Works Now

  • Browse track library (with pagination)
  • Play tracks from library
  • Add tracks to queue
  • Audio playback (fixed: added get-track-by-id with manual search)
  • Create empty playlists
  • View playlists

What Doesn't Work (Database Limitations)

  • Save queue as playlist (tracks don't persist - database update fails)
  • Load playlists (playlists are empty - no tracks saved)
  • Playlist playback (no tracks in playlists to play)
  • Add tracks to existing playlists (database update limitation)
  • Edit playlist metadata (database update limitation)
  • Remove tracks from playlists (database update limitation)

Root Cause

The Radiance default database backend has critical limitations:

  1. db:update queries don't persist changes
  2. Type mismatches (IDs stored as lists vs scalars)
  3. Query matching failures

Workaround

None available with current database backend. Full playlist functionality requires PostgreSQL migration.

Recent Fix (2025-10-04)

Added missing get-track-by-id function to enable audio streaming:

(defun get-track-by-id (track-id)
  "Get a track by its ID"
  (let ((tracks (db:select "tracks" (db:query (:= "_id" track-id)))))
    (when (> (length tracks) 0)
      (first tracks))))

This function is required by the /tracks/:id/stream endpoint for audio playback.