hoangsonww / hoangsonww/MetaWave-MP3-App
Feature: Server-Side ID3 Artwork Embed + Duration Extraction (Batch-safe, Lossless)
- Dominant language
- TypeScript
- Stars
- 15
- Forks
- 10
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Today cover art is stored separately (Supabase Storage) and referenced by `cover_art_url`. Users often want the artwork **embedded into the MP3 file’s ID3 tag** so the file “travels” with its art (players, iTunes, phones). Let’s add a **server-side pipeline** that embeds JPEG/PNG into APIC, extracts **duration** (if missing), and re-uploads the updated file—working for **single** and **batch** flows with progress + rollback safety.
## Goals
* ✅ Embed chosen cover art into each MP3’s ID3 (APIC), non-destructively.
* ✅ Extract and persist `duration_secs` if missing (or when user requests refresh).
* ✅ Support **batch cover** flow without timeouts (queued jobs).
* ✅ Keep original file as versioned backup; show “Embedded” state in UI.
* ✅ Respect current 10MB object limit (skip/flag files that exceed).
## High-Level Design
### 1) Worker / Function
* **Option A (recommended):** Supabase Edge Function (Deno) that pulls the MP3 from Storage, embeds art, pushes back.
* **Option B:** Next.js `/api/media/embed` (Node) running on Vercel/host; uses a **queue** (Redis/RabbitMQ) for batch work.
**Libraries (Node option):**
* `node-id3` (APIC write, text frames) or `music-metadata` (read) for metadata.
* Fallback to `ffmpeg` only if we hit edge cases (avoid if possible).
**Flow:**
1. Download `tracks.file_url` (signed) and `cover_art_url`.
2. Write APIC (cover) and ensure title/artist frames are preserved (no data loss).
3. If `duration_secs` is null → read from metadata and return.
4. Upload **new object** to `tracks/{owner_id}/{uuid}-embedded.mp3` (keep original).
5. Update `tracks.file_url` → new path; set `tracks.embedded = true`, `tracks.duration_secs`, `tracks.file_size`.
6. Emit audit event.
### 2) Job Orchestration (Batch)
* New **queue** table or in-memory worker:
* `media_jobs(id, user_id, track_id, kind('embed'), status('queued'|'running'|'done'|'error'), attempts, error, created_at, updated_at, meta jsonb)`
* Batch dialog posts N jobs; worker consumes sequentially with **per-user concurrency = 1** to avoid rate-limit spikes.
### 3) Storage & Versioning
* Keep originals; suffix `-embedded.mp3` to avoid overwrite.
* Add **MD5/etag** check to skip re-processing identical pairs (same MP3 + same image). Store this in `media_jobs.meta.hash`.
### 4) API Surface
* `POST /api/media/embed`
```json
{ "trackIds": ["uuid1","uuid2"], "force": false }
```
* `GET /api/media/jobs?trackId=…` — streaming status for progress UI.
* `POST /api/media/duration/refresh` — optional: refresh duration only.
### 5) DB / Schema
Add minimal columns to `tracks`:
```sql
ALTER TABLE tracks
ADD COLUMN embedded boolean NOT NULL DEFAULT false,
ADD COLUMN duration_secs integer,
ADD COLUMN file_size bigint;
```
New jobs table (if using DB queue):
```sql
CREATE TABLE media_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
track_id uuid NOT NULL REFERENCES tracks (id) ON DELETE CASCADE,
kind text NOT NULL CHECK (kind IN ('embed','duration_refresh')),
status text NOT NULL CHECK (status IN ('queued','running','done','error')),
attempts int NOT NULL DEFAULT 0,
error text,
meta jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX media_jobs_user_status_idx ON media_jobs (user_id, status);
CREATE INDEX media_jobs_track_idx ON media_jobs (track_id);
```
### 6) Frontend (Next.js)
* **TrackCard**:
* Show badge “Embedded” if `embedded = true`.
* If missing `duration_secs` → display “—” and button “Refresh”.
* **Batch Cover Dialog**:
* After batch cover set, offer **“Embed cover into files”** checkbox.
* Display per-item progress: queued → running → done/error.
* **Toasts & Retry**: on error, show retry; keep partial successes.
### 7) Error Handling & Safety
* Max file size guard (respect Supabase 10MB); skip + mark error gracefully.
* If ID3 write fails, **do not** update DB pointer; keep original intact.
* Include **content-type sanity check** (image/\*, audio/mpeg).
* Timeouts: per job 30–60s; batch can span many jobs with retry (3 attempts, backoff).
### 8) Observability
* Counters: `media.embed.started/succeeded/failed`, `media.duration.refreshed`.
* Timers: `media.embed.ms`, `download.ms`, `upload.ms`.
* Add lightweight admin page to inspect recent `media_jobs`.
## Non-Goals (v1)
* Waveform generation server-side (remain client).
* Re-muxing formats; we strictly **ID3 tag update only**.
* FLAC/OGG/AAC support (future).
## Acceptance Criteria
* ✅ Single embed: MP3 updated with APIC; most players show cover immediately.
* ✅ Batch of 50: completes without server timeout; per-item progress shown.
* ✅ Originals preserved; new `file_url` points to `-embedded.mp3`.
* ✅ `duration_secs` filled for files lacking it.
* ✅ Skips duplicates via hash; idempotent retries produce one final asset.
* ✅ Clear error messaging when size >10MB or invalid MIME.
## Tasks
### Backend
* [ ] Create `media_jobs` table and repository.
* [ ] Implement `/api/media/embed` (enqueue + immediate single mode).
* [ ] Worker: download → ID3 APIC write → upload → update DB (transaction).
* [ ] Hashing (mp3\_etag + image\_etag) to short-circuit repeats.
* [ ] Duration extraction (`music-metadata`) and persistence.
* [ ] Unit tests: ID3 write, duration parse, error rollback, idempotency.
### Frontend
* [ ] TrackCard: Embedded badge; duration refresh button.
* [ ] Batch Cover Dialog: “Embed into files” checkbox + progress UI.
* [ ] Jobs polling hook (`useMediaJobs(trackIds)` with exponential backoff).
* [ ] Toasts for per-item success/error + retry.
### Ops
* [ ] Configure env for signed download/upload URLs.
* [ ] Add metrics logging; wire to existing GitHub Actions checks.
---
Contributor guide
Assessment
This issue has not been assessed yet.