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.
This commit is contained in:
@@ -5,9 +5,11 @@ Payment-free, self-hosted software that turns cover art and audio into YouTube v
|
|||||||
**Image:** `atakanozban/songs2vid:latest`
|
**Image:** `atakanozban/songs2vid:latest`
|
||||||
**License:** MIT
|
**License:** MIT
|
||||||
|
|
||||||
|
**Primary source hub:** https://git.atakanozban.com/Songs2VID
|
||||||
|
**Repo:** https://git.atakanozban.com/Songs2VID/songs2vid
|
||||||
**Docs:** https://docs.songs2vid.com
|
**Docs:** https://docs.songs2vid.com
|
||||||
**Source:** https://git.atakanozban.com/Songs2VID/songs2vid
|
**n8n (npm):** https://www.npmjs.com/package/n8n-nodes-songs2vid
|
||||||
**Hosted SaaS (separate):** https://songs2vid.com
|
**Web UI (secondary mirror):** https://songs2vid.com
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -43,8 +45,11 @@ Next.js 15 · PostgreSQL/Prisma · Redis/BullMQ · NextAuth (Google + YouTube)
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
|
- Primary source hub: https://git.atakanozban.com/Songs2VID
|
||||||
- Full documentation: https://docs.songs2vid.com
|
- Full documentation: https://docs.songs2vid.com
|
||||||
- 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
|
- 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
|
||||||
|
|||||||
@@ -88,10 +88,11 @@ the paid cloud terms for [songs2vid.com](https://songs2vid.com).
|
|||||||
|
|
||||||
## 🔗 Links
|
## 🔗 Links
|
||||||
|
|
||||||
|
- **Primary source hub:** [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID)
|
||||||
- Docs: [docs.songs2vid.com](https://docs.songs2vid.com)
|
- Docs: [docs.songs2vid.com](https://docs.songs2vid.com)
|
||||||
- Docker Hub: [atakanozban/songs2vid](https://hub.docker.com/r/atakanozban/songs2vid)
|
- Docker Hub: [atakanozban/songs2vid](https://hub.docker.com/r/atakanozban/songs2vid)
|
||||||
- Source: [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID)
|
- n8n node (npm): [`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid)
|
||||||
- Hosted SaaS (separate product): [songs2vid.com](https://songs2vid.com)
|
- Web UI / hosted SaaS (secondary mirror): [songs2vid.com](https://songs2vid.com)
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ Equivalent path: `POST $BASE_URL/api/v1/jobs` with the same JSON.
|
|||||||
| `job.failed` | All items failed |
|
| `job.failed` | All items failed |
|
||||||
| `job.partial` | Mix of success and failure |
|
| `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).
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Legacy example without webhook (same metadata shape)</summary>
|
<summary>Legacy example without webhook (same metadata shape)</summary>
|
||||||
|
|||||||
@@ -65,4 +65,4 @@ Returns the endpoint list, requirements, `layoutTemplates` (every art-track enum
|
|||||||
|
|
||||||
## Next
|
## 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).
|
||||||
|
|||||||
+2
-1
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Automate self-hosted Songs2VID with the community package **[`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid)**.
|
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
|
## Install
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# n8n-nodes-songs2vid
|
# 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/).
|
||||||
|
|
||||||
|
**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)
|
**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).
|
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
|
## Links
|
||||||
|
|
||||||
- Guide: https://songs2vid.com/docs/n8n
|
- **Primary source hub:** https://git.atakanozban.com/Songs2VID
|
||||||
- REST endpoints: https://songs2vid.com/docs/api/endpoints
|
- Guide: https://docs.songs2vid.com/docs/n8n
|
||||||
- Workflow template: https://songs2vid.com/docs/n8n/workflow-template.json
|
- 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
|
- npm: https://www.npmjs.com/package/n8n-nodes-songs2vid
|
||||||
|
- Web UI: https://songs2vid.com
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class Songs2VidApi implements ICredentialType {
|
|||||||
|
|
||||||
displayName = "Songs2VID API";
|
displayName = "Songs2VID API";
|
||||||
|
|
||||||
documentationUrl = "https://songs2vid.com/docs/n8n";
|
documentationUrl = "https://git.atakanozban.com/Songs2VID";
|
||||||
|
|
||||||
properties: INodeProperties[] = [
|
properties: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,12 +6,21 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"credentialDocumentation": [
|
"credentialDocumentation": [
|
||||||
{
|
{
|
||||||
"url": "https://songs2vid.com/docs/n8n"
|
"url": "https://git.atakanozban.com/Songs2VID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://docs.songs2vid.com/docs/n8n"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryDocumentation": [
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
"youtube"
|
"youtube"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://songs2vid.com/docs/n8n",
|
"homepage": "https://git.atakanozban.com/Songs2VID",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Songs2VID",
|
"name": "Songs2VID",
|
||||||
"url": "https://songs2vid.com"
|
"url": "https://git.atakanozban.com/Songs2VID"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"directory": "integrations/n8n"
|
"directory": "integrations/n8n"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://songs2vid.com/docs/n8n"
|
"url": "https://git.atakanozban.com/Songs2VID/songs2vid/issues"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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,
|
"height": 240,
|
||||||
"width": 320
|
"width": 320
|
||||||
},
|
},
|
||||||
|
|||||||
+23
-2
@@ -2,6 +2,7 @@ import { spawn } from "child_process";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import ffmpegStatic from "ffmpeg-static";
|
import ffmpegStatic from "ffmpeg-static";
|
||||||
|
import { parseFile } from "music-metadata";
|
||||||
import { getVideoAttributionText } from "../branding";
|
import { getVideoAttributionText } from "../branding";
|
||||||
import { getResolution } from "../constants";
|
import { getResolution } from "../constants";
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +39,20 @@ function getFfmpegPath(): string {
|
|||||||
|
|
||||||
export { getFfmpegPath };
|
export { getFfmpegPath };
|
||||||
|
|
||||||
|
async function getAudioDurationSeconds(audioPath: string): Promise<number> {
|
||||||
|
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<boolean> {
|
async function fileExists(p: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await fs.access(p);
|
await fs.access(p);
|
||||||
@@ -196,6 +211,10 @@ export async function encodeVideo(options: {
|
|||||||
|
|
||||||
await fs.mkdir(path.dirname(options.outputPath), { recursive: true });
|
await fs.mkdir(path.dirname(options.outputPath), { recursive: true });
|
||||||
|
|
||||||
|
const outputDurationSec = formatOutputDurationSeconds(
|
||||||
|
await getAudioDurationSeconds(options.audioPath),
|
||||||
|
);
|
||||||
|
|
||||||
const settings = normalizeWatermarkSettings(options.watermark, options.includeWatermark);
|
const settings = normalizeWatermarkSettings(options.watermark, options.includeWatermark);
|
||||||
const layoutSettings = options.layout
|
const layoutSettings = options.layout
|
||||||
? normalizeLayoutSettings(options.layout)
|
? normalizeLayoutSettings(options.layout)
|
||||||
@@ -269,7 +288,8 @@ export async function encodeVideo(options: {
|
|||||||
);
|
);
|
||||||
pushAudioEncodeArgs(args, audioEncode);
|
pushAudioEncodeArgs(args, audioEncode);
|
||||||
args.push(
|
args.push(
|
||||||
"-shortest",
|
"-t",
|
||||||
|
outputDurationSec,
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
options.outputPath,
|
options.outputPath,
|
||||||
@@ -366,7 +386,8 @@ export async function encodeVideo(options: {
|
|||||||
);
|
);
|
||||||
pushAudioEncodeArgs(args, audioEncode);
|
pushAudioEncodeArgs(args, audioEncode);
|
||||||
args.push(
|
args.push(
|
||||||
"-shortest",
|
"-t",
|
||||||
|
outputDurationSec,
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
options.outputPath,
|
options.outputPath,
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ export const APP_LIMITS: PlanLimits = {
|
|||||||
id3TagSupport: true,
|
id3TagSupport: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SUPPORT_EMAIL = "songs2vid@atakanozban.com";
|
export const SUPPORT_EMAIL = "support@songs2vid.com";
|
||||||
export const GITEA_ISSUES_URL =
|
export const GITEA_ISSUES_URL =
|
||||||
process.env.NEXT_PUBLIC_GITEA_ISSUES_URL ?? "https://git.atakanozban.com/Songs2VID/songs2vid/issues";
|
process.env.NEXT_PUBLIC_GITEA_ISSUES_URL ?? "https://git.atakanozban.com/Songs2VID/songs2vid/issues";
|
||||||
export const GITEA_URL =
|
export const GITEA_URL =
|
||||||
|
|||||||
Reference in New Issue
Block a user