Close OSS self-host gaps: in-repo API docs, legal notes, and packaging.

Add MIT license and docs/api, strip SaaS status/admin remnants from robots and footer, align env/compose/README with payment-free product truth.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Atakan Doğan Özban
2026-08-03 07:36:08 +02:00
co-authored by Cursor
parent e5d1bc86a6
commit c702686726
21 changed files with 594 additions and 38 deletions
+10 -6
View File
@@ -1,19 +1,23 @@
# Songs2VID OSS is always self-hosted and has no billing configuration.
# Songs2VID OSS is always self-hosted and payment-free.
# Do not set Stripe, billing, credits, pricing, or S2VID_EDITION variables — they are unused.
# --- Required ---
DATABASE_URL="postgresql://songs2vid:songs2vid@localhost:5433/songs2vid"
REDIS_URL="redis://localhost:6380"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="replace-with-a-long-random-secret"
# Optional; falls back to NEXTAUTH_SECRET.
# TOKEN_ENCRYPTION_KEY="replace-with-another-long-random-secret"
GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# --- Optional ---
# TOKEN_ENCRYPTION_KEY="replace-with-another-long-random-secret" # falls back to NEXTAUTH_SECRET
UPLOAD_DIR="./uploads"
# FFMPEG_PATH="C:/path/to/ffmpeg.exe" # override bundled / image ffmpeg
# S2VID_PORT=3000 # host port for docker-compose.yml (default 3000)
# S2VID_IMAGE_TAG=latest # Docker Hub tag when using published image
NEXT_PUBLIC_DOCS_URL="https://docs.songs2vid.com"
NEXT_PUBLIC_GITEA_ISSUES_URL="https://git.atakanozban.com/Songs2VID/songs2vid/issues"
NEXT_PUBLIC_GITEA_URL="https://git.atakanozban.com/Songs2VID"
NEXT_PUBLIC_DOCKER_HUB_URL="https://hub.docker.com/r/atakanozban/songs2vid"
# Optional: override the bundled ffmpeg-static binary.
# FFMPEG_PATH="C:/path/to/ffmpeg.exe"
+2
View File
@@ -3,6 +3,8 @@ node_modules/
.next/
.env
.env.local
*.tsbuildinfo
tsconfig.tsbuildinfo
website/node_modules/
website/build/
website/.docusaurus/
+4 -2
View File
@@ -3,6 +3,7 @@
Payment-free, self-hosted software that turns cover art and audio into YouTube videos (FFmpeg + BullMQ). Full entitlements in every deployment — no plans, credits, Stripe, or paywall.
**Image:** `atakanozban/songs2vid:latest`
**License:** MIT
**Docs:** https://docs.songs2vid.com
**Source:** https://git.atakanozban.com/Songs2VID/songs2vid
@@ -21,12 +22,12 @@ Payment-free, self-hosted software that turns cover art and audio into YouTube v
```bash
docker pull atakanozban/songs2vid:latest
docker compose up -d --build
docker compose up -d
```
Open http://localhost:3000 → sign in with Google → dashboard.
No Stripe or billing environment variables are required.
No Stripe, billing, or edition environment variables are required. Watermarks are optional (no free-tier badge paywall).
## What you get
@@ -46,3 +47,4 @@ Next.js 15 · PostgreSQL/Prisma · Redis/BullMQ · NextAuth (Google + YouTube)
- Getting started: https://docs.songs2vid.com/docs/getting-started
- Environment variables: https://docs.songs2vid.com/docs/environment
- REST API: https://docs.songs2vid.com/docs/api/overview
- In-repo API notes: https://git.atakanozban.com/Songs2VID/songs2vid/src/branch/main/docs/api
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Atakan Doğan Özban
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+24 -4
View File
@@ -7,6 +7,8 @@ plans, credits, purchases, subscriptions, paywalls, or Stripe.
Sign in with Google, open the dashboard, and create videos. Settings cover account,
YouTube connection, and API keys only.
**License:** [MIT](./LICENSE)
## Quick start (Docker)
Published image: [`atakanozban/songs2vid:latest`](https://hub.docker.com/r/atakanozban/songs2vid)
@@ -23,15 +25,16 @@ Published image: [`atakanozban/songs2vid:latest`](https://hub.docker.com/r/ataka
4. Start the stack (web + worker + Postgres + Redis):
```bash
docker compose up -d --build
docker compose up -d
```
Or pull the published image and run Compose after pointing services at
`atakanozban/songs2vid:latest` (same env and volumes).
Compose uses the published image by default (`atakanozban/songs2vid:latest`) and can also
build from this repo (`docker compose up -d --build`).
Open http://localhost:3000 → sign in → dashboard.
No Stripe keys or billing env vars are required.
No Stripe keys, billing env vars, or edition flags are required. Watermarks are optional;
OSS does not force a free-tier SaaS badge paywall.
## Features
@@ -42,6 +45,12 @@ No Stripe keys or billing env vars are required.
- REST API for automation (keys under **Dashboard → Settings → API key**)
- Always-on self-hosted entitlements (no quota paywall)
## API documentation
- In-repo: [docs/api/overview.md](./docs/api/overview.md) and [docs/api/endpoints.md](./docs/api/endpoints.md)
- Live site: [docs.songs2vid.com/docs/api/overview](https://docs.songs2vid.com/docs/api/overview)
- Machine-readable discovery: `GET /api/v1` (no auth; no billing routes)
## Local development
```bash
@@ -55,6 +64,17 @@ npm run dev:all
- App: http://localhost:3000
- `dev:all` runs Next.js and the BullMQ worker
## Environment
See [`.env.example`](./.env.example) for the required and optional variables. Matching guide:
[docs.songs2vid.com/docs/environment](https://docs.songs2vid.com/docs/environment).
## Legal (self-hosted)
In-app **Privacy** and **Terms** pages describe self-hosted responsibility: the operator of
each instance controls data and access; this software does not process payments. They are not
the paid cloud terms for [songs2vid.com](https://songs2vid.com).
## Stack
- Next.js 15, TypeScript, Tailwind
+3
View File
@@ -8,6 +8,9 @@ export async function GET() {
authentication: "Authorization: Bearer <api_key>",
requirements: ["YouTube account connected"],
rateLimit: "100,000 requests per minute per account",
billing: false,
notes:
"OSS self-hosted: full entitlements, no Stripe/credits/paywall. In-repo docs: docs/api/",
guidance: {
recommended:
"For most jobs (especially 5+ audio files): POST /api/v1/upload per file, then POST /api/v1/jobs with the returned paths",
+1 -1
View File
@@ -8,7 +8,7 @@ const inter = Inter({ subsets: ["latin"] });
const SITE_NAME = "Songs2VID";
const DEFAULT_TITLE = "Songs2VID";
const DEFAULT_DESCRIPTION =
"Self-hosted audio-to-video creation and YouTube uploading.";
"Payment-free self-hosted audio-to-video creation and YouTube uploading.";
export const metadata: Metadata = {
title: DEFAULT_TITLE,
+65 -4
View File
@@ -1,15 +1,76 @@
import { LegalPageLayout } from "@/components/LegalPageLayout";
import { LEGAL_OPERATOR } from "@/lib/legal/constants";
import { DOCS_URL, GITEA_URL } from "@/lib/plans";
export default function PrivacyPage() {
return (
<LegalPageLayout title="Privacy Policy">
<p>
Songs2VID is self-hosted software. Your operator controls the deployment, database, uploads,
logs, and Google OAuth configuration. Songs2VID does not include payment processing.
This policy describes how a <strong>self-hosted Songs2VID</strong> instance typically
handles data. Songs2VID OSS does not include payment processing, plans, credits, or
Stripe. The person or organization that operates this deployment (the &quot;operator&quot;)
controls the server, database, uploads, logs, and Google OAuth configuration, and is
responsible for privacy compliance for their users.
</p>
<h2>1. Who is responsible</h2>
<p>
Google and YouTube process account and upload data according to their own policies. Contact
the operator of this instance for data access or deletion requests.
For this software package as published by {LEGAL_OPERATOR.legalName}, contact:{" "}
<a href={`mailto:${LEGAL_OPERATOR.email}`}>{LEGAL_OPERATOR.email}</a>. For data on{" "}
<em>this running instance</em>, contact the operator of the deployment you signed into
not necessarily the hosted SaaS at songs2vid.com, which is a separate product with its own
policies.
</p>
<h2>2. Data this software processes</h2>
<ul>
<li>Account profile from Google sign-in (email, name, avatar)</li>
<li>YouTube OAuth tokens (encrypted at rest when configured) and channel metadata</li>
<li>Uploaded cover art, audio, optional logos/fonts, and derived video files</li>
<li>Job metadata (titles, tags, privacy, layouts, watermarks)</li>
<li>API key hashes and rate-limit counters</li>
</ul>
<h2>3. Google and YouTube</h2>
<p>
Google and YouTube process account and upload data under their own terms and policies.
Operators must configure OAuth correctly and respect{" "}
<a
href="https://developers.google.com/terms/api-services-user-data-policy"
target="_blank"
rel="noopener noreferrer"
>
Google API Services User Data Policy
</a>{" "}
and{" "}
<a
href="https://developers.google.com/youtube/terms/developer-policies"
target="_blank"
rel="noopener noreferrer"
>
YouTube API Services Policies
</a>
.
</p>
<h2>4. Your choices</h2>
<p>
Use <strong>Dashboard Settings</strong> to export or delete your account data on this
instance, or ask the operator. You can also revoke Google access in your{" "}
<a
href="https://security.google.com/settings/security/permissions"
target="_blank"
rel="noopener noreferrer"
>
Google Account permissions
</a>
.
</p>
<h2>5. Documentation</h2>
<p>
Product docs: <a href={DOCS_URL}>{DOCS_URL}</a>. Source:{" "}
<a href={GITEA_URL}>{GITEA_URL}</a>.
</p>
</LegalPageLayout>
);
+7 -2
View File
@@ -1,12 +1,17 @@
import type { MetadataRoute } from "next";
function siteUrl() {
const raw = process.env.NEXTAUTH_URL?.trim() || "http://localhost:3000";
return raw.replace(/\/$/, "");
}
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/admin", "/api/admin"],
disallow: ["/api/", "/dashboard", "/jobs"],
},
sitemap: "https://songs2vid.com/sitemap.xml",
sitemap: `${siteUrl()}/sitemap.xml`,
};
}
+14 -4
View File
@@ -1,17 +1,27 @@
import type { MetadataRoute } from "next";
const SITE_URL = "https://songs2vid.com";
function siteUrl() {
const raw = process.env.NEXTAUTH_URL?.trim() || "http://localhost:3000";
return raw.replace(/\/$/, "");
}
export default function sitemap(): MetadataRoute.Sitemap {
const base = siteUrl();
return [
{
url: SITE_URL,
url: base,
lastModified: new Date(),
changeFrequency: "daily",
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${SITE_URL}/privacy`,
url: `${base}/privacy`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.3,
},
{
url: `${base}/terms`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.3,
+40 -4
View File
@@ -1,15 +1,51 @@
import { LegalPageLayout } from "@/components/LegalPageLayout";
import { LEGAL_OPERATOR } from "@/lib/legal/constants";
import { DOCS_URL, GITEA_URL } from "@/lib/plans";
export default function TermsPage() {
return (
<LegalPageLayout title="Terms of Use">
<p>
Songs2VID is provided as open-source, self-hosted software without warranty. The operator of
each instance is responsible for availability, configuration, and user access.
Songs2VID OSS is open-source, <strong>payment-free, self-hosted software</strong>. These
terms describe use of the software and of instances that run it. They are{" "}
<strong>not</strong> the paid cloud Terms of Service for songs2vid.com (a separate hosted
product with its own billing and policies).
</p>
<h2>1. Software license and warranty</h2>
<p>
You are responsible for the media you process and upload, including compliance with
copyright law and YouTube&apos;s terms.
The source is provided under the license in the repository <code>LICENSE</code> file. The
software is provided <strong>without warranty</strong> of any kind. The operator of each
instance is responsible for availability, security, backups, configuration, and who may
sign in.
</p>
<h2>2. No paid features in this edition</h2>
<p>
This edition has no Stripe integration, subscriptions, credits, pricing pages, or paywalls.
Full layout, watermark, API, and playlist features are available to signed-in users of the
instance. Any paid offering lives only on the separate hosted SaaS product.
</p>
<h2>3. Your content and YouTube</h2>
<p>
You are solely responsible for media you upload and for compliance with copyright law and
YouTube&apos;s terms. Connecting Google/YouTube authorizes the instance to upload on your
behalf within the scopes granted.
</p>
<h2>4. Acceptable use</h2>
<p>
Do not use the software to infringe rights, abuse YouTube or Google APIs, or circumvent
another party&apos;s security. Operators may suspend access on their instances.
</p>
<h2>5. Contact</h2>
<p>
Package / project contact:{" "}
<a href={`mailto:${LEGAL_OPERATOR.email}`}>{LEGAL_OPERATOR.email}</a>. Instance-specific
support: contact your operator. Docs: <a href={DOCS_URL}>{DOCS_URL}</a>. Source:{" "}
<a href={GITEA_URL}>{GITEA_URL}</a>.
</p>
</LegalPageLayout>
);
+2 -1
View File
@@ -58,7 +58,8 @@ export function ApiKeySettings({ initialStatus, initialRateLimit }: Props) {
return (
<div className="space-y-4">
<p className="text-sm text-gray-400">
Use the REST API to upload files and create video jobs programmatically.
Use the REST API to upload files and create video jobs programmatically. There are no
billing endpoints in this self-hosted edition.
</p>
<p className="text-xs text-gray-500">
Rate limit: {initialRateLimit.limit} requests per {initialRateLimit.windowSeconds} seconds.
+8 -5
View File
@@ -1,5 +1,6 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { DOCS_URL, GITEA_URL } from "@/lib/plans";
function LegalLink({
href,
@@ -33,7 +34,7 @@ export function LegalFooter() {
return (
<footer className="mt-auto w-full border-t border-gray-800 bg-black/40 py-8 text-xs text-gray-500">
<div className="mx-auto flex max-w-4xl flex-wrap items-center justify-center gap-x-4 gap-y-2 px-6 md:justify-start">
<span>© {year} Songs2VID. All rights reserved.</span>
<span>© {year} Songs2VID. MIT licensed.</span>
<span aria-hidden="true" className="hidden sm:inline">
|
</span>
@@ -50,16 +51,18 @@ export function LegalFooter() {
<span aria-hidden="true" className="hidden sm:inline">
|
</span>
<LegalLink href="/terms">Terms of Service</LegalLink>
<LegalLink href="/terms">Terms of Use</LegalLink>
<span aria-hidden="true" className="hidden sm:inline">
|
</span>
<LegalLink href="https://docs.songs2vid.com" external>Documentation</LegalLink>
<LegalLink href={DOCS_URL} external>
Documentation
</LegalLink>
<span aria-hidden="true" className="hidden sm:inline">
|
</span>
<LegalLink href="https://status.atakanozban.com/status/2" external>
Service Status
<LegalLink href={GITEA_URL} external>
Source
</LegalLink>
</div>
</footer>
+2 -1
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import type { ReactNode } from "react";
import { Logo } from "@/components/Logo";
import { LEGAL_LAST_UPDATED } from "@/lib/legal/constants";
import { DOCS_URL } from "@/lib/plans";
type Props = {
title: string;
@@ -45,7 +46,7 @@ export function LegalPageLayout({ title, description, children }: Props) {
Terms
</Link>
<a
href="https://docs.songs2vid.com"
href={DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="hover:text-gray-300"
+2
View File
@@ -27,6 +27,7 @@ services:
retries: 10
web:
image: atakanozban/songs2vid:${S2VID_IMAGE_TAG:-latest}
build: .
restart: unless-stopped
ports:
@@ -49,6 +50,7 @@ services:
sh -c "npx prisma db push && node server.js"
worker:
image: atakanozban/songs2vid:${S2VID_IMAGE_TAG:-latest}
build: .
restart: unless-stopped
env_file:
+12
View File
@@ -0,0 +1,12 @@
# Songs2VID OSS documentation
This folder ships with the OSS repo so API reference stays available without the docs site.
| Doc | Contents |
|-----|----------|
| [API overview](./api/overview.md) | Auth, rate limits, recommended flows |
| [API endpoints](./api/endpoints.md) | curl examples for upload, jobs, playlists, errors |
Live docs (same product truth for self-host): [docs.songs2vid.com](https://docs.songs2vid.com)
Setup and env: see the root [README](../README.md) and [`.env.example`](../.env.example).
+305
View File
@@ -0,0 +1,305 @@
# 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"
```
Live HTML docs: [docs.songs2vid.com/docs/api/endpoints](https://docs.songs2vid.com/docs/api/endpoints).
## Discovery
```bash
curl "$BASE_URL/api/v1"
```
Returns the endpoint list and requirements (**no auth**). No billing routes are listed or implemented.
## 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 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
| `type` | Formats | Max size |
|--------|---------|----------|
| `image` | JPEG, PNG, WebP, GIF | 500 MB |
| `audio` | MP3, WAV, FLAC | 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"
}
}
```
`audioTags` is present for MP3 when tags are readable; otherwise `null`.
## Create job from paths (recommended)
Self-hosted unlocks per-track covers, custom watermarks/fonts, and art-track layouts. Max batch size: **100**. Watermarks are optional — nothing forces the default Songs2VID badge.
```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 Artist - My Track (Official Audio)",
"songTitle": "My Track",
"artist": "My Artist",
"description": "",
"tags": "electronic",
"privacy": "PUBLIC",
"categoryId": "10",
"resolution": "1920x1080",
"notifySubscribers": true,
"madeForKids": false,
"embeddable": true,
"creativeCommons": false,
"includeWatermark": true,
"layout": {
"template": "COVER_LEFT_TEXT_RIGHT",
"blurAmount": 60,
"blurOpacity": 85,
"textPadding": 48,
"titleArtistGap": 12,
"textOffsetX": 0,
"textOffsetY": 0
},
"watermark": {
"mode": "default",
"fontKey": "montserrat",
"position": "bottom-right",
"offsetX": 24,
"offsetY": 24
},
"playlistId": null
}
}]
}'
```
### Success response
```json
{
"jobId": "clxxxxxxxx",
"itemCount": 1,
"status": "PENDING",
"playlist": null
}
```
### Metadata fields
| Field | Type | Notes |
|-------|------|--------|
| `title` | string | YouTube video title |
| `songTitle` | string \| null | On-video song title for art-track (max **120**) |
| `artist` | string \| null | On-video artist line (max **80**) |
| `description` | string | YouTube description |
| `tags` | string | Comma-separated |
| `privacy` | string | `PUBLIC` \| `PRIVATE` \| `UNLISTED` |
| `categoryId` | string | YouTube category ID |
| `resolution` | string | See resolutions below |
| `notifySubscribers` | boolean | YouTube notify flag |
| `madeForKids` | boolean | Made for kids |
| `embeddable` | boolean | Allow embedding |
| `creativeCommons` | boolean | CC vs standard YouTube license |
| `includeWatermark` | boolean | Apply watermark settings |
| `imagePath` | string \| null | Per-track cover |
| `playlistId` | string \| null | Existing playlist ID |
| `layout` | object | Art-track layout |
| `watermark` | object | Watermark settings |
Snake_case aliases are accepted for layout/watermark fields.
### Resolutions
| Value | Aspect |
|-------|--------|
| `1920x1080` | 16:9 |
| `1280x720` | 16:9 |
| `854x480` | 16:9 |
| `720x720` | 1:1 |
| `640x360` | 16:9 |
| `426x240` | 16:9 |
### YouTube categories
| 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 |
### Watermark fields
`watermark.mode`: `none` | `default` | `text` | `logo`
- `none` — no overlay
- `default` — built-in Songs2VID badge PNG (`assets/watermark.png`)
- `logo` — requires prior `type=logo` upload; set `logoPath`
- `text` — custom string (max **80**); set `text`
`watermark.position`: `top-left` | `top-right` | `bottom-left` | `bottom-right` | `center`
`watermark.offsetX` / `offsetY`: `0``200` (default `20`)
`watermark.fontKey`: `system` | `inter` | `montserrat` | `roboto` | `oswald` | `playfair` | `custom` (styles art-track text and text watermarks)
### Art-track layouts
`metadata.layout.template`:
| 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 |
Fine-tuning (defaults in parentheses): `blurAmount` (55), `blurOpacity` (100), `textPadding` (48), `titleArtistGap` (10), `textOffsetX` (0), `textOffsetY` (0).
## YouTube playlists
```bash
curl "$BASE_URL/api/v1/playlists" \
-H "Authorization: Bearer $API_KEY"
```
```bash
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"}'
```
Or pass `createPlaylist` on a job / batch body to create a playlist and attach all videos.
## One-shot batch (small packs only)
```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={"defaults":{"privacy":"PUBLIC"},"items":[{"title":"Track One"},{"title":"Track Two"}]}'
```
Prefer two-step upload + jobs for larger packs.
## 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 statuses
| Status | Meaning |
|--------|---------|
| `PENDING` | Queued |
| `PROCESSING` | Encoding or uploading |
| `COMPLETED` | All items succeeded |
| `FAILED` | All items failed |
| `PARTIAL` | Mix of completed and failed |
### Item statuses
| Status | Meaning |
|--------|---------|
| `PENDING` | Waiting |
| `ENCODING` | FFmpeg building video |
| `UPLOADING` | Uploading to YouTube |
| `COMPLETED` | Live (`youtubeVideoId` set) |
| `FAILED` | Failed (`error` message) |
YouTube channel daily upload caps are enforced by Google, not Songs2VID.
## HTTP errors
| Status | When |
|--------|------|
| `400` | Validation error |
| `401` | Missing or invalid API key |
| `403` | YouTube not connected, or request not allowed |
| `404` | Job not found |
| `429` | API rate limit exceeded |
There is no `402` payment / credits status in OSS.
+66
View File
@@ -0,0 +1,66 @@
# API overview
Programmatic uploads and batch jobs for self-hosted Songs2VID. Generate your API key under **Dashboard → Settings → API key**.
Keys start with `s2yt_live_` and are shown once at creation. The OSS / Docker image always allows API use — there is no plan, credit, or paywall gate.
There are **no billing endpoints** in this edition.
## Authentication
Send the key on every request:
```http
Authorization: Bearer s2yt_live_your_key_here
```
Requirements:
- YouTube channel connected (sign in with Google OAuth that includes YouTube scopes)
OAuth setup: root [README](../../README.md) and Googles [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 OSS uses 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. 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.
## 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`.
## Discovery
```http
GET /api/v1
```
Returns the endpoint list and requirements (no auth). Useful as a machine-readable catalog of what this instance exposes.
## Next
See [Endpoints](./endpoints.md) for curl examples. Hosted HTML docs: [docs.songs2vid.com/docs/api/overview](https://docs.songs2vid.com/docs/api/overview).
+1 -1
View File
@@ -1,7 +1,7 @@
import { SUPPORT_EMAIL } from "@/lib/plans";
import { BRAND_DOMAIN, BRAND_NAME } from "@/lib/branding";
export const LEGAL_LAST_UPDATED = "July 30, 2026";
export const LEGAL_LAST_UPDATED = "August 3, 2026";
export const LEGAL_OPERATOR = {
name: BRAND_NAME,
+3 -2
View File
@@ -39,10 +39,11 @@ export function isYouTubeUploadLimitError(message: string) {
export const YOUTUBE_UPLOAD_LIMIT_USER_MESSAGE =
"YouTube upload limit reached: this Google/YouTube account has exceeded the number of videos " +
"it may upload right now. This is YouTube's own daily limit, not your Songs2VID plan quota. " +
"it may upload right now. This is YouTube's own daily limit, not a Songs2VID usage cap " +
"(self-hosted OSS does not enforce a video quota paywall). " +
"Try again later (often after 24 hours) or use a different YouTube channel.";
/** User-facing message for the dashboard (distinct from Songs2VID plan quota). */
/** User-facing message for the dashboard (distinct from YouTube's own caps). */
export function formatYouTubeErrorForUser(err: unknown): string {
const raw = extractYouTubeErrorMessage(err);
if (isYouTubeUploadLimitError(raw)) return YOUTUBE_UPLOAD_LIMIT_USER_MESSAGE;
+1
View File
@@ -3,6 +3,7 @@
"description": "Payment-free self-hosted audio-to-video app with YouTube upload",
"version": "0.1.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "next dev",
"dev:all": "concurrently -n web,worker -c blue,green \"npm run dev\" \"npm run worker\"",