7.6: file name customization
This commit is contained in:
@@ -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!",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
78
src/modules/processing/createFilename.js
Normal file
78
src/modules/processing/createFilename.js
Normal 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(',)', ')')
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user