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:
co-authored by
Cursor
parent
e5d1bc86a6
commit
c702686726
+10
-6
@@ -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"
|
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 "operator")
|
||||||
|
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
@@ -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
@@ -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
@@ -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'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'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'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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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 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 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,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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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\"",
|
||||||
|
|||||||
Reference in New Issue
Block a user