feat: YouTube music download system with admin dashboard

Sidecar architecture: edge functions (auth + DB) → youtube-worker container
(youtubei.js for playlist metadata, yt-dlp for audio downloads).

- Edge functions: youtube-playlist, youtube-process, youtube-status (CRUD)
- youtube-worker sidecar: Node.js + yt-dlp, downloads M4A to Supabase Storage
- Admin music page: import playlists, process queue, inline genre editing
- 12 music genres with per-track assignment and import-time defaults
- DB migrations: download_jobs, download_items tables with genre column
- Deploy script and CI workflow for edge functions + sidecar
This commit is contained in:
Millian Lamiaux
2026-03-26 10:47:05 +01:00
parent 8926de58e5
commit 3d8d9efd70
21 changed files with 2426 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
-- ============================================================
-- YouTube Playlist Download Jobs
-- ============================================================
-- Tracks playlist-level download jobs and per-video items
-- Used by the youtube-playlist, youtube-process, youtube-status edge functions
-- Download jobs table (one row per playlist import)
CREATE TABLE IF NOT EXISTS public.download_jobs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
playlist_url TEXT NOT NULL,
playlist_title TEXT,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
total_items INTEGER NOT NULL DEFAULT 0,
completed_items INTEGER NOT NULL DEFAULT 0,
failed_items INTEGER NOT NULL DEFAULT 0,
created_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Download items table (one row per video in the playlist)
CREATE TABLE IF NOT EXISTS public.download_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
job_id UUID NOT NULL REFERENCES public.download_jobs(id) ON DELETE CASCADE,
video_id TEXT NOT NULL,
title TEXT,
duration_seconds INTEGER,
thumbnail_url TEXT,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'downloading', 'completed', 'failed')),
storage_path TEXT,
public_url TEXT,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- ============================================================
-- INDEXES
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_download_jobs_created_by ON public.download_jobs(created_by);
CREATE INDEX IF NOT EXISTS idx_download_jobs_status ON public.download_jobs(status);
CREATE INDEX IF NOT EXISTS idx_download_jobs_created_at ON public.download_jobs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_download_items_job_id ON public.download_items(job_id);
CREATE INDEX IF NOT EXISTS idx_download_items_status ON public.download_items(status);
CREATE INDEX IF NOT EXISTS idx_download_items_created_at ON public.download_items(created_at);
-- ============================================================
-- TRIGGERS
-- ============================================================
CREATE TRIGGER update_download_jobs_updated_at
BEFORE UPDATE ON public.download_jobs
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- ============================================================
-- ROW LEVEL SECURITY
-- ============================================================
ALTER TABLE public.download_jobs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.download_items ENABLE ROW LEVEL SECURITY;
-- Admin-only read/write (same pattern as existing tables)
CREATE POLICY "Allow admin read access" ON public.download_jobs
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid())
);
CREATE POLICY "Allow admin write access" ON public.download_jobs
FOR ALL USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid())
);
CREATE POLICY "Allow admin read access" ON public.download_items
FOR SELECT USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid())
);
CREATE POLICY "Allow admin write access" ON public.download_items
FOR ALL USING (
EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid())
);
-- ============================================================
-- STORAGE BUCKET: workout-audio
-- ============================================================
-- Create via Supabase Dashboard or CLI:
-- supabase storage create workout-audio --public
--
-- Configuration:
-- Bucket name: workout-audio
-- Public: true
-- Allowed MIME types: audio/mp4, audio/webm, audio/mpeg
-- Max file size: 100MB
-- Path pattern: {job_id}/{video_id}.m4a

View File

@@ -0,0 +1,18 @@
-- ============================================================
-- Music Genre Classification
-- ============================================================
-- Adds per-track genre to download_items so users can filter
-- workout music by genre preference.
-- Genre enum values: edm, hip-hop, pop, rock, latin, house,
-- drum-and-bass, dubstep, r-and-b, country, metal, ambient
ALTER TABLE public.download_items
ADD COLUMN IF NOT EXISTS genre TEXT
CHECK (genre IS NULL OR genre IN (
'edm', 'hip-hop', 'pop', 'rock', 'latin', 'house',
'drum-and-bass', 'dubstep', 'r-and-b', 'country', 'metal', 'ambient'
));
CREATE INDEX IF NOT EXISTS idx_download_items_genre
ON public.download_items(genre);