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")) { res.send(renderPage({
if (internetExplorerRedirect.newNT.includes(req.header("user-agent").split('NT ')[1].split(';')[0])) { "hash": commitHash,
res.redirect(internetExplorerRedirect.new) "type": "default",
} else { "lang": languageCode(req),
res.redirect(internetExplorerRedirect.old) "useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent
} }))
} else {
res.send(renderPage({
"hash": commitHash,
"type": "default",
"lang": languageCode(req),
"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://')) {
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), return apiJSON(0, { t: errorUnsupported(lang) });
host = hostname[hostname.length - 2], }
patternMatch; let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
if (host === "youtu") { host = hostname[hostname.length - 2],
patternMatch;
// 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":
host = "soundcloud" if (url.substring(0, 30) === "https://soundcloud.app.goo.gl/"){
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}` host = "soundcloud"
} url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
if (host === "tumblr" && !url.includes("blog/view")) {
if (url.slice(-1) == '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], '');
}
if (host && host.length < 20 && host in patterns && patterns[host]["enabled"]) {
for (let i in patterns[host]["patterns"]) {
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]);
if (patternMatch) break;
} }
if (patternMatch) { break;
return await match(host, patternMatch, url, lang, obj); case "tumblr":
} else return apiJSON(0, { t: errorUnsupported(lang) }); if (!url.includes("blog/view")) {
} else return apiJSON(0, { t: errorUnsupported(lang) }); if (url.slice(-1) == '/') url = url.slice(0, -1);
} else return apiJSON(0, { t: errorUnsupported(lang) }); url = url.replace(url.split('/')[5], '');
}
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"]) {
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]);
if (patternMatch) break;
}
if (!patternMatch) {
return apiJSON(0, { t: errorUnsupported(lang) });
}
return await match(host, patternMatch, url, lang, obj);
} 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"'))) {
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength <= maxVideoDuration) {
let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).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: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorEmptyDownload' }; return { error: 'ErrorEmptyDownload' };
} }
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;
}).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`
};
} 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"]) {
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`;
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 };
}
} else {
return { error: 'ErrorEmptyDownload' }; 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],
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 = ''});
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3];
if (!audio.length > 0) {
return { typeId: 1, urls: video };
}
return {
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

@ -4,32 +4,31 @@ 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 clientid;
for (let script of scripts) {
let url = script[1];
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
let scrf = await fetch(url).then((r) => {return r.text()}).catch(() => {return false});
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
if (id && typeof id[0] === 'string') {
clientid = id[0].match(/[A-Za-z0-9]{32}/)[0];
break;
}
}
cachedID.version = sc_version;
cachedID.id = clientid;
return clientid;
} }
let scripts = sc.matchAll(/<script.+src="(.+)">/g);
let clientid;
for (let script of scripts) {
let url = script[1];
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
let scrf = await fetch(url).then((r) => {return r.text()}).catch(() => { return false });
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
if (id && typeof id[0] === 'string') {
clientid = id[0].match(/[A-Za-z0-9]{32}/)[0];
break;
}
}
cachedID.version = scVersion;
cachedID.id = clientid;
return clientid;
} catch (e) { } catch (e) {
return false; return false;
} }
@ -41,38 +40,50 @@ export default async function(obj) {
if (!obj.author && !obj.song && obj.shortLink) { if (!obj.author && !obj.song && obj.shortLink) {
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
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 (obj.author && obj.song) { if (obj.author && obj.song) {
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`, { html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`, {
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.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])
if (!json["media"]["transcodings"]) {
return { error: 'ErrorEmptyDownload' }
}
let clientId = await findClientID();
if (!clientId) {
return { error: 'ErrorSoundCloudNoClientId' }
}
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive")
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:") {
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 });
if (!file) {
return { error: 'ErrorCouldntFetch' };
}
return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
fileMetadata: {
title: json.title,
artist: json.user.username,
}
} }
if (!html) return { error: 'ErrorCouldntFetch'};
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) {
let clientId = await findClientID();
if (clientId) {
let fileUrlBase = json.media.transcodings[0]["url"].replace("/hls", "/progressive")
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 (json.duration < maxAudioDuration) {
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}`,
fileMetadata: {
title: json.title,
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,18 +12,18 @@ 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":
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true }) t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
break; break;
case "douyin": case "douyin":
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) {
@ -32,7 +32,7 @@ export default async function(obj) {
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
redirect: "manual", redirect: "manual",
headers: { "user-agent": userAgent } headers: { "user-agent": userAgent }
}).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.slice(0, 17) === '<a href="https://' && html.includes('/video/')) { if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
@ -41,12 +41,14 @@ 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), {
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"} headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
}).then((r) => {return r.json()}).catch(() => {return false}); }).then((r) => { return r.json() }).catch(() => { return false });
detail = selector(detail, obj.host, obj.postId); detail = selector(detail, obj.host, 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"); video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0]
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark videoFilename = `${filenameBase}_video.mp4`
if (!obj.noWatermark) { 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"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play");
videoFilename = `${filenameBase}_video.mp4` 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

@ -5,11 +5,12 @@ export default async function(obj) {
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', ''); let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, { let html = await fetch(`https://${user}.tumblr.com/post/${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('property="og:video" content="https://va.media.tumblr.com/')) { 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` } return { error: 'ErrorEmptyDownload' }
} else 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` }
} catch (e) { } catch (e) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -36,23 +36,22 @@ 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"]) {
let single, multiple = [], media = req_status["extended_entities"]["media"]; return { error: 'ErrorNoVideosInTweet' }
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true }) }
if (media.length > 1) { let single, multiple = [], media = req_status["extended_entities"]["media"];
for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
} else if (media.length > 0) { if (media.length > 1) {
single = bestQuality(media[0]["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 { } else if (media.length === 1) {
return { error: 'ErrorNoVideosInTweet' } single = bestQuality(media[0]["video_info"]["variants"])
} } else {
if (single) { return { error: 'ErrorNoVideosInTweet' }
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` } }
} else if (multiple) { if (single) {
return { picker: multiple } return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
} else { } else if (multiple) {
return { error: 'ErrorNoVideosInTweet' } return { picker: multiple }
}
} else { } else {
return { error: 'ErrorNoVideosInTweet' } return { error: 'ErrorNoVideosInTweet' }
} }
@ -67,34 +66,33 @@ 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) {
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' };
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: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }
} }
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' };
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", "")
}
}
} }
} 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,45 +34,46 @@ 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));
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 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 bestVideo = masterJSON_Video[0]
let bestAudio = masterJSON_Audio[0] let bestAudio = masterJSON_Audio[0]
switch (type) { switch (type) {
case "parcel": case "parcel":
if (obj.quality != "max") { if (obj.quality != "max") {
let pref = parseInt(quality[obj.quality], 10) let pref = parseInt(quality[obj.quality], 10)
for (let i in masterJSON_Video) { for (let i in masterJSON_Video) {
let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10) let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10)
if (currQuality < pref) { if (currQuality < pref) {
break; break;
} else if (currQuality == pref) { } else if (currQuality == pref) {
bestVideo = masterJSON_Video[i] bestVideo = masterJSON_Video[i]
}
} }
} }
let baseUrl = masterJSONURL.split("/sep/")[0] }
let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`; let baseUrl = masterJSONURL.split("/sep/")[0]
let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[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` } 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 case "chop": // TO-DO: support chop type of streams
default: default:
return { error: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }
}
} else {
return { error: 'ErrorEmptyDownload' }
} }
default: default:
return { error: 'ErrorEmptyDownload' } return { error: 'ErrorEmptyDownload' }

