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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
let ua = navigator.userAgent.toLowerCase(); let ua = navigator.userAgent.toLowerCase();
let isIOS = ua.match("iphone os"); let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || 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 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>` let notification = `<div class="notification-dot"></div>`
@ -442,7 +442,7 @@ window.onload = () => {
notificationCheck(); notificationCheck();
if (isIOS) sSet("downloadPopup", "true"); if (isIOS) sSet("downloadPopup", "true");
let urlQuery = new URLSearchParams(window.location.search).get("u"); let urlQuery = new URLSearchParams(window.location.search).get("u");
if (urlQuery !== null) { if (urlQuery !== null && regex.test(urlQuery)) {
eid("url-input-area").value = urlQuery; eid("url-input-area").value = urlQuery;
button(); 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.", "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", "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.", "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!", "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.", "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.", "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", "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.", "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", "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": "ты можешь помочь!", "DonateSub": "ты можешь помочь!",
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!", "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!",
"DonateVia": "открыть", "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(); loadLoc();
export function replaceBase(s) { 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) { export function replaceAll(lang, str, string, replacement) {
let s = replaceBase(str[string]) let s = replaceBase(str[string])

View file

@ -1,11 +1,16 @@
{ {
"current": { "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", "version": "4.4",
"title": "over 1 million monthly requests. thank you.", "title": "over 1 million monthly requests. thank you.",
"banner": "onemillionr.webp", "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." "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", "version": "4.3.2",
"title": "twitter improvements & changelog overhaul", "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!" "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, genericUserAgent = config.genericUserAgent,
repo = packageJson["bugs"]["url"].replace('/issues', ''), repo = packageJson["bugs"]["url"].replace('/issues', ''),
authorInfo = config.authorInfo, authorInfo = config.authorInfo,
supportedLanguages = config.supportedLanguages,
quality = config.quality, quality = config.quality,
internetExplorerRedirect = config.internetExplorerRedirect, internetExplorerRedirect = config.internetExplorerRedirect,
donations = config.donations, donations = config.donations,

View file

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

View file

@ -1,6 +1,8 @@
import { apiJSON } from "../sub/utils.js"; import { apiJSON } from "../sub/utils.js";
import { errorUnsupported, genericError } from "../sub/errors.js"; import { errorUnsupported, genericError } from "../sub/errors.js";
import loc from "../../localization/manager.js";
import { testers } from "./servicesPatternTesters.js"; import { testers } from "./servicesPatternTesters.js";
import bilibili from "../services/bilibili.js"; import bilibili from "../services/bilibili.js";
@ -105,7 +107,9 @@ export default async function (host, patternMatch, url, lang, obj) {
default: default:
return apiJSON(0, { t: errorUnsupported(lang) }); 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) { } catch (e) {
return apiJSON(0, { t: genericError(lang, host) }) return apiJSON(0, { t: genericError(lang, host) })
} }

View file

@ -1,105 +1,109 @@
import { audioIgnore, services, supportedAudio } from "../config.js" import { audioIgnore, services, supportedAudio } from "../config.js"
import { apiJSON } from "../sub/utils.js" import { apiJSON } from "../sub/utils.js"
import loc from "../../localization/manager.js";
export default function(r, host, ip, audioFormat, isAudioOnly) { export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
if (!r.error) { if (!isAudioOnly && !r.picker) {
if (!isAudioOnly && !r.picker) { switch (host) {
switch (host) { case "twitter":
case "twitter": return apiJSON(1, { u: r.urls });
return apiJSON(1, { u: r.urls }); case "vk":
case "vk": return apiJSON(2, {
return apiJSON(2, { type: "bridge", u: r.urls, service: host, ip: ip,
type: "bridge", u: r.urls, service: host, ip: ip, filename: r.filename,
filename: r.filename, });
}); case "bilibili":
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, { return apiJSON(2, {
type: "render", u: r.urls, service: host, ip: ip, type: "render", u: r.urls, service: host, ip: ip,
filename: r.filename, filename: r.filename
time: r.time
}); });
case "youtube": } else {
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 }); return apiJSON(1, { u: r.urls });
case "vimeo": }
return apiJSON(1, { u: r.urls }); }
} } else if (r.picker) {
} else if (r.picker) { switch (host) {
switch (host) { case "douyin":
case "douyin": case "tiktok":
case "tiktok": let type = "render";
let type = "render"; if (audioFormat === "mp3" || audioFormat === "best") {
if (audioFormat === "mp3" || audioFormat === "best") { audioFormat = "mp3"
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"
type = "bridge" type = "bridge"
} }
} return apiJSON(5, {
if ((audioFormat === "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) { type: type,
audioFormat = services[host]["bestAudio"] picker: r.picker,
type = "bridge" u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
} else if (audioFormat === "best") { filename: r.audioFilename, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false,
audioFormat = "m4a" })
copy = true case "twitter":
if (r.audioFilename.includes("twitterspaces")) { return apiJSON(5, {
audioFormat = "mp3" picker: r.picker, service: host
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
})
} }
} else { } 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": { "config": {
"bilibili": { "bilibili": {
"alias": "bilibili.com", "alias": "bilibili.com",
@ -12,12 +12,12 @@
"enabled": true "enabled": true
}, },
"twitter": { "twitter": {
"alias": "twitter, twitter spaces", "alias": "twitter posts & spaces",
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"], "patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"],
"enabled": true "enabled": true
}, },
"vk": { "vk": {
"alias": "vk clips, vk video", "alias": "vk video & clips",
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"], "patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"],
"quality_match": { "quality_match": {
"2160": 7, "2160": 7,
@ -47,7 +47,7 @@
"enabled": true "enabled": true
}, },
"youtube": { "youtube": {
"alias": "youtube, youtube music, youtube shorts", "alias": "youtube videos & shorts & music",
"patterns": ["watch?v=:id"], "patterns": ["watch?v=:id"],
"quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"], "quality_match": ["2160", "1440", "1080", "720", "480", "360", "240", "144"],
"bestAudio": "opus", "bestAudio": "opus",
@ -59,26 +59,34 @@
"enabled": true "enabled": true
}, },
"tumblr": { "tumblr": {
"patterns": ["post/:id", "blog/view/:user/:id"], "patterns": ["post/:id", "blog/view/:user/:id", ":user/:id", ":user/:id/:trackingId"],
"enabled": true "enabled": true
}, },
"tiktok": { "tiktok": {
"alias": "tiktok videos & slideshow & audio",
"patterns": [":user/video/:postId", ":id", "t/:id"], "patterns": [":user/video/:postId", ":id", "t/:id"],
"audioFormats": ["best", "m4a", "mp3"], "audioFormats": ["best", "m4a", "mp3"],
"enabled": true "enabled": true
}, },
"douyin": { "douyin": {
"alias": "douyin videos & slideshow & audio",
"patterns": ["video/:postId", ":id"], "patterns": ["video/:postId", ":id"],
"enabled": true "enabled": true
}, },
"vimeo": { "vimeo": {
"patterns": [":id"], "patterns": [":id"],
"resolutionMatch": {
"3840": "2160",
"1920": "1080",
"1280": "720",
"960": "480"
},
"enabled": true "enabled": true
}, },
"soundcloud": { "soundcloud": {
"patterns": [":author/:song", ":shortLink"], "patterns": [":author/:song", ":shortLink"],
"bestAudio": "none", "bestAudio": "none",
"clientid": "1TLciEOiKE0PThutYu5Xj0kc8R4twD9p", "clientid": "YeTcsotswIIc4sse5WZsXszVxMtP6eLc",
"enabled": true "enabled": true
} }
} }

View file

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

View file

@ -1,29 +1,27 @@
import got from "got"; import { maxVideoDuration } from "../config.js";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxVideoDuration } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { 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 = 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});
let data = (JSON.parse(req.body))[0]["data"]["children"][0]["data"]; 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) { 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], 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`; 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 } }); await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {audio = ''});
} catch (err) {
audio = ''
}
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3] let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]
if (audio.length > 0) { if (audio.length > 0) {
return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` }; return { typeId: 2, type: "render", urls: [video, audio], audioFilename: `reddit_${id}_audio`, filename: `reddit_${id}.mp4` };
} else { } else {
return { typeId: 1, urls: video, audioFilename: loc(obj.lang, 'ErrorEmptyDownload') }; return { typeId: 1, urls: video };
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') }; return { error: 'ErrorEmptyDownload' };
} }
} catch (err) { } 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"; import { genericUserAgent, maxAudioDuration, services } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let html; let html;
if (!obj.author && !obj.song && obj.shortLink) { if (!obj.author && !obj.song && obj.shortLink) {
html = await got.get(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { headers: { "user-agent": genericUserAgent } }); html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
html = html.body headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return await r.text()}).catch(() => {return false});
} }
if (obj.author && obj.song) { if (obj.author && obj.song) {
html = await got.get(`https://soundcloud.com/${obj.author}/${obj.song}`, { headers: { "user-agent": genericUserAgent } }); html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, {
html = html.body 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":')) { 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]) let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) { if (json["media"]["transcodings"]) {
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${services["soundcloud"]["clientid"]}&track_authorization=${json.track_authorization}`; 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 (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
if (json.duration < maxAudioDuration) { if (json.duration < maxAudioDuration) {
let file = await got.get(fileUrl, { headers: { "user-agent": genericUserAgent } }); let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
file = JSON.parse(file.body).url if (!file) return { error: 'ErrorCouldntFetch' };
return { return {
urls: file, urls: file,
audioFilename: `soundcloud_${json.id}`, audioFilename: `soundcloud_${json.id}`,
@ -29,11 +30,11 @@ export default async function(obj) {
artist: json.user.username, 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: 'ErrorEmptyDownload' }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'soundcloud') } } else return { error: ['ErrorBrokenLink', 'soundcloud'] }
} catch (e) { } 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"; import { genericUserAgent } from "../config.js";
let userAgent = genericUserAgent.split(' Chrome/1')[0] let userAgent = genericUserAgent.split(' Chrome/1')[0]
let config = { let config = {
tiktok: { tiktok: {
short: "https://vt.tiktok.com/", 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: { douyin: {
short: "https://v.douyin.com/", short: "https://v.douyin.com/",
@ -14,34 +12,46 @@ let config = {
} }
} }
function selector(j, h, id) { function selector(j, h, id) {
let t; if (j) {
switch (h) { let t;
case "tiktok": switch (h) {
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true }) case "tiktok":
break; t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
case "douyin": break;
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true }) case "douyin":
break; 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 (t.length > 0) { return t[0] } else return false
} else return false
} }
export default async function(obj) { export default async function(obj) {
try { try {
if (!obj.postId) { if (!obj.postId) {
let html = await got.get(`${config[obj.host]["short"]}${obj.id}`, { followRedirect: false, headers: { "user-agent": userAgent } }); let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
html = html.body; redirect: "manual",
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) obj.postId = html.split('video/')[1].split('?')[0].replace("/", '') 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; let detail;
try { detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
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"} }); 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); }).then(async (r) => {return await r.json()}).catch(() => {return false});
if (!detail) throw new Error()
} catch (e) { detail = selector(detail, obj.host, obj.postId);
return { error: loc(obj.lang, 'ErrorCouldntFetch') }
} if (!detail) return { error: 'ErrorCouldntFetch' }
let video, videoFilename, audioFilename, isMp3, audio, images, let video, videoFilename, audioFilename, isMp3, audio, images,
filenameBase = `${obj.host}_${obj.postId}`; filenameBase = `${obj.host}_${obj.postId}`;
if (obj.host == "tiktok") { if (obj.host == "tiktok") {
@ -101,6 +111,6 @@ export default async function(obj) {
isMp3: isMp3, isMp3: isMp3,
} }
} catch (e) { } 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"; import { genericUserAgent } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', ''); let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
if (user.length <= 32) { let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
let html = await got.get(`https://${user}.tumblr.com/post/${obj.id}`, { headers: { "user-agent": genericUserAgent } }); headers: {"user-agent": genericUserAgent}
html.on('error', (err) => { }).then(async (r) => {return await r.text()}).catch(() => {return false});
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'tumblr') }; if (!html) return { error: 'ErrorCouldntFetch' };
}); if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
html = html.body 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` }
if (html.includes('<!-- GOOGLE CAROUSEL --><script type="application/ld+json">')) { } else return { error: 'ErrorEmptyDownload' }
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') }
} catch (e) { } 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"; import { genericUserAgent } from "../config.js";
function bestQuality(arr) { function bestQuality(arr) {
@ -10,33 +8,34 @@ const apiURL = "https://api.twitter.com/1.1"
export default async function(obj) { export default async function(obj) {
try { try {
let _headers = { let _headers = {
"User-Agent": genericUserAgent, "user-agent": genericUserAgent,
"Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
"Host": "api.twitter.com", "host": "api.twitter.com"
"Content-Type": "application/json",
"Content-Length": 0
}; };
let req_act = await got.post(`${apiURL}/guest/activate.json`, { headers: _headers }); let req_act = await fetch(`${apiURL}/guest/activate.json`, {
req_act = JSON.parse(req_act.body) 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"]; _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` 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) { if (!obj.spaceId) {
// kind of wonky but it works :D let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch((e) => { return false});
let req_status = {} if (!req_status) {
try { _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
req_status = await got.get(showURL, { headers: _headers }); delete _headers["x-guest-token"]
} catch (e) {
try { req_act = await fetch(`${apiURL}/guest/activate.json`, {
_headers.Authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; method: "POST",
delete _headers["x-guest-token"] headers: _headers
req_act = await got.post(`${apiURL}/guest/activate.json`, { headers: _headers }); }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
req_act = JSON.parse(req_act.body) if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"];
req_status = await got.get(showURL, { headers: _headers }); _headers["x-guest-token"] = req_act["guest_token"];
} catch(err) {} 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: 'ErrorCouldntFetch' }
if (req_status == {}) return { error: loc(obj.lang, 'ErrorCouldntFetch') }
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) { if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
let single, multiple = [], media = 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 }) 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) { } else if (media.length > 0) {
single = bestQuality(media[0]["video_info"]["variants"]) single = bestQuality(media[0]["video_info"]["variants"])
} else { } else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') } return { error: 'ErrorNoVideosInTweet' }
} }
if (single) { if (single) {
return { urls: single, audioFilename: `twitter_${obj.id}_audio` } return { urls: single, audioFilename: `twitter_${obj.id}_audio` }
} else if (multiple) { } else if (multiple) {
return { picker: multiple } return { picker: multiple }
} else { } else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') } return { error: 'ErrorNoVideosInTweet' }
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') } return { error: 'ErrorNoVideosInTweet' }
} }
} else { } else {
_headers["host"] = "twitter.com" _headers["host"] = "twitter.com"
_headers["content-type"] = "application/json"
let query = { 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} 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); 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) => {
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { return r.status == 200 ? await r.json() : false;
let streamStatus = await got.get(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }); }).catch((e) => {return false});
streamStatus = JSON.parse(streamStatus.body);
let participants = AudioSpaceById.data.audioSpace.participants.speakers if (AudioSpaceById) {
let listOfParticipants = `Twitter Space speakers: ` if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
for (let i in participants) { 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;});
listOfParticipants += `@${participants[i]["twitter_screen_name"]}, ` if (!streamStatus) return { error: 'ErrorCouldntFetch' };
}
listOfParticipants = listOfParticipants.slice(0, -2); let participants = AudioSpaceById.data.audioSpace.participants.speakers
return { let listOfParticipants = `Twitter Space speakers: `
urls: streamStatus.source.noRedirectPlaybackUrl, for (let i in participants) {
audioFilename: `twitterspaces_${obj.spaceId}`, listOfParticipants += `@${participants[i]["twitter_screen_name"]}, `
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", "")
} }
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 { } else {
return { error: loc(obj.lang, 'TwitterSpaceWasntRecorded') }; return { error: 'ErrorEmptyDownload' }
} }
} }
} catch (err) { } catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: 'ErrorBadFetch' };
} }
} }

