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:
parent
c7a9723847
commit
3432c91482
21 changed files with 479 additions and 453 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cobalt",
|
||||
"description": "save what you love",
|
||||
"version": "4.8",
|
||||
"version": "4.9-dev",
|
||||
"author": "wukko",
|
||||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
|
|
|
@ -6,7 +6,7 @@ import * as fs from "fs";
|
|||
import rateLimit from "express-rate-limit";
|
||||
|
||||
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 renderPage from "./modules/pageRender/page.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();
|
||||
});
|
||||
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({
|
||||
verify: (req, res, buf) => {
|
||||
try {
|
||||
|
@ -150,20 +157,12 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
|
|||
res.redirect('/api/json')
|
||||
});
|
||||
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({
|
||||
"hash": commitHash,
|
||||
"type": "default",
|
||||
"lang": languageCode(req),
|
||||
"useragent": req.header('user-agent') ? req.header('user-agent') : genericUserAgent
|
||||
}))
|
||||
}
|
||||
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) => {
|
||||
res.redirect('/icons/favicon.ico');
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"streamLifespan": 120000,
|
||||
"maxVideoDuration": 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": {
|
||||
"name": "wukko",
|
||||
"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": {
|
||||
"crypto": {
|
||||
"bitcoin": "bc1q59jyyjvrzj4c22rkk3ljeecq6jmpyscgz9spnd",
|
||||
|
|
|
@ -10,32 +10,43 @@ import match from "./processing/match.js";
|
|||
export async function getJSON(originalURL, lang, obj) {
|
||||
try {
|
||||
let url = decodeURIComponent(originalURL);
|
||||
if (!url.includes('http://')) {
|
||||
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
|
||||
host = hostname[hostname.length - 2],
|
||||
patternMatch;
|
||||
if (host === "youtu") {
|
||||
if (url.startsWith('http://')) {
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
|
||||
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";
|
||||
url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`;
|
||||
}
|
||||
if (host === "goo" && url.substring(0, 30) === "https://soundcloud.app.goo.gl/") {
|
||||
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;
|
||||
break;
|
||||
case "goo":
|
||||
if (url.substring(0, 30) === "https://soundcloud.app.goo.gl/"){
|
||||
host = "soundcloud"
|
||||
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
|
||||
}
|
||||
if (patternMatch) {
|
||||
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) });
|
||||
break;
|
||||
case "tumblr":
|
||||
if (!url.includes("blog/view")) {
|
||||
if (url.slice(-1) == '/') url = url.slice(0, -1);
|
||||
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) {
|
||||
return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') });
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ export async function buildFront() {
|
|||
try {
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/front/cobalt.js', 'src/front/cobalt.css'],
|
||||
outdir: `min/`,
|
||||
outdir: 'min/',
|
||||
minify: true,
|
||||
loader: { ".js": "js", ".css": "css" }
|
||||
loader: { '.js': 'js', '.css': 'css' }
|
||||
})
|
||||
} catch (e) {
|
||||
return;
|
||||
|
|
|
@ -15,7 +15,6 @@ export const
|
|||
repo = packageJson["bugs"]["url"].replace('/issues', ''),
|
||||
authorInfo = config.authorInfo,
|
||||
quality = config.quality,
|
||||
internetExplorerRedirect = config.internetExplorerRedirect,
|
||||
donations = config.donations,
|
||||
ffmpegArgs = config.ffmpegArgs,
|
||||
supportedAudio = config.supportedAudio,
|
||||
|
|
|
@ -1,28 +1,37 @@
|
|||
import { genericUserAgent, maxVideoDuration } from "../config.js";
|
||||
|
||||
// TO-DO: quality picking
|
||||
export default async function(obj) {
|
||||
try {
|
||||
let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then((r) => {return r.text()}).catch(() => {return false});
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
}).then((r) => { return r.text() }).catch(() => { return false });
|
||||
if (!html) {
|
||||
return { error: 'ErrorCouldntFetch' };
|
||||
}
|
||||
|
||||
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) {
|
||||
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
|
||||
if (streamData.data.timelength <= maxVideoDuration) {
|
||||
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 {
|
||||
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]);
|
||||
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) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
}
|
||||
|
|
|
@ -1,26 +1,39 @@
|
|||
import { maxVideoDuration } from "../config.js";
|
||||
|
||||
// TO-DO: add support for gifs (#80)
|
||||
export default async function(obj) {
|
||||
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});
|
||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||
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' };
|
||||
}
|
||||
data = data[0]["data"]["children"][0]["data"];
|
||||
|
||||
if ("reddit_video" in data["secure_media"] && data["secure_media"]["reddit_video"]["duration"] * 1000 < maxVideoDuration) {
|
||||
let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
|
||||
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
|
||||
|
||||
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 {
|
||||
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],
|
||||
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) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
}
|
||||
|
|
|
@ -4,32 +4,31 @@ let cachedID = {}
|
|||
|
||||
async function findClientID() {
|
||||
try {
|
||||
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 sc = await fetch('https://soundcloud.com/').then((r) => { return r.text() }).catch(() => { return false });
|
||||
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
|
||||
} 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) {
|
||||
return false;
|
||||
}
|
||||
|
@ -41,38 +40,50 @@ export default async function(obj) {
|
|||
if (!obj.author && !obj.song && obj.shortLink) {
|
||||
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
|
||||
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) {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`, {
|
||||
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) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
}
|
||||
|
|
|
@ -12,18 +12,18 @@ let config = {
|
|||
}
|
||||
}
|
||||
function selector(j, h, id) {
|
||||
if (j) {
|
||||
let t;
|
||||
switch (h) {
|
||||
case "tiktok":
|
||||
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
|
||||
break;
|
||||
case "douyin":
|
||||
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true })
|
||||
break;
|
||||
}
|
||||
if (t.length > 0) { return t[0] } else return false
|
||||
} else return false
|
||||
if (!j) return false
|
||||
let t;
|
||||
switch (h) {
|
||||
case "tiktok":
|
||||
t = j["aweme_list"].filter((v) => { if (v["aweme_id"] == id) return true })
|
||||
break;
|
||||
case "douyin":
|
||||
t = j['item_list'].filter((v) => { if (v["aweme_id"] == id) return true })
|
||||
break;
|
||||
}
|
||||
if (!t.length > 0) return false
|
||||
return t[0]
|
||||
}
|
||||
|
||||
export default async function(obj) {
|
||||
|
@ -32,7 +32,7 @@ export default async function(obj) {
|
|||
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
|
||||
redirect: "manual",
|
||||
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.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("/", '')
|
||||
}
|
||||
}
|
||||
if (!obj.postId) return { error: 'ErrorCantGetID' };
|
||||
if (!obj.postId) {
|
||||
return { error: 'ErrorCantGetID' };
|
||||
}
|
||||
|
||||
let detail;
|
||||
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
|
||||
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
|
||||
}).then((r) => {return r.json()}).catch(() => {return false});
|
||||
}).then((r) => { return r.json() }).catch(() => { return false });
|
||||
|
||||
detail = selector(detail, obj.host, obj.postId);
|
||||
|
||||
|
@ -60,20 +62,19 @@ export default async function(obj) {
|
|||
images = detail["images"] ? detail["images"] : false
|
||||
}
|
||||
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]
|
||||
videoFilename = `${filenameBase}_video.mp4`
|
||||
video = obj.host === "tiktok" ? detail["video"]["download_addr"]["url_list"][0] : detail['video']['play_addr']['url_list'][0]
|
||||
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 {
|
||||
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")) {
|
||||
audio = detail["music"]["play_url"]["url_list"][0]
|
||||
audioFilename = `${filenameBase}_audio`
|
||||
} else {
|
||||
audio = fallback
|
||||
audioFilename = `${filenameBase}_audio_fv` // fv - from video
|
||||
}
|
||||
if (audio.slice(-4) === ".mp3") isMp3 = true;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ export default async function(obj) {
|
|||
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
|
||||
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
|
||||
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('property="og:video" content="https://va.media.tumblr.com/')) {
|
||||
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }
|
||||
} else return { error: 'ErrorEmptyDownload' }
|
||||
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` }
|
||||
} catch (e) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
if (!req_status) return { error: 'ErrorCouldntFetch' }
|
||||
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
|
||||
let single, multiple = [], media = req_status["extended_entities"]["media"];
|
||||
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
|
||||
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"])}) }
|
||||
} else if (media.length > 0) {
|
||||
single = bestQuality(media[0]["video_info"]["variants"])
|
||||
} else {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
if (single) {
|
||||
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
if (!req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
let single, multiple = [], media = req_status["extended_entities"]["media"];
|
||||
media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
|
||||
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"])}) }
|
||||
} else if (media.length === 1) {
|
||||
single = bestQuality(media[0]["video_info"]["variants"])
|
||||
} else {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
if (single) {
|
||||
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
|
@ -67,34 +66,33 @@ export default async function(obj) {
|
|||
return r.status == 200 ? r.json() : false;
|
||||
}).catch((e) => {return false});
|
||||
|
||||
if (AudioSpaceById) {
|
||||
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
|
||||
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then((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 {
|
||||
if (!AudioSpaceById) {
|
||||
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) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { quality, services } from "../config.js";
|
||||
import { maxVideoDuration, quality, services } from "../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
try {
|
||||
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' };
|
||||
|
||||
let downloadType = "";
|
||||
let downloadType = "dash";
|
||||
if (JSON.stringify(api).includes('"progressive":[{')) {
|
||||
downloadType = "progressive";
|
||||
} else if (JSON.stringify(api).includes('"files":{"dash":{"')) downloadType = "dash";
|
||||
}
|
||||
|
||||
switch(downloadType) {
|
||||
case "progressive":
|
||||
|
@ -19,10 +19,13 @@ export default async function(obj) {
|
|||
let pref = parseInt(quality[obj.quality], 10)
|
||||
for (let i in all) {
|
||||
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)
|
||||
if (currQuality === pref) {
|
||||
best = all[i];
|
||||
break
|
||||
}
|
||||
if (currQuality < pref) {
|
||||
break;
|
||||
} else if (currQuality == pref) {
|
||||
best = all[i]
|
||||
best = all[i-1];
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,45 +34,46 @@ export default async function(obj) {
|
|||
}
|
||||
return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
|
||||
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 masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false});
|
||||
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
|
||||
if (masterJSON.video) {
|
||||
let type = "";
|
||||
if (masterJSON.base_url.includes("parcel")) {
|
||||
type = "parcel"
|
||||
} else if (masterJSON.base_url == "../") {
|
||||
type = "chop"
|
||||
}
|
||||
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width));
|
||||
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;});
|
||||
|
||||
let bestVideo = masterJSON_Video[0]
|
||||
let bestAudio = masterJSON_Audio[0]
|
||||
switch (type) {
|
||||
case "parcel":
|
||||
if (obj.quality != "max") {
|
||||
let pref = parseInt(quality[obj.quality], 10)
|
||||
for (let i in masterJSON_Video) {
|
||||
let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10)
|
||||
if (currQuality < pref) {
|
||||
break;
|
||||
} else if (currQuality == pref) {
|
||||
bestVideo = masterJSON_Video[i]
|
||||
}
|
||||
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
|
||||
if (!masterJSON.video) {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
let type = "parcel";
|
||||
if (masterJSON.base_url == "../") {
|
||||
type = "chop"
|
||||
}
|
||||
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width));
|
||||
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter((a)=> {if (a['mime_type'] === "audio/mp4") return true;});
|
||||
|
||||
let bestVideo = masterJSON_Video[0]
|
||||
let bestAudio = masterJSON_Audio[0]
|
||||
switch (type) {
|
||||
case "parcel":
|
||||
if (obj.quality != "max") {
|
||||
let pref = parseInt(quality[obj.quality], 10)
|
||||
for (let i in masterJSON_Video) {
|
||||
let currQuality = parseInt(services.vimeo.resolutionMatch[masterJSON_Video[i]["width"]], 10)
|
||||
if (currQuality < pref) {
|
||||
break;
|
||||
} else if (currQuality == pref) {
|
||||
bestVideo = masterJSON_Video[i]
|
||||
}
|
||||
}
|
||||
let baseUrl = masterJSONURL.split("/sep/")[0]
|
||||
let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`;
|
||||
let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
|
||||
}
|
||||
let baseUrl = masterJSONURL.split("/sep/")[0]
|
||||
let videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`;
|
||||
let audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
|
||||
|
||||
return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` }
|
||||
case "chop": // TO-DO: support chop type of streams
|
||||
default:
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
} else {
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
return { urls: [videoUrl, audioUrl], audioFilename: `vimeo_${obj.id}_audio`, filename: `vimeo_${obj.id}_${bestVideo["width"]}x${bestVideo["height"]}.mp4` }
|
||||
case "chop": // TO-DO: support chop type of streams
|
||||
default:
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
}
|
||||
default:
|
||||
return { error: 'ErrorEmptyDownload' }
|
||||
|
|
|
@ -9,49 +9,45 @@ export default async function(obj) {
|
|||
headers: {"user-agent": genericUserAgent}
|
||||
}).then((r) => {return r.text()}).catch(() => {return false});
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
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 {
|
||||
if (!html.includes(`{"lang":`)) {
|
||||
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) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
}
|
||||
|
|
|
@ -5,93 +5,88 @@ import selectQuality from "../stream/selectQuality.js";
|
|||
export default async function(obj) {
|
||||
try {
|
||||
let infoInitial = await ytdl.getInfo(obj.id);
|
||||
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 {
|
||||
if (!infoInitial) {
|
||||
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) {
|
||||
return { error: 'ErrorBadFetch' };
|
||||
}
|
||||
|
|
|
@ -33,22 +33,15 @@ console.log(
|
|||
)
|
||||
|
||||
rl.question(q, r1 => {
|
||||
if (r1) {
|
||||
ob['selfURL'] = `https://${r1}/`
|
||||
} else {
|
||||
ob['selfURL'] = `http://localhost`
|
||||
}
|
||||
ob['selfURL'] = `http://localhost:9000/`
|
||||
ob['port'] = 9000
|
||||
if (r1) ob['selfURL'] = `https://${r1}/`
|
||||
|
||||
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['port'] = 9000
|
||||
} else if (!r1 && r2) {
|
||||
ob['selfURL'] = `http://localhost:${r2}/`
|
||||
ob['port'] = r2
|
||||
} else {
|
||||
ob['port'] = r2
|
||||
}
|
||||
if (r2) ob['port'] = r2
|
||||
if (!r1 && r2) ob['selfURL'] = `http://localhost:${r2}/`
|
||||
final()
|
||||
});
|
||||
})
|
||||
|
|
|
@ -43,16 +43,14 @@ export function createStream(obj) {
|
|||
export function verifyStream(ip, id, hmac, exp) {
|
||||
try {
|
||||
let streamInfo = streamCache.get(id);
|
||||
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 {
|
||||
if (!streamInfo) {
|
||||
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) {
|
||||
return { status: 500, body: { status: "error", text: "Internal Server Error" } };
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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) {
|
||||
return array.sort().reduce(function (prev, curr) {
|
||||
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) {
|
||||
if (services[service]["quality"][quality]) {
|
||||
return quality
|
||||
} else {
|
||||
if (!services[service]["quality"][quality]) {
|
||||
let s = Object.keys(services[service]["quality_match"]).filter((q) => {
|
||||
if (q <= quality) {
|
||||
return true
|
||||
|
@ -25,5 +24,6 @@ export default function(service, quality, maxQuality) {
|
|||
})
|
||||
return closest(quality, s)
|
||||
}
|
||||
return quality
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,24 +5,24 @@ import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } fro
|
|||
export default function(res, ip, id, hmac, exp) {
|
||||
try {
|
||||
let streamInfo = verifyStream(ip, id, hmac, exp);
|
||||
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 {
|
||||
if (streamInfo.error) {
|
||||
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) {
|
||||
res.status(500).json({ status: "error", text: "Internal Server Error" });
|
||||
|
|
|
@ -27,39 +27,41 @@ export function streamDefault(streamInfo, res) {
|
|||
}
|
||||
export function streamLiveRender(streamInfo, res) {
|
||||
try {
|
||||
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 {
|
||||
if (!streamInfo.urls.length === 2) {
|
||||
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) {
|
||||
res.end();
|
||||
}
|
||||
|
@ -93,6 +95,7 @@ export function streamAudioOnly(streamInfo, res) {
|
|||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
|
||||
ffmpegProcess.stdio[3].pipe(res);
|
||||
|
||||
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('close', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
|
||||
|
@ -125,6 +128,7 @@ export function streamVideoOnly(streamInfo, res) {
|
|||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`);
|
||||
ffmpegProcess.stdio[3].pipe(res);
|
||||
|
||||
ffmpegProcess.on('disconnect', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('close', () => ffmpegProcess.kill());
|
||||
ffmpegProcess.on('exit', () => ffmpegProcess.kill());
|
||||
|
|
|
@ -103,25 +103,24 @@ export function checkJSONPost(obj) {
|
|||
}
|
||||
try {
|
||||
let objKeys = Object.keys(obj);
|
||||
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 {
|
||||
if (!(objKeys.length < 8 && obj.url)) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue