diff --git a/docs/API.md b/docs/API.md
index 7d5fa7d4..6b3c105e 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -14,18 +14,19 @@ Request Body Type: ``application/json``
Response Body Type: ``application/json``
### Request Body Variables
-| key | type | variables | default | description |
-|:--------------------|:------------|:----------------------------------|:----------|:-------------------------------------------------------------------------------|
-| ``url`` | ``string`` | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
-| ``vCodec`` | ``string`` | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. |
-| ``vQuality`` | ``string`` | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. |
-| ``aFormat`` | ``string`` | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
-| ``isAudioOnly`` | ``boolean`` | ``true / false`` | ``false`` | |
-| ``isNoTTWatermark`` | ``boolean`` | ``true / false`` | ``false`` | Changes whether downloaded TikTok videos have watermarks. |
-| ``isTTFullAudio`` | ``boolean`` | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
-| ``isAudioMuted`` | ``boolean`` | ``true / false`` | ``false`` | Disables audio track in video downloads. |
-| ``dubLang`` | ``boolean`` | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. |
-| ``disableMetadata`` | ``boolean`` | ``true / false`` | ``false`` | Disables file metadata when set to ``true``. |
+| key | type | variables | default | description |
+|:--------------------|:------------|:-------------------------------------|:------------|:-------------------------------------------------------------------------------|
+| ``url`` | ``string`` | Sharable URL encoded as URI | ``null`` | **Must** be included in every request. |
+| ``vCodec`` | ``string`` | ``h264 / av1 / vp9`` | ``h264`` | Applies only to YouTube downloads. ``h264`` is recommended for phones. |
+| ``vQuality`` | ``string`` | ``144 / ... / 2160 / max`` | ``720`` | ``720`` quality is recommended for phones. |
+| ``aFormat`` | ``string`` | ``best / mp3 / ogg / wav / opus`` | ``mp3`` | |
+| ``filenamePattern`` | ``boolean`` | ``classic / pretty / basic / nerdy`` | ``classic`` | Changes the way files are named. Previews can be seen in the web app. |
+| ``isAudioOnly`` | ``boolean`` | ``true / false`` | ``false`` | |
+| ``isNoTTWatermark`` | ``boolean`` | ``true / false`` | ``false`` | Changes whether downloaded TikTok videos have watermarks. |
+| ``isTTFullAudio`` | ``boolean`` | ``true / false`` | ``false`` | Enables download of original sound used in a TikTok video. |
+| ``isAudioMuted`` | ``boolean`` | ``true / false`` | ``false`` | Disables audio track in video downloads. |
+| ``dubLang`` | ``boolean`` | ``true / false`` | ``false`` | Backend uses Accept-Language for YouTube video audio tracks when ``true``. |
+| ``disableMetadata`` | ``boolean`` | ``true / false`` | ``false`` | Disables file metadata when set to ``true``. |
### Response Body Variables
| key | type | variables |
diff --git a/package.json b/package.json
index 2b39799b..41099c3a 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
},
"homepage": "https://github.com/wukko/cobalt#readme",
"dependencies": {
+ "content-disposition-header": "^0.6.0",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"esbuild": "^0.14.51",
diff --git a/src/front/cobalt.js b/src/front/cobalt.js
index 5a69bdfe..1757dc6a 100644
--- a/src/front/cobalt.js
+++ b/src/front/cobalt.js
@@ -17,7 +17,8 @@ const switchers = {
"aFormat": ["mp3", "best", "ogg", "wav", "opus"],
"dubLang": ["original", "auto"],
"vimeoDash": ["false", "true"],
- "audioMode": ["false", "true"]
+ "audioMode": ["false", "true"],
+ "filenamePattern": ["classic", "pretty", "basic", "nerdy"]
};
const checkboxes = [
"alwaysVisibleButton",
@@ -372,6 +373,7 @@ async function download(url) {
let req = {
url: encodeURIComponent(url.split("&")[0].split('%')[0]),
aFormat: sGet("aFormat").slice(0, 4),
+ filenamePattern: sGet("filenamePattern"),
dubLang: false
}
if (sGet("dubLang") === "auto") {
diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json
index 0ce31dc5..d90792c0 100644
--- a/src/localization/languages/en.json
+++ b/src/localization/languages/en.json
@@ -145,6 +145,11 @@
"DataTransferSuccess": "btw, your settings have been transferred automatically :)",
"DataTransferError": "something went wrong when transferring your preferences. you'll have to open settings and configure cobalt by hand.",
"SupportNotAffiliated": "cobalt is not affiliated with any services listed above.",
- "SponsoredBy": "sponsored by"
+ "SponsoredBy": "sponsored by",
+ "FilenamePattern": "file name preset",
+ "FilenamePatternClassic": "classic",
+ "FilenamePatternPretty": "pretty",
+ "FilenamePatternBasic": "basic",
+ "FilenamePatternNerdy": "nerdy"
}
}
diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json
index db0fa5b2..d55d163a 100644
--- a/src/localization/languages/ru.json
+++ b/src/localization/languages/ru.json
@@ -147,6 +147,11 @@
"DataTransferError": "при переносе настроек что-то пошло не так. придётся зайти в настройки и настроить кобальт вручную.",
"SupportNotAffiliated": "кобальт не аффилирован ни с одним из перечисленных выше сервисов.",
"SupportMetaNoticeRU": "деятельность meta platforms inc. (владелец instagram) запрещена на территории россии.",
- "SponsoredBy": "спонсируется"
+ "SponsoredBy": "спонсируется",
+ "FilenamePattern": "шаблон названий файлов",
+ "FilenamePatternClassic": "классический",
+ "FilenamePatternPretty": "красивый",
+ "FilenamePatternBasic": "простой",
+ "FilenamePatternNerdy": "полный"
}
}
diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js
index 3de0110a..6f818b6a 100644
--- a/src/modules/pageRender/page.js
+++ b/src/modules/pageRender/page.js
@@ -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,26 @@ export default function(obj) {
}]
})
})
+ + settingsCategory({
+ name: "filename",
+ title: t('FilenamePattern'),
+ body: switcher({
+ name: "filenamePattern",
+ items: [{
+ action: "classic",
+ text: t('FilenamePatternClassic')
+ }, {
+ action: "pretty",
+ text: t('FilenamePatternPretty')
+ }, {
+ action: "basic",
+ text: t('FilenamePatternBasic')
+ }, {
+ action: "nerdy",
+ text: t('FilenamePatternNerdy')
+ }]
+ })
+ })
+ settingsCategory({
name: "accessibility",
title: t('Accessibility'),
diff --git a/src/modules/processing/createFilename.js b/src/modules/processing/createFilename.js
new file mode 100644
index 00000000..8c4f832c
--- /dev/null
+++ b/src/modules/processing/createFilename.js
@@ -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 (1080p, h264, 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(',)', ')')
+}
diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js
index af0a1d90..18021995 100644
--- a/src/modules/processing/match.js
+++ b/src/modules/processing/match.js
@@ -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;
@@ -147,7 +147,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) })
}
diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js
index 32ce4a52..eedf8ec1 100644
--- a/src/modules/processing/matchActionDecider.js
+++ b/src/modules/processing/matchActionDecider.js
@@ -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
}
diff --git a/src/modules/processing/services/pinterest.js b/src/modules/processing/services/pinterest.js
index 335d7360..086573c6 100644
--- a/src/modules/processing/services/pinterest.js
+++ b/src/modules/processing/services/pinterest.js
@@ -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`
+ }
}
diff --git a/src/modules/processing/services/rutube.js b/src/modules/processing/services/rutube.js
index c62191df..6b0c6071 100644
--- a/src/modules/processing/services/rutube.js
+++ b/src/modules/processing/services/rutube.js
@@ -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
}
}
diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js
index 99c812e1..dc9e2e25 100644
--- a/src/modules/processing/services/soundcloud.js
+++ b/src/modules/processing/services/soundcloud.js
@@ -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
}
}
diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js
index 487032a8..a2510413 100644
--- a/src/modules/processing/services/twitter.js
+++ b/src/modules/processing/services/twitter.js
@@ -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 {
diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js
index 3765b64e..dc300f7c 100644
--- a/src/modules/processing/services/vimeo.js
+++ b/src/modules/processing/services/vimeo.js
@@ -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' }
diff --git a/src/modules/processing/services/vine.js b/src/modules/processing/services/vine.js
index 21596d48..ea3a519c 100644
--- a/src/modules/processing/services/vine.js
+++ b/src/modules/processing/services/vine.js
@@ -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' }
}
diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js
index 38da4103..2e03d6bf 100644
--- a/src/modules/processing/services/youtube.js
+++ b/src/modules/processing/services/youtube.js
@@ -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 = video.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' }
}
diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js
index ffc868bb..e979e347 100644
--- a/src/modules/stream/types.js
+++ b/src/modules/stream/types.js
@@ -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());
diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js
index 720acb85..28b6abbd 100644
--- a/src/modules/sub/utils.js
+++ b/src/modules/sub/utils.js
@@ -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,