View file

@ -9,49 +9,45 @@ 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":`)) {
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 }));
let repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"];
if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) {
repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"];
}
let attr = repr[repr.length - 1]["_attributes"];
let selectedQuality;
let qualities = Object.keys(services.vk.quality_match);
for (let i in qualities) {
if (qualities[i] == attr["height"]) {
selectedQuality = `url${attr["height"]}`;
break;
}
if (qualities[i] == attr["width"]) {
selectedQuality = `url${attr["width"]}`;
break;
}
}
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 userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"];
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`
};
} else {
return { error: 'ErrorEmptyDownload' };
}
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: 'ErrorEmptyDownload' }; return { error: 'ErrorEmptyDownload' };
} }
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
if (!js["mvData"]["is_active_live"] == '0') {
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 repr = mpd["MPD"]["Period"]["AdaptationSet"]["Representation"];
if (!mpd["MPD"]["Period"]["AdaptationSet"]["Representation"]) {
repr = mpd["MPD"]["Period"]["AdaptationSet"][0]["Representation"];
}
let attr = repr[repr.length - 1]["_attributes"];
let selectedQuality;
let qualities = Object.keys(services.vk.quality_match);
for (let i in qualities) {
if (qualities[i] == attr["height"]) {
selectedQuality = `url${attr["height"]}`;
break;
}
if (qualities[i] == attr["width"]) {
selectedQuality = `url${attr["width"]}`;
break;
}
}
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 userRepr = repr[services.vk.representation_match[userQuality]]["_attributes"];
if (!selectedQuality in js["player"]["params"][0]) {
return { error: 'ErrorEmptyDownload' };
}
return {
urls: js["player"]["params"][0][`url${userQuality}`],
filename: `vk_${obj.userId}_${obj.videoId}_${userRepr["width"]}x${userRepr['height']}.mp4`
};
} catch (err) { } catch (err) {
return { error: 'ErrorBadFetch' }; return { error: 'ErrorBadFetch' };
} }

View file

@ -5,93 +5,88 @@ 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) {
let info = infoInitial.formats;
if (!info[0]["isLive"]) {
let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true;
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (!obj.isAudioOnly) {
video = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) {
if (obj.quality != "max") {
if (a["hasAudio"] && mq[obj.quality] == a["height"]) {
fullVideoMatch.push(a)
} else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) {
videoMatch.push(a);
}
}
return true
}
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (obj.quality != "max") {
if (videoMatch.length == 0) {
let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim())
videoMatch = video.filter((a) => {
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true;
})
} else if (fullVideoMatch.length > 0) {
videoMatch = [fullVideoMatch[0]]
}
} else videoMatch = [video[0]];
if (obj.quality == "los") videoMatch = [video[video.length - 1]];
}
let generalMeta = {
title: infoInitial.videoDetails.title,
artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(),
}
if (audio[0]["approxDurationMs"] <= maxVideoDuration) {
if (!obj.isAudioOnly && videoMatch.length > 0) {
if (video.length > 0 && audio.length > 0) {
if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) {
return {
type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
};
} else {
return {
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}`
};
}
} else {
return { error: 'ErrorBadFetch' };
}
} else if (!obj.isAudioOnly) {
return {
type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}`
};
} else if (audio.length > 0) {
let r = {
type: "render",
isAudioOnly: true,
urls: audio[0]["url"],
audioFilename: `youtube_${obj.id}_audio`,
fileMetadata: generalMeta
};
if (infoInitial.videoDetails.description) {
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
if (isAutoGenAudio) {
let descItems = infoInitial.videoDetails.description.split("\n\n")
r.fileMetadata.album = descItems[2]
r.fileMetadata.copyright = descItems[3]
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
}
}
return r
} else {
return { error: 'ErrorBadFetch' };
}
} else {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
} else {
return { error: 'ErrorLiveVideo' };
}
} else {
return { error: 'ErrorCantConnectToServiceAPI' }; return { error: 'ErrorCantConnectToServiceAPI' };
} }
let info = infoInitial.formats;
if (info[0]["isLive"]) {
return { error: 'ErrorLiveVideo' };
}
let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true;
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (!obj.isAudioOnly) {
video = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format) {
if (obj.quality != "max") {
if (a["hasAudio"] && mq[obj.quality] == a["height"]) {
fullVideoMatch.push(a)
} else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) {
videoMatch.push(a);
}
}
return true
}
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (obj.quality != "max") {
if (videoMatch.length == 0) {
let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim())
videoMatch = video.filter((a) => {
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true;
})
} else if (fullVideoMatch.length > 0) {
videoMatch = [fullVideoMatch[0]]
}
} else videoMatch = [video[0]];
if (obj.quality == "los") videoMatch = [video[video.length - 1]];
}
let generalMeta = {
title: infoInitial.videoDetails.title,
artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(),
}
if (audio[0]["approxDurationMs"] > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
if (!obj.isAudioOnly && videoMatch.length > 0) {
if (video.length === 0 && audio.length === 0) {
return { error: 'ErrorBadFetch' };
}
if (videoMatch[0]["hasVideo"] && videoMatch[0]["hasAudio"]) {
return {
type: "bridge", urls: videoMatch[0]["url"], time: videoMatch[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${videoMatch[0]["width"]}x${videoMatch[0]["height"]}.${obj.format}`
};
}
return {
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}`
};
} else if (!obj.isAudioOnly) {
return {
type: "render", urls: [video[0]["url"], audio[0]["url"]], time: video[0]["approxDurationMs"],
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}`
};
} else if (audio.length > 0) {
let r = {
type: "render",
isAudioOnly: true,
urls: audio[0]["url"],
audioFilename: `youtube_${obj.id}_audio`,
fileMetadata: generalMeta
};
if (infoInitial.videoDetails.description) {
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
if (isAutoGenAudio) {
let descItems = infoInitial.videoDetails.description.split("\n\n")
r.fileMetadata.album = descItems[2]
r.fileMetadata.copyright = descItems[3]
if (descItems[4].startsWith("Released on:")) r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim();
}
}
return r
} else {
return { error: 'ErrorBadFetch' };
}
} 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'] = `http://localhost:9000/`
ob['selfURL'] = `https://${r1}/` ob['port'] = 9000
} else { if (r1) ob['selfURL'] = `https://${r1}/`
ob['selfURL'] = `http://localhost`
}
console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)")) console.log(Bright("\nGreat! Now, what's the port it'll be running on? (9000)"))
rl.question(q, r2 => { rl.question(q, r2 => {
if (!r1 && !r2) { if (r2) ob['port'] = r2
ob['selfURL'] = `http://localhost:9000/` if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/`
ob['port'] = 9000
} else if (!r1 && r2) {
ob['selfURL'] = `http://localhost:${r2}/`
ob['port'] = r2
} else {
ob['port'] = 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) {
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())) {
return streamInfo;
} else {
return { error: 'Unauthorized', status: 401 };
}
} else {
return { error: 'this stream token does not exist', status: 400 }; return { error: 'this stream token does not exist', status: 400 };
} }
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())) {
return streamInfo;
}
return { error: 'Unauthorized', status: 401 };
} 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,24 +5,24 @@ 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) {
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
streamAudioOnly(streamInfo, res);
} else {
switch (streamInfo.type) {
case "render":
streamLiveRender(streamInfo, res);
break;
case "mute":
streamVideoOnly(streamInfo, res);
break;
default:
streamDefault(streamInfo, res);
break;
}
}
} else {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
return;
}
if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
streamAudioOnly(streamInfo, res);
return;
}
switch (streamInfo.type) {
case "render":
streamLiveRender(streamInfo, res);
break;
case "mute":
streamVideoOnly(streamInfo, res);
break;
default:
streamDefault(streamInfo, res);
break;
} }
} 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,39 +27,41 @@ 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) {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', streamInfo.urls[0],
'-i', streamInfo.urls[1],
'-map', '0:v',
'-map', '1:a',
];
args = args.concat(ffmpegArgs[format])
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
args.push('-f', format, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
res.on('finish', () => ffmpegProcess.kill());
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
} else {
res.end(); res.end();
return;
} }
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8',
'-i', streamInfo.urls[0],
'-i', streamInfo.urls[1],
'-map', '0:v',
'-map', '1:a',
];
args = args.concat(ffmpegArgs[format])
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
args.push('-f', format, 'pipe:3');
const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[3].pipe(res);
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
ffmpegProcess.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
res.on('finish', () => ffmpegProcess.kill());
res.on('close', () => ffmpegProcess.kill());
ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill();
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,25 +103,24 @@ 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)) {
let defKeys = Object.keys(def);
for (let i in objKeys) {
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else {
if (apiVar.allowed[objKeys[i]] && 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 return false
} }
let defKeys = Object.keys(def);
for (let i in objKeys) {
if (String(objKeys[i]) !== "url" && defKeys.includes(objKeys[i])) {
if (apiVar.booleanOnly.includes(objKeys[i])) {
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
} else {
if (apiVar.allowed[objKeys[i]] && 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
} catch (e) { } catch (e) {
return false; return false;
} }