Align preview typography with FFmpeg output, add video/song title split, and ship OSS updates.
Separate YouTube video titles from on-video song/artist fields with Pro gating, serve curated fonts and watermark assets for 1:1 preview parity, and include billing/API/docs/deploy stack for self-hosted release.
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Endpoints
|
||||
|
||||
Set these for the examples below:
|
||||
|
||||
```bash
|
||||
export BASE_URL="http://localhost:3000" # local / self-hosted app
|
||||
export API_KEY="s2yt_live_your_key_here"
|
||||
```
|
||||
|
||||
Local API docs: with `npm run dev:all` (or `npm run docs:dev`) open [http://localhost:3001/docs/api/overview](http://localhost:3001/docs/api/overview).
|
||||
|
||||
## Discovery
|
||||
|
||||
```bash
|
||||
curl "$BASE_URL/api/v1"
|
||||
```
|
||||
|
||||
Returns the endpoint list and requirements (**no auth**).
|
||||
|
||||
## Upload a file (two-step)
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/v1/upload" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "file=@cover.jpg" \
|
||||
-F "type=image"
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/v1/upload" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "file=@track1.mp3" \
|
||||
-F "type=audio"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Optional: PNG watermark logo
|
||||
curl -X POST "$BASE_URL/api/v1/upload" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "file=@logo.png" \
|
||||
-F "type=logo"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Optional: custom watermark font (.ttf / .otf, max 10 MB)
|
||||
curl -X POST "$BASE_URL/api/v1/upload" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "file=@Brand.ttf" \
|
||||
-F "type=font"
|
||||
```
|
||||
|
||||
### Request fields
|
||||
|
||||
| Field | Required | Notes |
|
||||
|-------|----------|--------|
|
||||
| `file` | Yes | Multipart file |
|
||||
| `type` | Yes | `image` \| `audio` \| `logo` \| `font` |
|
||||
|
||||
### Allowed files (self-hosted)
|
||||
|
||||
| `type` | Formats | Max size |
|
||||
|--------|---------|----------|
|
||||
| `image` | JPEG, PNG, WebP, GIF | 500 MB |
|
||||
| `audio` | MP3, WAV, FLAC (also accepts related MIME types) | 500 MB |
|
||||
| `logo` | PNG only | 500 MB |
|
||||
| `font` | `.ttf` / `.otf` | **10 MB** |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "/uploads/.../track.mp3",
|
||||
"filename": "track.mp3",
|
||||
"size": 4123456,
|
||||
"audioTags": {
|
||||
"title": "Song Title",
|
||||
"artist": "Artist Name",
|
||||
"album": "Album",
|
||||
"genre": "Electronic",
|
||||
"year": "2024"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Notes |
|
||||
|-------|--------|
|
||||
| `path` | Absolute path on the server — pass this into job create |
|
||||
| `filename` | Original filename |
|
||||
| `size` | Bytes |
|
||||
| `audioTags` | Present for MP3 when tags are readable; otherwise `null`. Fields may be omitted when missing in the file |
|
||||
|
||||
Upload the shared cover once (`type=image`), each audio (`type=audio`), optionally a PNG logo (`type=logo`), and optionally a custom font (`type=font`) for text watermarks.
|
||||
|
||||
## Create job from paths (recommended)
|
||||
|
||||
Use the exact `path` strings returned by upload. Add one `items[]` entry per track.
|
||||
|
||||
Self-hosted deployments unlock per-track covers, custom watermarks/fonts, and art-track layouts. Max batch size: **100**.
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/v1/jobs" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"imagePath": "/uploads/.../cover.jpg",
|
||||
"items": [{
|
||||
"audioPath": "/uploads/.../track.mp3",
|
||||
"audioFilename": "track.mp3",
|
||||
"metadata": {
|
||||
"title": "My Track",
|
||||
"artist": "My Artist",
|
||||
"description": "",
|
||||
"tags": "electronic",
|
||||
"privacy": "PUBLIC",
|
||||
"categoryId": "10",
|
||||
"resolution": "1920x1080",
|
||||
"notifySubscribers": true,
|
||||
"madeForKids": false,
|
||||
"embeddable": true,
|
||||
"creativeCommons": false,
|
||||
"includeWatermark": true,
|
||||
"imagePath": "/uploads/.../track-cover.jpg",
|
||||
"layout": {
|
||||
"template": "COVER_LEFT_TEXT_RIGHT",
|
||||
"blurAmount": 60,
|
||||
"blurOpacity": 85,
|
||||
"textPadding": 48,
|
||||
"titleArtistGap": 12,
|
||||
"textOffsetX": 0,
|
||||
"textOffsetY": 0
|
||||
},
|
||||
"watermark": {
|
||||
"mode": "text",
|
||||
"text": "My Label",
|
||||
"fontKey": "montserrat",
|
||||
"position": "bottom-right",
|
||||
"offsetX": 24,
|
||||
"offsetY": 24
|
||||
},
|
||||
"playlistId": null
|
||||
}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
### Success response
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": "clxxxxxxxx",
|
||||
"itemCount": 1,
|
||||
"status": "PENDING",
|
||||
"playlist": null
|
||||
}
|
||||
```
|
||||
|
||||
`playlist` is set when you pass `createPlaylist` (see [YouTube playlists](#youtube-playlists)).
|
||||
|
||||
### Metadata fields
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|--------|
|
||||
| `title` | string | Video title |
|
||||
| `artist` | string \| null | On-video artist line (max **80**) |
|
||||
| `description` | string | YouTube description |
|
||||
| `tags` | string | Comma-separated (quoted tags supported) |
|
||||
| `privacy` | string | `PUBLIC` \| `PRIVATE` \| `UNLISTED` |
|
||||
| `categoryId` | string | YouTube category ID (see below) |
|
||||
| `resolution` | string | One of the supported values (see below) |
|
||||
| `notifySubscribers` | boolean | YouTube upload notify flag |
|
||||
| `madeForKids` | boolean | COPPA / made for kids |
|
||||
| `embeddable` | boolean | Allow embedding |
|
||||
| `creativeCommons` | boolean | CC license vs standard YouTube |
|
||||
| `includeWatermark` | boolean | Apply watermark settings |
|
||||
| `imagePath` | string \| null | Per-track cover (overrides job `imagePath`) |
|
||||
| `playlistId` | string \| null | Existing playlist ID |
|
||||
| `layout` | object | Art-track layout (see below) |
|
||||
| `watermark` | object | Watermark settings (see below) |
|
||||
|
||||
Snake_case aliases are accepted for layout/watermark fields (e.g. `blur_amount`, `layout_template`).
|
||||
|
||||
### Resolutions
|
||||
|
||||
| Value | Aspect |
|
||||
|-------|--------|
|
||||
| `1920x1080` | 16:9 |
|
||||
| `1280x720` | 16:9 |
|
||||
| `854x480` | 16:9 |
|
||||
| `720x720` | 1:1 |
|
||||
| `640x360` | 16:9 |
|
||||
| `426x240` | 16:9 |
|
||||
|
||||
Self-hosted allows all of these.
|
||||
|
||||
### YouTube categories
|
||||
|
||||
Pass `categoryId` as a string ID. Common values:
|
||||
|
||||
| ID | Name |
|
||||
|----|------|
|
||||
| `1` | Film & Animation |
|
||||
| `2` | Autos & Vehicles |
|
||||
| `10` | Music |
|
||||
| `15` | Pets & Animals |
|
||||
| `17` | Sports |
|
||||
| `19` | Travel & Events |
|
||||
| `20` | Gaming |
|
||||
| `22` | People & Blogs |
|
||||
| `23` | Comedy |
|
||||
| `24` | Entertainment |
|
||||
| `25` | News & Politics |
|
||||
| `26` | Howto & Style |
|
||||
| `27` | Education |
|
||||
| `28` | Science & Technology |
|
||||
| `29` | Nonprofits & Activism |
|
||||
|
||||
Official reference: [YouTube Data API — VideoCategories](https://developers.google.com/youtube/v3/docs/videoCategories/list).
|
||||
|
||||
### Watermark fields
|
||||
|
||||
`watermark.position`: `top-left` | `top-right` | `bottom-left` | `bottom-right` | `center`.
|
||||
|
||||
`watermark.mode`: `none` | `default` | `text` | `logo` (logo requires prior `type=logo` upload; set `logoPath`).
|
||||
|
||||
`watermark.offsetX` / `offsetY`: `0`–`200` (default `20`) — pixels from the chosen anchor.
|
||||
|
||||
`watermark.fontKey` (text mode): `system` | `inter` | `montserrat` | `roboto` | `oswald` | `playfair` | `custom`. For `custom`, upload with `type=font` first and set `fontPath` to the returned path. Text max length: **80**.
|
||||
|
||||
For a full walkthrough of composition controls, see [Video editing](../video-editing.md).
|
||||
|
||||
### Art-track layouts
|
||||
|
||||
`metadata.layout.template` (or flat `layout_template` / `layoutTemplate`):
|
||||
|
||||
| Enum | Description |
|
||||
|------|-------------|
|
||||
| `COVER_LEFT_TEXT_RIGHT` | Cover left, title & artist right |
|
||||
| `COVER_TOP_TEXT_BOTTOM` | Cover top, title & artist below |
|
||||
| `COVER_RIGHT_TEXT_LEFT` | Cover right, title & artist left |
|
||||
| `CENTERED_COMPACT` | Centered cover + text stack |
|
||||
|
||||
Optional fine-tuning (clamped; camelCase or snake_case):
|
||||
|
||||
| Field | Range | Default | Purpose |
|
||||
|-------|-------|---------|---------|
|
||||
| `blurAmount` / `blur_amount` | 0–100 | 55 | Background `boxblur` intensity |
|
||||
| `blurOpacity` / `blur_opacity` | 0–100 | 100 | Blurred fill vs black |
|
||||
| `textPadding` / `text_padding` | 16–120 | 48 | Padding around cover and text |
|
||||
| `titleArtistGap` / `title_artist_gap` | 0–64 | 10 | Space between title and artist |
|
||||
| `textOffsetX` / `text_offset_x` | −120–120 | 0 | Shift text block horizontally |
|
||||
| `textOffsetY` / `text_offset_y` | −120–120 | 0 | Shift text block vertically |
|
||||
|
||||
Also set `metadata.artist` (max 80) for the on-video artist line.
|
||||
|
||||
Omit `layout.template` (or use classic letterbox) when you only want a black-padded cover. Free-form cover coordinates (`x`, `y`, `coverX`, …) and layout-level `offsetX`/`offsetY` are **rejected** (use `textOffsetX`/`textOffsetY` instead; watermark offsets stay under `watermark`).
|
||||
|
||||
Invalid template strings return **400**:
|
||||
|
||||
```json
|
||||
{ "error": "Invalid layout template. Refer to API documentation for valid enum values." }
|
||||
```
|
||||
|
||||
## YouTube playlists
|
||||
|
||||
List existing playlists, create a new one, or create one inline when starting a job. Pass `playlistId` in item metadata / batch `defaults`, or use `createPlaylist` to make a playlist and attach all videos to it.
|
||||
|
||||
Privacy may be `public`, `unlisted`, or `private`. If playlist permission was just added, sign out and sign in again so OAuth includes `youtube.force-ssl`.
|
||||
|
||||
YouTube playlist API reference: [Playlists: insert](https://developers.google.com/youtube/v3/docs/playlists/insert).
|
||||
|
||||
```bash
|
||||
# List playlists
|
||||
curl "$BASE_URL/api/v1/playlists" \
|
||||
-H "Authorization: Bearer $API_KEY"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Create a playlist
|
||||
curl -X POST "$BASE_URL/api/v1/playlists" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"My Album","description":"From Songs2VID","privacy":"unlisted"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"playlistId": "PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
Or create one inline with a job / batch request:
|
||||
|
||||
```json
|
||||
{
|
||||
"createPlaylist": {
|
||||
"title": "My Album",
|
||||
"description": "Uploaded via Songs2VID",
|
||||
"privacy": "private"
|
||||
},
|
||||
"defaults": { "privacy": "PUBLIC" },
|
||||
"items": [{ "title": "Track One" }]
|
||||
}
|
||||
```
|
||||
|
||||
## One-shot batch (small packs only)
|
||||
|
||||
Upload one cover image and a few audio files in a single multipart request. **Not recommended for large batches** — use two-step if you see FormData parse errors.
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE_URL/api/v1/jobs/batch" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-F "image=@cover.jpg" \
|
||||
-F "audio=@track1.mp3" \
|
||||
-F "audio=@track2.mp3" \
|
||||
-F 'metadata={"createPlaylist":{"title":"My Album","privacy":"unlisted"},"defaults":{"privacy":"PUBLIC"},"items":[{"title":"Track One"},{"title":"Track Two"}]}'
|
||||
```
|
||||
|
||||
Optional `metadata` JSON supports `defaults` applied to every item and per-item overrides in `items`. Item order should match the order of `audio` files.
|
||||
|
||||
When item metadata is omitted, batch defaults include privacy `PUBLIC`, resolution `1920x1080`, and watermark off unless overridden in `defaults`.
|
||||
|
||||
**Multipart tips**
|
||||
|
||||
- Do not set `Content-Type` manually for multipart; the client must include the boundary
|
||||
- In Postman: Body → form-data; each audio field key must be exactly `audio` (type File)
|
||||
- If a file field shows a warning, re-select the file from disk
|
||||
|
||||
## Poll job status
|
||||
|
||||
```bash
|
||||
curl "$BASE_URL/api/v1/jobs/JOB_ID" \
|
||||
-H "Authorization: Bearer $API_KEY"
|
||||
```
|
||||
|
||||
```bash
|
||||
curl "$BASE_URL/api/v1/jobs?limit=10" \
|
||||
-H "Authorization: Bearer $API_KEY"
|
||||
```
|
||||
|
||||
`GET /api/v1/jobs` accepts `limit` (default **20**, max **100**).
|
||||
|
||||
### Job response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "clxxxxxxxx",
|
||||
"status": "PROCESSING",
|
||||
"createdAt": "2026-07-25T12:00:00.000Z",
|
||||
"completedAt": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "clitemxxx",
|
||||
"audioFilename": "track.mp3",
|
||||
"title": "My Track",
|
||||
"description": "",
|
||||
"tags": "electronic",
|
||||
"privacy": "PUBLIC",
|
||||
"categoryId": "10",
|
||||
"resolution": "1920x1080",
|
||||
"status": "ENCODING",
|
||||
"youtubeVideoId": null,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Job statuses
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `PENDING` | Queued; worker has not started |
|
||||
| `PROCESSING` | At least one item is encoding or uploading |
|
||||
| `COMPLETED` | All items succeeded |
|
||||
| `FAILED` | All items failed |
|
||||
| `PARTIAL` | Mix of completed and failed items |
|
||||
|
||||
### Item statuses
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `PENDING` | Waiting in the queue |
|
||||
| `ENCODING` | FFmpeg is building the video |
|
||||
| `UPLOADING` | Uploading to YouTube |
|
||||
| `COMPLETED` | Live on YouTube (`youtubeVideoId` set) |
|
||||
| `FAILED` | Failed (`error` contains a message) |
|
||||
|
||||
### Pipeline notes
|
||||
|
||||
- Each item is encoded, then uploaded; the local MP4 is removed after a successful upload
|
||||
- Queue retries: **2** attempts with exponential backoff (5s base)
|
||||
- Worker concurrency: **2** items in parallel
|
||||
- On item failure, reserved allowance for that item is released
|
||||
|
||||
YouTube upload limits (channel daily caps, etc.) are enforced by Google, not Songs2VID. See [YouTube Data API — Quota and compliance](https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits).
|
||||
|
||||
## HTTP errors
|
||||
|
||||
| Status | When |
|
||||
|--------|------|
|
||||
| `400` | Validation error (bad file type, invalid layout, missing fields, bad JSON) |
|
||||
| `401` | Missing or invalid API key |
|
||||
| `403` | YouTube not connected, or edition/plan does not allow API features |
|
||||
| `404` | Job not found |
|
||||
| `429` | API rate limit exceeded — body includes `retryAfterSeconds`; header `Retry-After` is set |
|
||||
|
||||
Example rate-limit body:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "API rate limit exceeded. Try again shortly.",
|
||||
"retryAfterSeconds": 42
|
||||
}
|
||||
```
|
||||
|
||||
Self-hosted rate limits are effectively unlimited for normal use. See [API overview](./overview.md).
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# API overview
|
||||
|
||||
Programmatic uploads and batch jobs for self-hosted Songs2VID. Generate your API key under **Dashboard → Settings → API access**.
|
||||
|
||||
Keys start with `s2yt_live_` and are shown once at creation.
|
||||
|
||||
## Authentication
|
||||
|
||||
Send the key on every request:
|
||||
|
||||
```http
|
||||
Authorization: Bearer s2yt_live_your_key_here
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
- `S2VID_EDITION=selfhosted` (Compose sets this by default)
|
||||
- YouTube channel connected (sign in with Google OAuth that includes YouTube scopes)
|
||||
|
||||
OAuth setup: [Getting started](../getting-started.md) and Google’s [OAuth 2.0 for Web Server Applications](https://developers.google.com/identity/protocols/oauth2/web-server). YouTube scopes/API: [YouTube Data API Overview](https://developers.google.com/youtube/v3/getting-started).
|
||||
|
||||
## Rate limits
|
||||
|
||||
Self-hosted editions use a very high per-account ceiling (effectively unlimited for normal automation). You will rarely see `429`.
|
||||
|
||||
If a limit is hit, the response is **429** with `retryAfterSeconds` and a `Retry-After` header. See [Endpoints — HTTP errors](./endpoints.md#http-errors).
|
||||
|
||||
## Choosing a flow
|
||||
|
||||
### Recommended: two-step (especially 5+ audio files)
|
||||
|
||||
1. Upload each file with `POST /api/v1/upload`
|
||||
2. Create the job with `POST /api/v1/jobs` (JSON paths)
|
||||
|
||||
This avoids huge multipart bodies. Self-hosted max batch size is **100** tracks per job.
|
||||
|
||||
### One-shot batch: small packs only
|
||||
|
||||
`POST /api/v1/jobs/batch` accepts one cover image and a few audio files in a single multipart request. Large bodies often fail with:
|
||||
|
||||
```text
|
||||
failed to parse body as FormData
|
||||
```
|
||||
|
||||
Prefer two-step for albums or long tracklists.
|
||||
|
||||
### Multipart tips
|
||||
|
||||
- Do not set `Content-Type` manually for multipart; the client must include the boundary
|
||||
- In Postman: Body → form-data; each audio field key must be exactly `audio` (type File)
|
||||
- If a file field shows a warning, re-select the file from disk
|
||||
|
||||
## Job lifecycle
|
||||
|
||||
1. Create job → status `PENDING`
|
||||
2. Worker picks items → `ENCODING` → `UPLOADING` → `COMPLETED` or `FAILED`
|
||||
3. Job rolls up to `COMPLETED`, `FAILED`, or `PARTIAL`
|
||||
|
||||
Poll with `GET /api/v1/jobs/:id`. Full status tables and response shapes: [Endpoints — Poll job status](./endpoints.md#poll-job-status).
|
||||
|
||||
## Discovery
|
||||
|
||||
```http
|
||||
GET /api/v1
|
||||
```
|
||||
|
||||
Returns the endpoint list and requirements (no auth).
|
||||
|
||||
## Next
|
||||
|
||||
See [Endpoints](./endpoints.md) for curl examples covering upload, jobs, layouts, watermarks, playlists, batch, resolutions, categories, and errors. For composition concepts (templates, blur, fine-tuning, fonts), see [Video editing](../video-editing.md).
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Production notes
|
||||
|
||||
Self-hosting in production means running the **web app** and the **worker** against shared Postgres, Redis, and upload storage, with a public HTTPS URL for OAuth.
|
||||
|
||||
How you package that (bare metal, systemd, Kubernetes, Docker, a PaaS) is up to you. The Compose files in this repo are **optional examples**, not a required stack.
|
||||
|
||||
## What you must configure
|
||||
|
||||
| Requirement | Notes |
|
||||
|-------------|--------|
|
||||
| `S2VID_EDITION=selfhosted` | Unlimited allowance, API, and Pro layout features. Root Compose injects this when you use that example. |
|
||||
| `NEXTAUTH_URL` | Exact public origin users open (e.g. `https://songs2vid.example.com`), no trailing slash |
|
||||
| `NEXTAUTH_SECRET` | Strong random secret |
|
||||
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | OAuth Web client from Google Cloud |
|
||||
| OAuth redirect URI | `https://YOUR_DOMAIN/api/auth/callback/google` — must match `NEXTAUTH_URL` |
|
||||
| YouTube Data API v3 | Enabled on the same Google Cloud project |
|
||||
| Worker process | Same `DATABASE_URL`, `REDIS_URL`, and `UPLOAD_DIR` as the web app |
|
||||
| Persistent uploads | Shared volume or disk for both web and worker |
|
||||
|
||||
Env reference: [Environment variables](./environment.md). Local setup: [Getting started](./getting-started.md).
|
||||
|
||||
### Google OAuth (required)
|
||||
|
||||
1. Create a project in [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Enable [YouTube Data API v3](https://developers.google.com/youtube/v3/getting-started)
|
||||
3. Create OAuth 2.0 Web credentials ([guide](https://developers.google.com/identity/protocols/oauth2/web-server))
|
||||
4. Add the authorized redirect URI ([URI rules](https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation)):
|
||||
|
||||
```text
|
||||
https://YOUR_DOMAIN/api/auth/callback/google
|
||||
```
|
||||
|
||||
Users sign in with Google; they do not need their own Cloud credentials.
|
||||
|
||||
### After go-live
|
||||
|
||||
1. Open `NEXTAUTH_URL` and sign in
|
||||
2. Confirm the YouTube channel connects
|
||||
3. Generate an API key under **Dashboard → Settings → API access** if you automate uploads
|
||||
4. Run a small test job (dashboard or [API](./api/overview.md))
|
||||
|
||||
### Common issues
|
||||
|
||||
| Symptom | Check |
|
||||
|---------|--------|
|
||||
| OAuth redirect mismatch | Redirect URI must match `NEXTAUTH_URL` + `/api/auth/callback/google` exactly |
|
||||
| Jobs stuck in `PENDING` | Worker is running and shares Redis + upload storage with web |
|
||||
| Encode / upload failures | FFmpeg available where the worker runs; disk space for uploads |
|
||||
| YouTube errors | Channel permissions or Google limits — [YouTube quota & compliance](https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits) |
|
||||
|
||||
Put any reverse proxy you like in front (Caddy, nginx, Traefik, cloud load balancer) and terminate TLS there so `NEXTAUTH_URL` is HTTPS.
|
||||
|
||||
## Optional examples in this repo
|
||||
|
||||
These are starting points only. Adapt or ignore them.
|
||||
|
||||
### Root `docker-compose.yml`
|
||||
|
||||
Builds web + worker + Postgres + Redis with `S2VID_EDITION=selfhosted`. Useful for a quick all-in-one box. App port defaults to `${S2VID_PORT:-3000}`.
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Compose reference: [Docker Compose docs](https://docs.docker.com/compose/).
|
||||
|
||||
### `deploy/songs2vid/` (Caddy sample)
|
||||
|
||||
Sample layout under `deploy/songs2vid/`: Compose services plus a [Caddyfile](https://caddyserver.com/docs/caddyfile) that reverse-proxies the app, optionally serves a static docs build, and optionally puts Prisma Studio behind basic auth.
|
||||
|
||||
Only relevant if you choose Caddy. Useful links:
|
||||
|
||||
- [Caddy documentation](https://caddyserver.com/docs/)
|
||||
- [Automatic HTTPS](https://caddyserver.com/docs/automatic-https)
|
||||
- [`reverse_proxy`](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)
|
||||
- [`basicauth`](https://caddyserver.com/docs/caddyfile/directives/basicauth) · [`caddy hash-password`](https://caddyserver.com/docs/command-line#caddy-hash-password)
|
||||
|
||||
Edit hostnames in the sample Caddyfile to your domains. Do not commit real basic-auth password hashes.
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Environment variables
|
||||
|
||||
Two files exist on purpose — they are not duplicates you both fill with secrets.
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| **`.env.example`** | Safe template committed to git. Shows names and placeholders. No real secrets. |
|
||||
| **`.env`** | Your real local or production secrets. Gitignored. **The app reads only this.** |
|
||||
|
||||
Workflow: copy once (`cp .env.example .env`), then edit **only** `.env`. Leave `.env.example` as the shared checklist.
|
||||
|
||||
## Required for local development
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `DATABASE_URL` | Postgres connection string (Docker defaults work out of the box) |
|
||||
| `REDIS_URL` | Redis for BullMQ and rate limiting |
|
||||
| `NEXTAUTH_URL` | Public app URL, e.g. `http://localhost:3000` or `https://songs2vid.example.com` |
|
||||
| `NEXTAUTH_SECRET` | Long random string (e.g. `openssl rand -base64 32`) |
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |
|
||||
|
||||
Create OAuth credentials in [Google Cloud Console](https://console.cloud.google.com/). Guide: [Setting up OAuth 2.0](https://support.google.com/cloud/answer/6158849). Enable [YouTube Data API v3](https://developers.google.com/youtube/v3/getting-started).
|
||||
|
||||
## Optional
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `TOKEN_ENCRYPTION_KEY` | Encrypts YouTube tokens at rest; falls back to `NEXTAUTH_SECRET` if unset |
|
||||
| `UPLOAD_DIR` | Upload storage path; defaults to `./uploads` (Compose uses `/app/uploads`) |
|
||||
| `FFMPEG_PATH` | Override bundled `ffmpeg-static` binary |
|
||||
| `S2VID_EDITION` | Set to `selfhosted` for unlimited video allowance, API access, and all layout features |
|
||||
| `S2VID_PORT` | Host port for the optional root `docker-compose.yml` example (default `3000`) |
|
||||
| `NEXT_PUBLIC_GITEA_URL` | Footer / open-source link |
|
||||
| `NEXT_PUBLIC_GITEA_ISSUES_URL` | Bug report link |
|
||||
| `NEXT_PUBLIC_DOCKER_HUB_URL` | Docker image link |
|
||||
| `NEXT_PUBLIC_DOCS_URL` | Docusaurus docs site. Omit locally to use `http://localhost:3001` when `NEXTAUTH_URL` is localhost; production default `https://docs.songs2vid.com` |
|
||||
| `ADMIN_API_KEY` | Optional Bearer token for internal admin HTTP routes. **Not required** for normal self-hosted operation |
|
||||
|
||||
## Notes
|
||||
|
||||
- User API keys are generated in Dashboard → Settings (hashed at rest). They are not env vars.
|
||||
- Self-hosted deployments should set `S2VID_EDITION=selfhosted` (the optional root Compose example does this for you).
|
||||
- In production, `NEXTAUTH_URL` must match the public HTTPS URL users open in the browser, and the same origin must be listed as an OAuth redirect URI (`…/api/auth/callback/google`). See [Production notes](./deploy.md).
|
||||
- Never commit `.env` or put production secrets in `.env.example`.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Getting started
|
||||
|
||||
Run Songs2VID locally for development or self-hosting.
|
||||
|
||||
## 1. Environment file
|
||||
|
||||
Copy the template, then edit **only** `.env` with your real values:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
See [Environment variables](./environment.md) for required vs optional keys.
|
||||
|
||||
## 2. PostgreSQL and Redis
|
||||
|
||||
For local app development (infra only):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
Or run the full stack (web + worker + DB) with the self-hosted edition — an optional Compose example:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Compose is not required; any Postgres + Redis that match your `.env` works. See [Docker Compose](https://docs.docker.com/compose/) if you use the examples above.
|
||||
|
||||
## 3. Install and migrate
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
## 4. Google OAuth
|
||||
|
||||
In [Google Cloud Console](https://console.cloud.google.com/) (server-side only — end users never enter credentials):
|
||||
|
||||
1. Enable [YouTube Data API v3](https://developers.google.com/youtube/v3/getting-started)
|
||||
2. Create OAuth 2.0 Web credentials ([OAuth 2.0 for web server apps](https://developers.google.com/identity/protocols/oauth2/web-server))
|
||||
3. Add an authorized redirect URI ([URI validation](https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation)):
|
||||
- Local: `http://localhost:3000/api/auth/callback/google`
|
||||
- Production: `https://YOUR_DOMAIN/api/auth/callback/google`
|
||||
4. Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in `.env`
|
||||
|
||||
Also set `NEXTAUTH_URL` to the same origin users open in the browser.
|
||||
|
||||
## 5. FFmpeg and fonts
|
||||
|
||||
The worker needs FFmpeg. The `ffmpeg-static` npm package is used by default. Set `FFMPEG_PATH` only if you want a system binary instead.
|
||||
|
||||
For curated watermark fonts (Inter, Montserrat, etc.), ensure files exist under `assets/fonts`:
|
||||
|
||||
```bash
|
||||
node scripts/fetch-watermark-fonts.mjs
|
||||
```
|
||||
|
||||
Custom `.ttf` / `.otf` uploads work without this step. See [Video editing](./video-editing.md).
|
||||
|
||||
## 6. Start app, worker, and docs
|
||||
|
||||
One command for everything:
|
||||
|
||||
```bash
|
||||
npm run dev:all
|
||||
```
|
||||
|
||||
| Process | URL / role |
|
||||
|---------|------------|
|
||||
| Next.js app | [http://localhost:3000](http://localhost:3000) |
|
||||
| BullMQ worker | Encodes videos and uploads to YouTube |
|
||||
| Docusaurus docs | [http://localhost:3001](http://localhost:3001) |
|
||||
|
||||
Or run them separately:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
npm run worker
|
||||
npm run docs:dev
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Users sign in with Google via OAuth 2.0. The app connects their YouTube channel automatically — end users do not need Google Cloud credentials or API keys.
|
||||
|
||||
## Production
|
||||
|
||||
For public HTTPS, OAuth redirect URIs, and worker checklist, see [Production notes](./deploy.md). Repo Compose/Caddy files there are optional examples only.
|
||||
|
||||
## Useful scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `npm run dev:all` | App (3000) + worker + docs (3001) together |
|
||||
| `npm run dev` | Next.js dev server only |
|
||||
| `npm run worker` | Background job processor only |
|
||||
| `npm run docs:dev` | Documentation site only (port 3001) |
|
||||
| `npm run db:push` | Push Prisma schema to the database |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run docs:build` | Build the documentation site |
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
slug: /intro
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
**Songs2VID** turns a cover image and one or more audio files into YouTube-ready videos, then uploads them to your channel.
|
||||
|
||||
This documentation is for **self-hosted / open-source** deployments (`S2VID_EDITION=selfhosted`).
|
||||
|
||||
## What you can do
|
||||
|
||||
- Combine one image with multiple tracks in a batch
|
||||
- Set per-video metadata (title, artist, description, tags, privacy, resolution)
|
||||
- Art-track layouts with blur backgrounds and fine-tuning (padding, title/artist gap, text offsets)
|
||||
- Custom watermarks: text or logo, positions/offsets, curated or uploaded fonts
|
||||
- Unique cover image per track
|
||||
- Add uploads to YouTube playlists
|
||||
- Use the REST API for automation
|
||||
|
||||
See [Video editing](./video-editing.md) for composition controls in the dashboard and API.
|
||||
|
||||
## Self-host
|
||||
|
||||
Follow [Getting started](./getting-started.md). Set `S2VID_EDITION=selfhosted` for unlimited video allowance, API access, and all layout features (the optional root Compose example sets this for you).
|
||||
|
||||
For local development, `npm run dev:all` starts the app (port 3000), worker, and this docs site (port 3001).
|
||||
|
||||
Going to a public URL? See [Production notes](./deploy.md) (OAuth redirect, worker, HTTPS). Docker/Caddy samples in the repo are optional.
|
||||
|
||||
## REST API
|
||||
|
||||
Programmatic uploads and job creation. Generate an API key under **Dashboard → Settings → API access**. Start with [API overview](./api/overview.md).
|
||||
|
||||
## Stack
|
||||
|
||||
- Next.js 15 (App Router, TypeScript, Tailwind)
|
||||
- PostgreSQL + Prisma
|
||||
- Redis + BullMQ
|
||||
- NextAuth (Google OAuth with YouTube scopes)
|
||||
- FFmpeg for encoding
|
||||
- YouTube Data API v3
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Video editing
|
||||
|
||||
Self-hosted Songs2VID includes a **Layout Studio** on the dashboard upload form: art-track compositions, blur backgrounds, typography, and watermarks with live preview. The same options are available on the [REST API](./api/endpoints.md).
|
||||
|
||||
## Classic vs art-track
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Classic** (no template) | Cover letterboxed on a black frame — simple and reliable |
|
||||
| **Art-track template** | Cover + title/artist arranged by a fixed template, with a blurred cover fill behind |
|
||||
|
||||
Templates are enum-based. Free-form cover coordinates (`x`, `y`, `coverX`, …) are rejected so encoding stays predictable.
|
||||
|
||||
### Templates
|
||||
|
||||
| Template | Layout |
|
||||
|----------|--------|
|
||||
| `COVER_LEFT_TEXT_RIGHT` | Cover on the left; title & artist on the right |
|
||||
| `COVER_TOP_TEXT_BOTTOM` | Cover on top; title & artist below |
|
||||
| `COVER_RIGHT_TEXT_LEFT` | Cover on the right; title & artist on the left |
|
||||
| `CENTERED_COMPACT` | Centered cover with a compact title/artist stack |
|
||||
|
||||
Pick a template in the UI under **Composition**, or set `metadata.layout.template` in the API.
|
||||
|
||||
## Title and artist
|
||||
|
||||
Each track can have:
|
||||
|
||||
- **Title** — used on the video and as the YouTube title (often prefilled from the filename)
|
||||
- **Artist** — drawn under the title on art-track layouts (max 80 characters)
|
||||
|
||||
Artist is especially useful when ID3 tags or your API payload include it.
|
||||
|
||||
## Blur background
|
||||
|
||||
When an art-track template is active, the encoder builds a full-frame background from the cover:
|
||||
|
||||
- **Blur amount** (`blurAmount`, `0`–`100`, default `55`) — FFmpeg `boxblur` intensity
|
||||
- **Background opacity** (`blurOpacity`, `0`–`100`, default `100`) — how strong the blurred fill is versus solid black (`0` = black, `100` = full blur)
|
||||
|
||||
Use lower opacity for a darker, more subdued frame; higher for a soft wash of the artwork.
|
||||
|
||||
## Fine-tuning
|
||||
|
||||
All values are clamped. These nudge the composition inside the template — they are not free-form canvas placement.
|
||||
|
||||
| Control | Field | Range | Default | What it does |
|
||||
|---------|-------|-------|---------|--------------|
|
||||
| Padding | `textPadding` | 16–120 px | 48 | Space around cover and text |
|
||||
| Title ↔ artist | `titleArtistGap` | 0–64 px | 10 | Vertical gap between title and artist |
|
||||
| Text horizontal | `textOffsetX` | −120–120 px | 0 | Shift the text block left/right |
|
||||
| Text vertical | `textOffsetY` | −120–120 px | 0 | Shift the text block up/down |
|
||||
|
||||
In the dashboard, sliders update the live preview. Via API, nest them under `metadata.layout` (camelCase or snake_case aliases are accepted).
|
||||
|
||||
## Per-track covers
|
||||
|
||||
Upload a shared cover for the batch, then optionally set a different image per item (`metadata.imagePath` after uploading with `type=image`). Useful for singles that share an album batch but need distinct artwork.
|
||||
|
||||
## Watermarks
|
||||
|
||||
Modes:
|
||||
|
||||
| Mode | Effect |
|
||||
|------|--------|
|
||||
| `none` | No watermark |
|
||||
| `default` | Built-in Songs2VID branding |
|
||||
| `text` | Custom text with optional typography |
|
||||
| `logo` | Custom PNG (upload with `type=logo`, then set `logoPath`) |
|
||||
|
||||
### Position and offset
|
||||
|
||||
- **Position**: `top-left` · `top-right` · `bottom-left` · `bottom-right` · `center`
|
||||
- **Offsets** (`offsetX` / `offsetY`): `0`–`200` px from the chosen anchor (default `20`)
|
||||
|
||||
### Typography (text mode)
|
||||
|
||||
Curated fonts (bundled under `assets/fonts` after fetch):
|
||||
|
||||
- `system` — FFmpeg default
|
||||
- `inter` · `montserrat` · `roboto` · `oswald` · `playfair`
|
||||
- `custom` — your `.ttf` / `.otf` (max 10 MB; upload with `type=font`, set `fontKey: "custom"` and `fontPath`)
|
||||
|
||||
Refresh curated files if missing:
|
||||
|
||||
```bash
|
||||
node scripts/fetch-watermark-fonts.mjs
|
||||
```
|
||||
|
||||
Text length is capped at 80 characters.
|
||||
|
||||
## Dashboard workflow
|
||||
|
||||
1. Add audio (and optional per-track covers)
|
||||
2. Open **Layout Studio** on a track
|
||||
3. Choose classic or a template, then tune blur, padding, gaps, and text offsets
|
||||
4. Configure watermark mode, font, and position
|
||||
5. Preview updates live; submit to enqueue encoding
|
||||
|
||||
## API
|
||||
|
||||
See [Endpoints](./api/endpoints.md) for curl examples. Layout and watermark objects live under each item’s `metadata`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "My Track",
|
||||
"artist": "My Artist",
|
||||
"layout": {
|
||||
"template": "COVER_LEFT_TEXT_RIGHT",
|
||||
"blurAmount": 60,
|
||||
"blurOpacity": 85,
|
||||
"textPadding": 48,
|
||||
"titleArtistGap": 12,
|
||||
"textOffsetX": 0,
|
||||
"textOffsetY": 0
|
||||
},
|
||||
"watermark": {
|
||||
"mode": "text",
|
||||
"text": "My Label",
|
||||
"fontKey": "montserrat",
|
||||
"position": "bottom-right",
|
||||
"offsetX": 24,
|
||||
"offsetY": 24
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user