refactoring & fixes

- added duration check to vimeo module
- fixed quality picking in vimeo module for progressive video type
- dropping requests from ie users instead of redirecting
- probably something else but i forgot to be honest
This commit is contained in:
wukko 2023-02-09 20:45:17 +06:00
parent c7a9723847
commit 3432c91482
21 changed files with 479 additions and 453 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "4.8", "version": "4.9-dev",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View file

@ -6,7 +6,7 @@ import * as fs from "fs";
import rateLimit from "express-rate-limit"; import rateLimit from "express-rate-limit";
import { shortCommit } from "./modules/sub/currentCommit.js"; import { shortCommit } from "./modules/sub/currentCommit.js";
import { appName, genericUserAgent, version, internetExplorerRedirect } from "./modules/config.js"; import { appName, genericUserAgent, version } from "./modules/config.js";
import { getJSON } from "./modules/api.js"; import { getJSON } from "./modules/api.js";
import renderPage from "./modules/pageRender/page.js"; import renderPage from "./modules/pageRender/page.js";
import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js"; import { apiJSON, checkJSONPost, languageCode } from "./modules/sub/utils.js";
@ -57,6 +57,13 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
} }
next(); next();
}); });
app.use((req, res, next) => {
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) {
res.destroy()
}
next();
});
app.use('/api/json', express.json({ app.use('/api/json', express.json({
verify: (req, res, buf) => { verify: (req, res, buf) => {
try { try {
@ -150,20 +157,12 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
res.redirect('/api/json') res.redirect('/api/json')
}); });
app.get("/", (req, res) => { app.get("/", (req, res) => {
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) {
if (internetExplorerRedirect.newNT.includes(req.header("user-agent").split('NT ')[1].split(';')[0])) {
res.redirect(internetExplorerRedirect.new)
} else {
res.redirect(internetExplorerRedirect.old)
}
} else {
res.send(renderPage({ res.send(renderPage({
"hash": commitHash, "hash": commitHash,
"type": "default", "type": "default",
"lang": languageCode(req), "lang": languageCode(req),
"useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent "useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent
})) }))
}
}); });
app.get("/favicon.ico", (req, res) => { app.get("/favicon.ico", (req, res) => {
res.redirect('/icons/favicon.ico'); res.redirect('/icons/favicon.ico');

View file

@ -2,7 +2,7 @@
"streamLifespan": 120000, "streamLifespan": 120000,
"maxVideoDuration": 7500000, "maxVideoDuration": 7500000,
"maxAudioDuration": 7500000, "maxAudioDuration": 7500000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
"authorInfo": { "authorInfo": {
"name": "wukko", "name": "wukko",
"link": "https://wukko.me/", "link": "https://wukko.me/",
@ -18,11 +18,6 @@
} }
} }
}, },
"internetExplorerRedirect": {
"newNT": ["6.1", "6.2", "6.3", "10.0"],
"old": "https://mypal-browser.org/",
"new": "https://www.mozilla.org/firefox/new/"
},
"donations": { "donations": {
"crypto": { "crypto": {
"bitcoin": "bc1q59jyyjvrzj4c22rkk3ljeecq6jmpyscgz9spnd", "bitcoin": "bc1q59jyyjvrzj4c22rkk3ljeecq6jmpyscgz9spnd",

View file

@ -10,32 +10,43 @@ import match from "./processing/match.js";
export async function getJSON(originalURL, lang, obj) { export async function getJSON(originalURL, lang, obj) {
try { try {
let url = decodeURIComponent(originalURL); let url = decodeURIComponent(originalURL);
if (!url.includes('http://')) { if (url.startsWith('http://')) {
return apiJSON(0, { t: errorUnsupported(lang) });
}
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2], host = hostname[hostname.length - 2],
patternMatch; patternMatch;
if (host === "youtu") {
// TO-DO: bring all tests into one unified module instead of placing them in several places
switch(host) {
case "youtu":
host = "youtube"; host = "youtube";
url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`; url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`;
} break;
if (host === "goo" && url.substring(0, 30) === "https://soundcloud.app.goo.gl/") { case "goo":
if (url.substring(0, 30) === "https://soundcloud.app.goo.gl/"){
host = "soundcloud" host = "soundcloud"
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}` url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
} }
if (host === "tumblr" && !url.includes("blog/view")) { break;
case "tumblr":
if (!url.includes("blog/view")) {
if (url.slice(-1) == '/') url = url.slice(0, -1); if (url.slice(-1) == '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], ''); url = url.replace(url.split('/')[5], '');
} }
if (host && host.length < 20 && host in patterns && patterns[host]["enabled"]) { break;
}
if (!(host && host.length < 20 && host in patterns && patterns[host]["enabled"])) {
return apiJSON(0, { t: errorUnsupported(lang) });
}
for (let i in patterns[host]["patterns"]) { for (let i in patterns[host]["patterns"]) {
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]); patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]);
if (patternMatch) break; if (patternMatch) break;
} }
if (patternMatch) { if (!patternMatch) {
return apiJSON(0, { t: errorUnsupported(lang) });
}
return await match(host, patternMatch, url, lang, obj); return await match(host, patternMatch, url, lang, obj);
} else return apiJSON(0, { t: errorUnsupported(lang) });
} else return apiJSON(0, { t: errorUnsupported(lang) });
} else return apiJSON(0, { t: errorUnsupported(lang) });
} catch (e) { } catch (e) {
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }); return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });
} }

View file

@ -4,9 +4,9 @@ export async function buildFront() {
try { try {
await esbuild.build({ await esbuild.build({
entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'], entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'],
outdir: `min/`, outdir: 'min/',
minify: true, minify: true,
loader: { ".js": "js", ".css": "css" } loader: { '.js': 'js', '.css': 'css' }
}) })
} catch (e) { } catch (e) {
return; return;

View file

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

View file

@ -1,28 +1,37 @@
import { genericUserAgent, maxVideoDuration } from "../config.js"; import { genericUserAgent, maxVideoDuration } from "../config.js";
// TO-DO: quality picking
export default async function(obj) { export default async function(obj) {
try { try {
let html = await fetch(`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((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) {
return { error: 'ErrorCouldntFetch' };
}
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) { if (!(html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"'))) {
return { error: 'ErrorEmptyDownload' };
}
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) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
let video = streamData["data"]["dash"]["video"].filter((v) => { let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => { let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).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` };
} else { return {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; urls: [video[0]["baseUrl"], audio[0]["baseUrl"]],
} time: streamData.data.timelength,
} else { audioFilename: `bilibili_${obj.id}_audio`,
return { error: 'ErrorEmptyDownload' }; filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4`
} };
} catch (e) { } catch (e) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -1,26 +1,39 @@
import { maxVideoDuration } from "../config.js"; import { maxVideoDuration } from "../config.js";
// TO-DO: add support for gifs (#80)
export default async function(obj) { export default async function(obj) {
try { try {
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false }); let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => { return r.json() }).catch(() => { return false });
if (!data) return { error: 'ErrorCouldntFetch' }; if (!data) {
return { error: 'ErrorCouldntFetch' };
}
data = data[0]["data"]["children"][0]["data"]; 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"]) {
return { error: 'ErrorEmptyDownload' };
}
if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
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`;
await fetch(audio, {method: "HEAD"}).then((r) => {if (r.status != 200) audio = ''}).catch(() => {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] 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` }; if (!audio.length > 0) {
} else {
return { typeId: 1, urls: video }; return { typeId: 1, urls: video };
} }
} else { return {
return { error: 'ErrorEmptyDownload' }; typeId: 2,
} type: "render",
urls: [video, audio],
audioFilename: `reddit_${id}_audio`,
filename: `reddit_${id}.mp4`
};
} catch (err) { } catch (err) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -5,11 +5,12 @@ let cachedID = {}
async function findClientID() { async function findClientID() {
try { try {
let sc = await fetch('https://soundcloud.com/').then((r) => { return r.text() }).catch(() => { return false }); let sc = await fetch('https://soundcloud.com/').then((r) => { return r.text() }).catch(() => { return false });
let sc_version = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/)); let scVersion = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/));
if (cachedID.version == sc_version) { if (cachedID.version == scVersion) {
return cachedID.id return cachedID.id
} else { }
let scripts = sc.matchAll(/<script.+src="(.+)">/g); let scripts = sc.matchAll(/<script.+src="(.+)">/g);
let clientid; let clientid;
for (let script of scripts) { for (let script of scripts) {
@ -25,11 +26,9 @@ async function findClientID() {
break; break;
} }
} }
cachedID.version = sc_version; cachedID.version = scVersion;
cachedID.id = clientid; cachedID.id = clientid;
return clientid; return clientid;
}
} catch (e) { } catch (e) {
return false; return false;
} }
@ -48,18 +47,35 @@ export default async function(obj) {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.text() }).catch(() => { return false });
} }
if (!html) return { error: 'ErrorCouldntFetch'}; if (!html) {
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) { return { error: 'ErrorCouldntFetch'};
}
if (!(html.includes('<script>window.__sc_hydration = ')
&& html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},')
&& html.includes('{"hydratable":"sound","data":'))) {
return { error: ['ErrorBrokenLink', 'soundcloud'] }
}
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"]) {
return { error: 'ErrorEmptyDownload' }
}
let clientId = await findClientID(); let clientId = await findClientID();
if (clientId) { if (!clientId) {
return { error: 'ErrorSoundCloudNoClientId' }
}
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive") let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive")
let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${clientId}&track_authorization=${json.track_authorization}`; let fileUrl = `${fileUrlBase}${fileUrlBase.includes("?") ? "&" : "?"}client_id=${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) { return { error: 'ErrorEmptyDownload' }
}
if (json.duration > maxAudioDuration) {
return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
}
let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false }); let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => { return false });
if (!file) return { error: 'ErrorCouldntFetch' }; if (!file) {
return { error: 'ErrorCouldntFetch' };
}
return { return {
urls: file, urls: file,
audioFilename: `soundcloud_${json.id}`, audioFilename: `soundcloud_${json.id}`,
@ -68,11 +84,6 @@ export default async function(obj) {
artist: json.user.username, artist: json.user.username,
} }
} }
} else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
}
} else return { error: 'ErrorSoundCloudNoClientId' }
} else return { error: 'ErrorEmptyDownload' }
} else return { error: ['ErrorBrokenLink', 'soundcloud'] }
} catch (e) { } catch (e) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -12,7 +12,7 @@ let config = {
} }
} }
function selector(j, h, id) { function selector(j, h, id) {
if (j) { if (!j) return false
let t; let t;
switch (h) { switch (h) {
case "tiktok": case "tiktok":
@ -22,8 +22,8 @@ function selector(j, h, id) {
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true }) t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true })
break; break;
} }
if (t.length > 0) { return t[0] } else return false if (!t.length > 0) return false
} else return false return t[0]
} }
export default async function(obj) { export default async function(obj) {
@ -41,7 +41,9 @@ export default async function(obj) {
obj.postId = html.split('/v/')[1].split('.html')[0].replace("/", '') obj.postId = html.split('/v/')[1].split('.html')[0].replace("/", '')
} }
} }
if (!obj.postId) return { error: 'ErrorCantGetID' }; if (!obj.postId) {
return { error: 'ErrorCantGetID' };
}
let detail; let detail;
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), { detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
@ -60,20 +62,19 @@ export default async function(obj) {
images = detail["images"] ? detail["images"] : false images = detail["images"] ? detail["images"] : false
} }
if (!obj.isAudioOnly && !images) { if (!obj.isAudioOnly && !images) {
video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play");
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
if (!obj.noWatermark) {
video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0] video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0]
videoFilename = `${filenameBase}_video.mp4` videoFilename = `${filenameBase}_video.mp4`
if (obj.noWatermark) {
video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play");
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
} }
} else { } else {
let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0]; let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0];
audio = fallback;
audioFilename = `${filenameBase}_audio_fv`; // fv - from video
if (obj.fullAudio || fallback.includes("music")) { if (obj.fullAudio || fallback.includes("music")) {
audio = detail["music"]["play_url"]["url_list"][0] audio = detail["music"]["play_url"]["url_list"][0]
audioFilename = `${filenameBase}_audio` audioFilename = `${filenameBase}_audio`
} else {
audio = fallback
audioFilename = `${filenameBase}_audio_fv` // fv - from video
} }
if (audio.slice(-4) === ".mp3") isMp3 = true; if (audio.slice(-4) === ".mp3") isMp3 = true;
} }

