From 61de303dc4b4fa98eb61fbf50a57eec50c53bc52 Mon Sep 17 00:00:00 2001 From: wukko Date: Thu, 10 Jul 2025 00:49:33 +0600 Subject: [PATCH] api: add support for newgrounds closes #620, replaces #1368 Co-authored-by: hyperdefined --- api/README.md | 1 + api/src/processing/match-action.js | 1 + api/src/processing/match.js | 8 ++ api/src/processing/service-config.js | 6 ++ api/src/processing/service-patterns.js | 3 + api/src/processing/services/newgrounds.js | 103 ++++++++++++++++++++++ api/src/util/tests/newgrounds.json | 42 +++++++++ 7 files changed, 164 insertions(+) create mode 100644 api/src/processing/services/newgrounds.js create mode 100644 api/src/util/tests/newgrounds.json diff --git a/api/README.md b/api/README.md index 36c1dc89..3bdfb519 100644 --- a/api/README.md +++ b/api/README.md @@ -23,6 +23,7 @@ if the desired service isn't supported yet, feel free to create an appropriate i | instagram | ✅ | ✅ | ✅ | ➖ | ➖ | | facebook | ✅ | ❌ | ✅ | ➖ | ➖ | | loom | ✅ | ❌ | ✅ | ✅ | ➖ | +| newgrounds | ✅ | ✅ | ✅ | ✅ | ✅ | | ok.ru | ✅ | ❌ | ✅ | ✅ | ✅ | | pinterest | ✅ | ✅ | ✅ | ➖ | ➖ | | reddit | ✅ | ✅ | ✅ | ❌ | ❌ | diff --git a/api/src/processing/match-action.js b/api/src/processing/match-action.js index 6d1b0254..5852b19d 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -180,6 +180,7 @@ export default function({ case "ok": case "xiaohongshu": + case "newgrounds": params = { type: "proxy" }; break; diff --git a/api/src/processing/match.js b/api/src/processing/match.js index 957c80d0..1265297c 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -29,6 +29,7 @@ import loom from "./services/loom.js"; import facebook from "./services/facebook.js"; import bluesky from "./services/bluesky.js"; import xiaohongshu from "./services/xiaohongshu.js"; +import newgrounds from "./services/newgrounds.js"; let freebind; @@ -268,6 +269,13 @@ export default async function({ host, patternMatch, params, authType }) { }); break; + case "newgrounds": + r = await newgrounds({ + ...patternMatch, + quality: params.videoQuality, + }); + break; + default: return createResponse("error", { code: "error.api.service.unsupported" diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 3ffcf10a..906c23da 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -74,6 +74,12 @@ export const services = { "url_shortener/:shortLink" ], }, + newgrounds: { + patterns: [ + "portal/view/:id", + "audio/listen/:audioId", + ] + }, reddit: { patterns: [ "comments/:id", diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index fd6daef9..e68a20be 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -79,4 +79,7 @@ export const testers = { "xiaohongshu": pattern => pattern.id?.length <= 24 && pattern.token?.length <= 64 || pattern.shareId?.length <= 24, + + "newgrounds": pattern => + pattern.id?.length <= 12 || pattern.audioId?.length <= 12, } diff --git a/api/src/processing/services/newgrounds.js b/api/src/processing/services/newgrounds.js new file mode 100644 index 00000000..7519a6cf --- /dev/null +++ b/api/src/processing/services/newgrounds.js @@ -0,0 +1,103 @@ +import { genericUserAgent } from "../../config.js"; + +const getVideo = async ({ id, quality }) => { + const json = await fetch(`https://www.newgrounds.com/portal/video/${id}`, { + headers: { + "User-Agent": genericUserAgent, + "X-Requested-With": "XMLHttpRequest", // required to get the JSON response + } + }) + .then(r => r.json()) + .catch(() => {}); + + if (!json) return { error: "fetch.empty" }; + + const videoSources = json.sources; + const videoQualities = Object.keys(videoSources); + + if (videoQualities.length === 0) { + return { error: "fetch.empty" }; + } + + const bestVideo = videoSources[videoQualities[0]]?.[0], + userQuality = quality === "2160" ? "4k" : `${quality}p`, + preferredVideo = videoSources[userQuality]?.[0], + video = preferredVideo || bestVideo, + videoQuality = preferredVideo ? userQuality : videoQualities[0]; + + if (!bestVideo || !video.type.includes("mp4")) { + return { error: "fetch.empty" }; + } + + const fileMetadata = { + title: decodeURIComponent(json.title), + artist: decodeURIComponent(json.author), + } + + return { + urls: video.src, + filenameAttributes: { + service: "newgrounds", + id, + title: fileMetadata.title, + author: fileMetadata.artist, + extension: "mp4", + qualityLabel: videoQuality, + resolution: videoQuality, + }, + fileMetadata, + } +} + +const getMusic = async ({ id }) => { + const html = await fetch(`https://www.newgrounds.com/audio/listen/${id}`, { + headers: { + "User-Agent": genericUserAgent, + } + }) + .then(r => r.text()) + .catch(() => {}); + + if (!html) return { error: "fetch.fail" }; + + const params = JSON.parse( + `{${html.split(',"params":{')[1]?.split(',"images":')[0]}}` + ); + if (!params) return { error: "fetch.empty" }; + + if (!params.name || !params.artist || !params.filename || !params.icon) { + return { error: "fetch.empty" }; + } + + const fileMetadata = { + title: decodeURIComponent(params.name), + artist: decodeURIComponent(params.artist), + } + + return { + urls: params.filename, + filenameAttributes: { + service: "newgrounds", + id, + title: fileMetadata.title, + author: fileMetadata.artist, + }, + fileMetadata, + cover: + params.icon.includes(".png?") || params.icon.includes(".jpg?") + ? params.icon + : undefined, + isAudioOnly: true, + bestAudio: "mp3", + } +} + +export default function({ id, audioId, quality }) { + if (id) { + return getVideo({ id, quality }); + } else if (audioId) { + return getMusic({ id: audioId }); + } + + return { error: "fetch.empty" }; +} diff --git a/api/src/util/tests/newgrounds.json b/api/src/util/tests/newgrounds.json new file mode 100644 index 00000000..e0c9c83d --- /dev/null +++ b/api/src/util/tests/newgrounds.json @@ -0,0 +1,42 @@ +[ + { + "name": "regular video", + "url": "https://www.newgrounds.com/portal/view/938050", + "params": {}, + "expected": { + "code": 200, + "status": "tunnel" + } + }, + { + "name": "regular video (audio only)", + "url": "https://www.newgrounds.com/portal/view/938050", + "params": { + "downloadMode": "audio" + }, + "expected": { + "code": 200, + "status": "tunnel" + } + }, + { + "name": "regular video (muted)", + "url": "https://www.newgrounds.com/portal/view/938050", + "params": { + "downloadMode": "mute" + }, + "expected": { + "code": 200, + "status": "tunnel" + } + }, + { + "name": "regular music", + "url": "https://www.newgrounds.com/audio/listen/500476", + "params": {}, + "expected": { + "code": 200, + "status": "tunnel" + } + } +]