diff --git a/package.json b/package.json index 2c77d38..70b996f 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/cobalt.js b/src/cobalt.js index 8ee8fd9..5a0861e 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -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') }) diff --git a/src/config.json b/src/config.json index 3eafd78..98bdd3f 100644 --- a/src/config.json +++ b/src/config.json @@ -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"] diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 3a3c2fe..a4068ad 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -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); diff --git a/src/front/cobalt.js b/src/front/cobalt.js index bf8f23c..b8b5ddf 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -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 = `
` @@ -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(); } diff --git a/src/front/updateBanners/meowthstrong.webp b/src/front/updateBanners/meowthstrong.webp new file mode 100644 index 0000000..96ab305 Binary files /dev/null and b/src/front/updateBanners/meowthstrong.webp differ diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index b479269..745a89e 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -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 completely free to use. 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 hire me." + "DonateHireMe": "or, as an alternative, you can hire me.", + "DiscordServer": "join the live conversation about {appName} on the official discord server" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index b4c97dd..d40f770 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -107,6 +107,7 @@ "DonateSub": "ты можешь помочь!", "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!", "DonateVia": "открыть", - "DonateHireMe": "или же ты можешь пригласить меня на работу." + "DonateHireMe": "или же ты можешь пригласить меня на работу.", + "DiscordServer": "присоединяйся к живой беседе о {appName} прямо на его официальном discord сервере" } } diff --git a/src/localization/manager.js b/src/localization/manager.js index ba32c7b..497c1bc 100644 --- a/src/localization/manager.js +++ b/src/localization/manager.js @@ -15,7 +15,7 @@ export function loadLoc() { } loadLoc(); export function replaceBase(s) { - return s.replace(/\n/g, '
').replace(/{appName}/g, appName).replace(/{repo}/g, repo) + return s.replace(/\n/g, '
').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]) diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index aa8be72..43f86c7 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -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
• vimeo module has been revamped, all sorts of videos should now be supported.\n• vimeo audio downloads! you now can download audios from more recent videos.\n• {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n• vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n• youtube videos with community warnings should now be possible to download.
\nuser interface improvements:\n
• list of supported services is now MUCH easier to read.\n• banners in changelog history should no longer overlap each other.\n• bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.
\ninternal improvements:\n
• cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n• 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• moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on github or npm!\n• ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n• \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n• 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• other code optimizations. there's less clutter overall.
\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, please feel free to do it on github!\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!" diff --git a/src/modules/config.js b/src/modules/config.js index 6e3d81c..8f27e95 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -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, diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 2c33443..2c5c188 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -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 `
• ${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: `
${enabledServices}.
` + }, { + text: obj.lang !== "ru" ? loc(obj.lang, 'FollowTwitter') : "", + classes: ["desc-padding"] }, { text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')), classes: ["bottom-link"] diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index e5ebbb6..a7cc586 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -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) }) } diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 04d5900..7f03b8c 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -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 + }) } } diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 9678c8f..c0cd70a 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -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 } } diff --git a/src/modules/services/bilibili.js b/src/modules/services/bilibili.js index 554f2f7..d831132 100644 --- a/src/modules/services/bilibili.js +++ b/src/modules/services/bilibili.js @@ -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('')[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' }; } } diff --git a/src/modules/services/reddit.js b/src/modules/services/reddit.js index 28e068e..4feee5a 100644 --- a/src/modules/services/reddit.js +++ b/src/modules/services/reddit.js @@ -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' }; } } diff --git a/src/modules/services/soundcloud.js b/src/modules/services/soundcloud.js index b2d476b..08cbc7f 100644 --- a/src/modules/services/soundcloud.js +++ b/src/modules/services/soundcloud.js @@ -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('')[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' }; } } diff --git a/src/modules/services/tiktok.js b/src/modules/services/tiktok.js index ff85137..d38b8c5 100644 --- a/src/modules/services/tiktok.js +++ b/src/modules/services/tiktok.js @@ -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®ion=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®ion=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) === ' {return await r.text()}).catch(() => {return false}); + if (!html) return { error: 'ErrorCouldntFetch' }; + + if (html.slice(0, 17) === ' {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' }; } } diff --git a/src/modules/services/tumblr.js b/src/modules/services/tumblr.js index 2aee6ce..23306ca 100644 --- a/src/modules/services/tumblr.js +++ b/src/modules/services/tumblr.js @@ -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('')[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' }; } } diff --git a/src/modules/services/twitter.js b/src/modules/services/twitter.js index 73c5898..923c882 100644 --- a/src/modules/services/twitter.js +++ b/src/modules/services/twitter.js @@ -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' }; } } diff --git a/src/modules/services/vimeo.js b/src/modules/services/vimeo.js index 5b887d5..0c2b06c 100644 --- a/src/modules/services/vimeo.js +++ b/src/modules/services/vimeo.js @@ -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' }; } } diff --git a/src/modules/services/vk.js b/src/modules/services/vk.js index 890c4d8..3d77ffd 100644 --- a/src/modules/services/vk.js +++ b/src/modules/services/vk.js @@ -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' }; } } diff --git a/src/modules/services/youtube.js b/src/modules/services/youtube.js index 768f087..6d247e4 100644 --- a/src/modules/services/youtube.js +++ b/src/modules/services/youtube.js @@ -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' }; } } diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index e32c5ca..548e597 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -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