View file

@ -7,9 +7,10 @@ export default async function(obj) {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then((r) => { return r.text() }).catch(() => { return false }); }).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) { if (!html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
return { error: 'ErrorEmptyDownload' }
}
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` } 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) { } catch (e) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -36,12 +36,14 @@ export default async function(obj) {
req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
} }
if (!req_status) return { error: 'ErrorCouldntFetch' } if (!req_status) return { error: 'ErrorCouldntFetch' }
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) { if (!req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
return { error: 'ErrorNoVideosInTweet' }
}
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 })
if (media.length > 1) { if (media.length > 1) {
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }
} else if (media.length > 0) { } else if (media.length === 1) {
single = bestQuality(media[0]["video_info"]["variants"]) single = bestQuality(media[0]["video_info"]["variants"])
} else { } else {
return { error: 'ErrorNoVideosInTweet' } return { error: 'ErrorNoVideosInTweet' }
@ -53,9 +55,6 @@ export default async function(obj) {
} else { } else {
return { error: 'ErrorNoVideosInTweet' } return { error: 'ErrorNoVideosInTweet' }
} }
} else {
return { error: 'ErrorNoVideosInTweet' }
}
} else { } else {
_headers["host"] = "twitter.com" _headers["host"] = "twitter.com"
_headers["content-type"] = "application/json" _headers["content-type"] = "application/json"
@ -67,9 +66,14 @@ export default async function(obj) {
return r.status == 200 ? r.json() : false; return r.status == 200 ? r.json() : false;
}).catch((e) => {return false}); }).catch((e) => {return false});
if (AudioSpaceById) { if (!AudioSpaceById) {
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { return { error: 'ErrorEmptyDownload' }
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then((r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;}); }
if (!AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
return { error: 'TwitterSpaceWasntRecorded' };
}
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`,
{ headers: _headers }).then((r) =>{return r.status == 200 ? r.json() : false;}).catch(() => {return false;});
if (!streamStatus) return { error: 'ErrorCouldntFetch' }; if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers let participants = AudioSpaceById.data.audioSpace.participants.speakers
@ -89,12 +93,6 @@ export default async function(obj) {
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "") // cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "")
} }
} }
} else {
return { error: 'TwitterSpaceWasntRecorded' };
}
} else {
return { error: 'ErrorEmptyDownload' }
}
} }
} catch (err) { } catch (err) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };

