From 16f74094b9081c4b431d3c4491e705e657707a67 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 12 Oct 2023 23:14:54 +0600 Subject: [PATCH] filename pattern customization - added metadata for rutube and vimeo. - added a picker for preferred filename pattern. - fixed content disposition header. - mute and audio dub tags don't appear together in a file name anymore. - youtube: dub file name tag doesn't appear anymore if audio track is default. --- docs/API.md | 25 +++--- package.json | 1 + src/front/cobalt.js | 4 +- src/localization/languages/en.json | 7 +- src/localization/languages/ru.json | 7 +- src/modules/pageRender/page.js | 26 ++++++- src/modules/processing/createFilename.js | 78 +++++++++++++++++++ src/modules/processing/match.js | 4 +- src/modules/processing/matchActionDecider.js | 13 +++- src/modules/processing/services/pinterest.js | 7 +- src/modules/processing/services/rutube.js | 17 +++- src/modules/processing/services/soundcloud.js | 17 ++-- src/modules/processing/services/twitter.js | 6 +- src/modules/processing/services/vimeo.js | 25 +++++- src/modules/processing/services/vine.js | 6 +- src/modules/processing/services/youtube.js | 51 ++++++++---- src/modules/stream/types.js | 10 +-- src/modules/sub/utils.js | 4 +- 18 files changed, 249 insertions(+), 59 deletions(-) create mode 100644 src/modules/processing/createFilename.js diff --git a/docs/API.md b/docs/API.md index 7d5fa7d..6b3c105 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 2b39799..41099c3 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 5a69bdf..1757dc6 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 0ce31dc..d90792c 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 db0fa5b..d55d163 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 3de0110..6f818b6 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 0000000..8c4f832 --- /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 af0a1d9..1802199 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 32ce4a5..eedf8ec 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 335d736..086573c 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 c62191d..6b0c607 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 99c812e..dc9e2e2 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 487032a..a251041 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 3765b64..dc300f7 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 21596d4..ea3a519 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 38da410..2e03d6b 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 ffc868b..e979e34 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 720acb8..28b6abb 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,