View file

@ -1,17 +1,17 @@
import got from "got"; import { quality, services } from "../config.js";
import loc from "../../localization/manager.js";
import { genericUserAgent, quality } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let api = await got.get(`https://player.vimeo.com/video/${obj.id}/config`, { headers: { "user-agent": genericUserAgent } }); let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return await r.json()}).catch(() => {return false});
api.on('error', (err) => { if (!api) return { error: 'ErrorCouldntFetch' };
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'vimeo') };
}); let downloadType = "";
api = api.body if (JSON.stringify(api).includes('"progressive":[{')) {
if (api.includes('}}},"progressive":[{')) { downloadType = "progressive";
api = JSON.parse(api) } else if (JSON.stringify(api).includes('"files":{"dash":{"')) downloadType = "dash";
if (api["request"]["files"]["progressive"]) {
switch(downloadType) {
case "progressive":
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width)); let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0] let best = all[0]
try { try {
@ -29,10 +29,52 @@ export default async function(obj) {
} catch (e) { } catch (e) {
best = all[0] best = all[0]
} }
return { urls: best["url"], audioFilename: loc(obj.lang, 'ErrorEmptyDownload') } return { urls: best["url"] };
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') } case "dash":
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'vimeo') } 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) { } 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 { xml2json } from "xml-js";
import loc from "../../localization/manager.js";
import { genericUserAgent, maxVideoDuration, services } from "../config.js"; import { genericUserAgent, maxVideoDuration, services } from "../config.js";
import selectQuality from "../stream/selectQuality.js"; import selectQuality from "../stream/selectQuality.js";
export default async function(obj) { export default async function(obj) {
try { try {
let html; let html;
let isClip = obj.url.includes("vk.com/clip"); html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
headers: {"user-agent": genericUserAgent}
if (isClip) { }).then(async (r) => {return await r.text()}).catch(() => {return false});
html = await got.post("https://vk.com/al_video.php?act=show", { if (!html) return { error: 'ErrorCouldntFetch' };
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;
}
if (html.includes(`{"lang":`)) { 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"]["is_active_live"] == '0') {
if (js["mvData"]["duration"] <= maxVideoDuration / 1000) { if (js["mvData"]["duration"] <= maxVideoDuration / 1000) {
let mpd = JSON.parse(xml2json(js["player"]["params"][0]["manifest"], { compact: true, spaces: 4 })); 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]) { if (selectedQuality in js["player"]["params"][0]) {
return { return {
urls: js["player"]["params"][0][`url${userQuality}`], urls: js["player"]["params"][0][`url${userQuality}`],
filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`, filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`
audioFilename: loc(obj.lang, 'ErrorEmptyDownload')
}; };
} else { } else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') }; return { error: 'ErrorEmptyDownload' };
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorLengthLimit', maxVideoDuration / 60000) }; return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorLiveVideo') }; return { error: 'ErrorLiveVideo' };
} }
} else { } else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') }; return { error: 'ErrorEmptyDownload' };
} }
} catch (err) { } catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: 'ErrorBadFetch' };
} }
} }

