From 0f363c56206f83ad14bd2ae6d3ebf36e70da25e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Do=C4=9Fan=20=C3=96zban?= Date: Mon, 10 Aug 2026 19:08:10 +0200 Subject: [PATCH] Fix still-image encodes ending ~50s past audio and refresh support/docs links. Cap FFmpeg output with probed audio duration (FFmpeg 5.x -shortest bug), point SUPPORT_EMAIL to support@songs2vid.com, and align Hub/README/n8n docs with the Gitea + npm package links. --- DOCKERHUB_OVERVIEW.md | 9 +++++-- README.md | 5 ++-- docs/api/endpoints.md | 2 +- docs/api/overview.md | 2 +- docs/n8n.md | 3 ++- integrations/n8n/README.md | 14 +++++++---- .../credentials/Songs2VidApi.credentials.ts | 2 +- .../n8n/nodes/Songs2Vid/Songs2Vid.node.json | 13 ++++++++-- integrations/n8n/package.json | 6 ++--- .../n8n/workflows/workflow-template.json | 2 +- lib/ffmpeg/encode.ts | 25 +++++++++++++++++-- lib/plans.ts | 2 +- 12 files changed, 63 insertions(+), 22 deletions(-) diff --git a/DOCKERHUB_OVERVIEW.md b/DOCKERHUB_OVERVIEW.md index 32b35e1..629fe65 100644 --- a/DOCKERHUB_OVERVIEW.md +++ b/DOCKERHUB_OVERVIEW.md @@ -5,9 +5,11 @@ Payment-free, self-hosted software that turns cover art and audio into YouTube v **Image:** `atakanozban/songs2vid:latest` **License:** MIT +**Primary source hub:** https://git.atakanozban.com/Songs2VID +**Repo:** https://git.atakanozban.com/Songs2VID/songs2vid **Docs:** https://docs.songs2vid.com -**Source:** https://git.atakanozban.com/Songs2VID/songs2vid -**Hosted SaaS (separate):** https://songs2vid.com +**n8n (npm):** https://www.npmjs.com/package/n8n-nodes-songs2vid +**Web UI (secondary mirror):** https://songs2vid.com ## Quick start @@ -43,8 +45,11 @@ Next.js 15 · PostgreSQL/Prisma · Redis/BullMQ · NextAuth (Google + YouTube) ## Links +- Primary source hub: https://git.atakanozban.com/Songs2VID - Full documentation: https://docs.songs2vid.com - 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 +- n8n node: https://www.npmjs.com/package/n8n-nodes-songs2vid +- Web UI: https://songs2vid.com diff --git a/README.md b/README.md index dfc7d29..21eb334 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,11 @@ the paid cloud terms for [songs2vid.com](https://songs2vid.com). ## 🔗 Links +- **Primary source hub:** [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID) - Docs: [docs.songs2vid.com](https://docs.songs2vid.com) - Docker Hub: [atakanozban/songs2vid](https://hub.docker.com/r/atakanozban/songs2vid) -- Source: [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID) -- Hosted SaaS (separate product): [songs2vid.com](https://songs2vid.com) +- n8n node (npm): [`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid) +- Web UI / hosted SaaS (secondary mirror): [songs2vid.com](https://songs2vid.com) ## 📄 License diff --git a/docs/api/endpoints.md b/docs/api/endpoints.md index 2372194..0cb877f 100644 --- a/docs/api/endpoints.md +++ b/docs/api/endpoints.md @@ -164,7 +164,7 @@ Equivalent path: `POST $BASE_URL/api/v1/jobs` with the same JSON. | `job.failed` | All items failed | | `job.partial` | Mix of success and failure | -Community node: [`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid) — see [docs/n8n.md](../n8n.md) and [docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n). +Community node: [`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid) — primary source [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID); see [docs/n8n.md](../n8n.md) and [docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n).
Legacy example without webhook (same metadata shape) diff --git a/docs/api/overview.md b/docs/api/overview.md index 8e556b2..0dcb670 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -65,4 +65,4 @@ Returns the endpoint list, requirements, `layoutTemplates` (every art-track enum ## Next -See [Endpoints](./endpoints.md) for curl examples. n8n: [docs/n8n.md](../n8n.md). Hosted HTML docs: [docs.songs2vid.com/docs/api/overview](https://docs.songs2vid.com/docs/api/overview). +See [Endpoints](./endpoints.md) for curl examples. n8n: [docs/n8n.md](../n8n.md). Primary source: [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID). Hosted HTML docs: [docs.songs2vid.com/docs/api/overview](https://docs.songs2vid.com/docs/api/overview). diff --git a/docs/n8n.md b/docs/n8n.md index f773ec6..32ca2c6 100644 --- a/docs/n8n.md +++ b/docs/n8n.md @@ -2,7 +2,8 @@ Automate self-hosted Songs2VID with the community package **[`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid)**. -Full hosted guide: [https://docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n) +**Primary source hub:** [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID) +Full hosted guide: [https://docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n) · Web UI: [songs2vid.com](https://songs2vid.com) ## Install diff --git a/integrations/n8n/README.md b/integrations/n8n/README.md index 164b8a9..1cf5188 100644 --- a/integrations/n8n/README.md +++ b/integrations/n8n/README.md @@ -1,8 +1,10 @@ # n8n-nodes-songs2vid -Official community node for **[Songs2VID](https://songs2vid.com)** — turn audio + cover art into YouTube-ready videos from [n8n](https://n8n.io/). +Official community node for **[Songs2VID](https://git.atakanozban.com/Songs2VID)** — turn audio + cover art into YouTube-ready videos from [n8n](https://n8n.io/). -**Documentation:** [https://docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n) · in-repo [docs/n8n.md](../../docs/n8n.md) +**Primary source hub:** [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID) +**Documentation:** [https://docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n) · in-repo [docs/n8n.md](../../docs/n8n.md) +**Web UI (secondary mirror):** [songs2vid.com](https://songs2vid.com) Self-hosted OSS: set **Base URL** to your instance. API keys work without a paid plan (YouTube must be connected). @@ -70,10 +72,12 @@ There is no Delete Render endpoint — finished videos live on YouTube. ## Links -- Guide: https://songs2vid.com/docs/n8n -- REST endpoints: https://songs2vid.com/docs/api/endpoints -- Workflow template: https://songs2vid.com/docs/n8n/workflow-template.json +- **Primary source hub:** https://git.atakanozban.com/Songs2VID +- Guide: https://docs.songs2vid.com/docs/n8n +- REST endpoints: https://docs.songs2vid.com/docs/api/endpoints +- Workflow template: [`workflows/workflow-template.json`](./workflows/workflow-template.json) - npm: https://www.npmjs.com/package/n8n-nodes-songs2vid +- Web UI: https://songs2vid.com ## Development diff --git a/integrations/n8n/credentials/Songs2VidApi.credentials.ts b/integrations/n8n/credentials/Songs2VidApi.credentials.ts index 47dbb41..2ca7b9f 100644 --- a/integrations/n8n/credentials/Songs2VidApi.credentials.ts +++ b/integrations/n8n/credentials/Songs2VidApi.credentials.ts @@ -14,7 +14,7 @@ export class Songs2VidApi implements ICredentialType { displayName = "Songs2VID API"; - documentationUrl = "https://songs2vid.com/docs/n8n"; + documentationUrl = "https://git.atakanozban.com/Songs2VID"; properties: INodeProperties[] = [ { diff --git a/integrations/n8n/nodes/Songs2Vid/Songs2Vid.node.json b/integrations/n8n/nodes/Songs2Vid/Songs2Vid.node.json index 40c9c4e..9765d7e 100644 --- a/integrations/n8n/nodes/Songs2Vid/Songs2Vid.node.json +++ b/integrations/n8n/nodes/Songs2Vid/Songs2Vid.node.json @@ -6,12 +6,21 @@ "resources": { "credentialDocumentation": [ { - "url": "https://songs2vid.com/docs/n8n" + "url": "https://git.atakanozban.com/Songs2VID" + }, + { + "url": "https://docs.songs2vid.com/docs/n8n" } ], "primaryDocumentation": [ { - "url": "https://songs2vid.com/docs/api/endpoints" + "url": "https://git.atakanozban.com/Songs2VID" + }, + { + "url": "https://docs.songs2vid.com/docs/api/endpoints" + }, + { + "url": "https://songs2vid.com" } ] } diff --git a/integrations/n8n/package.json b/integrations/n8n/package.json index 527bafd..60b7260 100644 --- a/integrations/n8n/package.json +++ b/integrations/n8n/package.json @@ -10,10 +10,10 @@ "youtube" ], "license": "MIT", - "homepage": "https://songs2vid.com/docs/n8n", + "homepage": "https://git.atakanozban.com/Songs2VID", "author": { "name": "Songs2VID", - "url": "https://songs2vid.com" + "url": "https://git.atakanozban.com/Songs2VID" }, "repository": { "type": "git", @@ -21,7 +21,7 @@ "directory": "integrations/n8n" }, "bugs": { - "url": "https://songs2vid.com/docs/n8n" + "url": "https://git.atakanozban.com/Songs2VID/songs2vid/issues" }, "main": "index.js", "scripts": { diff --git a/integrations/n8n/workflows/workflow-template.json b/integrations/n8n/workflows/workflow-template.json index 356844a..195d69d 100644 --- a/integrations/n8n/workflows/workflow-template.json +++ b/integrations/n8n/workflows/workflow-template.json @@ -35,7 +35,7 @@ }, { "parameters": { - "content": "## Inputs\nProvide binary fields (`cover`, `audio`) or replace Manual Trigger with Drive/Dropbox.\n\nCredential: Header Auth → Name `Authorization`, Value `Bearer s2yt_live_…`\n\nSet env `SONGS2VID_WEBHOOK_URL` to this workflow's Production Webhook URL.", + "content": "## Inputs\nProvide binary fields (`cover`, `audio`) or replace Manual Trigger with Drive/Dropbox.\n\nCredential: Header Auth → Name `Authorization`, Value `Bearer s2yt_live_…`\n\nSet env `SONGS2VID_WEBHOOK_URL` to this workflow's Production Webhook URL.\n\nPrimary source: https://git.atakanozban.com/Songs2VID\nnpm: n8n-nodes-songs2vid · Web UI: https://songs2vid.com", "height": 240, "width": 320 }, diff --git a/lib/ffmpeg/encode.ts b/lib/ffmpeg/encode.ts index 9a3c282..01982ea 100644 --- a/lib/ffmpeg/encode.ts +++ b/lib/ffmpeg/encode.ts @@ -2,6 +2,7 @@ import { spawn } from "child_process"; import fs from "fs/promises"; import path from "path"; import ffmpegStatic from "ffmpeg-static"; +import { parseFile } from "music-metadata"; import { getVideoAttributionText } from "../branding"; import { getResolution } from "../constants"; import { @@ -38,6 +39,20 @@ function getFfmpegPath(): string { export { getFfmpegPath }; +async function getAudioDurationSeconds(audioPath: string): Promise { + const { format } = await parseFile(audioPath); + const duration = format.duration; + if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0) { + throw new Error(`Could not determine audio duration for ${audioPath}`); + } + return duration; +} + +/** FFmpeg 5.x mishandles `-shortest` with `-loop 1` still inputs (adds ~50s tail silence). */ +function formatOutputDurationSeconds(seconds: number): string { + return Math.max(0.01, seconds - 0.02).toFixed(3); +} + async function fileExists(p: string): Promise { try { await fs.access(p); @@ -196,6 +211,10 @@ export async function encodeVideo(options: { await fs.mkdir(path.dirname(options.outputPath), { recursive: true }); + const outputDurationSec = formatOutputDurationSeconds( + await getAudioDurationSeconds(options.audioPath), + ); + const settings = normalizeWatermarkSettings(options.watermark, options.includeWatermark); const layoutSettings = options.layout ? normalizeLayoutSettings(options.layout) @@ -269,7 +288,8 @@ export async function encodeVideo(options: { ); pushAudioEncodeArgs(args, audioEncode); args.push( - "-shortest", + "-t", + outputDurationSec, "-pix_fmt", "yuv420p", options.outputPath, @@ -366,7 +386,8 @@ export async function encodeVideo(options: { ); pushAudioEncodeArgs(args, audioEncode); args.push( - "-shortest", + "-t", + outputDurationSec, "-pix_fmt", "yuv420p", options.outputPath, diff --git a/lib/plans.ts b/lib/plans.ts index 07ad0d3..cd9d30a 100644 --- a/lib/plans.ts +++ b/lib/plans.ts @@ -29,7 +29,7 @@ export const APP_LIMITS: PlanLimits = { id3TagSupport: true, }; -export const SUPPORT_EMAIL = "songs2vid@atakanozban.com"; +export const SUPPORT_EMAIL = "support@songs2vid.com"; export const GITEA_ISSUES_URL = process.env.NEXT_PUBLIC_GITEA_ISSUES_URL ?? "https://git.atakanozban.com/Songs2VID/songs2vid/issues"; export const GITEA_URL =