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.
This commit is contained in:
Atakan Doğan Özban
2026-08-03 07:36:08 +02:00
parent 109f9a3dc3
commit 4bc12fce78
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" DATABASE_URL="postgresql://songs2vid:songs2vid@localhost:5433/songs2vid"
REDIS_URL="redis://localhost:6380" REDIS_URL="redis://localhost:6380"
NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="replace-with-a-long-random-secret" 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_ID="your-google-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-google-client-secret" 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" 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_DOCS_URL="https://docs.songs2vid.com"
NEXT_PUBLIC_GITEA_ISSUES_URL="https://git.atakanozban.com/Songs2VID/songs2vid/issues" NEXT_PUBLIC_GITEA_ISSUES_URL="https://git.atakanozban.com/Songs2VID/songs2vid/issues"
NEXT_PUBLIC_GITEA_URL="https://git.atakanozban.com/Songs2VID" NEXT_PUBLIC_GITEA_URL="https://git.atakanozban.com/Songs2VID"
NEXT_PUBLIC_DOCKER_HUB_URL="https://hub.docker.com/r/atakanozban/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/ .next/
.env .env
.env.local .env.local
*.tsbuildinfo
tsconfig.tsbuildinfo
website/node_modules/ website/node_modules/
website/build/ website/build/
website/.docusaurus/ website/.docusaurus/
+5 -3
View File
@@ -2,7 +2,8 @@
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. 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` **Image:** `atakanozban/songs2vid:latest`
**License:** MIT
**Docs:** https://docs.songs2vid.com **Docs:** https://docs.songs2vid.com
**Source:** https://git.atakanozban.com/Songs2VID/songs2vid **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 ```bash
docker pull atakanozban/songs2vid:latest docker pull atakanozban/songs2vid:latest
docker compose up -d --build docker compose up -d
``` ```
Open http://localhost:3000 → sign in with Google → dashboard. 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 ## 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 - Getting started: https://docs.songs2vid.com/docs/getting-started
- Environment variables: https://docs.songs2vid.com/docs/environment - Environment variables: https://docs.songs2vid.com/docs/environment
- REST API: https://docs.songs2vid.com/docs/api/overview - 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, Sign in with Google, open the dashboard, and create videos. Settings cover account,
YouTube connection, and API keys only. YouTube connection, and API keys only.
**License:** [MIT](./LICENSE)
## Quick start (Docker) ## Quick start (Docker)
Published image: [`atakanozban/songs2vid:latest`](https://hub.docker.com/r/atakanozban/songs2vid) 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): 4. Start the stack (web + worker + Postgres + Redis):
```bash ```bash
docker compose up -d --build docker compose up -d
``` ```
Or pull the published image and run Compose after pointing services at Compose uses the published image by default (`atakanozban/songs2vid:latest`) and can also
`atakanozban/songs2vid:latest` (same env and volumes). build from this repo (`docker compose up -d --build`).
Open http://localhost:3000 → sign in → dashboard. 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 ## Features
@@ -42,6 +45,12 @@ No Stripe keys or billing env vars are required.
- REST API for automation (keys under **Dashboard → Settings → API key**) - REST API for automation (keys under **Dashboard → Settings → API key**)
- Always-on self-hosted entitlements (no quota paywall) - 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 ## Local development
```bash ```bash
@@ -55,6 +64,17 @@ npm run dev:all
- App: http://localhost:3000 - App: http://localhost:3000
- `dev:all` runs Next.js and the BullMQ worker - `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 ## Stack
- Next.js 15, TypeScript, Tailwind - Next.js 15, TypeScript, Tailwind
+3
View File
@@ -8,6 +8,9 @@ export async function GET() {
authentication: "Authorization: Bearer <api_key>", authentication: "Authorization: Bearer <api_key>",
requirements: ["YouTube account connected"], requirements: ["YouTube account connected"],
rateLimit: "100,000 requests per minute per account", 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: { guidance: {
recommended: recommended:
"For most jobs (especially 5+ audio files): POST /api/v1/upload per file, then POST /api/v1/jobs with the returned paths", "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 SITE_NAME = "Songs2VID";
const DEFAULT_TITLE = "Songs2VID"; const DEFAULT_TITLE = "Songs2VID";
const DEFAULT_DESCRIPTION = 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 = { export const metadata: Metadata = {
title: DEFAULT_TITLE, title: DEFAULT_TITLE,
+65 -4
View File
@@ -1,15 +1,76 @@
import { LegalPageLayout } from "@/components/LegalPageLayout"; import { LegalPageLayout } from "@/components/LegalPageLayout";
import { LEGAL_OPERATOR } from "@/lib/legal/constants";
import { DOCS_URL, GITEA_URL } from "@/lib/plans";
export default function PrivacyPage() { export default function PrivacyPage() {
return ( return (
<LegalPageLayout title="Privacy Policy"> <LegalPageLayout title="Privacy Policy">
<p> <p>
Songs2VID is self-hosted software. Your operator controls the deployment, database, uploads, This policy describes how a <strong>self-hosted Songs2VID</strong> instance typically
logs, and Google OAuth configuration. Songs2VID does not include payment processing. 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> </p>
<h2>1. Who is responsible</h2>
<p> <p>
Google and YouTube process account and upload data according to their own policies. Contact For this software package as published by {LEGAL_OPERATOR.legalName}, contact:{" "}
the operator of this instance for data access or deletion requests. <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> </p>
</LegalPageLayout> </LegalPageLayout>
); );
+7 -2
View File
@@ -1,12 +1,17 @@
import type { MetadataRoute } from "next"; 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 { export default function robots(): MetadataRoute.Robots {
return { return {
rules: { rules: {
userAgent: "*", userAgent: "*",
allow: "/", 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"; 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 { export default function sitemap(): MetadataRoute.Sitemap {
const base = siteUrl();
return [ return [
{ {
url: SITE_URL, url: base,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: "daily", changeFrequency: "weekly",
priority: 1.0, priority: 1.0,
}, },
{ {
url: `${SITE_URL}/privacy`, url: `${base}/privacy`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.3,
},
{
url: `${base}/terms`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: "monthly", changeFrequency: "monthly",
priority: 0.3, priority: 0.3,
+40 -4
View File
@@ -1,15 +1,51 @@
import { LegalPageLayout } from "@/components/LegalPageLayout"; import { LegalPageLayout } from "@/components/LegalPageLayout";
import { LEGAL_OPERATOR } from "@/lib/legal/constants";
import { DOCS_URL, GITEA_URL } from "@/lib/plans";
export default function TermsPage() { export default function TermsPage() {
return ( return (
<LegalPageLayout title="Terms of Use"> <LegalPageLayout title="Terms of Use">
<p> <p>
Songs2VID is provided as open-source, self-hosted software without warranty. The operator of Songs2VID OSS is open-source, <strong>payment-free, self-hosted software</strong>. These
each instance is responsible for availability, configuration, and user access. 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> </p>
<h2>1. Software license and warranty</h2>
<p> <p>
You are responsible for the media you process and upload, including compliance with The source is provided under the license in the repository <code>LICENSE</code> file. The
copyright law and YouTube&apos;s terms. 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> </p>
</LegalPageLayout> </LegalPageLayout>
); );
+2 -1
View File
@@ -58,7 +58,8 @@ export function ApiKeySettings({ initialStatus, initialRateLimit }: Props) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-gray-400"> <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>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Rate limit: {initialRateLimit.limit} requests per {initialRateLimit.windowSeconds} seconds. Rate limit: {initialRateLimit.limit} requests per {initialRateLimit.windowSeconds} seconds.
+8 -5
View File
@@ -1,5 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { DOCS_URL, GITEA_URL } from "@/lib/plans";
function LegalLink({ function LegalLink({
href, href,
@@ -33,7 +34,7 @@ export function LegalFooter() {
return ( return (
<footer className="mt-auto w-full border-t border-gray-800 bg-black/40 py-8 text-xs text-gray-500"> <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"> <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 aria-hidden="true" className="hidden sm:inline">
| |
</span> </span>
@@ -50,16 +51,18 @@ export function LegalFooter() {
<span aria-hidden="true" className="hidden sm:inline"> <span aria-hidden="true" className="hidden sm:inline">
| |
</span> </span>
<LegalLink href="/terms">Terms of Service</LegalLink> <LegalLink href="/terms">Terms of Use</LegalLink>
<span aria-hidden="true" className="hidden sm:inline"> <span aria-hidden="true" className="hidden sm:inline">
| |
</span> </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 aria-hidden="true" className="hidden sm:inline">
| |
</span> </span>
<LegalLink href="https://status.atakanozban.com/status/2" external> <LegalLink href={GITEA_URL} external>
Service Status Source
</LegalLink> </LegalLink>
</div> </div>
</footer> </footer>
+2 -1
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Logo } from "@/components/Logo"; import { Logo } from "@/components/Logo";
import { LEGAL_LAST_UPDATED } from "@/lib/legal/constants"; import { LEGAL_LAST_UPDATED } from "@/lib/legal/constants";
import { DOCS_URL } from "@/lib/plans";
type Props = { type Props = {
title: string; title: string;
@@ -45,7 +46,7 @@ export function LegalPageLayout({ title, description, children }: Props) {
Terms Terms
</Link> </Link>
<a <a
href="https://docs.songs2vid.com" href={DOCS_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:text-gray-300" className="hover:text-gray-300"
+2
View File
@@ -27,6 +27,7 @@ services:
retries: 10 retries: 10
web: web:
image: atakanozban/songs2vid:${S2VID_IMAGE_TAG:-latest}
build: . build: .
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -49,6 +50,7 @@ services:
sh -c "npx prisma db push && node server.js" sh -c "npx prisma db push && node server.js"
worker: worker:
image: atakanozban/songs2vid:${S2VID_IMAGE_TAG:-latest}
build: . build: .
restart: unless-stopped restart: unless-stopped
env_file: 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 { SUPPORT_EMAIL } from "@/lib/plans";
import { BRAND_DOMAIN, BRAND_NAME } from "@/lib/branding"; 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 = { export const LEGAL_OPERATOR = {
name: BRAND_NAME, name: BRAND_NAME,
+3 -2
View File
@@ -39,10 +39,11 @@ export function isYouTubeUploadLimitError(message: string) {
export const YOUTUBE_UPLOAD_LIMIT_USER_MESSAGE = export const YOUTUBE_UPLOAD_LIMIT_USER_MESSAGE =
"YouTube upload limit reached: this Google/YouTube account has exceeded the number of videos " + "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."; "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 { export function formatYouTubeErrorForUser(err: unknown): string {
const raw = extractYouTubeErrorMessage(err); const raw = extractYouTubeErrorMessage(err);
if (isYouTubeUploadLimitError(raw)) return YOUTUBE_UPLOAD_LIMIT_USER_MESSAGE; 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", "description": "Payment-free self-hosted audio-to-video app with YouTube upload",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"license": "MIT",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev:all": "concurrently -n web,worker -c blue,green \"npm run dev\" \"npm run worker\"", "dev:all": "concurrently -n web,worker -c blue,green \"npm run dev\" \"npm run worker\"",