View file

@ -1,14 +1,14 @@
import { quality, services } from "../config.js"; import { maxVideoDuration, quality, services } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => {return r.json()}).catch(() => {return false}); let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => {return r.json()}).catch(() => {return false});
if (!api) return { error: 'ErrorCouldntFetch' }; if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = ""; let downloadType = "dash";
if (JSON.stringify(api).includes('"progressive":[{')) { if (JSON.stringify(api).includes('"progressive":[{')) {
downloadType = "progressive"; downloadType = "progressive";
} else if (JSON.stringify(api).includes('"files":{"dash":{"')) downloadType = "dash"; }
switch(downloadType) { switch(downloadType) {
case "progressive": case "progressive":
@ -19,10 +19,13 @@ export default async function(obj) {
let pref = parseInt(quality[obj.quality], 10) let pref = parseInt(quality[obj.quality], 10)
for (let i in all) { for (let i in all) {
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)
if (currQuality === pref) {
best = all[i];
break
}
if (currQuality < pref) { if (currQuality < pref) {
break; best = all[i-1];
} else if (currQuality == pref) { break
best = all[i]
} }
} }
} }
@ -31,14 +34,18 @@ export default async function(obj) {
} }
return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` }; return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
case "dash": case "dash":
if (api.video.duration > maxVideoDuration / 1000) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false}); let masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false});
if (!masterJSON) return { error: 'ErrorCouldntFetch' }; if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (masterJSON.video) { if (!masterJSON.video) {
let type = ""; return { error: 'ErrorEmptyDownload' }
if (masterJSON.base_url.includes("parcel")) { }
type = "parcel" let type = "parcel";
} else if (masterJSON.base_url == "../") { if (masterJSON.base_url == "../") {
type = "chop" type = "chop"
} }
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)); let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width));
@ -68,9 +75,6 @@ export default async function(obj) {
default: default:
return { error: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }
} }
} else {
return { error: 'ErrorEmptyDownload' }
}
default: default:
return { error: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }
} }

View file

@ -9,10 +9,16 @@ export default async function(obj) {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then((r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes(`{"lang":`)) { if (!html.includes(`{"lang":`)) {
return { error: 'ErrorEmptyDownload' };
}
let js = 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) { return { error: 'ErrorLiveVideo' };
}
if (js["mvData"]["duration"] > maxVideoDuration / 1000) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
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 }));
let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]; let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"];
@ -35,23 +41,13 @@ export default async function(obj) {
let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1) let maxQuality = js["player"]["params"][0][selectedQuality].split('type=')[1].slice(0, 1)
let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]); let userQuality = selectQuality('vk', obj.quality, Object.entries(services.vk.quality_match).reduce((r, [k, v]) => { r[v] = k; return r; })[maxQuality]);
let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"]; let userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"];
if (selectedQuality in js["player"]["params"][0]) { if (!selectedQuality in js["player"]["params"][0]) {
return { error: 'ErrorEmptyDownload' };
}
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`
}; };
} else {
return { error: 'ErrorEmptyDownload' };
}
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: 'ErrorEmptyDownload' };
}
} catch (err) { } catch (err) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -5,9 +5,13 @@ import selectQuality from "../stream/selectQuality.js";
export default async function(obj) { export default async function(obj) {
try { try {
let infoInitial = await ytdl.getInfo(obj.id); let infoInitial = await ytdl.getInfo(obj.id);
if (infoInitial) { if (!infoInitial) {
return { error: 'ErrorCantConnectToServiceAPI' };
}
let info = infoInitial.formats; let info = infoInitial.formats;
if (!info[0]["isLive"]) { if (info[0]["isLive"]) {
return { error: 'ErrorLiveVideo' };
}
let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => { let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true; if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true;
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
@ -40,23 +44,23 @@ export default async function(obj) {
title: infoInitial.videoDetails.title, title: infoInitial.videoDetails.title,
artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(), artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(),
} }
if (audio[0]["approxDurationMs"] <= maxVideoDuration) { if (audio[0]["approxDurationMs"] > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
if (!obj.isAudioOnly && videoMatch.length > 0) { if (!obj.isAudioOnly && videoMatch.length > 0) {
if (video.length > 0 && audio.length > 0) { if (video.length === 0 && audio.length === 0) {
return { error: 'ErrorBadFetch' };
}
if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) { if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) {
return { return {
type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"], type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
}; };
} else { }
return { return {
type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"], type: "render", urls: [videoMatch[0]["url"], audio[0]["url"]], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}` filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
}; };
}
} else {
return { error: 'ErrorBadFetch' };
}
} else if (!obj.isAudioOnly) { } else if (!obj.isAudioOnly) {
return { return {
type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"], type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"],
@ -83,15 +87,6 @@ export default async function(obj) {
} else { } else {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: 'ErrorCantConnectToServiceAPI' };
}
} catch (e) { } catch (e) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -33,22 +33,15 @@ console.log(
) )
rl.question(q, r1 => { rl.question(q, r1 => {
if (r1) {
ob['selfURL'] = `https://${r1}/`
} else {
ob['selfURL'] = `http://localhost`
}
console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)"))
rl.question(q, r2 => {
if (!r1 && !r2) {
ob['selfURL'] = `http://localhost:9000/` ob['selfURL'] = `http://localhost:9000/`
ob['port'] = 9000 ob['port'] = 9000
} else if (!r1 && r2) { if (r1) ob['selfURL'] = `https://${r1}/`
ob['selfURL'] = `http://localhost:${r2}/`
ob['port'] = r2 console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)"))
} else {
ob['port'] = r2 rl.question(q, r2 => {
} if (r2) ob['port'] = r2
if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/`
final() final()
}); });
}) })

View file

@ -43,16 +43,14 @@ export function createStream(obj) {
export function verifyStream(ip, id, hmac, exp) { export function verifyStream(ip, id, hmac, exp) {
try { try {
let streamInfo = streamCache.get(id); let streamInfo = streamCache.get(id);
if (streamInfo) { if (!streamInfo) {
return { error: 'this stream token does not exist', status: 400 };
}
let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt); let ghmac = sha256(`${id},${streamInfo.service},${ip},${exp}`, salt);
if (hmac == ghmac && exp.toString() == streamInfo.exp && ghmac == streamInfo.hmac && ip == streamInfo.ip && exp > Math.floor(new Date().getTime())) { if (hmac == ghmac && exp.toString() == streamInfo.exp && ghmac == streamInfo.hmac && ip == streamInfo.ip && exp > Math.floor(new Date().getTime())) {
return streamInfo; return streamInfo;
} else { }
return { error: 'Unauthorized', status: 401 }; return { error: 'Unauthorized', status: 401 };
}
} else {
return { error: 'this stream token does not exist', status: 400 };
}
} catch (e) { } catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } }; return { status: 500, body: { status: "error", text: "Internal Server Error" } };
} }

View file

@ -1,5 +1,6 @@
import { services, quality as mq } from "../config.js"; import { services, quality as mq } from "../config.js";
// TO-DO: remake entirety of this module to be more of how quality picking is done in vimeo module
function closest(goal, array) { function closest(goal, array) {
return array.sort().reduce(function (prev, curr) { return array.sort().reduce(function (prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
@ -15,9 +16,7 @@ export default function(service, quality, maxQuality) {
if (quality >= maxQuality || quality == maxQuality) return maxQuality; if (quality >= maxQuality || quality == maxQuality) return maxQuality;
if (quality < maxQuality) { if (quality < maxQuality) {
if (services[service]["quality"][quality]) { if (!services[service]["quality"][quality]) {
return quality
} else {
let s = Object.keys(services[service]["quality_match"]).filter((q) => { let s = Object.keys(services[service]["quality_match"]).filter((q) => {
if (q <= quality) { if (q <= quality) {
return true return true
@ -25,5 +24,6 @@ export default function(service, quality, maxQuality) {
}) })
return closest(quality, s) return closest(quality, s)
} }
return quality
} }
} }

View file

@ -5,10 +5,14 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } fro
export default function(res, ip, id, hmac, exp) { export default function(res, ip, id, hmac, exp) {
try { try {
let streamInfo = verifyStream(ip, id, hmac, exp); let streamInfo = verifyStream(ip, id, hmac, exp);
if (!streamInfo.error) { if (streamInfo.error) {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
return;
}
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") { if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
streamAudioOnly(streamInfo, res); streamAudioOnly(streamInfo, res);
} else { return;
}
switch (streamInfo.type) { switch (streamInfo.type) {
case "render": case "render":
streamLiveRender(streamInfo, res); streamLiveRender(streamInfo, res);
@ -20,10 +24,6 @@ export default function(res, ip, id, hmac, exp) {
streamDefault(streamInfo, res); streamDefault(streamInfo, res);
break; break;
} }
}
} else {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
}
} catch (e) { } catch (e) {
res.status(500).json({ status: "error", text: "Internal Server Error" }); res.status(500).json({ status: "error", text: "Internal Server Error" });
} }

View file

@ -27,7 +27,10 @@ export function streamDefault(streamInfo, res) {
} }
export function streamLiveRender(streamInfo, res) { export function streamLiveRender(streamInfo, res) {
try { try {
if (streamInfo.urls.length === 2) { if (!streamInfo.urls.length === 2) {
res.end();
return;
}
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8', '-loglevel', '-8',
'-i', streamInfo.urls[0], '-i', streamInfo.urls[0],
@ -48,6 +51,7 @@ export function streamLiveRender(streamInfo, res) {
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[3].pipe(res); ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill()); ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill()); ffmpegProcess.on('exit', () => ffmpegProcess.kill());
@ -57,9 +61,7 @@ export function streamLiveRender(streamInfo, res) {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
}); });
} else {
res.end();
}
} catch (e) { } catch (e) {
res.end(); res.end();
} }
@ -93,6 +95,7 @@ export function streamAudioOnly(streamInfo, res) {
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
ffmpegProcess.stdio[3].pipe(res); ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill()); ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill()); ffmpegProcess.on('exit', () => ffmpegProcess.kill());
@ -125,6 +128,7 @@ export function streamVideoOnly(streamInfo, res) {
res.setHeader('Connection', 'keep-alive'); res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`);
ffmpegProcess.stdio[3].pipe(res); ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill()); ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill()); ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill()); ffmpegProcess.on('exit', () => ffmpegProcess.kill());

View file

@ -103,7 +103,9 @@ export function checkJSONPost(obj) {
} }
try { try {
let objKeys = Object.keys(obj); let objKeys = Object.keys(obj);
if (objKeys.length < 8 && obj.url) { if (!(objKeys.length < 8 && obj.url)) {
return false
}
let defKeys = Object.keys(def); let defKeys = Object.keys(def);
for (let i in objKeys) { for (let i in objKeys) {
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) { if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
@ -119,9 +121,6 @@ export function checkJSONPost(obj) {
host = hostname[hostname.length - 2] host = hostname[hostname.length - 2]
def["url"] = encodeURIComponent(cleanURL(obj["url"], host)) def["url"] = encodeURIComponent(cleanURL(obj["url"], host))
return def return def
} else {
return false
}
} catch (e) { } catch (e) {
return false; return false;
} }