From af0fa1892a94cc728e3a00b2261bf94688f73acd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:04:00 +0000
Subject: [PATCH 1/6] Initial plan
From fe2d6e228a300118c58d6ec1f93195c3d13c8e2e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:10:13 +0000
Subject: [PATCH 2/6] Add 4K presets and automation fallbacks for phase3/phase7
Agent-Logs-Url: https://github.com/Dcode9/MPLayer/sessions/0c10a233-ebce-4372-bf11-c102eee689dd
Co-authored-by: Dcode9 <194372792+Dcode9@users.noreply.github.com>
---
.github/workflows/phase6-full-pipeline.yml | 14 ++-
.github/workflows/phase7-youtube-upload.yml | 14 ++-
.github/workflows/test-render.yml | 6 ++
package.json | 2 +
remotion/Root.jsx | 22 +++++
scripts/phase3-build.js | 66 ++++++++++++--
scripts/phase5-render-4k-logo.js | 60 +++++++++++++
scripts/phase5-render-4k.js | 60 +++++++++++++
scripts/phase7-upload.js | 99 +++++++++++++++++++--
9 files changed, 325 insertions(+), 18 deletions(-)
create mode 100644 scripts/phase5-render-4k-logo.js
create mode 100644 scripts/phase5-render-4k.js
diff --git a/.github/workflows/phase6-full-pipeline.yml b/.github/workflows/phase6-full-pipeline.yml
index 703e1ab..38997b8 100644
--- a/.github/workflows/phase6-full-pipeline.yml
+++ b/.github/workflows/phase6-full-pipeline.yml
@@ -4,9 +4,9 @@ on:
workflow_dispatch:
inputs:
song_query:
- description: Song query (used only when song_id is empty)
+ description: Song query (leave empty to auto-pick a trending query)
required: false
- default: Aaj Ki Raat
+ default: ''
type: string
song_id:
description: Optional direct JioSaavn song ID
@@ -21,6 +21,8 @@ on:
options:
- 16x9-logo
- 16x9-clean
+ - 4k-logo
+ - 4k-clean
max_seconds:
description: Clip duration cap in seconds (0 = full song)
required: false
@@ -59,8 +61,10 @@ jobs:
echo "[phase-6] Starting Phase 3"
if [ -n "$SONG_ID" ]; then
npm run phase3:build -- --song-id="$SONG_ID" --out=data/phase3-lyrics.json
- else
+ elif [ -n "$SONG_QUERY" ]; then
npm run phase3:build -- --query="$SONG_QUERY" --out=data/phase3-lyrics.json
+ else
+ npm run phase3:build -- --auto-trending --out=data/phase3-lyrics.json
fi
- name: Phase 5 - Render template lyrics video
@@ -76,6 +80,10 @@ jobs:
if [ "$RENDER_PRESET" = "16x9-clean" ]; then
npm run phase5:render:16x9 -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
+ elif [ "$RENDER_PRESET" = "4k-clean" ]; then
+ npm run phase5:render:4k -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
+ elif [ "$RENDER_PRESET" = "4k-logo" ]; then
+ npm run phase5:render:4k:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
else
npm run phase5:render:16x9:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase6-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
fi
diff --git a/.github/workflows/phase7-youtube-upload.yml b/.github/workflows/phase7-youtube-upload.yml
index 65c6171..08cf786 100644
--- a/.github/workflows/phase7-youtube-upload.yml
+++ b/.github/workflows/phase7-youtube-upload.yml
@@ -4,9 +4,9 @@ on:
workflow_dispatch:
inputs:
song_query:
- description: Song query (used only when song_id is empty)
+ description: Song query (leave empty to auto-pick a trending query)
required: false
- default: Aaj Ki Raat
+ default: ''
type: string
song_id:
description: Optional direct JioSaavn song ID
@@ -21,6 +21,8 @@ on:
options:
- 16x9-logo
- 16x9-clean
+ - 4k-logo
+ - 4k-clean
max_seconds:
description: Clip duration cap (0 means full song)
required: false
@@ -83,8 +85,10 @@ jobs:
echo "[phase-7] Starting Phase 3"
if [ -n "$SONG_ID" ]; then
npm run phase3:build -- --song-id="$SONG_ID" --out=data/phase3-lyrics.json
- else
+ elif [ -n "$SONG_QUERY" ]; then
npm run phase3:build -- --query="$SONG_QUERY" --out=data/phase3-lyrics.json
+ else
+ npm run phase3:build -- --auto-trending --out=data/phase3-lyrics.json
fi
- name: Phase 5 - Render video
@@ -100,6 +104,10 @@ jobs:
if [ "$RENDER_PRESET" = "16x9-clean" ]; then
npm run phase5:render:16x9 -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
+ elif [ "$RENDER_PRESET" = "4k-clean" ]; then
+ npm run phase5:render:4k -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
+ elif [ "$RENDER_PRESET" = "4k-logo" ]; then
+ npm run phase5:render:4k:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
else
npm run phase5:render:16x9:logo -- --lyrics=data/phase3-lyrics.json --out=output/phase7-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
fi
diff --git a/.github/workflows/test-render.yml b/.github/workflows/test-render.yml
index 3adfba0..e84b9e9 100644
--- a/.github/workflows/test-render.yml
+++ b/.github/workflows/test-render.yml
@@ -11,6 +11,8 @@ on:
options:
- 16x9-logo
- 16x9-clean
+ - 4k-logo
+ - 4k-clean
max_seconds:
description: Clip duration cap in seconds
required: false
@@ -53,6 +55,10 @@ jobs:
if [ "$RENDER_PRESET" = "16x9-clean" ]; then
npm run phase5:render:16x9 -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
+ elif [ "$RENDER_PRESET" = "4k-clean" ]; then
+ npm run phase5:render:4k -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
+ elif [ "$RENDER_PRESET" = "4k-logo" ]; then
+ npm run phase5:render:4k:logo -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
else
npm run phase5:render:16x9:logo -- --lyrics=data/test-lyrics.json --out=output/test-video.mp4 --max-seconds="$PHASE5_MAX_SECONDS"
fi
diff --git a/package.json b/package.json
index 7b63623..ac6e020 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,8 @@
"phase5:render": "node scripts/phase5-render.js",
"phase5:render:16x9": "node scripts/phase5-render-16x9.js",
"phase5:render:16x9:logo": "node scripts/phase5-render-16x9-logo.js",
+ "phase5:render:4k": "node scripts/phase5-render-4k.js",
+ "phase5:render:4k:logo": "node scripts/phase5-render-4k-logo.js",
"phase7:upload": "node scripts/phase7-upload.js",
"render:test": "node scripts/render-test.js",
"remotion:studio": "remotion studio remotion/index.jsx"
diff --git a/remotion/Root.jsx b/remotion/Root.jsx
index 23de0ce..4992917 100644
--- a/remotion/Root.jsx
+++ b/remotion/Root.jsx
@@ -6,6 +6,8 @@ import templateSongData from './template-song-data.json';
const FPS = 30;
const WIDTH_16X9 = 1920;
const HEIGHT_16X9 = 1080;
+const WIDTH_4K = 3840;
+const HEIGHT_4K = 2160;
const DEFAULT_DURATION_SECONDS = Number(templateSongData.song?.duration || 205);
const DEFAULT_DURATION_FRAMES = Math.max(1, Math.round(DEFAULT_DURATION_SECONDS * FPS));
@@ -51,6 +53,26 @@ export const RemotionRoot = () => {
height={HEIGHT_16X9}
defaultProps={defaultProps16x9Logo}
/>
+
+
+
+
>
);
};
diff --git a/scripts/phase3-build.js b/scripts/phase3-build.js
index c25563d..30bb361 100644
--- a/scripts/phase3-build.js
+++ b/scripts/phase3-build.js
@@ -7,12 +7,15 @@ const path = require('node:path');
const templateSongData = require('../remotion/template-song-data.json');
const PROJECT_ROOT = path.resolve(__dirname, '..');
+const TRENDING_FALLBACK_QUERY = 'Aaj Ki Raat';
+const APPLE_MUSIC_TRENDING_URL = 'https://rss.applemarketingtools.com/api/v2/in/music/most-played/25/songs.json';
const parseArgs = (argv) => {
const args = {
out: path.join(PROJECT_ROOT, 'data', 'phase3-lyrics.json'),
songId: '',
query: '',
+ autoTrending: false,
};
for (const arg of argv) {
@@ -23,6 +26,8 @@ const parseArgs = (argv) => {
args.songId = arg.slice('--song-id='.length).trim();
} else if (arg.startsWith('--query=')) {
args.query = arg.slice('--query='.length).trim();
+ } else if (arg === '--auto-trending') {
+ args.autoTrending = true;
}
}
@@ -31,32 +36,79 @@ const parseArgs = (argv) => {
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
-const buildTemplatePayload = ({songId, query}) => {
+const fetchTrendingSongQuery = async () => {
+ try {
+ const response = await fetch(APPLE_MUSIC_TRENDING_URL);
+ if (!response.ok) {
+ return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
+ }
+
+ const payload = await response.json();
+ const firstSong = payload?.feed?.results?.[0];
+ const title = String(firstSong?.name || '').trim();
+ const artist = String(firstSong?.artistName || '').trim();
+
+ if (!title || !artist) {
+ return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
+ }
+
+ return {
+ query: `${title} ${artist}`,
+ source: 'apple-music-trending',
+ };
+ } catch (_error) {
+ return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
+ }
+};
+
+const buildTemplatePayload = ({songId, query, songSelectionMethod, trendingSource}) => {
const payload = cloneJson(templateSongData);
payload.metadata = payload.metadata || {};
payload.metadata.generatedAt = new Date().toISOString();
payload.metadata.phase = 3;
payload.metadata.pipelineMode = 'template-only';
+ payload.metadata.songSelectionMethod = songSelectionMethod || payload.metadata.songSelectionMethod || 'template-default';
if (songId) {
payload.song = payload.song || {};
payload.song.id = songId;
- payload.metadata.songSelectionMethod = 'direct-id-template';
}
if (query) {
payload.metadata.query = query;
- if (!songId) {
- payload.metadata.songSelectionMethod = 'query-template';
- }
+ }
+
+ if (trendingSource) {
+ payload.metadata.trendingSource = trendingSource;
}
return payload;
};
-const runPhase3Build = async ({songId, query, out}) => {
- const payload = buildTemplatePayload({songId, query});
+const runPhase3Build = async ({songId, query, out, autoTrending}) => {
+ const normalizedSongId = String(songId || '').trim();
+ let normalizedQuery = String(query || '').trim();
+ let songSelectionMethod = 'template-default';
+ let trendingSource = '';
+
+ if (normalizedSongId) {
+ songSelectionMethod = 'direct-id-template';
+ } else if (normalizedQuery) {
+ songSelectionMethod = 'query-template';
+ } else if (autoTrending || String(process.env.PHASE3_AUTO_TRENDING || '').trim() === '1') {
+ const trending = await fetchTrendingSongQuery();
+ normalizedQuery = trending.query;
+ trendingSource = trending.source;
+ songSelectionMethod = 'auto-trending-template';
+ }
+
+ const payload = buildTemplatePayload({
+ songId: normalizedSongId,
+ query: normalizedQuery,
+ songSelectionMethod,
+ trendingSource,
+ });
await fs.mkdir(path.dirname(out), {recursive: true});
await fs.writeFile(out, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
diff --git a/scripts/phase5-render-4k-logo.js b/scripts/phase5-render-4k-logo.js
new file mode 100644
index 0000000..a36bf90
--- /dev/null
+++ b/scripts/phase5-render-4k-logo.js
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+'use strict';
+
+const path = require('node:path');
+
+const {runPhase5Render} = require('./phase5-render');
+
+const PROJECT_ROOT = path.resolve(__dirname, '..');
+
+const parsePositiveNumber = (value) => {
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed) || parsed < 0) {
+ return 0;
+ }
+ return parsed;
+};
+
+const parseArgs = (argv) => {
+ const args = {
+ lyrics: path.join(PROJECT_ROOT, 'data', 'phase3-lyrics.json'),
+ out: path.join(PROJECT_ROOT, 'output', 'phase5-video-4k-logo.mp4'),
+ maxSeconds: parsePositiveNumber(process.env.PHASE5_MAX_SECONDS || '0'),
+ concurrency: Number(process.env.PHASE5_RENDER_CONCURRENCY || 2),
+ crf: Number(process.env.PHASE5_RENDER_CRF || 20),
+ };
+
+ for (const arg of argv) {
+ if (arg.startsWith('--lyrics=')) {
+ const value = arg.slice('--lyrics='.length).trim();
+ args.lyrics = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
+ } else if (arg.startsWith('--out=')) {
+ const value = arg.slice('--out='.length).trim();
+ args.out = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
+ } else if (arg.startsWith('--max-seconds=')) {
+ args.maxSeconds = parsePositiveNumber(arg.slice('--max-seconds='.length).trim());
+ }
+ }
+
+ return args;
+};
+
+const main = async () => {
+ const args = parseArgs(process.argv.slice(2));
+
+ const result = await runPhase5Render({
+ ...args,
+ compositionId: 'LyricsTemplateVideo4kLogo',
+ });
+
+ console.log('[phase5-4k-logo] Render complete');
+ console.log(`[phase5-4k-logo] Output: ${path.relative(PROJECT_ROOT, result.out)}`);
+ console.log(
+ `[phase5-4k-logo] Video: ${result.width}x${result.height} @ ${result.fps}fps, ${result.durationInSeconds.toFixed(2)}s`,
+ );
+};
+
+main().catch((error) => {
+ console.error(`[phase5-4k-logo] Failed: ${error.message || error}`);
+ process.exit(1);
+});
diff --git a/scripts/phase5-render-4k.js b/scripts/phase5-render-4k.js
new file mode 100644
index 0000000..9ed8877
--- /dev/null
+++ b/scripts/phase5-render-4k.js
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+'use strict';
+
+const path = require('node:path');
+
+const {runPhase5Render} = require('./phase5-render');
+
+const PROJECT_ROOT = path.resolve(__dirname, '..');
+
+const parsePositiveNumber = (value) => {
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed) || parsed < 0) {
+ return 0;
+ }
+ return parsed;
+};
+
+const parseArgs = (argv) => {
+ const args = {
+ lyrics: path.join(PROJECT_ROOT, 'data', 'phase3-lyrics.json'),
+ out: path.join(PROJECT_ROOT, 'output', 'phase5-video-4k.mp4'),
+ maxSeconds: parsePositiveNumber(process.env.PHASE5_MAX_SECONDS || '0'),
+ concurrency: Number(process.env.PHASE5_RENDER_CONCURRENCY || 2),
+ crf: Number(process.env.PHASE5_RENDER_CRF || 20),
+ };
+
+ for (const arg of argv) {
+ if (arg.startsWith('--lyrics=')) {
+ const value = arg.slice('--lyrics='.length).trim();
+ args.lyrics = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
+ } else if (arg.startsWith('--out=')) {
+ const value = arg.slice('--out='.length).trim();
+ args.out = path.isAbsolute(value) ? value : path.join(PROJECT_ROOT, value);
+ } else if (arg.startsWith('--max-seconds=')) {
+ args.maxSeconds = parsePositiveNumber(arg.slice('--max-seconds='.length).trim());
+ }
+ }
+
+ return args;
+};
+
+const main = async () => {
+ const args = parseArgs(process.argv.slice(2));
+
+ const result = await runPhase5Render({
+ ...args,
+ compositionId: 'LyricsTemplateVideo4k',
+ });
+
+ console.log('[phase5-4k] Render complete');
+ console.log(`[phase5-4k] Output: ${path.relative(PROJECT_ROOT, result.out)}`);
+ console.log(
+ `[phase5-4k] Video: ${result.width}x${result.height} @ ${result.fps}fps, ${result.durationInSeconds.toFixed(2)}s`,
+ );
+};
+
+main().catch((error) => {
+ console.error(`[phase5-4k] Failed: ${error.message || error}`);
+ process.exit(1);
+});
diff --git a/scripts/phase7-upload.js b/scripts/phase7-upload.js
index f43d123..0ee1251 100644
--- a/scripts/phase7-upload.js
+++ b/scripts/phase7-upload.js
@@ -7,6 +7,7 @@ const path = require('node:path');
const {google} = require('googleapis');
const PROJECT_ROOT = path.resolve(__dirname, '..');
+const POLLINATIONS_METADATA_URL = 'https://text.pollinations.ai/';
const parseTags = (raw) => {
return String(raw || '')
@@ -15,6 +16,17 @@ const parseTags = (raw) => {
.filter(Boolean);
};
+const normalizeTags = (tags) => {
+ return Array.from(
+ new Set(
+ (tags || [])
+ .map((tag) => String(tag || '').trim())
+ .filter(Boolean)
+ .map((tag) => tag.slice(0, 30)),
+ ),
+ ).slice(0, 15);
+};
+
const fileExists = async (filePath) => {
try {
await fs.access(filePath);
@@ -33,6 +45,70 @@ const readJsonIfExists = async (filePath) => {
return JSON.parse(raw);
};
+const parseJsonObjectFromText = (rawText) => {
+ const text = String(rawText || '').trim();
+ if (!text) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(text);
+ } catch (_error) {
+ const start = text.indexOf('{');
+ const end = text.lastIndexOf('}');
+ if (start >= 0 && end > start) {
+ try {
+ return JSON.parse(text.slice(start, end + 1));
+ } catch (_nestedError) {
+ return null;
+ }
+ }
+ return null;
+ }
+};
+
+const generateAutoMetadata = async ({songName, artistName}) => {
+ if (String(process.env.PHASE7_AUTO_METADATA || '1').trim() === '0') {
+ return null;
+ }
+
+ const prompt = [
+ 'Return only valid JSON with keys: title, description, tags.',
+ `Song: ${songName}`,
+ `Artist: ${artistName}`,
+ 'Style: SEO-friendly YouTube lyrics video metadata.',
+ 'Constraints:',
+ '- title max 95 chars',
+ '- description 2 short lines, max 450 chars',
+ '- tags as array of 5-10 short strings',
+ ].join('\n');
+
+ try {
+ const response = await fetch(`${POLLINATIONS_METADATA_URL}${encodeURIComponent(prompt)}`);
+ if (!response.ok) {
+ return null;
+ }
+
+ const rawText = await response.text();
+ const parsed = parseJsonObjectFromText(rawText);
+ if (!parsed || typeof parsed !== 'object') {
+ return null;
+ }
+
+ const title = String(parsed.title || '').trim().slice(0, 95);
+ const description = String(parsed.description || '').trim().slice(0, 450);
+ const tags = normalizeTags(Array.isArray(parsed.tags) ? parsed.tags : []);
+
+ if (!title || !description || tags.length === 0) {
+ return null;
+ }
+
+ return {title, description, tags, source: 'pollinations'};
+ } catch (_error) {
+ return null;
+ }
+};
+
const runPhase7Upload = async () => {
const clientId = process.env.YOUTUBE_CLIENT_ID || '';
const clientSecret = process.env.YOUTUBE_CLIENT_SECRET || '';
@@ -55,18 +131,30 @@ const runPhase7Upload = async () => {
const artistName = phase3Data?.song?.artist || 'Unknown Artist';
const defaultTitle = `${songName} - ${artistName} | Lyrics Visualiser`;
- const title = (process.env.YOUTUBE_TITLE || '').trim() || defaultTitle;
-
const defaultDescription = [
`${songName} by ${artistName}`,
'',
'Rendered with template-only Remotion pipeline.',
].join('\n');
- const description = (process.env.YOUTUBE_DESCRIPTION || '').trim() || defaultDescription;
-
+ const manualTitle = (process.env.YOUTUBE_TITLE || '').trim();
+ const manualDescription = (process.env.YOUTUBE_DESCRIPTION || '').trim();
const tagsFromInput = parseTags(process.env.YOUTUBE_TAGS || '');
- const tags = tagsFromInput.length > 0 ? tagsFromInput : [songName, artistName, 'lyrics', 'music'];
+
+ const autoMetadata =
+ !manualTitle || !manualDescription || tagsFromInput.length === 0
+ ? await generateAutoMetadata({songName, artistName})
+ : null;
+
+ const title = manualTitle || autoMetadata?.title || defaultTitle;
+ const description = manualDescription || autoMetadata?.description || defaultDescription;
+ const tags = normalizeTags(
+ tagsFromInput.length > 0 ? tagsFromInput : autoMetadata?.tags || [songName, artistName, 'lyrics', 'music'],
+ );
+
+ const metadataSource = manualTitle || manualDescription || tagsFromInput.length > 0
+ ? 'manual-overrides'
+ : autoMetadata?.source || 'template-default';
const privacyStatus = ['private', 'public', 'unlisted'].includes(process.env.YOUTUBE_PRIVACY_STATUS || '')
? process.env.YOUTUBE_PRIVACY_STATUS
@@ -109,6 +197,7 @@ const runPhase7Upload = async () => {
title,
privacyStatus,
tags,
+ metadataSource,
sourceVideo: path.relative(PROJECT_ROOT, videoFile),
sourcePhase3Json: path.relative(PROJECT_ROOT, phase3File),
youtubeResponse: response.data || null,
From 6e69f96588ce6955e3755522105df5b4aee0497f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:11:41 +0000
Subject: [PATCH 3/6] Rename Remotion default props for 4K clarity
Agent-Logs-Url: https://github.com/Dcode9/MPLayer/sessions/0c10a233-ebce-4372-bf11-c102eee689dd
Co-authored-by: Dcode9 <194372792+Dcode9@users.noreply.github.com>
---
remotion/Root.jsx | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/remotion/Root.jsx b/remotion/Root.jsx
index 4992917..287f076 100644
--- a/remotion/Root.jsx
+++ b/remotion/Root.jsx
@@ -11,11 +11,11 @@ const HEIGHT_4K = 2160;
const DEFAULT_DURATION_SECONDS = Number(templateSongData.song?.duration || 205);
const DEFAULT_DURATION_FRAMES = Math.max(1, Math.round(DEFAULT_DURATION_SECONDS * FPS));
-const defaultProps16x9 = {
+const defaultPropsClean = {
songData: templateSongData,
};
-const defaultProps16x9Logo = {
+const defaultPropsWithLogo = {
songData: templateSongData,
showAnimatedLogo: true,
logoSrc: staticFile('assets/DTunes-transparent.svg'),
@@ -31,7 +31,7 @@ export const RemotionRoot = () => {
fps={FPS}
width={WIDTH_16X9}
height={HEIGHT_16X9}
- defaultProps={defaultProps16x9Logo}
+ defaultProps={defaultPropsWithLogo}
/>
{
fps={FPS}
width={WIDTH_16X9}
height={HEIGHT_16X9}
- defaultProps={defaultProps16x9}
+ defaultProps={defaultPropsClean}
/>
{
fps={FPS}
width={WIDTH_16X9}
height={HEIGHT_16X9}
- defaultProps={defaultProps16x9Logo}
+ defaultProps={defaultPropsWithLogo}
/>
{
fps={FPS}
width={WIDTH_4K}
height={HEIGHT_4K}
- defaultProps={defaultProps16x9}
+ defaultProps={defaultPropsClean}
/>
{
fps={FPS}
width={WIDTH_4K}
height={HEIGHT_4K}
- defaultProps={defaultProps16x9Logo}
+ defaultProps={defaultPropsWithLogo}
/>
>
);
From cb42b7181666efa365e612f8adb378ac8b897d25 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:12:46 +0000
Subject: [PATCH 4/6] Refine phase7 metadata source readability
Agent-Logs-Url: https://github.com/Dcode9/MPLayer/sessions/0c10a233-ebce-4372-bf11-c102eee689dd
Co-authored-by: Dcode9 <194372792+Dcode9@users.noreply.github.com>
---
scripts/phase7-upload.js | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/scripts/phase7-upload.js b/scripts/phase7-upload.js
index 0ee1251..a274108 100644
--- a/scripts/phase7-upload.js
+++ b/scripts/phase7-upload.js
@@ -152,9 +152,8 @@ const runPhase7Upload = async () => {
tagsFromInput.length > 0 ? tagsFromInput : autoMetadata?.tags || [songName, artistName, 'lyrics', 'music'],
);
- const metadataSource = manualTitle || manualDescription || tagsFromInput.length > 0
- ? 'manual-overrides'
- : autoMetadata?.source || 'template-default';
+ const hasManualMetadata = Boolean(manualTitle || manualDescription || tagsFromInput.length > 0);
+ const metadataSource = hasManualMetadata ? 'manual-overrides' : autoMetadata?.source || 'template-default';
const privacyStatus = ['private', 'public', 'unlisted'].includes(process.env.YOUTUBE_PRIVACY_STATUS || '')
? process.env.YOUTUBE_PRIVACY_STATUS
From c0f040747bdfbf8dd566377360383af1e7c00225 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:13:54 +0000
Subject: [PATCH 5/6] Add fetch timeouts for auto metadata and trending lookup
Agent-Logs-Url: https://github.com/Dcode9/MPLayer/sessions/0c10a233-ebce-4372-bf11-c102eee689dd
Co-authored-by: Dcode9 <194372792+Dcode9@users.noreply.github.com>
---
scripts/phase3-build.js | 4 +++-
scripts/phase7-upload.js | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/scripts/phase3-build.js b/scripts/phase3-build.js
index 30bb361..0c56708 100644
--- a/scripts/phase3-build.js
+++ b/scripts/phase3-build.js
@@ -38,7 +38,9 @@ const cloneJson = (value) => JSON.parse(JSON.stringify(value));
const fetchTrendingSongQuery = async () => {
try {
- const response = await fetch(APPLE_MUSIC_TRENDING_URL);
+ const response = await fetch(APPLE_MUSIC_TRENDING_URL, {
+ signal: AbortSignal.timeout(5000),
+ });
if (!response.ok) {
return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
}
diff --git a/scripts/phase7-upload.js b/scripts/phase7-upload.js
index a274108..d35f601 100644
--- a/scripts/phase7-upload.js
+++ b/scripts/phase7-upload.js
@@ -84,7 +84,9 @@ const generateAutoMetadata = async ({songName, artistName}) => {
].join('\n');
try {
- const response = await fetch(`${POLLINATIONS_METADATA_URL}${encodeURIComponent(prompt)}`);
+ const response = await fetch(`${POLLINATIONS_METADATA_URL}${encodeURIComponent(prompt)}`, {
+ signal: AbortSignal.timeout(7000),
+ });
if (!response.ok) {
return null;
}
From 9818d1706c66f81c90343b8ebc2a6846df254629 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:15:38 +0000
Subject: [PATCH 6/6] Parameterize metadata and trending constants
Agent-Logs-Url: https://github.com/Dcode9/MPLayer/sessions/0c10a233-ebce-4372-bf11-c102eee689dd
Co-authored-by: Dcode9 <194372792+Dcode9@users.noreply.github.com>
---
scripts/phase3-build.js | 5 +++--
scripts/phase7-upload.js | 12 ++++++++----
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/scripts/phase3-build.js b/scripts/phase3-build.js
index 0c56708..b50d2b9 100644
--- a/scripts/phase3-build.js
+++ b/scripts/phase3-build.js
@@ -7,7 +7,8 @@ const path = require('node:path');
const templateSongData = require('../remotion/template-song-data.json');
const PROJECT_ROOT = path.resolve(__dirname, '..');
-const TRENDING_FALLBACK_QUERY = 'Aaj Ki Raat';
+const TRENDING_FALLBACK_QUERY = String(process.env.PHASE3_TRENDING_FALLBACK_QUERY || 'Aaj Ki Raat').trim() || 'Aaj Ki Raat';
+const TRENDING_FETCH_TIMEOUT_MS = Number(process.env.PHASE3_TRENDING_FETCH_TIMEOUT_MS || 5000);
const APPLE_MUSIC_TRENDING_URL = 'https://rss.applemarketingtools.com/api/v2/in/music/most-played/25/songs.json';
const parseArgs = (argv) => {
@@ -39,7 +40,7 @@ const cloneJson = (value) => JSON.parse(JSON.stringify(value));
const fetchTrendingSongQuery = async () => {
try {
const response = await fetch(APPLE_MUSIC_TRENDING_URL, {
- signal: AbortSignal.timeout(5000),
+ signal: AbortSignal.timeout(TRENDING_FETCH_TIMEOUT_MS),
});
if (!response.ok) {
return {query: TRENDING_FALLBACK_QUERY, source: 'fallback'};
diff --git a/scripts/phase7-upload.js b/scripts/phase7-upload.js
index d35f601..f9c4cfb 100644
--- a/scripts/phase7-upload.js
+++ b/scripts/phase7-upload.js
@@ -8,6 +8,10 @@ const {google} = require('googleapis');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const POLLINATIONS_METADATA_URL = 'https://text.pollinations.ai/';
+const METADATA_FETCH_TIMEOUT_MS = Number(process.env.PHASE7_METADATA_FETCH_TIMEOUT_MS || 7000);
+const YOUTUBE_TITLE_MAX_LENGTH = 95;
+const YOUTUBE_DESCRIPTION_MAX_LENGTH = 450;
+const YOUTUBE_TAG_MAX_LENGTH = 30;
const parseTags = (raw) => {
return String(raw || '')
@@ -22,7 +26,7 @@ const normalizeTags = (tags) => {
(tags || [])
.map((tag) => String(tag || '').trim())
.filter(Boolean)
- .map((tag) => tag.slice(0, 30)),
+ .map((tag) => tag.slice(0, YOUTUBE_TAG_MAX_LENGTH)),
),
).slice(0, 15);
};
@@ -85,7 +89,7 @@ const generateAutoMetadata = async ({songName, artistName}) => {
try {
const response = await fetch(`${POLLINATIONS_METADATA_URL}${encodeURIComponent(prompt)}`, {
- signal: AbortSignal.timeout(7000),
+ signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS),
});
if (!response.ok) {
return null;
@@ -97,8 +101,8 @@ const generateAutoMetadata = async ({songName, artistName}) => {
return null;
}
- const title = String(parsed.title || '').trim().slice(0, 95);
- const description = String(parsed.description || '').trim().slice(0, 450);
+ const title = String(parsed.title || '').trim().slice(0, YOUTUBE_TITLE_MAX_LENGTH);
+ const description = String(parsed.description || '').trim().slice(0, YOUTUBE_DESCRIPTION_MAX_LENGTH);
const tags = normalizeTags(Array.isArray(parsed.tags) ? parsed.tags : []);
if (!title || !description || tags.length === 0) {