This commit is contained in:
wukko 2022-12-07 01:21:07 +06:00
parent 098a63638b
commit f13a82e152
25 changed files with 389 additions and 321 deletions

View file

@ -1,12 +1,12 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "4.4",
"version": "4.5",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",
"engines": {
"node": ">=14.16"
"node": ">=17.5"
},
"scripts": {
"start": "node src/cobalt",
@ -22,6 +22,7 @@
},
"homepage": "https://github.com/wukko/cobalt#readme",
"dependencies": {
"better-ytdl-core": "^1.0.1",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"esbuild": "^0.14.51",
@ -31,7 +32,6 @@
"got": "^12.1.0",
"node-cache": "^5.1.2",
"url-pattern": "1.0.3",
"xml-js": "^1.6.11",
"ytdl-core": "^4.11.2"
"xml-js": "^1.6.11"
}
}

View file

@ -24,23 +24,23 @@ app.disable('x-powered-by');
if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt && process.env.port) {
const apiLimiter = rateLimit({
windowMs: 20 * 60 * 1000,
max: 800,
windowMs: 1 * 60 * 1000,
max: 12,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
}
})
});
const apiLimiterStream = rateLimit({
windowMs: 6 * 60 * 1000,
max: 600,
windowMs: 1 * 60 * 1000,
max: 12,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
}
})
});
await buildFront();
app.use('/api/', apiLimiter);
@ -79,16 +79,14 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
let chck = checkJSONPost(request);
if (request.url && chck) {
chck["ip"] = ip;
let j = await getJSON(request.url.trim(), languageCode(req), chck)
let j = await getJSON(chck["url"], languageCode(req), chck)
res.status(j.status).json(j.body);
} else if (request.url && !chck) {
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorCouldntFetch') });
res.status(j.status).json(j.body);
} else {
try {
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink', process.env.selfURL) })
res.status(j.status).json(j.body);
}
catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorUnknownStatus') })
}
let j = apiJSON(3, { t: loc(languageCode(req), 'ErrorNoLink') })
res.status(j.status).json(j.body);
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') })

View file

@ -54,7 +54,7 @@
"supportedAudio": ["mp3", "ogg", "wav", "opus"],
"ffmpegArgs": {
"webm": ["-c:v", "copy", "-c:a", "copy"],
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "frag_keyframe+empty_moov"],
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
"copy": ["-c:a", "copy"],
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
"m4a": ["-movflags", "frag_keyframe+empty_moov"]

View file

