7.6: file name customization

This commit is contained in:
wukko
2023-10-15 11:06:48 +06:00
committed by GitHub
30 changed files with 1074 additions and 128 deletions

View File

@@ -1,5 +1,16 @@
{
"current": {
"version": "7.6",
"date": "October 15, 2023",
"title": "customizable file names, instagram stories, and first cobalt sponsor!",
"banner": {
"file": "meowthcenter.png",
"width": 851,
"height": 640
},
"content": "as many have (very) often requested, cobalt now lets you pick between several file name format styles!\ngo to <span class=\"text-backdrop\">settings > other</span> and change it to whichever you like! there's a preview of each style, so you know how exactly files are gonna look like.\n\nif you liked file names the way they were before, don't worry: classic style is still the default :)\n\non a different but not any less important note: cobalt is now sponsored by <a class=\"text-backdrop link\" href=\"https://royalehosting.net/\" target=\"_blank\">royalehosting.net</a>!\noverall service performance and stability is gonna be better, but also more content will be possible to download thanks to geniuine server locations. and yes, still no ads or trackers.\n\nthis update also includes a bunch of other changes, check them out:\n\nservice improvements:\n*; added support for instagram stories thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/194\" target=\"_blank\">#194</a>.\n*; fixed reddit support thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/221\" target=\"_blank\">#221</a>.\n*; added support for rich file names for youtube, vimeo, soundcloud, rutube, and vk.\n*; mute and audio dub file name tags don't appear together anymore.\n*; youtube: dub file name tag doesn't appear anymore if audio track is default.\n\ninterface improvements:\n*; added a list of sponsors to about tab. if you host an instance, it's disabled by default, but can be enabled with showSponsors env variable.\n*; about button now opens about tab when no new changelog is available.\n*; fixed download button thickness on ios.\n\nyou now can reach out to cobalt via email for support! it's located in the about tab along with other socials, such as discord.\n\ni hope you enjoy this long-awaited update and have a blissful day :D"
},
"history": [{
"version": "7.5",
"date": "September 16, 2023",
"title": "support for twitch clips and rutube!",
@@ -9,8 +20,7 @@
"height": 640
},
"content": "hey! this update (finally) adds support for twitch clips and rutube, among other smaller changes.\n\nservice improvements:\n*; added support for twitch clips. no vods, they're unnecessary. just clip whatever you want to download!\n*; added support for rutube in case you ever wanted to download something russian.\n\ninterface improvements:\n*; added a note about cobalt not being affiliated with any supported services.\n*; added a note about meta (the company) in russian.\n*; better russian localization. will keep improving it to make it sound not so robotic over time.\n\nother improvements:\n*; all official servers are now using the docker package. and so should you!\n*; moved the load balancer to poland. requests should be slightly faster now.\n*; minor codebase clean up.\n\nif you're confused about the new domain, read the older changelog! just scroll lower and press \"expand\".\n\ni hope you find this update useful and have a wonderful day :)\n\nbtw, cobalt has a pretty active community server on discord. go to about > support & source code to join!"
},
"history": [{
}, {
"version": "7.4",
"date": "September 9, 2023",
"title": "new domain, what's coming in future, bug fixes, and more!",

View File

@@ -35,12 +35,16 @@ const names = {
"📑": "boring_document",
"🧮": "abacus",
"😸": "cat_grin",
"📰": "newspaper"
"📰": "newspaper",
"🎞️": "film_frames",
"🎧": "headphone",
"📧": "email"
}
let sizing = {
18: 0.8,
22: 0.4,
30: 0.7,
32: 0.8,
48: 0.9,
64: 0.9,
78: 0.9

View File

@@ -332,7 +332,8 @@ export default function(obj) {
}])
})
+ settingsCategory({
name: t('SettingsCodecSubtitle'),
name: "codec",
title: t('SettingsCodecSubtitle'),
body: switcher({
name: "vCodec",
explanation: t('SettingsCodecDescription'),
@@ -349,7 +350,8 @@ export default function(obj) {
})
})
+ settingsCategory({
name: t('SettingsVimeoPrefer'),
name: "vimeo",
title: t('SettingsVimeoPrefer'),
body: switcher({
name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'),
@@ -426,6 +428,43 @@ export default function(obj) {
}]
})
})
+ settingsCategory({
name: "filename",
title: t('FilenameTitle'),
body: switcher({
name: "filenamePattern",
items: [{
action: "classic",
text: t('FilenamePatternClassic')
}, {
action: "basic",
text: t('FilenamePatternBasic')
}, {
action: "pretty",
text: t('FilenamePatternPretty')
}, {
action: "nerdy",
text: t('FilenamePatternNerdy')
}]
})
+ `<div id="filename-preview">
<div id="video-filename" class="filename-item line">
${emoji('🎞️', 32, 1, 1)}
<div class="filename-container">
<div class="filename-label">${t('Preview')}</div>
<div id="video-filename-text"></div>
</div>
</div>
<div id="audio-filename" class="filename-item">
${emoji('🎧', 32, 1, 1)}
<div class="filename-container">
<div class="filename-label">${t('Preview')}</div>
<div id="audio-filename-text"></div>
</div>
</div>
</div>`
+ explanation(t('FilenameDescription'))
})
+ settingsCategory({
name: "accessibility",
title: t('Accessibility'),
@@ -523,8 +562,8 @@ export default function(obj) {
<div id="popup-backdrop" onclick="hideAllPopups()"></div>
<div id="home" style="visibility:hidden">
${urgentNotice({
emoji: "👾",
text: t("UrgentFeatureUpdate71"),
emoji: "😸",
text: t("UrgentFilenameUpdate"),
visible: true,
action: "popup('about', 1, 'changelog')"
})}
@@ -574,7 +613,7 @@ export default function(obj) {
</div>
</body>
<script type="text/javascript">
let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}';
let defaultApiUrl = '${process.env.apiURL ? process.env.apiURL : ''}';
const loc = ${webLoc(t,
[
'ErrorNoInternet',
@@ -591,7 +630,10 @@ export default function(obj) {
'ClipboardErrorNoPermission',
'ClipboardErrorFirefox',
'DataTransferSuccess',
'DataTransferError'
'DataTransferError',
'FilenamePreviewVideoTitle',
'FilenamePreviewAudioTitle',
'FilenamePreviewAudioAuthor'
])}
</script>
<script type="text/javascript" src="cobalt.js"></script>

View File

@@ -0,0 +1,78 @@
export default function(f, template, isAudioOnly, isAudioMuted) {
let filename = '';
switch(template) {
default:
case "classic":
// youtube_MMK3L4W70g4_1920x1080_h264_mute.mp4
// youtube_MMK3L4W70g4_audio.mp3
filename += `${f.service}_${f.id}`;
if (!isAudioOnly) {
if (f.resolution) filename += `_${f.resolution}`;
if (f.youtubeFormat) filename += `_${f.youtubeFormat}`;
if (!isAudioMuted && f.youtubeDubName) filename += `_${f.youtubeDubName}`;
if (isAudioMuted) filename += '_mute';
filename += `.${f.extension}`
} else {
filename += `_audio`;
if (f.youtubeDubName) filename += `_${f.youtubeDubName}`;
}
break;
case "pretty":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, mute, youtube).mp4
// How secure is 256 bit security? - 3Blue1Brown (es, youtube).mp3
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}, `;
if (!isAudioMuted && f.youtubeDubName) filename += `${f.youtubeDubName}, `;
if (isAudioMuted) filename += 'mute, ';
filename += `${f.service}`;
filename += ')';
filename += `.${f.extension}`
} else {
filename += `- ${f.author} (`;
if (f.youtubeDubName) filename += `${f.youtubeDubName}, `;
filename += `${f.service})`
}
break;
case "basic":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru).mp4
// How secure is 256 bit security? - 3Blue1Brown (es).mp3
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}`;
if (!isAudioMuted && f.youtubeDubName) filename += `, ${f.youtubeDubName}`;
if (isAudioMuted) filename += ', mute';
filename += ')';
filename += `.${f.extension}`
} else {
filename += `- ${f.author}`;
if (f.youtubeDubName) filename += ` (${f.youtubeDubName})`;
}
break;
case "nerdy":
// Loossemble (루셈블) - 'Sensitive' MV (1080p, h264, ru, youtube, MMK3L4W70g4).mp4
// Loossemble (루셈블) - 'Sensitive' MV - Loossemble (ru, youtube, MMK3L4W70g4).mp4
filename += `${f.title} `;
if (!isAudioOnly) {
filename += '('
if (f.qualityLabel) filename += `${f.qualityLabel}, `;
if (f.youtubeFormat) filename += `${f.youtubeFormat}, `;
if (!isAudioMuted && f.youtubeDubName) filename += `${f.youtubeDubName}, `;
if (isAudioMuted) filename += 'mute, ';
filename += `${f.service}, ${f.id}`;
filename += ')'
filename += `.${f.extension}`
} else {
filename += `- ${f.author} (`;
if (f.youtubeDubName) filename += `${f.youtubeDubName}, `;
filename += `${f.service}, ${f.id})`
}
break;
}
return filename.replace(' ,', '').replace(', )', ')').replace(',)', ')')
}

View File

@@ -22,7 +22,7 @@ import streamable from "./services/streamable.js";
import twitch from "./services/twitch.js";
import rutube from "./services/rutube.js";
export default async function (host, patternMatch, url, lang, obj) {
export default async function(host, patternMatch, url, lang, obj) {
try {
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
@@ -150,7 +150,7 @@ export default async function (host, patternMatch, url, lang, obj) {
if (r.error) return apiJSON(0, { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) });
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata);
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, obj.filenamePattern);
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View File

@@ -1,14 +1,16 @@
import { audioIgnore, services, supportedAudio } from "../config.js";
import { apiJSON } from "../sub/utils.js";
import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js";
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata) {
export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) {
let action,
responseType = 2,
defaultParams = {
u: r.urls,
service: host,
filename: r.filename,
filename: r.filenameAttributes ?
createFilename(r.filenameAttributes, filenamePattern, isAudioOnly, isAudioMuted) : r.filename,
fileMetadata: !disableMetadata ? r.fileMetadata : false
},
params = {}
@@ -21,10 +23,13 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
else action = "video";
if (action === "picker" || action === "audio") {
defaultParams.filename = r.audioFilename;
if (!r.filenameAttributes) defaultParams.filename = r.audioFilename;
defaultParams.isAudioOnly = true;
defaultParams.audioFormat = audioFormat;
}
if (isAudioMuted && !r.filenameAttributes) {
defaultParams.filename = r.filename.replace('.', '_mute.')
}
switch (action) {
case "photo":
@@ -135,7 +140,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
} else if (audioFormat === "best") {
audioFormat = "m4a";
copy = true;
if (r.audioFilename.includes("twitterspaces")) {
if (!r.filenameAttributes && r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}

View File

@@ -20,5 +20,10 @@ export default async function(obj) {
if (!video) return { error: 'ErrorEmptyDownload' };
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
return { urls: video.url, filename: `pinterest_${pinId}.mp4`, audioFilename: `pinterest_${pinId}_audio` }
return {
urls: video.url,
filename: `pinterest_${pinId}.mp4`,
audioFilename: `pinterest_${pinId}_audio`
}
}

View File

@@ -1,5 +1,6 @@
import HLS from 'hls-parser';
import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality;
@@ -20,11 +21,23 @@ export default async function(obj) {
if (Number(quality) < bestQuality.resolution.height) {
bestQuality = m3u8.find((i) => (Number(quality) === i["resolution"].height));
}
let fileMetadata = {
title: cleanString(play.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(play.author.name.replace(/\p{Emoji}/gu, '').trim()),
}
return {
urls: bestQuality.uri,
isM3U8: true,
audioFilename: `rutube_${play.id}_audio`,
filename: `rutube_${play.id}_${bestQuality.resolution.width}x${bestQuality.resolution.height}.mp4`
filenameAttributes: {
service: "rutube",
id: play.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
qualityLabel: `${bestQuality.resolution.height}p`,
extension: "mp4"
},
fileMetadata: fileMetadata
}
}

View File

@@ -69,12 +69,19 @@ export default async function(obj) {
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false });
if (!file) return { error: 'ErrorCouldntFetch' };
let fileMetadata = {
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()),
}
return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
fileMetadata: {
title: cleanString(json.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(json.user.username.replace(/\p{Emoji}/gu, '').trim()),
}
filenameAttributes: {
service: "soundcloud",
id: json.id,
title: fileMetadata.title,
author: fileMetadata.artist
},
fileMetadata: fileMetadata
}
}

View File

@@ -66,7 +66,11 @@ export default async function(obj) {
}
if (single) {
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
return {
urls: single,
filename: `twitter_${obj.id}.mp4`,
audioFilename: `twitter_${obj.id}_audio`
}
} else if (multiple) {
return { picker: multiple }
} else {

View File

@@ -1,6 +1,6 @@
import { maxVideoDuration } from "../../config.js";
import { cleanString } from '../../sub/utils.js';
// vimeo you're fucked in the head for this
const resolutionMatch = {
"3840": "2160",
"2732": "1440",
@@ -33,6 +33,11 @@ export default async function(obj) {
let downloadType = "dash";
if (!obj.forceDash && JSON.stringify(api).includes('"progressive":[{')) downloadType = "progressive";
let fileMetadata = {
title: cleanString(api.video.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(api.video.owner.name.replace(/\p{Emoji}/gu, '').trim()),
}
if (downloadType !== "dash") {
if (qualityMatch[quality]) quality = qualityMatch[quality];
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
@@ -43,7 +48,11 @@ export default async function(obj) {
if (Number(quality) < Number(bestQuality)) best = all.find(i => i["quality"].split('p')[0] === quality);
if (!best) return { error: 'ErrorEmptyDownload' };
return { urls: best["url"], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4` }
return {
urls: best["url"],
audioFilename: `vimeo_${obj.id}_audio`,
filename: `vimeo_${obj.id}_${best["width"]}x${best["height"]}.mp4`
}
}
if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
@@ -77,8 +86,16 @@ export default async function(obj) {
return {
urls: audioUrl ? [videoUrl, audioUrl] : videoUrl,
isM3U8: audioUrl ? false : true,
audioFilename: `vimeo_${obj.id}_audio`,
filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4`
fileMetadata: fileMetadata,
filenameAttributes: {
service: "vimeo",
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`,
qualityLabel: `${bestVideo["height"]}p`,
extension: "mp4"
}
}
}
return { error: 'ErrorEmptyDownload' }

View File

@@ -2,7 +2,11 @@ export default async function(obj) {
let post = await fetch(`https://archive.vine.co/posts/${obj.id}.json`).then((r) => { return r.json() }).catch(() => { return false });
if (!post) return { error: 'ErrorEmptyDownload' };
if (post.videoUrl) return { urls: post.videoUrl.replace("http://", "https://"), filename: `vine_${obj.id}.mp4`, audioFilename: `vine_${obj.id}_audio` };
if (post.videoUrl) return {
urls: post.videoUrl.replace("http://", "https://"),
filename: `vine_${obj.id}.mp4`,
audioFilename: `vine_${obj.id}_audio`
}
return { error: 'ErrorEmptyDownload' }
}

View File

@@ -1,17 +1,21 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js";
import { cleanString } from "../../sub/utils.js";
const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"];
export default async function(o) {
let html, url,
quality = o.quality === "max" ? 2160 : o.quality,
filename = `vk_${o.userId}_${o.videoId}_`;
let html, url, quality = o.quality === "max" ? 2160 : o.quality;
html = await fetch(`https://vk.com/video${o.userId}_${o.videoId}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
}).then((r) => { return r.arrayBuffer() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
// decode cyrillic from windows-1251 because vk still uses apis from prehistoring times
let decoder = new TextDecoder('windows-1251');
html = decoder.decode(html);
if (!html.includes(`{"lang":`)) return { error: 'ErrorEmptyDownload' };
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
@@ -28,11 +32,23 @@ export default async function(o) {
if (Number(quality) > Number(o.quality)) quality = o.quality;
url = js.player.params[0][`url${quality}`];
filename += `${quality}p.mp4`
if (url && filename) return {
let fileMetadata = {
title: cleanString(js.player.params[0].md_title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(js.player.params[0].md_author.replace(/\p{Emoji}/gu, '').trim()),
}
if (url) return {
urls: url,
filename: filename
filenameAttributes: {
service: "vk",
id: `${o.userId}_${o.videoId}`,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${quality}p`,
qualityLabel: `${quality}p`,
extension: "mp4"
}
}
return { error: 'ErrorEmptyDownload' }
}

View File

@@ -54,13 +54,12 @@ export default async function(o) {
audio = adaptive_formats.find(i => checkBestAudio(i) && !i["is_dubbed"]);
if (o.dubLang) {
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang);
let dubbedAudio = adaptive_formats.find(i => checkBestAudio(i) && i["language"] === o.dubLang && !i["audio_track"].audio_is_default);
if (dubbedAudio) {
audio = dubbedAudio;
isDubbed = true
}
}
let fileMetadata = {
title: cleanString(info.basic_info.title.replace(/\p{Emoji}/gu, '').trim()),
artist: cleanString(info.basic_info.author.replace("- Topic", "").replace(/\p{Emoji}/gu, '').trim()),
@@ -72,13 +71,21 @@ export default async function(o) {
if (descItems[4].startsWith("Released on:")) {
fileMetadata.date = descItems[4].replace("Released on: ", '').trim()
}
};
}
let filenameAttributes = {
service: "youtube",
id: o.id,
title: fileMetadata.title,
author: fileMetadata.artist,
youtubeDubName: isDubbed ? o.dubLang : false
}
if (hasAudio && o.isAudioOnly) return {
type: "render",
isAudioOnly: true,
urls: audio.url,
audioFilename: `youtube_${o.id}_audio${isDubbed ? `_${o.dubLang}`:''}`,
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
let checkSingle = (i) => ((qual(i) === quality || qual(i) === bestQuality) && i["mime_type"].includes(c[o.format].codec)),
@@ -87,21 +94,33 @@ export default async function(o) {
if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') {
let single = info.streaming_data.formats.find(i => checkSingle(i));
if (single) return {
type: "bridge",
urls: single.url,
filename: `youtube_${o.id}_${single.width}x${single.height}_${o.format}.${c[o.format].container}`,
fileMetadata: fileMetadata
if (single) {
filenameAttributes.qualityLabel = single.quality_label;
filenameAttributes.resolution = `${single.width}x${single.height}`;
filenameAttributes.extension = c[o.format].container;
filenameAttributes.youtubeFormat = o.format;
return {
type: "bridge",
urls: single.url,
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
}
};
}
let video = adaptive_formats.find(i => ((Number(quality) > Number(bestQuality)) ? checkBestVideo(i) : checkRightVideo(i)));
if (video && audio) return {
type: "render",
urls: [video.url, audio.url],
filename: `youtube_${o.id}_${video.width}x${video.height}_${o.format}${isDubbed ? `_${o.dubLang}`:''}.${c[o.format].container}`,
fileMetadata: fileMetadata
};
if (video && audio) {
filenameAttributes.qualityLabel = video.quality_label;
filenameAttributes.resolution = `${video.width}x${video.height}`;
filenameAttributes.extension = c[o.format].container;
filenameAttributes.youtubeFormat = o.format;
return {
type: "render",
urls: [video.url, audio.url],
filenameAttributes: filenameAttributes,
fileMetadata: fileMetadata
}
}
return { error: 'ErrorYTTryOtherCodec' }
}

View File

@@ -3,6 +3,7 @@ import ffmpeg from "ffmpeg-static";
import { ffmpegArgs, genericUserAgent } from "../config.js";
import { getThreads, metadataManager } from "../sub/utils.js";
import { request } from 'undici';
import { create as contentDisposition } from "content-disposition-header";
function fail(res) {
if (!res.headersSent) res.sendStatus(500);
@@ -12,8 +13,7 @@ function fail(res) {
export async function streamDefault(streamInfo, res) {
try {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`;
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`);
res.setHeader('Content-disposition', contentDisposition(streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename));
const { body: stream, headers } = await request(streamInfo.urls, {
headers: { 'user-agent': genericUserAgent },
@@ -59,7 +59,7 @@ export async function streamLiveRender(streamInfo, res) {
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename));
res.on('error', () => {
ffmpegProcess.kill();
fail(res);
@@ -127,7 +127,7 @@ export function streamAudioOnly(streamInfo, res) {
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
res.setHeader('Content-Disposition', contentDisposition(`${streamInfo.filename}.${streamInfo.audioFormat}`));
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
@@ -163,7 +163,7 @@ export function streamVideoOnly(streamInfo, res) {
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}${streamInfo.mute ? '_mute' : ''}.${format}"`);
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename));
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());

View File

@@ -4,7 +4,8 @@ const apiVar = {
allowed: {
vCodec: ["h264", "av1", "vp9"],
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"]
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
},
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"]
}
@@ -94,6 +95,7 @@ export function checkJSONPost(obj) {
vCodec: "h264",
vQuality: "720",
aFormat: "mp3",
filenamePattern: "classic",
isAudioOnly: false,
isNoTTWatermark: false,
isTTFullAudio: false,