From ae91f8b12006f1881373256a666e8a43b9fccce8 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 12:22:36 +0000 Subject: [PATCH 01/68] api: move url extraction to url module --- src/modules/api.js | 23 ++++------------------- src/modules/processing/url.js | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/modules/api.js b/src/modules/api.js index c3549bb3..209bc14a 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,32 +1,17 @@ -import { services } from "./config.js"; - import { apiJSON } from "./sub/utils.js"; import { errorUnsupported } from "./sub/errors.js"; import loc from "../localization/manager.js"; import match from "./processing/match.js"; -import { getHostIfValid } from "./processing/url.js"; +import { extract } from "./processing/url.js"; export async function getJSON(url, lang, obj) { try { - const host = getHostIfValid(url); - - if (!host || !services[host].enabled) { + const parsed = extract(url); + if (parsed === null) { return apiJSON(0, { t: errorUnsupported(lang) }); } - let patternMatch; - for (const pattern of services[host].patterns) { - patternMatch = pattern.match( - url.pathname.substring(1) + url.search - ); - if (patternMatch) break; - } - - if (!patternMatch) { - return apiJSON(0, { t: errorUnsupported(lang) }); - } - - return await match(host, patternMatch, url, lang, obj) + return await match(parsed.host, parsed.patternMatch, url, lang, obj) } catch (e) { return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }) } diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index c723cb91..b0c60798 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -125,3 +125,29 @@ export function getHostIfValid(url) { return host.sld; } + + +export function extract(url) { + const host = getHostIfValid(url); + + if (!host || !services[host].enabled) { + return null; + } + + let patternMatch; + for (const pattern of services[host].patterns) { + patternMatch = pattern.match( + url.pathname.substring(1) + url.search + ); + + if (patternMatch) { + break; + } + } + + if (!patternMatch) { + return null; + } + + return { host, patternMatch }; +} \ No newline at end of file From c1958596e924aa4e0fe59e80c5d1b7edf57fa292 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 12:22:52 +0000 Subject: [PATCH 02/68] url: only export functions that are used externally --- src/modules/processing/url.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index b0c60798..51883b1b 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -2,7 +2,7 @@ import { services } from "../config.js"; import { strict as assert } from "node:assert"; import psl from "psl"; -export function aliasURL(url) { +function aliasURL(url) { assert(url instanceof URL); const host = psl.parse(url.hostname); @@ -75,7 +75,7 @@ export function aliasURL(url) { return url } -export function cleanURL(url) { +function cleanURL(url) { assert(url instanceof URL); const host = psl.parse(url.hostname).sld; let stripQuery = true; @@ -103,15 +103,7 @@ export function cleanURL(url) { return url } -export function normalizeURL(url) { - return cleanURL( - aliasURL( - new URL(url.replace(/^https\/\//, 'https://')) - ) - ); -} - -export function getHostIfValid(url) { +function getHostIfValid(url) { const host = psl.parse(url.hostname); if (host.error) return; @@ -126,6 +118,13 @@ export function getHostIfValid(url) { return host.sld; } +export function normalizeURL(url) { + return cleanURL( + aliasURL( + new URL(url.replace(/^https\/\//, 'https://')) + ) + ); +} export function extract(url) { const host = getHostIfValid(url); From c5c3682462a2712df4a4a7a7f5b8fa2224f2daa6 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 12:45:23 +0000 Subject: [PATCH 03/68] api: get rid of getJSON let's just call `match` directly --- src/core/api.js | 12 ++++++++++-- src/modules/api.js | 18 ------------------ src/test/test.js | 12 ++++++++++-- 3 files changed, 20 insertions(+), 22 deletions(-) delete mode 100644 src/modules/api.js diff --git a/src/core/api.js b/src/core/api.js index 31ed7dd5..3e4a2f8b 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -5,13 +5,15 @@ import { randomBytes } from "crypto"; const ipSalt = randomBytes(64).toString('hex'); import { env, version } from "../modules/config.js"; -import { getJSON } from "../modules/api.js"; +import match from "../modules/processing/match.js"; import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.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 ? { @@ -105,8 +107,14 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { let chck = checkJSONPost(request); if (!chck) throw new Error(); + + const parsed = extract(chck.url); + if (parsed === null) { + return apiJSON(0, { t: errorUnsupported(lang) }) + } - j = await getJSON(chck.url, lang, chck); + j = await match(parsed.host, parsed.patternMatch, chck.url, lang, chck) + .catch(() => apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') })) } else { j = apiJSON(0, { t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink') diff --git a/src/modules/api.js b/src/modules/api.js deleted file mode 100644 index 209bc14a..00000000 --- a/src/modules/api.js +++ /dev/null @@ -1,18 +0,0 @@ -import { apiJSON } from "./sub/utils.js"; -import { errorUnsupported } from "./sub/errors.js"; -import loc from "../localization/manager.js"; -import match from "./processing/match.js"; -import { extract } from "./processing/url.js"; - -export async function getJSON(url, lang, obj) { - try { - const parsed = extract(url); - if (parsed === null) { - return apiJSON(0, { t: errorUnsupported(lang) }); - } - - return await match(parsed.host, parsed.patternMatch, url, lang, obj) - } catch (e) { - return apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') }) - } -} diff --git a/src/test/test.js b/src/test/test.js index d8373ff7..b3bcff04 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -1,11 +1,14 @@ import "dotenv/config"; import "../modules/sub/alias-envs.js"; -import { getJSON } from "../modules/api.js"; import { services } from "../modules/config.js"; +import { extract } from "../modules/processing/url.js"; +import match from "../modules/processing/match.js"; import { loadJSON } from "../modules/sub/loadFromFs.js"; import { checkJSONPost } from "../modules/sub/utils.js"; +import { env } from "../modules/config.js"; +env.apiURL = 'http://localhost:9000' let tests = loadJSON('./src/test/tests.json'); let noTest = []; @@ -35,7 +38,12 @@ for (let i in services) { let chck = checkJSONPost(params); if (chck) { chck["ip"] = "d21ec524bc2ade41bef569c0361ac57728c69e2764b5cb3cb310fe36568ca53f"; // random sha256 - let j = await getJSON(chck["url"], "en", chck); + const parsed = extract(chck.url); + if (parsed === null) { + throw `Invalid URL: ${chck.url}` + } + + let j = await match(parsed.host, parsed.patternMatch, "en", chck); console.log('\nReceived:'); console.log(j) if (j.status === test.expected.code && j.body.status === test.expected.status) { From 0a7cdfbbfe1d90d1824b0415ed8c329f095e193b Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 12:46:08 +0000 Subject: [PATCH 04/68] match: remove redundant `url` argument --- src/core/api.js | 2 +- src/modules/processing/match.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 3e4a2f8b..8c790023 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -113,7 +113,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { return apiJSON(0, { t: errorUnsupported(lang) }) } - j = await match(parsed.host, parsed.patternMatch, chck.url, lang, chck) + j = await match(parsed.host, parsed.patternMatch, lang, chck) .catch(() => apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') })) } else { j = apiJSON(0, { diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 6bbd0d2a..4de3e9b8 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -28,7 +28,8 @@ import dailymotion from "./services/dailymotion.js"; import { env } from '../config.js'; let freebind; -export default async function(host, patternMatch, url, lang, obj) { +export default async function(host, patternMatch, lang, obj) { + const { url } = obj; assert(url instanceof URL); let dispatcher, requestIP; From c10012130b61bdf602c07c9c9abaeaa624d42d11 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 13:29:18 +0000 Subject: [PATCH 05/68] api: refactor /api/json POST handler --- src/core/api.js | 63 ++++++++++++++++-------------- src/localization/languages/en.json | 3 +- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 8c790023..ab9c1787 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -95,37 +95,42 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } }); + const acceptRegex = /^application\/json(; charset=utf-8)?$/; app.post('/api/json', async (req, res) => { - try { - let lang = languageCode(req); - let j = apiJSON(0, { t: "bad request" }); - try { - let contentCon = String(req.header('Content-Type')) === "application/json"; - let request = req.body; - if (contentCon && request.url) { - request.dubLang = request.dubLang ? lang : false; - - let chck = checkJSONPost(request); - if (!chck) throw new Error(); + const request = req.body; + const lang = languageCode(req); + const fail = (t) => { + const { status, body } = apiJSON(0, { t: loc(lang, t) }); + res.status(status).json(body); + } - const parsed = extract(chck.url); - if (parsed === null) { - return apiJSON(0, { t: errorUnsupported(lang) }) - } - - j = await match(parsed.host, parsed.patternMatch, lang, chck) - .catch(() => apiJSON(0, { t: loc(lang, 'ErrorSomethingWentWrong') })) - } else { - j = apiJSON(0, { - t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink') - }); - } - } catch (e) { - j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') }); - } - return res.status(j.status).json(j.body); - } catch (e) { - return res.destroy(); + if (!acceptRegex.test(req.header('Content-Type'))) { + return fail('ErrorInvalidContentType'); + } + + if (!request.url) { + return fail('ErrorNoLink'); + } + + request.dubLang = request.dubLang ? lang : false; + const normalizedRequest = checkJSONPost(request); + if (!normalizedRequest) { + return fail('ErrorCantProcess'); + } + + const parsed = extract(normalizedRequest.url); + if (parsed === null) { + return fail('ErrorUnsupported'); + } + + try { + const result = await match( + parsed.host, parsed.patternMatch, lang, normalizedRequest + ); + + res.status(result.status).json(result.body); + } catch { + fail('ErrorSomethingWentWrong'); } }); diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index b0987337..6e9c9677 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -155,6 +155,7 @@ "SettingsTikTokH265Description": "download 1080p videos from tiktok in h265/hevc format when available.", "SettingsYoutubeDub": "use browser language", "SettingsYoutubeDubDescription": "uses your browser's default language for youtube dubbed audio tracks. works even if cobalt ui isn't translated to your language.", - "UpdateIstream": "better service support and ux" + "UpdateIstream": "better service support and ux", + "ErrorInvalidContentType": "invalid content type header" } } From cc6345ff633b233f48f6e2f024a0e0057cb707eb Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 21:39:44 +0600 Subject: [PATCH 06/68] api: move request functions to separate file - request status id is no longer a cryptic number - descriptive function names --- src/core/api.js | 8 +- src/modules/processing/match.js | 16 +- src/modules/processing/matchActionDecider.js | 26 ++-- src/modules/processing/request.js | 154 +++++++++++++++++++ src/modules/processing/services/reddit.js | 6 +- src/modules/sub/utils.js | 104 ------------- 6 files changed, 183 insertions(+), 131 deletions(-) create mode 100644 src/modules/processing/request.js diff --git a/src/core/api.js b/src/core/api.js index ab9c1787..228ab6db 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -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'); } diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 4de3e9b8..73102dfb 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -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) }) } } diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index d440cff6..cc2698e3 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -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}) } diff --git a/src/modules/processing/request.js b/src/modules/processing/request.js new file mode 100644 index 00000000..9015e742 --- /dev/null +++ b/src/modules/processing/request.js @@ -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(); +} diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js index e022f62c..75669538 100644 --- a/src/modules/processing/services/reddit.js +++ b/src/modules/processing/services/reddit.js @@ -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`, diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 59954445..e965bd9a 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -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, ''); From dd778355999687d86111552de8042aea3a1f12ce Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 21:55:14 +0600 Subject: [PATCH 07/68] request: add missing critical error --- src/modules/processing/request.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/modules/processing/request.js b/src/modules/processing/request.js index 9015e742..9b7af098 100644 --- a/src/modules/processing/request.js +++ b/src/modules/processing/request.js @@ -22,6 +22,17 @@ const apiVar = { } export function createResponse(responseType, responseData) { + const internalError = (text) => { + return { + status: 500, + body: { + status: "error", + text: text || "Internal Server Error", + critical: true + } + } + } + try { let status = 200, response = {}; @@ -72,6 +83,8 @@ export function createResponse(responseType, responseData) { audio: audio } break; + case "critical": + return internalError(responseData.t) default: throw "unreachable" } @@ -83,13 +96,7 @@ export function createResponse(responseType, responseData) { } } } catch { - return { - status: 500, - body: { - status: "error", - text: "Internal Server Error" - } - } + return internalError() } } From 13524a4aa15b2c98375fcc56372158369bcb43a6 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 21:56:43 +0600 Subject: [PATCH 08/68] utils: clean up --- src/modules/sub/utils.js | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index e965bd9a..0db85649 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -1,30 +1,43 @@ const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; 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"] + const keys = Object.keys(obj); + const tags = [ + "album", + "copyright", + "title", + "artist", + "track", + "date" + ] let commands = [] - for (let i in keys) { if (tags.includes(keys[i])) commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) } + for (const i in keys) { + if (tags.includes(keys[i])) + commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) + } return commands; } export function cleanString(string) { - for (let i in forbiddenCharsString) { - string = string.replaceAll("/", "_").replaceAll(forbiddenCharsString[i], '') + for (const i in forbiddenCharsString) { + string = string.replaceAll("/", "_") + .replaceAll(forbiddenCharsString[i], '') } return string; } export function verifyLanguageCode(code) { - return RegExp(/[a-z]{2}/).test(String(code.slice(0, 2).toLowerCase())) ? String(code.slice(0, 2).toLowerCase()) : "en" + const languageCode = String(code.slice(0, 2).toLowerCase()); + if (RegExp(/[a-z]{2}/).test(languageCode)) { + return languageCode + } + return "en" } export function languageCode(req) { - return req.header('Accept-Language') ? verifyLanguageCode(req.header('Accept-Language')) : "en" -} -export function unicodeDecode(str) { - return str.replace(/\\u[\dA-F]{4}/gi, (unicode) => { - return String.fromCharCode(parseInt(unicode.replace(/\\u/g, ""), 16)); - }); + if (req.header('Accept-Language')) { + return verifyLanguageCode(req.header('Accept-Language')) + } + return "en" } export function cleanHTML(html) { let clean = html.replace(/ {4}/g, ''); From 98e05368ed411275e444486f9b0d9f7fe52d2779 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:28:09 +0600 Subject: [PATCH 09/68] api: raw stream status responses, clean up core --- src/core/api.js | 165 ++++++++++++++++++----------------- src/modules/stream/manage.js | 38 +++----- src/modules/stream/stream.js | 2 +- 3 files changed, 98 insertions(+), 107 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 228ab6db..cdcf216b 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -16,10 +16,10 @@ import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; import { extract } from "../modules/processing/url.js"; export function runAPI(express, app, gitCommit, gitBranch, __dirname) { - const corsConfig = !env.corsWildcard ? { + const corsConfig = env.corsWildcard ? {} : { origin: env.corsURL, optionsSuccessStatus: 200 - } : {}; + }; const apiLimiter = rateLimit({ windowMs: 60000, @@ -33,7 +33,8 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { "text": loc(languageCode(req), 'ErrorRateLimit') }); } - }); + }) + const apiLimiterStream = rateLimit({ windowMs: 60000, max: 25, @@ -41,12 +42,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), handler: (req, res) => { - return res.status(429).json({ - "status": "rate-limit", - "text": loc(languageCode(req), 'ErrorRateLimit') - }); + return res.sendStatus(429) } - }); + }) const startTime = new Date(); const startTimestamp = startTime.getTime(); @@ -56,7 +54,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.use('/api/:type', cors({ methods: ['GET', 'POST'], ...corsConfig - })); + })) app.use('/api/json', apiLimiter); app.use('/api/stream', apiLimiterStream); @@ -65,27 +63,27 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.use((req, res, next) => { try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') } next(); - }); + }) app.use('/api/json', express.json({ verify: (req, res, buf) => { - let acceptCon = String(req.header('Accept')) === "application/json"; - if (acceptCon) { + const acceptHeader = String(req.header('Accept')) === "application/json"; + if (acceptHeader) { if (buf.length > 720) throw new Error(); JSON.parse(buf); } else { throw new Error(); } } - })); + })) // handle express.json errors properly (https://github.com/expressjs/express/issues/4065) app.use('/api/json', (err, req, res, next) => { - let errorText = "invalid json body"; - let acceptCon = String(req.header('Accept')) !== "application/json"; + const errorText = "invalid json body"; + const acceptHeader = String(req.header('Accept')) !== "application/json"; - if (err || acceptCon) { - if (acceptCon) errorText = "invalid accept header"; + if (err || acceptHeader) { + if (acceptHeader) errorText = "invalid accept header"; return res.status(400).json({ status: "error", text: errorText @@ -93,12 +91,14 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } else { next(); } - }); + }) const acceptRegex = /^application\/json(; charset=utf-8)?$/; + app.post('/api/json', async (req, res) => { const request = req.body; const lang = languageCode(req); + const fail = (t) => { const { status, body } = createResponse("error", { t: loc(lang, t) }); res.status(status).json(body); @@ -132,80 +132,83 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } catch { fail('ErrorSomethingWentWrong'); } - }); + }) - app.get('/api/:type', (req, res) => { - try { - let j; - switch (req.params.type) { - case 'stream': - const q = req.query; - const checkQueries = q.t && q.e && q.h && q.s && q.i; - const checkBaseLength = q.t.length === 21 && q.e.length === 13; - const checkSafeLength = q.h.length === 43 && q.s.length === 43 && q.i.length === 22; - if (checkQueries && checkBaseLength && checkSafeLength) { - if (q.p) { - return res.status(200).json({ - status: "continue" - }) - } - let streamInfo = verifyStream(q.t, q.h, q.e, q.s, q.i); - if (streamInfo.error) { - return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); - } - return stream(res, streamInfo); - } + app.get('/api/stream', (req, res) => { + const id = String(req.query.id); + const exp = String(req.query.exp); + const sig = String(req.query.sig); + const sec = String(req.query.sec); + const iv = String(req.query.iv); - j = apiJSON(0, { - t: "bad request. stream link may be incomplete or corrupted." - }) - return res.status(j.status).json(j.body); - case 'istream': - if (!req.ip.endsWith('127.0.0.1')) - return res.sendStatus(403); - if (('' + req.query.t).length !== 21) - return res.sendStatus(400); - - let streamInfo = getInternalStream(req.query.t); - if (!streamInfo) return res.sendStatus(404); - streamInfo.headers = req.headers; - - return stream(res, { type: 'internal', ...streamInfo }); - case 'serverInfo': - return res.status(200).json({ - version: version, - commit: gitCommit, - branch: gitBranch, - name: env.apiName, - url: env.apiURL, - cors: Number(env.corsWildcard), - startTime: `${startTimestamp}` - }); - default: - j = apiJSON(0, { - t: "unknown response type" - }) - return res.status(j.status).json(j.body); + const checkQueries = id && exp && sig && sec && iv; + const checkBaseLength = id.length === 21 && exp.length === 13; + const checkSafeLength = sig.length === 43 && sec.length === 43 && iv.length === 22; + + if (checkQueries && checkBaseLength && checkSafeLength) { + // rate limit probe, will not return json after 8.0 + if (req.query.p) { + return res.status(200).json({ + status: "continue" + }) + } + try { + const streamInfo = verifyStream(id, sig, exp, sec, iv); + if (!streamInfo?.service) { + return res.sendStatus(streamInfo.status); + } + return stream(res, streamInfo); + } catch { + return res.destroy(); } - } catch (e) { - return res.status(500).json({ - status: "error", - text: loc(languageCode(req), 'ErrorCantProcess') - }); } - }); + return res.sendStatus(400); + }) + + app.get('/api/istream', (req, res) => { + try { + if (!req.ip.endsWith('127.0.0.1')) + return res.sendStatus(403); + if (String(req.query.id).length !== 21) + return res.sendStatus(400); + + const streamInfo = getInternalStream(req.query.id); + if (!streamInfo) return res.sendStatus(404); + streamInfo.headers = req.headers; + + return stream(res, { type: 'internal', ...streamInfo }); + } catch { + return res.destroy(); + } + }) + + app.get('/api/serverInfo', (req, res) => { + try { + return res.status(200).json({ + version: version, + commit: gitCommit, + branch: gitBranch, + name: env.apiName, + url: env.apiURL, + cors: Number(env.corsWildcard), + startTime: `${startTimestamp}` + }); + } catch { + return res.destroy(); + } + }) app.get('/api/status', (req, res) => { res.status(200).end() - }); + }) app.get('/favicon.ico', (req, res) => { res.sendFile(`${__dirname}/src/front/icons/favicon.ico`) - }); + }) app.get('/*', (req, res) => { - res.redirect('/api/json') - }); + res.redirect('/api/serverInfo') + }) app.listen(env.apiPort, env.listenAddress, () => { console.log(`\n` + @@ -214,5 +217,5 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { `URL: ${Cyan(`${env.apiURL}`)}\n` + `Port: ${env.apiPort}\n` ) - }); + }) } diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 7d19354f..86334fa2 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -1,6 +1,6 @@ import NodeCache from "node-cache"; import { randomBytes } from "crypto"; -import { nanoid } from 'nanoid'; +import { nanoid } from "nanoid"; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; import { streamLifespan, env } from "../config.js"; @@ -11,15 +11,6 @@ const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube']; -const streamNoAccess = { - error: "i couldn't verify if you have access to this stream. go back and try again!", - status: 401 -} -const streamNoExist = { - error: "this download link has expired or doesn't exist. go back and try again!", - status: 400 -} - const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, @@ -61,11 +52,11 @@ export function createStream(obj) { let streamLink = new URL('/api/stream', env.apiURL); const params = { - 't': streamID, - 'e': exp, - 'h': hmac, - 's': secret, - 'i': iv + 'id': streamID, + 'exp': exp, + 'sig': hmac, + 'sec': secret, + 'iv': iv } for (const [key, value] of Object.entries(params)) { @@ -96,7 +87,7 @@ export function createInternalStream(url, obj = {}) { }; let streamLink = new URL('/api/istream', `http://127.0.0.1:${env.apiPort}`); - streamLink.searchParams.set('t', streamID); + streamLink.searchParams.set('id', streamID); return streamLink.toString(); } @@ -106,7 +97,7 @@ export function destroyInternalStream(url) { return; } - const id = url.searchParams.get('t'); + const id = url.searchParams.get('id'); if (internalStreamCache[id]) { internalStreamCache[id].controller.abort(); @@ -141,22 +132,19 @@ export function verifyStream(id, hmac, exp, secret, iv) { const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); const cache = streamCache.get(id.toString()); - if (ghmac !== String(hmac)) return streamNoAccess; - if (!cache) return streamNoExist; + if (ghmac !== String(hmac)) return { status: 401 }; + if (!cache) return { status: 404 }; const streamInfo = JSON.parse(decryptStream(cache, iv, secret)); - if (!streamInfo) return streamNoExist; + if (!streamInfo) return { status: 404 }; if (Number(exp) <= new Date().getTime()) - return streamNoExist; + return { status: 404 }; return wrapStream(streamInfo); } catch { - return { - error: "something went wrong and i couldn't verify this stream. go back and try again!", - status: 500 - } + return { status: 500 }; } } diff --git a/src/modules/stream/stream.js b/src/modules/stream/stream.js index 3de1cb3e..3b3494b6 100644 --- a/src/modules/stream/stream.js +++ b/src/modules/stream/stream.js @@ -25,6 +25,6 @@ export default async function(res, streamInfo) { break; } } catch { - res.status(500).json({ status: "error", text: "Internal Server Error" }); + res.sendStatus(500); } } From c2678888be3538c841b45340563b4ef07425d5bb Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:28:31 +0600 Subject: [PATCH 10/68] loadFromFs: clean up --- src/modules/sub/loadFromFs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/sub/loadFromFs.js b/src/modules/sub/loadFromFs.js index 3cbb1614..0fab6057 100644 --- a/src/modules/sub/loadFromFs.js +++ b/src/modules/sub/loadFromFs.js @@ -3,14 +3,14 @@ import * as fs from "fs"; export function loadJSON(path) { try { return JSON.parse(fs.readFileSync(path, 'utf-8')) - } catch(e) { + } catch { return false } } export function loadFile(path) { try { return fs.readFileSync(path, 'utf-8') - } catch(e) { + } catch { return false } } From b4364c4921ee1ed34a55ad69bde72810b7812eda Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:36:24 +0600 Subject: [PATCH 11/68] match: remove useless errors module & clean up --- src/modules/processing/match.js | 40 +++++++++++++++++++++++---------- src/modules/sub/errors.js | 11 --------- 2 files changed, 28 insertions(+), 23 deletions(-) delete mode 100644 src/modules/sub/errors.js diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 73102dfb..5dfd2765 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -1,8 +1,7 @@ import { strict as assert } from "node:assert"; +import { env } from '../config.js'; import { createResponse } from "../processing/request.js"; -import { errorUnsupported, genericError, brokenLink } from "../sub/errors.js"; - import loc from "../../localization/manager.js"; import { testers } from "./servicesPatternTesters.js"; @@ -25,9 +24,9 @@ import streamable from "./services/streamable.js"; import twitch from "./services/twitch.js"; import rutube from "./services/rutube.js"; import dailymotion from "./services/dailymotion.js"; -import { env } from '../config.js'; let freebind; + export default async function(host, patternMatch, lang, obj) { const { url } = obj; assert(url instanceof URL); @@ -43,10 +42,20 @@ export default async function(host, patternMatch, lang, obj) { } try { - let r, isAudioOnly = !!obj.isAudioOnly, disableMetadata = !!obj.disableMetadata; + let r, + isAudioOnly = !!obj.isAudioOnly, + disableMetadata = !!obj.disableMetadata; - if (!testers[host]) return createResponse("error", { t: errorUnsupported(lang) }); - if (!(testers[host](patternMatch))) return createResponse("error", { t: brokenLink(lang, host) }); + if (!testers[host]) { + return createResponse("error", { + t: loc(lang, 'ErrorUnsupported') + }); + } + if (!(testers[host](patternMatch))) { + return createResponse("error", { + t: loc(lang, 'ErrorBrokenLink', host) + }); + } switch (host) { case "twitter": @@ -177,21 +186,26 @@ export default async function(host, patternMatch, lang, obj) { r = await dailymotion(patternMatch); break; default: - return createResponse("error", { t: errorUnsupported(lang) }); + return createResponse("error", { + t: loc(lang, 'ErrorUnsupported') + }); } if (r.isAudioOnly) isAudioOnly = true; let isAudioMuted = isAudioOnly ? false : obj.isAudioMuted; - if (r.error && r.critical) - return createResponse("critical", { t: loc(lang, r.error) }) - - if (r.error) + if (r.error && r.critical) { + return createResponse("critical", { + t: loc(lang, r.error) + }) + } + if (r.error) { return createResponse("error", { t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error) }) + } return matchActionDecider( r, host, obj.aFormat, isAudioOnly, @@ -200,6 +214,8 @@ export default async function(host, patternMatch, lang, obj) { requestIP ) } catch { - return createResponse("error", { t: genericError(lang, host) }) + return createResponse("error", { + t: loc(lang, 'ErrorBadFetch', host) + }) } } diff --git a/src/modules/sub/errors.js b/src/modules/sub/errors.js deleted file mode 100644 index c73ffe85..00000000 --- a/src/modules/sub/errors.js +++ /dev/null @@ -1,11 +0,0 @@ -import loc from "../../localization/manager.js"; - -export function errorUnsupported(lang) { - return loc(lang, 'ErrorUnsupported'); -} -export function brokenLink(lang, host) { - return loc(lang, 'ErrorBrokenLink', host); -} -export function genericError(lang, host) { - return loc(lang, 'ErrorBadFetch', host); -} From 58e525de6103907ddd6fd1a40a7ebdb6550cf3be Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:50:58 +0600 Subject: [PATCH 12/68] utils: fix variable name shadowing --- src/modules/sub/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index 0db85649..c0c373b7 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -27,9 +27,9 @@ export function cleanString(string) { return string; } export function verifyLanguageCode(code) { - const languageCode = String(code.slice(0, 2).toLowerCase()); - if (RegExp(/[a-z]{2}/).test(languageCode)) { - return languageCode + const code = String(code.slice(0, 2).toLowerCase()); + if (RegExp(/[a-z]{2}/).test(code)) { + return code } return "en" } From a305bdb78d0ff562a399f55c558457d656e5ce4f Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:51:26 +0600 Subject: [PATCH 13/68] core/api: no longer reassign to const --- src/core/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/api.js b/src/core/api.js index cdcf216b..c4087e0b 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -79,7 +79,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { // handle express.json errors properly (https://github.com/expressjs/express/issues/4065) app.use('/api/json', (err, req, res, next) => { - const errorText = "invalid json body"; + let errorText = "invalid json body"; const acceptHeader = String(req.header('Accept')) !== "application/json"; if (err || acceptHeader) { From fb6f384e46ebb3184d8e3dea9b18778c4954def0 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:52:01 +0600 Subject: [PATCH 14/68] request: actually import verifyLanguageCode --- src/modules/processing/request.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/processing/request.js b/src/modules/processing/request.js index 9b7af098..1b184bac 100644 --- a/src/modules/processing/request.js +++ b/src/modules/processing/request.js @@ -2,6 +2,7 @@ import ipaddr from "ipaddr.js"; import { normalizeURL } from "../processing/url.js"; import { createStream } from "../stream/manage.js"; +import { verifyLanguageCode } from "../sub/utils.js"; const apiVar = { allowed: { From 2f898f7ddeb4fe2e5c1e9c72c74e6a5effc04212 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 22:59:55 +0600 Subject: [PATCH 15/68] utils/verifyLanguageCode: fix variable names --- src/modules/sub/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/sub/utils.js b/src/modules/sub/utils.js index c0c373b7..678eb70e 100644 --- a/src/modules/sub/utils.js +++ b/src/modules/sub/utils.js @@ -27,9 +27,9 @@ export function cleanString(string) { return string; } export function verifyLanguageCode(code) { - const code = String(code.slice(0, 2).toLowerCase()); + const langCode = String(code.slice(0, 2).toLowerCase()); if (RegExp(/[a-z]{2}/).test(code)) { - return code + return langCode } return "en" } From 96d7075eed0b0ab5b00320260d0b97492f52fa22 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 15 May 2024 23:00:13 +0600 Subject: [PATCH 16/68] consoleText: remove unused functions --- src/modules/sub/consoleText.js | 35 +--------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/src/modules/sub/consoleText.js b/src/modules/sub/consoleText.js index 34a0915c..014584ae 100644 --- a/src/modules/sub/consoleText.js +++ b/src/modules/sub/consoleText.js @@ -1,49 +1,16 @@ function t(color, tt) { return color + tt + "\x1b[0m" } -export function Reset(tt) { - return "\x1b[0m" + tt -} + export function Bright(tt) { return t("\x1b[1m", tt) } -export function Dim(tt) { - return t("\x1b[2m", tt) -} -export function Underscore(tt) { - return t("\x1b[4m", tt) -} -export function Blink(tt) { - return t("\x1b[5m", tt) -} -export function Reverse(tt) { - return t("\x1b[7m", tt) -} -export function Hidden(tt) { - return t("\x1b[8m", tt) -} - -export function Black(tt) { - return t("\x1b[30m", tt) -} export function Red(tt) { return t("\x1b[31m", tt) } export function Green(tt) { return t("\x1b[32m", tt) } -export function Yellow(tt) { - return t("\x1b[33m", tt) -} -export function Blue(tt) { - return t("\x1b[34m", tt) -} -export function Magenta(tt) { - return t("\x1b[35m", tt) -} export function Cyan(tt) { return t("\x1b[36m", tt) } -export function White(tt) { - return t("\x1b[37m", tt) -} From b13919d89b896b969ce53b75e9013be0cd39dd3d Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 17:03:33 +0000 Subject: [PATCH 17/68] request: rename verifyRequest to normalizeRequest --- src/core/api.js | 4 ++-- src/modules/processing/request.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index c4087e0b..070ff3d6 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -7,7 +7,7 @@ const ipSalt = randomBytes(64).toString('hex'); import { env, version } from "../modules/config.js"; import match from "../modules/processing/match.js"; import { languageCode } from "../modules/sub/utils.js"; -import { createResponse, verifyRequest, getIP } from "../modules/processing/request.js"; +import { createResponse, normalizeRequest, 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"; @@ -113,7 +113,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } request.dubLang = request.dubLang ? lang : false; - const normalizedRequest = verifyRequest(request); + const normalizedRequest = normalizeRequest(request); if (!normalizedRequest) { return fail('ErrorCantProcess'); } diff --git a/src/modules/processing/request.js b/src/modules/processing/request.js index 1b184bac..f2f58c65 100644 --- a/src/modules/processing/request.js +++ b/src/modules/processing/request.js @@ -101,7 +101,7 @@ export function createResponse(responseType, responseData) { } } -export function verifyRequest(request) { +export function normalizeRequest(request) { try { let template = { url: normalizeURL(decodeURIComponent(request.url)), From 1a77a46396f4d5dc321bde8555fa432e3eb50cab Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 17:04:53 +0000 Subject: [PATCH 18/68] test: update imports --- src/test/test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/test.js b/src/test/test.js index b3bcff04..f26f3b95 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -5,7 +5,7 @@ import { services } from "../modules/config.js"; import { extract } from "../modules/processing/url.js"; import match from "../modules/processing/match.js"; import { loadJSON } from "../modules/sub/loadFromFs.js"; -import { checkJSONPost } from "../modules/sub/utils.js"; +import { normalizeRequest } from "../modules/processing/request.js"; import { env } from "../modules/config.js"; env.apiURL = 'http://localhost:9000' @@ -35,9 +35,8 @@ for (let i in services) { let params = {...{url: test.url}, ...test.params}; console.log(params); - let chck = checkJSONPost(params); + let chck = normalizeRequest(params); if (chck) { - chck["ip"] = "d21ec524bc2ade41bef569c0361ac57728c69e2764b5cb3cb310fe36568ca53f"; // random sha256 const parsed = extract(chck.url); if (parsed === null) { throw `Invalid URL: ${chck.url}` From 9c3d96de0dc4fcf2600f6a01d72352a394da83d8 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 17:05:09 +0000 Subject: [PATCH 19/68] test: remove twitter spaces tests not supported anymore --- src/test/tests.json | 64 --------------------------------------------- 1 file changed, 64 deletions(-) diff --git a/src/test/tests.json b/src/test/tests.json index a0ef8e6b..565fa5a4 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -172,70 +172,6 @@ "code": 400, "status": "error" } - }, { - "name": "recorded space by nyc (best)", - "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", - "params": { - "aFormat": "best", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "recorded space by nyc (mp3)", - "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", - "params": { - "aFormat": "mp3", - "isAudioOnly": false, - "isAudioMuted": false - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "recorded space by nyc (wav, isAudioMuted)", - "url": "https://twitter.com/i/spaces/1gqxvyLoYQkJB", - "params": { - "aFormat": "wav", - "isAudioOnly": false, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "recorded space by service95 & dualipa (mp3, isAudioMuted, isAudioOnly)", - "url": "https://twitter.com/i/spaces/1nAJErvvVXgxL", - "params": { - "aFormat": "mp3", - "isAudioOnly": true, - "isAudioMuted": true - }, - "expected": { - "code": 200, - "status": "stream" - } - }, { - "name": "unavailable space", - "url": "https://twitter.com/i/spaces/1OwGWwjRjVVGQ?s=20", - "params": {}, - "expected": { - "code": 400, - "status": "error" - } - }, { - "name": "inexistent space", - "url": "https://twitter.com/i/spaces/10Wkie2j29iiI", - "params": {}, - "expected": { - "code": 400, - "status": "error" - } }], "soundcloud": [{ "name": "public song (best)", From 44292ea622b2f9e10a0445156c7abba9634abdc8 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 17:13:00 +0000 Subject: [PATCH 20/68] processing: remove leftover douyin mentions --- src/modules/processing/matchActionDecider.js | 2 -- src/modules/processing/servicesConfig.json | 6 ------ 2 files changed, 8 deletions(-) diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index cc2698e3..f54bb3f5 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -67,7 +67,6 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di case "twitter": params = { picker: r.picker }; break; - case "douyin": case "tiktok": let pickerType = "render"; if (audioFormat === "mp3" || audioFormat === "best") { @@ -112,7 +111,6 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di break; case "vk": - case "douyin": case "tiktok": params = { type: "bridge" }; break; diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index eaea0167..0b98f1d5 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -59,12 +59,6 @@ "subdomains": ["vt", "vm"], "enabled": true }, - "douyin": { - "alias": "douyin videos & audio", - "patterns": ["video/:postId", ":id"], - "subdomains": ["v"], - "enabled": false - }, "vimeo": { "patterns": [":id", "video/:id", ":id/:password", "/channels/:user/:id"], "enabled": true, From e4646a9ff39ce0686de87413d4d031473915ffe5 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 15 May 2024 17:17:42 +0000 Subject: [PATCH 21/68] reddit: fix downloads not working without cookies --- src/modules/processing/services/reddit.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js index 75669538..00b35526 100644 --- a/src/modules/processing/services/reddit.js +++ b/src/modules/processing/services/reddit.js @@ -59,9 +59,13 @@ export default async function(obj) { let data = await fetch( url, { - headers: accessToken && { authorization: `Bearer ${accessToken}` } + headers: { + 'User-Agent': genericUserAgent, + accept: 'application/json', + authorization: accessToken && `Bearer ${accessToken}` + } } - ).then(r => r.json() ).catch(() => {}); + ).then(r => r.json()).catch(() => {}); if (!data || !Array.isArray(data)) return { error: 'ErrorCouldntFetch' }; From a70fc840dca081407c8412d988cd7d889a54a617 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 10:19:07 +0600 Subject: [PATCH 22/68] api: remove onDemand mention and slight clean up --- src/core/api.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 070ff3d6..24b935fd 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -58,17 +58,19 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.use('/api/json', apiLimiter); app.use('/api/stream', apiLimiterStream); - app.use('/api/onDemand', apiLimiter); app.use((req, res, next) => { - try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') } + try { + decodeURIComponent(req.path) + } catch { + return res.redirect('/') + } next(); }) app.use('/api/json', express.json({ verify: (req, res, buf) => { - const acceptHeader = String(req.header('Accept')) === "application/json"; - if (acceptHeader) { + if (String(req.header('Accept')) === "application/json") { if (buf.length > 720) throw new Error(); JSON.parse(buf); } else { From 391cf16c8766303119845c9eb9525e669927546f Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 13:54:11 +0600 Subject: [PATCH 23/68] api: configurable rate limit through env --- src/core/api.js | 10 +++++----- src/modules/config.js | 15 +++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 24b935fd..f31665aa 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -22,8 +22,8 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }; const apiLimiter = rateLimit({ - windowMs: 60000, - max: 20, + windowMs: env.rateLimitWindow * 1000, + max: env.rateLimitMax || 20, standardHeaders: true, legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), @@ -36,8 +36,8 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }) const apiLimiterStream = rateLimit({ - windowMs: 60000, - max: 25, + windowMs: env.rateLimitWindow * 1000, + max: env.rateLimitMax || 20, standardHeaders: true, legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), @@ -51,7 +51,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.set('trust proxy', ['loopback', 'uniquelocal']); - app.use('/api/:type', cors({ + app.use('/api', cors({ methods: ['GET', 'POST'], ...corsConfig })) diff --git a/src/modules/config.js b/src/modules/config.js index c66521e1..a8500747 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -28,18 +28,25 @@ const // API mode related environment variables apiEnvs = { + apiURL, apiPort: process.env.API_PORT || 9000, apiName: process.env.API_NAME || 'unknown', + listenAddress: process.env.API_LISTEN_ADDRESS, + freebindCIDR: process.platform === 'linux' && process.env.FREEBIND_CIDR, + corsWildcard: process.env.CORS_WILDCARD !== '0', corsURL: process.env.CORS_URL, + cookiePath: process.env.COOKIE_PATH, + tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO), + + rateLimitWindow: (process.env.RATELIMIT_WINDOW && parseInt(process.env.RATELIMIT_WINDOW)) || 60, + rateLimitMax: (process.env.RATELIMIT_MAX && parseInt(process.env.RATELIMIT_MAX)) || 20, + processingPriority: process.platform !== 'win32' && process.env.PROCESSING_PRIORITY - && parseInt(process.env.PROCESSING_PRIORITY), - tiktokDeviceInfo: process.env.TIKTOK_DEVICE_INFO && JSON.parse(process.env.TIKTOK_DEVICE_INFO), - freebindCIDR: process.platform === 'linux' && process.env.FREEBIND_CIDR, - apiURL + && parseInt(process.env.PROCESSING_PRIORITY) } export const From 40ff39d14bf3cdbc900d28a9f6992ce16550d805 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 13:54:43 +0600 Subject: [PATCH 24/68] docs: update list of environment variables --- docs/run-an-instance.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 25145224..95f7a659 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -54,14 +54,16 @@ sudo service nscd start |:----------------------|:----------|:------------------------|:------------| | `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. | | `API_LISTEN_ADDRESS` | `0.0.0.0` | `127.0.0.1` | changes address from which api server is accessible. **if you are using docker, you usually don't need to configure this.** | -| `API_URL` | ➖ | `https://co.wuk.sh/` | changes url from which api server is accessible.
***REQUIRED TO RUN API***. | +| `API_URL` | ➖ | `https://co.wuk.sh/` | changes url from which api server is accessible.
***REQUIRED TO RUN THE API***. | | `API_NAME` | `unknown` | `ams-1` | api server name that is shown in `/api/serverInfo`. | | `CORS_WILDCARD` | `1` | `0` | toggles cross-origin resource sharing.
`0`: disabled. `1`: enabled. | | `CORS_URL` | not used | `https://cobalt.tools/` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. | | `COOKIE_PATH` | not used | `/cookies.json` | path for cookie file relative to main folder. | +| `TIKTOK_DEVICE_INFO` | ➖ | *see below* | device info (including `iid` and `device_id`) for tiktok functionality. required for tiktok to work. see below for more info. | | `PROCESSING_PRIORITY` | not used | `10` | changes `nice` value* for ffmpeg subprocess. available only on unix systems. | -| `TIKTOK_DEVICE_INFO` | ➖ | *see below* | device info (including `iid` and `device_id`) for tiktok functionality. required for tiktok to work. | -| `FREEBIND_CIDR` | ➖ | `2001:db8::/32` | IPv6 prefix used for randomly assigning addresses to cobalt requests. only supported on linux systems. for more info, see below. | +| `FREEBIND_CIDR` | ➖ | `2001:db8::/32` | IPv6 prefix used for randomly assigning addresses to cobalt requests. only supported on linux systems. see below for more info. | +| `RATELIMIT_WINDOW` | `60` | `120` | rate limit time window in **seconds**. | +| `RATELIMIT_MAX` | `20` | `30` | max requests per time window. requests above this amount will be blocked for the rate limit window duration. | \* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)). From fb81c5732440beea8a66f67531c991a9db7f8b08 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 13:58:28 +0600 Subject: [PATCH 25/68] api: remove duplicate defaults --- src/core/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index f31665aa..686414af 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -23,7 +23,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { const apiLimiter = rateLimit({ windowMs: env.rateLimitWindow * 1000, - max: env.rateLimitMax || 20, + max: env.rateLimitMax, standardHeaders: true, legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), @@ -37,7 +37,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { const apiLimiterStream = rateLimit({ windowMs: env.rateLimitWindow * 1000, - max: env.rateLimitMax || 20, + max: env.rateLimitMax, standardHeaders: true, legacyHeaders: false, keyGenerator: req => generateHmac(getIP(req), ipSalt), From adf9267905eeda44a32123e5b54414b3b14a220c Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 13:59:53 +0600 Subject: [PATCH 26/68] api: expose rate limit headers --- src/core/api.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/api.js b/src/core/api.js index 686414af..c5110a05 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -53,7 +53,13 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.use('/api', cors({ methods: ['GET', 'POST'], - ...corsConfig + exposedHeaders: [ + 'Ratelimit-Limit', + 'Ratelimit-Policy', + 'Ratelimit-Remaining', + 'Ratelimit-Reset' + ], + ...corsConfig, })) app.use('/api/json', apiLimiter); From 592bfe24cf43dda7d3731724494e4abbbcb7876e Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 14:20:12 +0600 Subject: [PATCH 27/68] api: remove /api/status --- src/core/api.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index c5110a05..6cf980e1 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -206,10 +206,6 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } }) - app.get('/api/status', (req, res) => { - res.status(200).end() - }) - app.get('/favicon.ico', (req, res) => { res.sendFile(`${__dirname}/src/front/icons/favicon.ico`) }) From b5c81084c803fa698fc296f1593f71270e710511 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 14:20:40 +0600 Subject: [PATCH 28/68] api: clean up imports & consts --- src/core/api.js | 54 ++++++++++++++++++++------------------- src/modules/sub/crypto.js | 8 ++++-- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/core/api.js b/src/core/api.js index 6cf980e1..adc0a940 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -1,25 +1,40 @@ import cors from "cors"; import rateLimit from "express-rate-limit"; -import { randomBytes } from "crypto"; - -const ipSalt = randomBytes(64).toString('hex'); import { env, version } from "../modules/config.js"; -import match from "../modules/processing/match.js"; -import { languageCode } from "../modules/sub/utils.js"; -import { createResponse, normalizeRequest, getIP } from "../modules/processing/request.js"; + +import { generateHmac, generateSalt } from "../modules/sub/crypto.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js"; -import stream from "../modules/stream/stream.js"; +import { languageCode } from "../modules/sub/utils.js"; import loc from "../localization/manager.js"; -import { generateHmac } from "../modules/sub/crypto.js"; + +import { createResponse, normalizeRequest, getIP } from "../modules/processing/request.js"; import { verifyStream, getInternalStream } from "../modules/stream/manage.js"; import { extract } from "../modules/processing/url.js"; +import match from "../modules/processing/match.js"; +import stream from "../modules/stream/stream.js"; + +const acceptRegex = /^application\/json(; charset=utf-8)?$/; + +const ipSalt = generateSalt(); +const corsConfig = env.corsWildcard ? {} : { + origin: env.corsURL, + optionsSuccessStatus: 200 +} export function runAPI(express, app, gitCommit, gitBranch, __dirname) { - const corsConfig = env.corsWildcard ? {} : { - origin: env.corsURL, - optionsSuccessStatus: 200 - }; + const startTime = new Date(); + const startTimestamp = startTime.getTime(); + + const serverInfo = { + version: version, + commit: gitCommit, + branch: gitBranch, + name: env.apiName, + url: env.apiURL, + cors: Number(env.corsWildcard), + startTime: `${startTimestamp}` + } const apiLimiter = rateLimit({ windowMs: env.rateLimitWindow * 1000, @@ -45,9 +60,6 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { return res.sendStatus(429) } }) - - const startTime = new Date(); - const startTimestamp = startTime.getTime(); app.set('trust proxy', ['loopback', 'uniquelocal']); @@ -101,8 +113,6 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { } }) - const acceptRegex = /^application\/json(; charset=utf-8)?$/; - app.post('/api/json', async (req, res) => { const request = req.body; const lang = languageCode(req); @@ -192,15 +202,7 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { app.get('/api/serverInfo', (req, res) => { try { - return res.status(200).json({ - version: version, - commit: gitCommit, - branch: gitBranch, - name: env.apiName, - url: env.apiURL, - cors: Number(env.corsWildcard), - startTime: `${startTimestamp}` - }); + return res.status(200).json(serverInfo); } catch { return res.destroy(); } diff --git a/src/modules/sub/crypto.js b/src/modules/sub/crypto.js index 35b55b38..3a520156 100644 --- a/src/modules/sub/crypto.js +++ b/src/modules/sub/crypto.js @@ -1,6 +1,10 @@ -import { createHmac, createCipheriv, createDecipheriv } from "crypto"; +import { createHmac, createCipheriv, createDecipheriv, randomBytes } from "crypto"; -const algorithm = "aes256" +const algorithm = "aes256"; + +export function generateSalt() { + return randomBytes(64).toString('hex'); +} export function generateHmac(str, salt) { return createHmac("sha256", salt).update(str).digest("base64url"); From d1e8929ee245c7393f0425a78a499b35932522da Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 20:57:48 +0600 Subject: [PATCH 29/68] api: add DURATION_LIMIT env variable duration limit is now in seconds and customizable across instances --- src/config.json | 1 - src/modules/config.js | 3 ++- src/modules/processing/services/bilibili.js | 10 +++++----- src/modules/processing/services/dailymotion.js | 6 +++--- src/modules/processing/services/ok.js | 4 ++-- src/modules/processing/services/reddit.js | 6 +++--- src/modules/processing/services/rutube.js | 6 +++--- src/modules/processing/services/soundcloud.js | 6 +++--- src/modules/processing/services/twitch.js | 4 ++-- src/modules/processing/services/vimeo.js | 4 ++-- src/modules/processing/services/vk.js | 4 ++-- src/modules/processing/services/youtube.js | 4 ++-- 12 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/config.json b/src/config.json index 0a32d220..640b2433 100644 --- a/src/config.json +++ b/src/config.json @@ -1,6 +1,5 @@ { "streamLifespan": 90000, - "maxVideoDuration": 10800000, "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "authorInfo": { "support": { diff --git a/src/modules/config.js b/src/modules/config.js index a8500747..737d9716 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -44,6 +44,8 @@ const rateLimitWindow: (process.env.RATELIMIT_WINDOW && parseInt(process.env.RATELIMIT_WINDOW)) || 60, rateLimitMax: (process.env.RATELIMIT_MAX && parseInt(process.env.RATELIMIT_MAX)) || 20, + durationLimit: (process.env.DURATION_LIMIT && parseInt(process.env.DURATION_LIMIT)) || 10800, + processingPriority: process.platform !== 'win32' && process.env.PROCESSING_PRIORITY && parseInt(process.env.PROCESSING_PRIORITY) @@ -54,7 +56,6 @@ export const audioIgnore = servicesConfigJson.audioIgnore, version = packageJson.version, streamLifespan = config.streamLifespan, - maxVideoDuration = config.maxVideoDuration, genericUserAgent = config.genericUserAgent, repo = packageJson.bugs.url.replace('/issues', ''), authorInfo = config.authorInfo, diff --git a/src/modules/processing/services/bilibili.js b/src/modules/processing/services/bilibili.js index 6da110bf..0475a44c 100644 --- a/src/modules/processing/services/bilibili.js +++ b/src/modules/processing/services/bilibili.js @@ -1,4 +1,4 @@ -import { genericUserAgent, maxVideoDuration } from "../../config.js"; +import { genericUserAgent, env } from "../../config.js"; // TO-DO: higher quality downloads (currently requires an account) @@ -39,8 +39,8 @@ async function com_download(id) { } let streamData = JSON.parse(html.split('')[0]); - if (streamData.data.timelength > maxVideoDuration) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (streamData.data.timelength > env.durationLimit * 1000) { + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; } const [ video, audio ] = extractBestQuality(streamData.data.dash); @@ -79,8 +79,8 @@ async function tv_download(id) { return { error: 'ErrorEmptyDownload' }; } - if (video.duration > maxVideoDuration) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (video.duration > env.durationLimit * 1000) { + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; } return { diff --git a/src/modules/processing/services/dailymotion.js b/src/modules/processing/services/dailymotion.js index 1993ecad..c1fca95d 100644 --- a/src/modules/processing/services/dailymotion.js +++ b/src/modules/processing/services/dailymotion.js @@ -1,5 +1,5 @@ import HLSParser from 'hls-parser'; -import { maxVideoDuration } from '../../config.js'; +import { env } from '../../config.js'; let _token; @@ -73,8 +73,8 @@ export default async function({ id }) { return { error: 'ErrorEmptyDownload' } } - if (media.duration * 1000 > maxVideoDuration) { - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (media.duration > env.durationLimit) { + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; } const manifest = await fetch(media.hlsURL).then(r => r.text()).catch(() => {}); diff --git a/src/modules/processing/services/ok.js b/src/modules/processing/services/ok.js index bde163f6..52e0903f 100644 --- a/src/modules/processing/services/ok.js +++ b/src/modules/processing/services/ok.js @@ -1,4 +1,4 @@ -import { genericUserAgent, maxVideoDuration } from "../../config.js"; +import { genericUserAgent, env } from "../../config.js"; import { cleanString } from "../../sub/utils.js"; const resolutions = { @@ -29,7 +29,7 @@ export default async function(o) { if (videoData.provider !== "UPLOADED_ODKL") return { error: 'ErrorUnsupported' }; if (videoData.movie.is_live) return { error: 'ErrorLiveVideo' }; - if (videoData.movie.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (videoData.movie.duration > env.durationLimit) return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; let videos = videoData.videos.filter(v => !v.disallowed); let bestVideo = videos.find(v => resolutions[v.name] === quality) || videos[videos.length - 1]; diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js index 00b35526..d64f013b 100644 --- a/src/modules/processing/services/reddit.js +++ b/src/modules/processing/services/reddit.js @@ -1,4 +1,4 @@ -import { genericUserAgent, maxVideoDuration } from "../../config.js"; +import { genericUserAgent, env } from "../../config.js"; import { getCookie, updateCookieValues } from "../cookie/manager.js"; async function getAccessToken() { @@ -79,8 +79,8 @@ export default async function(obj) { if (!data.secure_media?.reddit_video) return { error: 'ErrorEmptyDownload' }; - if (data.secure_media?.reddit_video?.duration * 1000 > maxVideoDuration) - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (data.secure_media?.reddit_video?.duration > env.durationLimit) + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; let audio = false, video = data.secure_media?.reddit_video?.fallback_url?.split('?')[0], diff --git a/src/modules/processing/services/rutube.js b/src/modules/processing/services/rutube.js index b26bfb19..77f13512 100644 --- a/src/modules/processing/services/rutube.js +++ b/src/modules/processing/services/rutube.js @@ -1,6 +1,6 @@ import HLS from 'hls-parser'; -import { maxVideoDuration } from "../../config.js"; +import { env } from "../../config.js"; import { cleanString } from '../../sub/utils.js'; async function requestJSON(url) { @@ -35,8 +35,8 @@ export default async function(obj) { if (play.detail || !play.video_balancer) return { error: 'ErrorEmptyDownload' }; if (play.live_streams?.hls) return { error: 'ErrorLiveVideo' }; - if (play.duration > maxVideoDuration) - return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (play.duration > env.durationLimit * 1000) + return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; let m3u8 = await fetch(play.video_balancer.m3u8) .then(r => r.text()) diff --git a/src/modules/processing/services/soundcloud.js b/src/modules/processing/services/soundcloud.js index 2288b5f3..a305d35c 100644 --- a/src/modules/processing/services/soundcloud.js +++ b/src/modules/processing/services/soundcloud.js @@ -1,4 +1,4 @@ -import { maxVideoDuration } from "../../config.js"; +import { env } from "../../config.js"; import { cleanString } from "../../sub/utils.js"; const cachedID = { @@ -77,8 +77,8 @@ export default async function(obj) { if (fileUrl.substring(0, 54) !== "https://api-v2.soundcloud.com/media/soundcloud:tracks:") return { error: 'ErrorEmptyDownload' }; - if (json.duration > maxVideoDuration) - return { error: ['ErrorLengthAudioConvert', maxVideoDuration / 60000] }; + if (json.duration > env.durationLimit * 1000) + return { error: ['ErrorLengthAudioConvert', env.durationLimit / 60] }; let file = await fetch(fileUrl).then(async (r) => { return (await r.json()).url }).catch(() => {}); if (!file) return { error: 'ErrorCouldntFetch' }; diff --git a/src/modules/processing/services/twitch.js b/src/modules/processing/services/twitch.js index a23f0139..0a26e1d3 100644 --- a/src/modules/processing/services/twitch.js +++ b/src/modules/processing/services/twitch.js @@ -1,4 +1,4 @@ -import { maxVideoDuration } from "../../config.js"; +import { env } from "../../config.js"; import { cleanString } from '../../sub/utils.js'; const gqlURL = "https://gql.twitch.tv/gql"; @@ -34,7 +34,7 @@ export default async function (obj) { let clipMetadata = req_metadata.data.clip; - if (clipMetadata.durationSeconds > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (clipMetadata.durationSeconds > env.durationLimit) return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; if (!clipMetadata.videoQualities || !clipMetadata.broadcaster) return { error: 'ErrorEmptyDownload' }; let req_token = await fetch(gqlURL, { diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index 64c4f9c0..b59781ef 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -1,4 +1,4 @@ -import { maxVideoDuration } from "../../config.js"; +import { env } from "../../config.js"; import { cleanString } from '../../sub/utils.js'; const resolutionMatch = { @@ -63,7 +63,7 @@ export default async function(obj) { } } - if (api.video.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (api.video.duration > env.durationLimit) return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; let masterJSON = await fetch(masterJSONURL).then((r) => { return r.json() }).catch(() => { return false }); diff --git a/src/modules/processing/services/vk.js b/src/modules/processing/services/vk.js index d956c3b9..ac3d35e8 100644 --- a/src/modules/processing/services/vk.js +++ b/src/modules/processing/services/vk.js @@ -1,4 +1,4 @@ -import { genericUserAgent, maxVideoDuration } from "../../config.js"; +import { genericUserAgent, env } from "../../config.js"; import { cleanString } from "../../sub/utils.js"; const resolutions = ["2160", "1440", "1080", "720", "480", "360", "240"]; @@ -21,7 +21,7 @@ export default async function(o) { let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); if (Number(js.mvData.is_active_live) !== 0) return { error: 'ErrorLiveVideo' }; - if (js.mvData.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (js.mvData.duration > env.durationLimit) return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; for (let i in resolutions) { if (js.player.params[0][`url${resolutions[i]}`]) { diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index d11b2739..898f0884 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -1,5 +1,5 @@ import { Innertube, Session } from 'youtubei.js'; -import { maxVideoDuration } from '../../config.js'; +import { env } from '../../config.js'; import { cleanString } from '../../sub/utils.js'; import { fetch } from 'undici' @@ -92,7 +92,7 @@ export default async function(o) { if (bestQuality) bestQuality = qual(bestQuality); if (!bestQuality && !o.isAudioOnly || !hasAudio) return { error: 'ErrorYTTryOtherCodec' }; - if (info.basic_info.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; + if (info.basic_info.duration > env.durationLimit) return { error: ['ErrorLengthLimit', env.durationLimit / 60] }; let checkBestAudio = (i) => (i.has_audio && !i.has_video), audio = adaptive_formats.find(i => checkBestAudio(i) && !i.is_dubbed); From 9eed3b5b67187cf708418168fdb4e8209175e404 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 21:00:11 +0600 Subject: [PATCH 30/68] config: move streamLifespan --- src/config.json | 1 - src/modules/config.js | 2 +- src/modules/stream/manage.js | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/config.json b/src/config.json index 640b2433..c81828af 100644 --- a/src/config.json +++ b/src/config.json @@ -1,5 +1,4 @@ { - "streamLifespan": 90000, "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", "authorInfo": { "support": { diff --git a/src/modules/config.js b/src/modules/config.js index 737d9716..cfe9e36c 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -45,6 +45,7 @@ const rateLimitMax: (process.env.RATELIMIT_MAX && parseInt(process.env.RATELIMIT_MAX)) || 20, durationLimit: (process.env.DURATION_LIMIT && parseInt(process.env.DURATION_LIMIT)) || 10800, + streamLifespan: 90, processingPriority: process.platform !== 'win32' && process.env.PROCESSING_PRIORITY @@ -55,7 +56,6 @@ export const services = servicesConfigJson.config, audioIgnore = servicesConfigJson.audioIgnore, version = packageJson.version, - streamLifespan = config.streamLifespan, genericUserAgent = config.genericUserAgent, repo = packageJson.bugs.url.replace('/issues', ''), authorInfo = config.authorInfo, diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 86334fa2..e36932c5 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -3,7 +3,7 @@ import { randomBytes } from "crypto"; import { nanoid } from "nanoid"; import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js"; -import { streamLifespan, env } from "../config.js"; +import { env } from "../config.js"; import { strict as assert } from "assert"; // optional dependency @@ -12,7 +12,7 @@ const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); const M3U_SERVICES = ['dailymotion', 'vimeo', 'rutube']; const streamCache = new NodeCache({ - stdTTL: streamLifespan/1000, + stdTTL: env.streamLifespan/1000, checkperiod: 10, deleteOnExpire: true }) @@ -28,7 +28,7 @@ export function createStream(obj) { const streamID = nanoid(), iv = randomBytes(16).toString('base64url'), secret = randomBytes(32).toString('base64url'), - exp = new Date().getTime() + streamLifespan, + exp = new Date().getTime() + env.streamLifespan, hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt), streamData = { exp: exp, From c6f1184e55d7963e976f265c5ce02b9cf790dbf5 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 16 May 2024 21:28:42 +0600 Subject: [PATCH 31/68] services: clean up functions and improve readability --- src/modules/processing/services/bilibili.js | 2 +- src/modules/processing/services/ok.js | 18 ++++++--- src/modules/processing/services/pinterest.js | 8 ++-- src/modules/processing/services/reddit.js | 6 +-- src/modules/processing/services/rutube.js | 2 +- src/modules/processing/services/soundcloud.js | 21 +++++----- src/modules/processing/services/streamable.js | 5 ++- src/modules/processing/services/twitch.js | 10 +++-- src/modules/processing/services/vimeo.js | 40 ++++++++++++------- src/modules/processing/services/vine.js | 5 ++- src/modules/processing/services/vk.js | 5 ++- src/modules/processing/services/youtube.js | 2 +- 12 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/modules/processing/services/bilibili.js b/src/modules/processing/services/bilibili.js index 0475a44c..c562406a 100644 --- a/src/modules/processing/services/bilibili.js +++ b/src/modules/processing/services/bilibili.js @@ -31,7 +31,7 @@ function extractBestQuality(dashData) { async function com_download(id) { let html = await fetch(`https://bilibili.com/video/${id}`, { headers: { "user-agent": genericUserAgent } - }).then((r) => { return r.text() }).catch(() => { return false }); + }).then(r => r.text()).catch(() => {}); if (!html) return { error: 'ErrorCouldntFetch' }; if (!(html.includes('')[0] + const data = JSON.parse(json) + detail = data["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"] + } catch { + return { error: 'ErrorCouldntFetch' }; + } let video, videoFilename, audioFilename, audio, images, - filenameBase = `tiktok_${detail.author.unique_id}_${postId}`, + filenameBase = `tiktok_${detail.author.uniqueId}_${postId}`, bestAudio = 'm4a'; - images = detail.image_post_info?.images; + images = detail.imagePost?.images; - let playAddr = detail.video.play_addr_h264; + let playAddr = detail.video.playAddr; if (obj.h265) { - playAddr = detail.video.bit_rate[0].play_addr - } - if (!playAddr && detail.video.play_addr) { - playAddr = detail.video.play_addr + const h265PlayAddr = detail.video.bitrateInfo.find(b => b.CodecType.includes("h265"))?.PlayAddr.UrlList[0] + playAddr = h265PlayAddr || playAddr } if (!obj.isAudioOnly && !images) { - video = playAddr.url_list[0]; + video = playAddr; videoFilename = `${filenameBase}.mp4`; } else { - let fallback = playAddr.url_list[0]; - audio = fallback; + audio = playAddr; audioFilename = `${filenameBase}_audio`; - if (obj.fullAudio || fallback.includes("music")) { - audio = detail.music.play_url.url_list[0] - audioFilename = `${filenameBase}_audio_original` + + if (obj.fullAudio || !audio) { + audio = detail.music.playUrl; + audioFilename += `_original` } - if (audio.slice(-4) === ".mp3") bestAudio = 'mp3'; + if (audio.includes("mime_type=audio_mpeg")) bestAudio = 'mp3'; } if (video) return { @@ -80,12 +85,9 @@ export default async function(obj) { bestAudio } if (images) { - let imageLinks = []; - for (let i in images) { - let sel = images[i].display_image.url_list; - sel = sel.filter(p => p.includes(".jpeg?")) - imageLinks.push({url: sel[0]}) - } + let imageLinks = images + .map(i => i.imageURL.urlList.find(p => p.includes(".jpeg?"))) + .map(url => ({ url })) return { picker: imageLinks, urls: audio, diff --git a/src/modules/stream/shared.js b/src/modules/stream/shared.js index 8555f8ba..42df2758 100644 --- a/src/modules/stream/shared.js +++ b/src/modules/stream/shared.js @@ -1,4 +1,5 @@ import { genericUserAgent } from "../config.js"; +import { cookie as tiktokCookie } from "../processing/services/tiktok.js"; const defaultHeaders = { 'user-agent': genericUserAgent @@ -13,6 +14,9 @@ const serviceHeaders = { origin: 'https://www.youtube.com', referer: 'https://www.youtube.com', DNT: '?1' + }, + tiktok: { + cookie: tiktokCookie } } @@ -22,5 +26,7 @@ export function closeResponse(res) { } export function getHeaders(service) { - return { ...defaultHeaders, ...serviceHeaders[service] } + // Converting all header values to strings + return Object.entries({ ...defaultHeaders, ...serviceHeaders[service] }) + .reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {}) } \ No newline at end of file From e13f0b9649c77e2402f5685ec58735451e202142 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 02:44:45 +0600 Subject: [PATCH 51/68] readme: update royale link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ce676ac..3e363c64 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ if you want to run your own instance for whatever purpose, [follow this guide](/ it's *highly* recommended to use a docker compose method unless you run for developing/debugging purposes. ## partners -cobalt is sponsored by [royalehosting.net](https://royalehosting.net/), all main instances are currently hosted on their network :) +cobalt is sponsored by [royalehosting.net](https://royalehosting.net/?partner=cobalt), all main instances are currently hosted on their network :) ## ethics and disclaimer cobalt is a tool for easing content downloads from internet and takes ***zero liability***. you are responsible for what you download, how you use and distribute that content. please be mindful when using content of others and always credit original creators. fair use and credits benefit everyone. From cb72a96f48552235cb54f14b121850f2e2128189 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 02:45:20 +0600 Subject: [PATCH 52/68] changelog: remove tiktok broken notice --- src/modules/changelog/changelog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index 5085960c..e90a1a33 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -9,7 +9,7 @@ "width": 1736, "height": 1440 }, - "content": "yesterday, cobalt hit 1 million users around the world! it's an absolutely insane milestone for us and we're incredibly grateful to everyone saving and creating what they love with help of cobalt. thank you for being our friends.\n\nin anticipation of 7 figure user count, we've revamped the cobalt codebase and infrastructure to be faster and more reliable than ever. a combination of many changes has resulted into incredible download speeds (up to 30 MB/s, as tested by both developers in europe).\n\nnote: there's no backend instance in asia just yet, so if you're there, you might experience average speeds *for now*. you can help us afford a dedicated server in asia by donating to cobalt in the \"donate\" menu.\n\nchanges since the last major update\n\nservice improvements:\n*; youtube music support on the main instance is back!\n*; added support for pinterest images and gifs.\n*; cobalt will now use original soundcloud mp3 file when available.\n*; fixed a youtube bug that prevented some videos from downloading.\n\nupdate on tiktok downloads: they're currently not available, but we're working on a fix. follow our twitter/x account or related github issue for timely updates.\n\nui/ux improvements:\n*; cobalt web app is now fully optimized for ipad. you can add it to home screen from share menu to make it act like a native app!\n*; majorly reduced vertical padding when viewing cobalt in mobile web browser, allowing for more content at once. most noticeable on smaller screens.\n*; status bar color is now dynamic in the web browser on ios and web app on android.\n*; web app on android feels way more native than before.\n*; filename style icons are no longer blurry in safari.\n*; changelog notification no longer overlaps with dynamic island on newer iphones when cobalt is installed as a web app.\n*; fixed safe area padding.\n\nother changes:\n*; added support for freebind, made by one of the cobalt developers.\n*; rate limit and max video length limits are now customizable through environment variables.\n*; cobalt api now returns rate limit headers at all times.\n*; majorly cleaned up the codebase: removed unnecessary functions, rewrote those that were cryptic and confusing. it's way more comprehensible and contribution-friendly than ever before.\n*; moved the cobalt repo to our organization on github. everything stayed the same and all old links link back to it.\n\nnote for instance hosters:\nalong with cobalt repo, the docker image also moved! please update the url for it in your config along with watchtower args to include restarting containers (just in case) as seen in updated docker compose example. we're mirroring packages to old url for now, but it won't last forever.\n\nthat's it for now! hope you have an amazing day and share the 1 million celebration with us :)\n\njoin our discord server to discuss everything cobalt there" + "content": "yesterday, cobalt hit 1 million users around the world! it's an absolutely insane milestone for us and we're incredibly grateful to everyone saving and creating what they love with help of cobalt. thank you for being our friends.\n\nin anticipation of 7 figure user count, we've revamped the cobalt codebase and infrastructure to be faster and more reliable than ever. a combination of many changes has resulted into incredible download speeds (up to 30 MB/s, as tested by both developers in europe).\n\nnote: there's no backend instance in asia just yet, so if you're there, you might experience average speeds *for now*. you can help us afford a dedicated server in asia by donating to cobalt in the \"donate\" menu.\n\nchanges since the last major update\n\nservice improvements:\n*; youtube music support on the main instance is back!\n*; added support for pinterest images and gifs.\n*; cobalt will now use original soundcloud mp3 file when available.\n*; fixed a youtube bug that prevented some videos from downloading.\n\nui/ux improvements:\n*; cobalt web app is now fully optimized for ipad. you can add it to home screen from share menu to make it act like a native app!\n*; majorly reduced vertical padding when viewing cobalt in mobile web browser, allowing for more content at once. most noticeable on smaller screens.\n*; status bar color is now dynamic in the web browser on ios and web app on android.\n*; web app on android feels way more native than before.\n*; filename style icons are no longer blurry in safari.\n*; changelog notification no longer overlaps with dynamic island on newer iphones when cobalt is installed as a web app.\n*; fixed safe area padding.\n\nother changes:\n*; added support for freebind, made by one of the cobalt developers.\n*; rate limit and max video length limits are now customizable through environment variables.\n*; cobalt api now returns rate limit headers at all times.\n*; majorly cleaned up the codebase: removed unnecessary functions, rewrote those that were cryptic and confusing. it's way more comprehensible and contribution-friendly than ever before.\n*; moved the cobalt repo to our organization on github. everything stayed the same and all old links link back to it.\n\nnote for instance hosters:\nalong with cobalt repo, the docker image also moved! please update the url for it in your config along with watchtower args to include restarting containers (just in case) as seen in updated docker compose example. we're mirroring packages to old url for now, but it won't last forever.\n\nthat's it for now! hope you have an amazing day and share the 1 million celebration with us :)\n\njoin our discord server to discuss everything cobalt there" }, "history": [{ "version": "7.13", From e48d222d2e10200da31726883ee28f9d3b9ee0a5 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 02:46:00 +0600 Subject: [PATCH 53/68] docs/run-an-instance: update issue path --- docs/run-an-instance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 28b9dc77..af408d49 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -41,7 +41,7 @@ setup script installs all needed `npm` dependencies, but you have to install `no 4. done. ### ubuntu 22.04 workaround -`nscd` needs to be installed and running so that the `ffmpeg-static` binary can resolve DNS ([#101](https://github.com/wukko/cobalt/issues/101#issuecomment-1494822258)): +`nscd` needs to be installed and running so that the `ffmpeg-static` binary can resolve DNS ([#101](https://github.com/imputnet/cobalt/issues/101#issuecomment-1494822258)): ```bash sudo apt install nscd From 5199b3d8fd160f4eb19f7322d18b1978c52ec5e2 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 02:46:23 +0600 Subject: [PATCH 54/68] package: bump version to 7.14.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b00f21d6..f5e17282 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.14", + "version": "7.14.1", "author": "imput", "exports": "./src/cobalt.js", "type": "module", From 03b1248b5fe8412add1065c446aefed7e3869809 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 21 May 2024 21:27:23 +0000 Subject: [PATCH 55/68] url/extract: convert input to URL object if passed as string --- src/modules/processing/url.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 51883b1b..a006402c 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -127,6 +127,10 @@ export function normalizeURL(url) { } export function extract(url) { + if (!(url instanceof URL)) { + url = new URL(url); + } + const host = getHostIfValid(url); if (!host || !services[host].enabled) { From 2831bc06ad48282aa0fa3cc94936794d8f67d676 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 21 May 2024 21:27:35 +0000 Subject: [PATCH 56/68] tiktok: fix shortlink parsing --- src/modules/processing/services/tiktok.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/processing/services/tiktok.js b/src/modules/processing/services/tiktok.js index 6d3cf314..8edb2a5b 100644 --- a/src/modules/processing/services/tiktok.js +++ b/src/modules/processing/services/tiktok.js @@ -20,7 +20,8 @@ export default async function(obj) { if (!html) return { error: 'ErrorCouldntFetch' }; if (html.startsWith('", - "device_id": "", - "channel": "googleplay", - "app_name": "musical_ly", - "version_code": "310503", - "device_platform": "android", - "device_type": "Redmi+7", - "os_version": "13" -}' -``` - -you can compress the json to save space. if you're using a `.env` file then the line would would look like this (***note the quotes***): -``` -TIKTOK_DEVICE_INFO='{"iid":"","device_id":"","channel":"googleplay","app_name":"musical_ly","version_code":"310503","device_platform":"android","device_type":"Redmi+7","os_version":"13"}' -``` - #### FREEBIND_CIDR setting a `FREEBIND_CIDR` allows cobalt to pick a random IP for every download and use it for all requests it makes for that particular download. to use freebind in cobalt, you need to follow its [setup instructions](https://github.com/imputnet/freebind.js?tab=readme-ov-file#setup) first. if you configure this option while running cobalt From 1cbceea69c81bccf935e2a64e013210c7243666d Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 07:33:51 +0600 Subject: [PATCH 59/68] config.json: update troubleshooting link --- src/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.json b/src/config.json index 13633906..640fb2f5 100644 --- a/src/config.json +++ b/src/config.json @@ -52,7 +52,7 @@ "saveToGalleryShortcut": "https://www.icloud.com/shortcuts/14e9aebf04b24156acc34ceccf7e6fcd", "saveToFilesShortcut": "https://www.icloud.com/shortcuts/2134cd9d4d6b41448b2201f933542b2e", "statusPage": "https://status.cobalt.tools/", - "troubleshootingGuide": "https://github.com/wukko/cobalt/blob/current/docs/troubleshooting.md" + "troubleshootingGuide": "https://github.com/imputnet/cobalt/blob/current/docs/troubleshooting.md" }, "celebrations": { "01-01": "🎄", From ba75b90992ff5e386dfa42468a3eb6a5342208ef Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 07:54:38 +0600 Subject: [PATCH 60/68] docs: update main instance api url --- docs/api.md | 2 +- docs/examples/docker-compose.example.yml | 8 ++++---- docs/run-an-instance.md | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api.md b/docs/api.md index 57509669..87ef2489 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,7 +2,7 @@ this document provides info about methods and acceptable variables for all cobalt api requests. ``` -👍 you can use co.wuk.sh instance in your projects for free, just don't be an asshole. +👍 you can use api.cobalt.tools in your projects for free, just don't be an asshole. ``` ## POST: `/api/json` diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index 8f49b61c..fcbcfc63 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -17,8 +17,8 @@ services: #- 127.0.0.1:9000:9000 environment: - # replace https://co.wuk.sh/ with your instance's target url in same format - API_URL: "https://co.wuk.sh/" + # replace https://api.cobalt.tools/ with your instance's target url in same format + API_URL: "https://api.cobalt.tools/" # replace eu-nl with your instance's distinctive name API_NAME: "eu-nl" # if you want to use cookies when fetching data from services, uncomment the next line and the lines under volume @@ -49,8 +49,8 @@ services: environment: # replace https://cobalt.tools/ with your instance's target url in same format WEB_URL: "https://cobalt.tools/" - # replace https://co.wuk.sh/ with preferred api instance url - API_URL: "https://co.wuk.sh/" + # replace https://api.cobalt.tools/ with preferred api instance url + API_URL: "https://api.cobalt.tools/" labels: - com.centurylinklabs.watchtower.scope=cobalt diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 23d6682e..a440d11d 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -54,7 +54,7 @@ sudo service nscd start |:----------------------|:----------|:------------------------|:------------| | `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. | | `API_LISTEN_ADDRESS` | `0.0.0.0` | `127.0.0.1` | changes address from which api server is accessible. **if you are using docker, you usually don't need to configure this.** | -| `API_URL` | ➖ | `https://co.wuk.sh/` | changes url from which api server is accessible.
***REQUIRED TO RUN THE API***. | +| `API_URL` | ➖ | `https://api.cobalt.tools/` | changes url from which api server is accessible.
***REQUIRED TO RUN THE API***. | | `API_NAME` | `unknown` | `ams-1` | api server name that is shown in `/api/serverInfo`. | | `CORS_WILDCARD` | `1` | `0` | toggles cross-origin resource sharing.
`0`: disabled. `1`: enabled. | | `CORS_URL` | not used | `https://cobalt.tools/` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. | @@ -74,13 +74,13 @@ in a docker container, you also need to set the `API_LISTEN_ADDRESS` env to `127 `network_mode` for the container to `host`. ### variables for web -| variable name | default | example | description | -|:---------------------|:---------------------|:------------------------|:--------------------------------------------------------------------------------------| -| `WEB_PORT` | `9001` | `9001` | changes port from which frontend server is accessible. | -| `WEB_URL` | ➖ | `https://cobalt.tools/` | changes url from which frontend server is accessible.
***REQUIRED TO RUN WEB***. | -| `API_URL` | `https://co.wuk.sh/` | `https://co.wuk.sh/` | changes url which is used for api requests by frontend clients. | -| `SHOW_SPONSORS` | `0` | `1` | toggles sponsor list in about popup.
`0`: disabled. `1`: enabled. | -| `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo.
`0`: disabled. `1`: enabled. | -| `PLAUSIBLE_HOSTNAME` | ➖ | `plausible.io`* | enables plausible analytics with provided hostname as receiver backend. | +| variable name | default | example | description | +|:---------------------|:----------------------------|:----------------------------|:--------------------------------------------------------------------------------------| +| `WEB_PORT` | `9001` | `9001` | changes port from which frontend server is accessible. | +| `WEB_URL` | ➖ | `https://cobalt.tools/` | changes url from which frontend server is accessible.
***REQUIRED TO RUN WEB***. | +| `API_URL` | `https://api.cobalt.tools/` | `https://api.cobalt.tools/` | changes url which is used for api requests by frontend clients. | +| `SHOW_SPONSORS` | `0` | `1` | toggles sponsor list in about popup.
`0`: disabled. `1`: enabled. | +| `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo.
`0`: disabled. `1`: enabled. | +| `PLAUSIBLE_HOSTNAME` | ➖ | `plausible.io`* | enables plausible analytics with provided hostname as receiver backend. | \* don't use plausible.io as receiver backend unless you paid for their cloud service. use your own domain when hosting community edition of plausible. refer to their [docs](https://plausible.io/docs) when needed. From 6c7aa57978a9a79bfd19d8e7fabda2e074265ef8 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 07:54:49 +0600 Subject: [PATCH 61/68] setup: update main instance api url --- src/modules/setup.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/setup.js b/src/modules/setup.js index 80032fcd..137f6271 100644 --- a/src/modules/setup.js +++ b/src/modules/setup.js @@ -36,7 +36,7 @@ function setup() { rl.question(q, r1 => { switch (r1.toLowerCase()) { case 'api': - console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: co.wuk.sh")); + console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: api.cobalt.tools")); rl.question(q, apiURL => { ob.API_URL = `http://localhost:9000/`; @@ -83,13 +83,13 @@ function setup() { if (webPort && (webURL === "localhost" || !webURL)) ob.WEB_URL = `http://localhost:${webPort}/`; console.log( - Bright("\nOne last thing: what default API domain should be used? (co.wuk.sh)\nIf it's hosted locally, make sure to include the port:") + Cyan(" localhost:9000") + Bright("\nOne last thing: what default API domain should be used? (api.cobalt.tools)\nIf it's hosted locally, make sure to include the port:") + Cyan(" localhost:9000") ); rl.question(q, apiURL => { ob.API_URL = `https://${apiURL.toLowerCase()}/`; if (apiURL.includes(':')) ob.API_URL = `http://${apiURL.toLowerCase()}/`; - if (!apiURL) ob.API_URL = "https://co.wuk.sh/"; + if (!apiURL) ob.API_URL = "https://api.cobalt.tools/"; final() }) }); From 209fa32ec2a0212dfc912133492a2738eee84585 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 07:54:58 +0600 Subject: [PATCH 62/68] readme: update main instance api url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e363c64..c67631be 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ this list is not final and keeps expanding over time. if support for a service y ## cobalt api cobalt has an open api that you can use in your projects *for free~*. it's easy and straightforward to use, [check out the docs](/docs/api.md) to learn how to use it. -✅ you can use the main api instance ([co.wuk.sh](https://co.wuk.sh/)) in your **personal** projects. +✅ you can use the main api instance ([api.cobalt.tools](https://api.cobalt.tools/)) in your **personal** projects. ❌ you cannot use the free api commercially (anywhere that's gated behind paywalls or ads). host your own instance for this. we reserve the right to restrict abusive/excessive access to the main instance api. From 18a159dab87d9e8a28032dd7aa0e872f1a49d0ec Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 15:56:49 +0600 Subject: [PATCH 63/68] servicesConfig: add support for m.tiktok links --- src/modules/processing/servicesConfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index f714b827..a862da71 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -55,8 +55,8 @@ }, "tiktok": { "alias": "tiktok videos, photos & audio", - "patterns": [":user/video/:postId", ":id", "t/:id", ":user/photo/:postId"], - "subdomains": ["vt", "vm"], + "patterns": [":user/video/:postId", ":id", "t/:id", ":user/photo/:postId", "v/:id.html"], + "subdomains": ["vt", "vm", "m"], "enabled": true }, "vimeo": { From 46f1b296d708d815824d3308805f9fe1a9067f3f Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 15:59:01 +0600 Subject: [PATCH 64/68] gitignore: allow package-lock.json & clean up --- .gitignore | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index a21273d6..a7c47612 100644 --- a/.gitignore +++ b/.gitignore @@ -4,18 +4,10 @@ desktop.ini # npm node_modules -package-lock.json # secrets .env -# page build -min -build - -# stuff i already made but delayed -future - # docker docker-compose.yml @@ -24,3 +16,6 @@ docker-compose.yml # cookie file cookies.json + +# page build +build From 6412baf79b7314e7df07fb25ef424138d32e0bc6 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 22 May 2024 09:57:28 +0000 Subject: [PATCH 65/68] chore: commit npm lockfile so that we can do stuff like `npm clean-install` --- package-lock.json | 1139 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1139 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9052067e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1139 @@ +{ + "name": "cobalt", + "version": "7.14.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cobalt", + "version": "7.14.1", + "license": "AGPL-3.0", + "dependencies": { + "content-disposition-header": "0.6.0", + "cors": "^2.8.5", + "dotenv": "^16.0.1", + "esbuild": "^0.14.51", + "express": "^4.18.1", + "express-rate-limit": "^6.3.0", + "ffmpeg-static": "^5.1.0", + "hls-parser": "^0.10.7", + "ipaddr.js": "2.1.0", + "nanoid": "^4.0.2", + "node-cache": "^5.1.2", + "psl": "1.9.0", + "set-cookie-parser": "2.6.0", + "undici": "^5.19.1", + "url-pattern": "1.0.3", + "youtubei.js": "^9.3.0" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "freebind": "^0.2.2" + } + }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition-header": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/content-disposition-header/-/content-disposition-header-0.6.0.tgz", + "integrity": "sha512-+PKF6Y7JHgwD2Dld48L5jozC6pGDdnioJHfM7VyfF92pAU6wDF/N8K/cfecoq7b19KnelOnjntkVyN5hMm2F+w==" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-rate-limit": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz", + "integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==", + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, + "node_modules/ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "hasInstallScript": true, + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/freebind": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/freebind/-/freebind-0.2.2.tgz", + "integrity": "sha512-H3PdpY9gMa46ra2CkDLZxAqvJiiazb5ZpQcvmOriB0cPVRzFSi+pXY/m3mN31tkZhE/0D4O+pCGTMezh/jwLnw==", + "optional": true, + "dependencies": { + "ipaddr.js": "2.1.0", + "syscall-napi": "0.0.6", + "undici": "^5.27.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hls-parser": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/hls-parser/-/hls-parser-0.10.9.tgz", + "integrity": "sha512-BGtoMd3sbhndHYKVQEoEmpuRe6++iWQc38x84x5uqitYmP6V5opKHWaYJT4GAwhnVRaNhMTuZbS0AruzPw4qgw==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/jintr": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-1.1.0.tgz", + "integrity": "sha512-Tu9wk3BpN2v+kb8yT6YBtue+/nbjeLFv4vvVC4PJ7oCidHKbifWhvORrAbQfxVIQZG+67am/mDagpiGSVtvrZg==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "dependencies": { + "acorn": "^8.8.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/syscall-napi": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/syscall-napi/-/syscall-napi-0.0.6.tgz", + "integrity": "sha512-qHbwjyFXAAekKUXxl70lhDiBYJ3e7XM7kQwu7LV3F0pHMenKox+VcZPZkRkhdmL/wNJD3NmrMGnL7161kdecUQ==", + "hasInstallScript": true, + "optional": true + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-pattern": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/url-pattern/-/url-pattern-1.0.3.tgz", + "integrity": "sha512-uQcEj/2puA4aq1R3A2+VNVBgaWYR24FdWjl7VNW83rnWftlhyzOZ/tBjezRiC2UkIzuxC8Top3IekN3vUf1WxA==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/youtubei.js": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-9.4.0.tgz", + "integrity": "sha512-8plCOZD2WabqWSEgZU3RjzigIIeR7sF028EERJENYrC9xO/6awpLMZfeoE1gNrNEbKcA+bzbMvonqlvBdxGdKg==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "dependencies": { + "jintr": "^1.1.0", + "tslib": "^2.5.0", + "undici": "^5.19.1" + } + } + } +} From ae36f2d39f9afeae80ae870c43ae10528aa7f303 Mon Sep 17 00:00:00 2001 From: wukko Date: Wed, 22 May 2024 16:02:40 +0600 Subject: [PATCH 66/68] license: update copyright --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index f79473af..7d1945a3 100644 --- a/LICENSE +++ b/LICENSE @@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - cobalt is possibly the nicest social media downloader out there. - Copyright (C) 2022 wukko + save what you love with cobalt. + Copyright (C) 2024 imput This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by From 701182d753a59c2635512b8a39b41a46a9c6dfaf Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 22 May 2024 10:05:03 +0000 Subject: [PATCH 67/68] build: enable layer caching for docker image built on github --- .github/workflows/docker.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0d166c74..ee3d757b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -55,3 +55,5 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 3197cee473aa9f65cd0ea5ba2929db6afe33ebc0 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 22 May 2024 10:21:38 +0000 Subject: [PATCH 68/68] build: clean up dockerfile and use clean-install --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c98785d9..0896c116 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,10 @@ WORKDIR /app COPY package*.json ./ -RUN apt-get update && \ - apt-get install -y git python3 build-essential && \ - npm install && \ +RUN apt-get update && \ + apt-get install -y git python3 build-essential && \ + npm ci && \ + npm cache clean --force && \ apt purge --autoremove -y python3 build-essential && \ rm -rf ~/.cache/ /var/lib/apt/lists/*