@ -310,6 +310,7 @@ input[type="checkbox"] {
background-color: var(--accent-button-bg);
max-height: 300px;
margin-bottom: 2rem;
float: left;
}
.changelog-img {
object-fit: cover;
@ -370,6 +371,9 @@ input[type="checkbox"] {
height: var(--without-padding);
scrollbar-width: none;
}
.bullpadding {
padding-left: 0.58rem;
}
#popup-header {
position: relative;
background: var(--background);

View file

@ -1,7 +1,7 @@
let ua = navigator.userAgent.toLowerCase();
let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os");
let version = 18;
let version = 19;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
let notification = `<div class="notification-dot"></div>`
@ -442,7 +442,7 @@ window.onload = () => {
notificationCheck();
if (isIOS) sSet("downloadPopup", "true");
let urlQuery = new URLSearchParams(window.location.search).get("u");
if (urlQuery !== null) {
if (urlQuery !== null && regex.test(urlQuery)) {
eid("url-input-area").value = urlQuery;
button();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

View file

@ -27,7 +27,7 @@
"ErrorNoLink": "i can't guess what you want to download! please give me a link.",
"ErrorPageRenderFail": "something went wrong and page couldn't render. if it's a recurring or critical issue, please {ContactLink}. it'd be useful if you provided current commit hash ({s}) and error recreation steps. thank you in advance :D",
"ErrorRateLimit": "you're making too many requests. calm down and try again in a bit.",
"ErrorCouldntFetch": "couldn't get any info about your link. check if your link is correct and try again.",
"ErrorCouldntFetch": "couldn't get any info about your link. check if it's correct and try again.",
"ErrorLengthLimit": "current length limit is {s} minutes. video that you tried to download is longer than {s} minutes. pick something else!",
"ErrorBadFetch": "an error occurred when i tried to get info about your link. are you sure it works? check if it does, and try again.",
"ErrorCorruptedStream": "this download is unfortunately corrupted. try again or try a different format and resolution.",
@ -107,6 +107,7 @@
"DonateSub": "help me keep it up",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but hey! turns out keeping up a web service used by hundreds of thousands of people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated.",
"DonateVia": "donate via",
"DonateHireMe": "or, as an alternative, you can <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">hire me</a>."
"DonateHireMe": "or, as an alternative, you can <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">hire me</a>.",
"DiscordServer": "join the live conversation about {appName} on the <a class=\"text-backdrop\" href=\"{discord}\" target=\"_blank\">official discord server</a>"
}
}

View file

@ -107,6 +107,7 @@
"DonateSub": "ты можешь помочь!",
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!",
"DonateVia": "открыть",
"DonateHireMe": "или же ты можешь <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>."
"DonateHireMe": "или же ты можешь <a class=\"text-backdrop\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>.",
"DiscordServer": "присоединяйся к живой беседе о {appName} прямо на его <a class=\"text-backdrop\" href=\"{discord}\" target=\"_blank\">официальном discord сервере</a>"
}
}

View file