View file

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

View file

@ -1,5 +1,14 @@
import { createStream } from "../stream/manage.js"; 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) { export function apiJSON(type, obj) {
try { try {
switch (type) { switch (type) {
@ -53,7 +62,11 @@ export function msToTime(d) {
return r; return r;
} }
export function cleanURL(url, host) { 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/')) { if (url.includes('youtube.com/shorts/')) {
url = url.split('?')[0].replace('shorts/', 'watch?v='); url = url.split('?')[0].replace('shorts/', 'watch?v=');
} }
@ -65,7 +78,7 @@ export function cleanURL(url, host) {
url = url.substring(0, url.length - 1); url = url.substring(0, url.length - 1);
} }
} }
return url return url.slice(0, 128)
} }
export function languageCode(req) { export function languageCode(req) {
return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en" return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en"
@ -84,20 +97,23 @@ export function checkJSONPost(obj) {
isNoTTWatermark: false, isNoTTWatermark: false,
isTTFullAudio: false isTTFullAudio: false
} }
let booleanOnly = ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio"]
try { try {
let objKeys = Object.keys(obj); let objKeys = Object.keys(obj);
if (objKeys.length < 8) { if (objKeys.length < 8 && obj.url) {
let defKeys = Object.keys(def); let defKeys = Object.keys(def);
for (let i in objKeys) { for (let i in objKeys) {
if (defKeys.includes(objKeys[i])) { if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (booleanOnly.includes(objKeys[i])) { if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else { } 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 return def
} else { } else {
return false return false