api: move request functions to separate file
- request status id is no longer a cryptic number - descriptive function names
This commit is contained in:
parent
c10012130b
commit
cc6345ff63
6 changed files with 183 additions and 131 deletions
|
@ -6,14 +6,14 @@ const ipSalt = randomBytes(64).toString('hex');
|
|||
|
||||
import { env, version } from "../modules/config.js";
|
||||
import match from "../modules/processing/match.js";
|
||||
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
|
||||
import { languageCode } from "../modules/sub/utils.js";
|
||||
import { createResponse, verifyRequest, getIP } from "../modules/processing/request.js";
|
||||
import { Bright, Cyan } from "../modules/sub/consoleText.js";
|
||||
import stream from "../modules/stream/stream.js";
|
||||
import loc from "../localization/manager.js";
|
||||
import { generateHmac } from "../modules/sub/crypto.js";
|
||||
import { verifyStream, getInternalStream } from "../modules/stream/manage.js";
|
||||
import { extract } from "../modules/processing/url.js";
|
||||
import { errorUnsupported } from "../modules/sub/errors.js";
|
||||
|
||||
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
||||
const corsConfig = !env.corsWildcard ? {
|
||||
|
@ -100,7 +100,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
const request = req.body;
|
||||
const lang = languageCode(req);
|
||||
const fail = (t) => {
|
||||
const { status, body } = apiJSON(0, { t: loc(lang, t) });
|
||||
const { status, body } = createResponse("error", { t: loc(lang, t) });
|
||||
res.status(status).json(body);
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
|
|||
}
|
||||
|
||||
request.dubLang = request.dubLang ? lang : false;
|
||||
const normalizedRequest = checkJSONPost(request);
|
||||
const normalizedRequest = verifyRequest(request);
|
||||
if (!normalizedRequest) {
|
||||
return fail('ErrorCantProcess');
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import { createResponse } from "../processing/request.js";
|
||||
import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js";
|
||||
|
||||
import loc from "../../localization/manager.js";
|
||||
|
@ -45,8 +45,8 @@ export default async function(host, patternMatch, lang, obj) {
|
|||
try {
|
||||
let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata;
|
||||
|
||||
if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
if (!(testers[host](patternMatch))) return apiJSON(0, { t: brokenLink(lang, host) });
|
||||
if (!testers[host]) return createResponse("error", { t: errorUnsupported(lang) });
|
||||
if (!(testers[host](patternMatch))) return createResponse("error", { t: brokenLink(lang, host) });
|
||||
|
||||
switch (host) {
|
||||
case "twitter":
|
||||
|
@ -177,17 +177,17 @@ export default async function(host, patternMatch, lang, obj) {
|
|||
r = await dailymotion(patternMatch);
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
return createResponse("error", { t: errorUnsupported(lang) });
|
||||
}
|
||||
|
||||
if (r.isAudioOnly) isAudioOnly = true;
|
||||
let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted;
|
||||
|
||||
if (r.error && r.critical)
|
||||
return apiJSON(6, { t: loc(lang, r.error) })
|
||||
return createResponse("critical", { t: loc(lang, r.error) })
|
||||
|
||||
if (r.error)
|
||||
return apiJSON(0, {
|
||||
return createResponse("error", {
|
||||
t: Array.isArray(r.error)
|
||||
? loc(lang, r.error[0], r.error[1])
|
||||
: loc(lang, r.error)
|
||||
|
@ -199,7 +199,7 @@ export default async function(host, patternMatch, lang, obj) {
|
|||
obj.filenamePattern, obj.twitterGif,
|
||||
requestIP
|
||||
)
|
||||
} catch (e) {
|
||||
return apiJSON(0, { t: genericError(lang, host) })
|
||||
} catch {
|
||||
return createResponse("error", { t: genericError(lang, host) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { audioIgnore, services, supportedAudio } from "../config.js";
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import { createResponse } from "../processing/request.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
import createFilename from "./createFilename.js";
|
||||
|
||||
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif, requestIP) {
|
||||
let action,
|
||||
responseType = 2,
|
||||
responseType = "stream",
|
||||
defaultParams = {
|
||||
u: r.urls,
|
||||
service: host,
|
||||
|
@ -36,10 +36,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||
|
||||
switch (action) {
|
||||
default:
|
||||
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
|
||||
return createResponse("error", { t: loc(lang, 'ErrorEmptyDownload') });
|
||||
|
||||
case "photo":
|
||||
responseType = 1;
|
||||
responseType = "redirect";
|
||||
break;
|
||||
|
||||
case "gif":
|
||||
|
@ -56,11 +56,12 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||
u: Array.isArray(r.urls) ? r.urls[0] : r.urls,
|
||||
mute: true
|
||||
}
|
||||
if (host === "reddit" && r.typeId === 1) responseType = 1;
|
||||
if (host === "reddit" && r.typeId === "redirect")
|
||||
responseType = "redirect";
|
||||
break;
|
||||
|
||||
case "picker":
|
||||
responseType = 5;
|
||||
responseType = "picker";
|
||||
switch (host) {
|
||||
case "instagram":
|
||||
case "twitter":
|
||||
|
@ -98,7 +99,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||
if (Array.isArray(r.urls)) {
|
||||
params = { type: "render" }
|
||||
} else {
|
||||
responseType = 1;
|
||||
responseType = "redirect";
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -106,7 +107,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||
if (r.type === "remux") {
|
||||
params = { type: r.type };
|
||||
} else {
|
||||
responseType = 1;
|
||||
responseType = "redirect";
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -121,14 +122,15 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||
case "tumblr":
|
||||
case "pinterest":
|
||||
case "streamable":
|
||||
responseType = 1;
|
||||
responseType = "redirect";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "audio":
|
||||
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) {
|
||||
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') })
|
||||
if (audioIgnore.includes(host)
|
||||
|| (host === "reddit" && r.typeId === "redirect")) {
|
||||
return createResponse("error", { t: loc(lang, 'ErrorEmptyDownload') })
|
||||
}
|
||||
|
||||
let processType = "render",
|
||||
|
@ -178,5 +180,5 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
|
|||
break;
|
||||
}
|
||||
|
||||
return apiJSON(responseType, {...defaultParams, ...params})
|
||||
return createResponse(responseType, {...defaultParams, ...params})
|
||||
}
|
||||
|
|
154
src/modules/processing/request.js
Normal file
154
src/modules/processing/request.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
import ipaddr from "ipaddr.js";
|
||||
|
||||
import { normalizeURL } from "../processing/url.js";
|
||||
import { createStream } from "../stream/manage.js";
|
||||
|
||||
const apiVar = {
|
||||
allowed: {
|
||||
vCodec: ["h264", "av1", "vp9"],
|
||||
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
|
||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
|
||||
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
|
||||
},
|
||||
booleanOnly: [
|
||||
"isAudioOnly",
|
||||
"isTTFullAudio",
|
||||
"isAudioMuted",
|
||||
"dubLang",
|
||||
"disableMetadata",
|
||||
"twitterGif",
|
||||
"tiktokH265"
|
||||
]
|
||||
}
|
||||
|
||||
export function createResponse(responseType, responseData) {
|
||||
try {
|
||||
let status = 200,
|
||||
response = {};
|
||||
|
||||
switch(responseType) {
|
||||
case "error":
|
||||
status = 400;
|
||||
break;
|
||||
|
||||
case "rate-limit":
|
||||
status = 429;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (responseType) {
|
||||
case "error":
|
||||
case "success":
|
||||
case "rate-limit":
|
||||
response = {
|
||||
text: responseData.t
|
||||
}
|
||||
break;
|
||||
|
||||
case "redirect":
|
||||
response = {
|
||||
url: responseData.u
|
||||
}
|
||||
break;
|
||||
|
||||
case "stream":
|
||||
response = {
|
||||
url: createStream(responseData)
|
||||
}
|
||||
break;
|
||||
|
||||
case "picker":
|
||||
let pickerType = "various",
|
||||
audio = false;
|
||||
|
||||
if (responseData.service === "tiktok") {
|
||||
audio = responseData.u
|
||||
pickerType = "images"
|
||||
}
|
||||
|
||||
response = {
|
||||
pickerType: pickerType,
|
||||
picker: responseData.picker,
|
||||
audio: audio
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw "unreachable"
|
||||
}
|
||||
return {
|
||||
status,
|
||||
body: {
|
||||
status: responseType,
|
||||
...response
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
status: "error",
|
||||
text: "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyRequest(request) {
|
||||
try {
|
||||
let template = {
|
||||
url: normalizeURL(decodeURIComponent(request.url)),
|
||||
vCodec: "h264",
|
||||
vQuality: "720",
|
||||
aFormat: "mp3",
|
||||
filenamePattern: "classic",
|
||||
isAudioOnly: false,
|
||||
isTTFullAudio: false,
|
||||
isAudioMuted: false,
|
||||
disableMetadata: false,
|
||||
dubLang: false,
|
||||
twitterGif: false,
|
||||
tiktokH265: false
|
||||
}
|
||||
|
||||
const requestKeys = Object.keys(request);
|
||||
const templateKeys = Object.keys(template);
|
||||
|
||||
if (requestKeys.length > templateKeys.length + 1 || !request.url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const i in requestKeys) {
|
||||
const key = requestKeys[i];
|
||||
const item = request[key];
|
||||
|
||||
if (String(key) !== "url" && templateKeys.includes(key)) {
|
||||
if (apiVar.booleanOnly.includes(key)) {
|
||||
template[key] = !!item;
|
||||
} else if (apiVar.allowed[key] && apiVar.allowed[key].includes(item)) {
|
||||
template[key] = String(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (template.dubLang)
|
||||
template.dubLang = verifyLanguageCode(request.dubLang);
|
||||
|
||||
return template
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getIP(req) {
|
||||
const strippedIP = req.ip.replace(/^::ffff:/, '');
|
||||
const ip = ipaddr.parse(strippedIP);
|
||||
if (ip.kind() === 'ipv4') {
|
||||
return strippedIP;
|
||||
}
|
||||
|
||||
const prefix = 56;
|
||||
const v6Bytes = ip.toByteArray();
|
||||
v6Bytes.fill(0, prefix / 8);
|
||||
|
||||
return ipaddr.fromByteArray(v6Bytes).toString();
|
||||
}
|
|
@ -68,7 +68,7 @@ export default async function(obj) {
|
|||
data = data[0]?.data?.children[0]?.data;
|
||||
|
||||
if (data?.url?.endsWith('.gif')) return {
|
||||
typeId: 1,
|
||||
typeId: "redirect",
|
||||
urls: data.url
|
||||
}
|
||||
|
||||
|
@ -106,12 +106,12 @@ export default async function(obj) {
|
|||
let id = video.split('/')[3];
|
||||
|
||||
if (!audio) return {
|
||||
typeId: 1,
|
||||
typeId: "redirect",
|
||||
urls: video
|
||||
}
|
||||
|
||||
return {
|
||||
typeId: 2,
|
||||
typeId: "stream",
|
||||
type: "render",
|
||||
urls: [video, audioFileLink],
|
||||
audioFilename: `reddit_${id}_audio`,
|
||||
|
|
|
@ -1,58 +1,5 @@
|
|||
import { normalizeURL } from "../processing/url.js";
|
||||
import { createStream } from "../stream/manage.js";
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
const apiVar = {
|
||||
allowed: {
|
||||
vCodec: ["h264", "av1", "vp9"],
|
||||
vQuality: ["max", "4320", "2160", "1440", "1080", "720", "480", "360", "240", "144"],
|
||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
|
||||
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
|
||||
},
|
||||
booleanOnly: [
|
||||
"isAudioOnly",
|
||||
"isTTFullAudio",
|
||||
"isAudioMuted",
|
||||
"dubLang",
|
||||
"disableMetadata",
|
||||
"twitterGif",
|
||||
"tiktokH265"
|
||||
]
|
||||
}
|
||||
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
|
||||
|
||||
export function apiJSON(type, obj) {
|
||||
try {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return { status: 400, body: { status: "error", text: obj.t } };
|
||||
case 1:
|
||||
return { status: 200, body: { status: "redirect", url: obj.u } };
|
||||
case 2:
|
||||
return { status: 200, body: { status: "stream", url: createStream(obj) } };
|
||||
case 3:
|
||||
return { status: 200, body: { status: "success", text: obj.t } };
|
||||
case 4:
|
||||
return { status: 429, body: { status: "rate-limit", text: obj.t } };
|
||||
case 5:
|
||||
let pickerType = "various", audio = false
|
||||
switch (obj.service) {
|
||||
case "douyin":
|
||||
case "tiktok":
|
||||
audio = obj.u
|
||||
pickerType = "images"
|
||||
break;
|
||||
}
|
||||
return { status: 200, body: { status: "picker", pickerType: pickerType, picker: obj.picker, audio: audio } };
|
||||
case 6: // critical error, action should be taken by balancer/other server software
|
||||
return { status: 500, body: { status: "error", text: obj.t, critical: true } };
|
||||
default:
|
||||
return { status: 400, body: { status: "error", text: "Bad Request" } };
|
||||
}
|
||||
} catch (e) {
|
||||
return { status: 500, body: { status: "error", text: "Internal Server Error", critical: true } };
|
||||
}
|
||||
}
|
||||
export function metadataManager(obj) {
|
||||
let keys = Object.keys(obj);
|
||||
let tags = ["album", "composer", "genre", "copyright", "encoded_by", "title", "language", "artist", "album_artist", "performer", "disc", "publisher", "track", "encoder", "compilation", "date", "creation_time", "comment"]
|
||||
|
@ -79,57 +26,6 @@ export function unicodeDecode(str) {
|
|||
return String.fromCharCode(parseInt(unicode.replace(/\\u/g, ""), 16));
|
||||
});
|
||||
}
|
||||
export function checkJSONPost(obj) {
|
||||
let def = {
|
||||
url: normalizeURL(decodeURIComponent(obj.url)),
|
||||
vCodec: "h264",
|
||||
vQuality: "720",
|
||||
aFormat: "mp3",
|
||||
filenamePattern: "classic",
|
||||
isAudioOnly: false,
|
||||
isTTFullAudio: false,
|
||||
isAudioMuted: false,
|
||||
disableMetadata: false,
|
||||
dubLang: false,
|
||||
twitterGif: false,
|
||||
tiktokH265: false
|
||||
}
|
||||
try {
|
||||
let objKeys = Object.keys(obj);
|
||||
let defKeys = Object.keys(def);
|
||||
if (objKeys.length > defKeys.length + 1 || !obj.url) return false;
|
||||
|
||||
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]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (def.dubLang)
|
||||
def.dubLang = verifyLanguageCode(obj.dubLang);
|
||||
|
||||
return def
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
export function getIP(req) {
|
||||
const strippedIP = req.ip.replace(/^::ffff:/, '');
|
||||
const ip = ipaddr.parse(strippedIP);
|
||||
if (ip.kind() === 'ipv4') {
|
||||
return strippedIP;
|
||||
}
|
||||
|
||||
const prefix = 56;
|
||||
const v6Bytes = ip.toByteArray();
|
||||
v6Bytes.fill(0, prefix / 8);
|
||||
|
||||
return ipaddr.fromByteArray(v6Bytes).toString();
|
||||
}
|
||||
export function cleanHTML(html) {
|
||||
let clean = html.replace(/ {4}/g, '');
|
||||
clean = clean.replace(/\n/g, '');
|
||||
|
|
Loading…
Reference in a new issue