@ -15,7 +15,7 @@ export function loadLoc() {
}
loadLoc();
export function replaceBase(s) {
return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo)
return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo)// .replace(/{discord}/g, socials.discord)
}
export function replaceAll(lang, str, string, replacement) {
let s = replaceBase(str[string])

View file

@ -1,11 +1,16 @@
{
"current": {
"version": "4.5",
"title": "better, faster, stronger, stable",
"banner": "meowthstrong.webp",
"content": "your favorite social media downloader just got even better! this update includes a ton of imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n<div class=\"bullpadding\">&bull; vimeo module has been revamped, all sorts of videos should now be supported.\n&bull; vimeo audio downloads! you now can download audios from more recent videos.\n&bull; {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n&bull; vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n&bull; youtube videos with community warnings should now be possible to download.</div>\nuser interface improvements:\n<div class=\"bullpadding\">&bull; list of supported services is now MUCH easier to read.\n&bull; banners in changelog history should no longer overlap each other.\n&bull; bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.</div>\ninternal improvements:\n<div class=\"bullpadding\">&bull; cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n&bull; better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n&bull; moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n&bull; ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n&bull; \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n&bull; all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n&bull; other code optimizations. there's less clutter overall.</div>\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
},
"history": [{
"version": "4.4",
"title": "over 1 million monthly requests. thank you.",
"banner": "onemillionr.webp",
"content": "this is a huge milestone for me, i cannot express enough how grateful i am for each and every one of you.\nthank you for using cobalt, and thank you for showing that people love the web that's friendly and bullshit-free. i'm hoping to never disappoint you in the future and keep up the good work.\n\nthank you <3\n\nif you want to thank ME, check out the renovated donations tab, which now is also linked alongside bottom action buttons."
},
"history": [{
}, {
"version": "4.3.2",
"title": "twitter improvements & changelog overhaul",
"content": "- you can download explicit content from twitter.\n- direct video links from twitter are properly supported (video/1, video/2, etc.).\n- changelog history got support for banners.\n- changelog categories are not messy anymore.\n- {appName} version in changelogs is now highlighted.\n- changelog history got separators to make text easier to read.\n- changelog history can be collapsed after loading.\n- download button takes less time to change back to pressable state.\n\nif you're a developer and would like to play around with cobalt's api, then read more about it in older changelogs below!"

View file

@ -14,7 +14,6 @@ export const
genericUserAgent = config.genericUserAgent,
repo = packageJson["bugs"]["url"].replace('/issues', ''),
authorInfo = config.authorInfo,
supportedLanguages = config.supportedLanguages,
quality = config.quality,
internetExplorerRedirect = config.internetExplorerRedirect,
donations = config.donations,

View file

@ -10,8 +10,8 @@ let com = getCommitInfo();
let enabledServices = Object.keys(s).filter((p) => {
if (s[p].enabled) return true;
}).sort().map((p) => {
return s[p].alias ? s[p].alias : p
}).join(', ')
return `<br>&bull; ${s[p].alias ? s[p].alias : p}`
}).join(';').substring(4)
let donate = ``
let donateLinks = ``
@ -81,9 +81,13 @@ export default function(obj) {
body: [{
text: loc(obj.lang, 'AboutSummary')
}, {
text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.`
text: `${loc(obj.lang, 'AboutSupportedServices')}`,
nopadding: true
}, {
text: obj.lang !== "ru" ? loc(obj.lang, 'FollowTwitter') : ""
text: `<div class="bullpadding">${enabledServices}.</div>`
}, {
text: obj.lang !== "ru" ? loc(obj.lang, 'FollowTwitter') : "",
classes: ["desc-padding"]
}, {
text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')),
classes: ["bottom-link"]

View file

@ -1,6 +1,8 @@
import { apiJSON } from "../sub/utils.js";
import { errorUnsupported, genericError } from "../sub/errors.js";
import loc from "../../localization/manager.js";
import { testers } from "./servicesPatternTesters.js";
import bilibili from "../services/bilibili.js";
@ -105,7 +107,9 @@ export default async function (host, patternMatch, url, lang, obj) {
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}
return matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly)
return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang) : apiJSON(0, {
t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error)
});
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View file

@ -1,105 +1,109 @@
import { audioIgnore, services, supportedAudio } from "../config.js"
import { apiJSON } from "../sub/utils.js"
import loc from "../../localization/manager.js";
export default function(r, host, ip, audioFormat, isAudioOnly) {
if (!r.error) {
if (!isAudioOnly && !r.picker) {
switch (host) {
case "twitter":
return apiJSON(1, { u: r.urls });
case "vk":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "bilibili":
export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
if (!isAudioOnly && !r.picker) {
switch (host) {
case "twitter":
return apiJSON(1, { u: r.urls });
case "vk":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "bilibili":
return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename,
time: r.time
});
case "youtube":
return apiJSON(2, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename,
time: r.time,
});
case "reddit":
return apiJSON(r.typeId, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "tiktok":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "douyin":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "tumblr":
return apiJSON(1, { u: r.urls });
case "vimeo":
if (r.filename) {
return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename,
time: r.time
filename: r.filename
});
case "youtube":
return apiJSON(2, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename,
time: r.time,
});
case "reddit":
return apiJSON(r.typeId, {
type: r.type, u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "tiktok":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "douyin":
return apiJSON(2, {
type: "bridge", u: r.urls, service: host, ip: ip,
filename: r.filename,
});
case "tumblr":
} else {
return apiJSON(1, { u: r.urls });
case "vimeo":
return apiJSON(1, { u: r.urls });
}
} else if (r.picker) {
switch (host) {
case "douyin":
case "tiktok":
let type = "render";
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3"
type = "bridge"
}
return apiJSON(5, {
type: type,
picker: r.picker,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false,
})
case "twitter":
return apiJSON(5, {
picker: r.picker, service: host
})
}
} else {
if (host === "reddit" && r.typeId === 1 || audioIgnore.includes(host)) return apiJSON(0, { t: r.audioFilename });
let type = "render";
let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host == "tiktok" || host == "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) {
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3"
type = "bridge"
}
} else if (audioFormat === "best") {
audioFormat = "m4a"
}
}
} else if (r.picker) {
switch (host) {
case "douyin":
case "tiktok":
let type = "render";
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3"
type = "bridge"
}
}
if ((audioFormat === "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"]
type = "bridge"
} else if (audioFormat === "best") {
audioFormat = "m4a"
copy = true
if (r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}
}
return apiJSON(2, {
type: type,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, isAudioOnly: true,
audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false
})
return apiJSON(5, {
type: type,
picker: r.picker,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false,
})
case "twitter":
return apiJSON(5, {
picker: r.picker, service: host
})
}
} else {
return apiJSON(0, { t: r.error });
if ((host === "reddit" && r.typeId === 1) || (host === "vimeo" && !r.filename) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
let type = "render";
let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host == "tiktok" || host == "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) {
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3"
type = "bridge"
}
} else if (audioFormat === "best") {
audioFormat = "m4a"
type = "bridge"
}
}
if ((audioFormat === "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"]
type = "bridge"
} else if (audioFormat === "best") {
audioFormat = "m4a"
copy = true
if (r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}
}
return apiJSON(2, {
type: type,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, isAudioOnly: true,
audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false
})
}
}

View file

@ -1,5 +1,5 @@
{
"audioIgnore": ["vk", "vimeo"],
"audioIgnore": ["vk"],
"config": {
"bilibili": {
"alias": "bilibili.com",
@ -12,12 +12,12 @@
"enabled": true
},
"twitter": {
"alias": "twitter, twitter spaces",
"alias": "twitter posts & spaces",
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"],
"enabled": true
},
"vk": {
"alias": "vk clips, vk video",
"alias": "vk video & clips",
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"],
"quality_match": {
"2160": 7,
@ -47,7 +47,7 @@
"enabled": true
},
"youtube": {
"alias": "youtube, youtube music, youtube shorts",
"alias": "youtube videos & shorts & music",
"patterns": ["watch?v=:id"],
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
"bestAudio": "opus",
@ -59,26 +59,34 @@
"enabled": true
},
"tumblr": {
"patterns": ["post/:id", "blog/view/:user/:id"],
"patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"],
"enabled": true
},
"tiktok": {
"alias": "tiktok videos & slideshow & audio",
"patterns": [":user/video/:postId", ":id", "t/:id"],
"audioFormats": ["best", "m4a", "mp3"],
"enabled": true
},
"douyin": {
"alias": "douyin videos & slideshow & audio",
"patterns": ["video/:postId", ":id"],
"enabled": true
},
"vimeo": {
"patterns": [":id"],
"resolutionMatch": {
"3840": "2160",
"1920": "1080",
"1280": "720",
"960": "480"
},
"enabled": true
},
"soundcloud": {
"patterns": [":author/:song", ":shortLink"],
"bestAudio": "none",
"clientid": "1TLciEOiKE0PThutYu5Xj0kc8R4twD9p",
"clientid": "YeTcsotswIIc4sse5WZsXszVxMtP6eLc",
"enabled": true
}
}

View file

@ -1,16 +1,12 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxVideoDuration } from "../config.js";
export default async function(obj) {
try {
let html = await got.get(`https://bilibili.com/video/${obj.id}`, {
headers: { "user-agent": genericUserAgent }
});
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'bilibili') };
});
html = html.body;
let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) {
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength <= maxVideoDuration) {
@ -22,12 +18,12 @@ export default async function(obj) {
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
return { urls: [video[0]["baseUrl"], audio[0]["baseUrl"]], time: streamData.data.timelength, audioFilename: `bilibili_${obj.id}_audio`, filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4` };
} else {
return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) };
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
return { error: 'ErrorEmptyDownload' };
}
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,29 +1,27 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxVideoDuration } from "../config.js";
import { maxVideoDuration } from "../config.js";
export default async function(obj) {
try {
let req = await got.get(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`, { headers: { "user-agent": genericUserAgent } });
let data = (JSON.parse(req.body))[0]["data"]["children"][0]["data"];
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then(async (r) => {return await r.json()}).catch(() => {return false});
if (!data) return { error: 'ErrorCouldntFetch' };
data = data[0]["data"]["children"][0]["data"];
if ("reddit_video" in data["secure_media"] && data["secure_media"]["reddit_video"]["duration"] * 1000 < maxVideoDuration) {
let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
try {
await got.head(audio, { headers: { "user-agent": genericUserAgent } });
} catch (err) {
audio = ''
}
await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''});
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]
if (audio.length > 0) {
return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` };
} else {
return { typeId: 1, urls: video, audioFilename: loc(obj.lang, 'ErrorEmptyDownload') };
return { typeId: 1, urls: video };
}
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
return { error: 'ErrorEmptyDownload' };
}
} catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,26 +1,27 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxAudioDuration, services } from "../config.js";
export default async function(obj) {
try {
let html;
if (!obj.author && !obj.song && obj.shortLink) {
html = await got.get(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { headers: { "user-agent": genericUserAgent } });
html = html.body
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false});
}
if (obj.author && obj.song) {
html = await got.get(`https://soundcloud.com/${obj.author}/${obj.song}`, { headers: { "user-agent": genericUserAgent } });
html = html.body
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, {
headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false});
}
if (!html) return { error: 'ErrorCouldntFetch'};
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) {
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${services["soundcloud"]["clientid"]}&track_authorization=${json.track_authorization}`;
if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
if (json.duration < maxAudioDuration) {
let file = await got.get(fileUrl, { headers: { "user-agent": genericUserAgent } });
file = JSON.parse(file.body).url
let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
if (!file) return { error: 'ErrorCouldntFetch' };
return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
@ -29,11 +30,11 @@ export default async function(obj) {
artist: json.user.username,
}
}
} else return { error: loc(obj.lang, 'ErrorLengthAudioConvert', maxAudioDuration / 60000) }
} else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
}
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'soundcloud') }
} else return { error: 'ErrorEmptyDownload' }
} else return { error: ['ErrorBrokenLink', 'soundcloud'] }
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,12 +1,10 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent } from "../config.js";
let userAgent = genericUserAgent.split(' Chrome/1')[0]
let config = {
tiktok: {
short: "https://vt.tiktok.com/",
api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9&region=US&carrier_region=US", // ill always find more endpoints lmfao
api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9&region=US&carrier_region=US",
},
douyin: {
short: "https://v.douyin.com/",
@ -14,34 +12,46 @@ let config = {
}
}
function selector(j, h, id) {
let t;
switch (h) {
case "tiktok":
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
break;
case "douyin":
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true })
break;
}
if (t.length > 0) { return t[0] } else return false
if (j) {
let t;
switch (h) {
case "tiktok":
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
break;
case "douyin":
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true })
break;
}
if (t.length > 0) { return t[0] } else return false
} else return false
}
export default async function(obj) {
try {
if (!obj.postId) {
let html = await got.get(`${config[obj.host]["short"]}${obj.id}`, { followRedirect: false, headers: { "user-agent": userAgent } });
html = html.body;
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) obj.postId = html.split('video/')[1].split('?')[0].replace("/", '')
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
redirect: "manual",
headers: { "user-agent": userAgent }
}).then(async (r) => {return await r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
obj.postId = html.split('/video/')[1].split('?')[0].replace("/", '')
} else if (html.slice(0, 32) === '<a href="https://m.tiktok.com/v/' && html.includes('/v/')) {
obj.postId = html.split('/v/')[1].split('.html')[0].replace("/", '')
}
}
if (!obj.postId) return { error: loc(obj.lang, 'ErrorCantGetID') };
if (!obj.postId) return { error: 'ErrorCantGetID' };
let detail;
try {
detail = await got.get(config[obj.host]["api"].replace("{postId}", obj.postId), { headers: {"User-Agent":"TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"} });
detail = selector(JSON.parse(detail.body), obj.host, obj.postId);
if (!detail) throw new Error()
} catch (e) {
return { error: loc(obj.lang, 'ErrorCouldntFetch') }
}
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
}).then(async (r) => {return await r.json()}).catch(() => {return false});
detail = selector(detail, obj.host, obj.postId);
if (!detail) return { error: 'ErrorCouldntFetch' }
let video, videoFilename, audioFilename, isMp3, audio, images,
filenameBase = `${obj.host}_${obj.postId}`;
if (obj.host == "tiktok") {
@ -101,6 +111,6 @@ export default async function(obj) {
isMp3: isMp3,
}
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,24 +1,16 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent } from "../config.js";
export default async function(obj) {
try {
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
if (user.length <= 32) {
let html = await got.get(`https://${user}.tumblr.com/post/${obj.id}`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'tumblr') };
});
html = html.body
if (html.includes('<!-- GOOGLE CAROUSEL --><script type="application/ld+json">')) {
let json = JSON.parse(html.split('<!-- GOOGLE CAROUSEL --><script type="application/ld+json">')[1].split('</script>')[0])
if (json["video"] && json["video"]["contentUrl"]) {
return { urls: json["video"]["contentUrl"], audioFilename: `tumblr_${obj.id}_audio` }
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'tumblr') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'tumblr') }
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"/>')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }
} else return { error: 'ErrorEmptyDownload' }
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,5 +1,3 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent } from "../config.js";
function bestQuality(arr) {
@ -10,33 +8,34 @@ const apiURL = "https://api.twitter.com/1.1"
export default async function(obj) {
try {
let _headers = {
"User-Agent": genericUserAgent,
"Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
"Host": "api.twitter.com",
"Content-Type": "application/json",
"Content-Length": 0
"user-agent": genericUserAgent,
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
"host": "api.twitter.com"
};
let req_act = await got.post(`${apiURL}/guest/activate.json`, { headers: _headers });
req_act = JSON.parse(req_act.body)
let req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST",
headers: _headers
}).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`
if (!obj.spaceId) {
// kind of wonky but it works :D
let req_status = {}
try {
req_status = await got.get(showURL, { headers: _headers });
} catch (e) {
try {
_headers.Authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
delete _headers["x-guest-token"]
req_act = await got.post(`${apiURL}/guest/activate.json`, { headers: _headers });
req_act = JSON.parse(req_act.body)
_headers["x-guest-token"] = req_act["guest_token"];
req_status = await got.get(showURL, { headers: _headers });
} catch(err) {}
let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch((e) => { return false});
if (!req_status) {
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
delete _headers["x-guest-token"]
req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST",
headers: _headers
}).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
}
req_status = JSON.parse(req_status.body);
if (req_status == {}) return { error: loc(obj.lang, 'ErrorCouldntFetch') }
if (!req_status) return { error: 'ErrorCouldntFetch' }
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
let single, multiple = [], media = req_status["extended_entities"]["media"];
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
@ -45,51 +44,59 @@ export default async function(obj) {
} else if (media.length > 0) {
single = bestQuality(media[0]["video_info"]["variants"])
} else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
return { error: 'ErrorNoVideosInTweet' }
}
if (single) {
return { urls: single, audioFilename: `twitter_${obj.id}_audio` }
} else if (multiple) {
return { picker: multiple }
} else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
return { error: 'ErrorNoVideosInTweet' }
}
} else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
return { error: 'ErrorNoVideosInTweet' }
}
} else {
_headers["host"] = "twitter.com"
_headers["content-type"] = "application/json"
let query = {
variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true}
}
let AudioSpaceById = await got.get(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers });
AudioSpaceById = JSON.parse(AudioSpaceById.body);
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
let streamStatus = await got.get(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers });
streamStatus = JSON.parse(streamStatus.body);
let participants = AudioSpaceById.data.audioSpace.participants.speakers
let listOfParticipants = `Twitter Space speakers: `
for (let i in participants) {
listOfParticipants += `@${participants[i]["twitter_screen_name"]}, `
}
listOfParticipants = listOfParticipants.slice(0, -2);
return {
urls: streamStatus.source.noRedirectPlaybackUrl,
audioFilename: `twitterspaces_${obj.spaceId}`,
isAudioOnly: true,
fileMetadata: {
title: AudioSpaceById.data.audioSpace.metadata.title,
artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`,
comment: listOfParticipants,
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "")
let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then(async (r) => {
return r.status == 200 ? await r.json() : false;
}).catch((e) => {return false});
if (AudioSpaceById) {
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then(async (r) => {return r.status == 200 ? await r.json() : false;}).catch(() => {return false;});
if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers
let listOfParticipants = `Twitter Space speakers: `
for (let i in participants) {
listOfParticipants += `@${participants[i]["twitter_screen_name"]}, `
}
listOfParticipants = listOfParticipants.slice(0, -2);
return {
urls: streamStatus.source.noRedirectPlaybackUrl,
audioFilename: `twitterspaces_${obj.spaceId}`,
isAudioOnly: true,
fileMetadata: {
title: AudioSpaceById.data.audioSpace.metadata.title,
artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`,
comment: listOfParticipants,
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "")
}
}
} else {
return { error: 'TwitterSpaceWasntRecorded' };
}
} else {
return { error: loc(obj.lang, 'TwitterSpaceWasntRecorded') };
return { error: 'ErrorEmptyDownload' }
}
}
} catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,17 +1,17 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent, quality } from "../config.js";
import { quality, services } from "../config.js";
export default async function(obj) {
try {
let api = await got.get(`https://player.vimeo.com/video/${obj.id}/config`, { headers: { "user-agent": genericUserAgent } });
api.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'vimeo') };
});
api = api.body
if (api.includes('}}},"progressive":[{')) {
api = JSON.parse(api)
if (api["request"]["files"]["progressive"]) {
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return await r.json()}).catch(() => {return false});
if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = "";
if (JSON.stringify(api).includes('"progressive":[{')) {
downloadType = "progressive";
} else if (JSON.stringify(api).includes('"files":{"dash":{"')) downloadType = "dash";
switch(downloadType) {
case "progressive":
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0]
try {
@ -29,10 +29,52 @@ export default async function(obj) {
} catch (e) {
best = all[0]
}
return { urls: best["url"], audioFilename: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'vimeo') }
return { urls: best["url"] };
case "dash":
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then(async (r) => {return await r.json()}).catch(() => {return false});
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (masterJSON.video) {
let type = "";
if (masterJSON.base_url.includes("parcel")) {
type = "parcel"
} else if (masterJSON.base_url == "../") {
type = "chop"
}
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width));
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;});
let bestVideo = masterJSON_Video[0]
let bestAudio = masterJSON_Audio[0]
switch (type) {
case "parcel":
if (obj.quality != "max") {
let pref = parseInt(quality[obj.quality], 10)
for (let i in masterJSON_Video) {
let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10)
if (currQuality < pref) {
break;
} else if (currQuality == pref) {
bestVideo = masterJSON_Video[i]
}
}
}
let baseUrl = masterJSONURL.split("/sep/")[0]
let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`;
let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` }
case "chop": // TO-DO: support chop type of streams
default:
return { error: 'ErrorEmptyDownload' }
}
} else {
return { error: 'ErrorEmptyDownload' }
}
default:
return { error: 'ErrorEmptyDownload' }
}
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,36 +1,16 @@
import got from "got";
import { xml2json } from "xml-js";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxVideoDuration, services } from "../config.js";
import selectQuality from "../stream/selectQuality.js";
export default async function(obj) {
try {
let html;
let isClip = obj.url.includes("vk.com/clip");
if (isClip) {
html = await got.post("https://vk.com/al_video.php?act=show", {
headers: {
"user-agent": genericUserAgent,
"referer": `https://vk.com/clips-${obj.userId}?z=clip-${obj.userId}_${obj.videoId}`
},
body: `_nol={"0":"clips-${obj.userId}","z":"clip-${obj.userId}_${obj.videoId}"}&act=show&al=1&autoplay=0&list=&module=&video=-${obj.userId}_${obj.videoId}`
});
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'vk') };
});
html = html.body;
} else {
html = await got.get(`https://vk.com/video-${obj.userId}_${obj.videoId}`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'vk') };
});
html = html.body;
}
html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes(`{"lang":`)) {
let js = isClip ? JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']],"static":')[0]) : JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
if (js["mvData"]["is_active_live"] == '0') {
if (js["mvData"]["duration"] <= maxVideoDuration / 1000) {
let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 }));
@ -58,22 +38,21 @@ export default async function(obj) {
if (selectedQuality in js["player"]["params"][0]) {
return {
urls: js["player"]["params"][0][`url${userQuality}`],
filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`,
audioFilename: loc(obj.lang, 'ErrorEmptyDownload')
filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`
};
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
return { error: 'ErrorEmptyDownload' };
}
} else {
return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) };
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: loc(obj.lang, 'ErrorLiveVideo') };
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
return { error: 'ErrorEmptyDownload' };
}
} catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,5 +1,4 @@
import ytdl from "ytdl-core";
import loc from "../../localization/manager.js";
import ytdl from "better-ytdl-core";
import { maxVideoDuration, quality as mq } from "../config.js";
import selectQuality from "../stream/selectQuality.js";
@ -56,7 +55,7 @@ export default async function(obj) {
};
}
} else {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
} else if (!obj.isAudioOnly) {
return {
@ -82,18 +81,18 @@ export default async function(obj) {
}
return r
} else {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
} else {
return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) };
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: loc(obj.lang, 'ErrorLiveVideo') };
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI') };
return { error: 'ErrorCantConnectToServiceAPI' };
}
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
return { error: 'ErrorBadFetch' };
}
}

View file

@ -1,5 +1,14 @@
import { createStream } from "../stream/manage.js";
let apiVar = {
allowed: {
vFormat: ["mp4", "webm"],
vQuality: ["max", "hig", "mid", "low", "los"],
aFormat: ["best", "mp3", "ogg", "wav", "opus"]
},
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio"]
}
export function apiJSON(type, obj) {
try {
switch (type) {
@ -53,7 +62,11 @@ export function msToTime(d) {
return r;
}
export function cleanURL(url, host) {
url = url.replace('}', '').replace('{', '').replace(')', '').replace('(', '').replace(' ', '').replace('@', '');
let forbiddenChars = ['}', '{', '(', ')', '\\', '@', '%', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'"]
for (let i in forbiddenChars) {
url = url.replaceAll(forbiddenChars[i], '')
}
url = url.replace('https//', 'https://')
if (url.includes('youtube.com/shorts/')) {
url = url.split('?')[0].replace('shorts/', 'watch?v=');
}
@ -65,7 +78,7 @@ export function cleanURL(url, host) {
url = url.substring(0, url.length - 1);
}
}
return url
return url.slice(0, 128)
}
export function languageCode(req) {
return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en"
@ -84,20 +97,23 @@ export function checkJSONPost(obj) {
isNoTTWatermark: false,
isTTFullAudio: false
}
let booleanOnly = ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio"]
try {
let objKeys = Object.keys(obj);
if (objKeys.length < 8) {
if (objKeys.length < 8 && obj.url) {
let defKeys = Object.keys(def);
for (let i in objKeys) {
if (defKeys.includes(objKeys[i])) {
if (booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else {
def[objKeys[i]] = obj[objKeys[i]]
if (apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
}
}
}
obj["url"] = decodeURIComponent(String(obj["url"]))
let hostname = obj["url"].replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2]
def["url"] = encodeURIComponent(cleanURL(obj["url"], host))
return def
} else {
return false