From 985bedf5fbd694f8e52c226db0535573ff1ee635 Mon Sep 17 00:00:00 2001 From: hyperdefined Date: Thu, 8 Aug 2024 13:17:26 -0400 Subject: [PATCH] feat: newgrounds video & audio support --- api/README.md | 1 + api/src/processing/match-action.js | 1 + api/src/processing/match.js | 12 ++ api/src/processing/service-config.js | 3 + api/src/processing/service-patterns.js | 5 + api/src/processing/services/newgrounds.js | 157 ++++++++++++++++++++++ api/src/util/tests.json | 41 +++++- 7 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 api/src/processing/services/newgrounds.js diff --git a/api/README.md b/api/README.md index 38104626..547ea655 100644 --- a/api/README.md +++ b/api/README.md @@ -26,6 +26,7 @@ this list is not final and keeps expanding over time. if support for a service y | 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 31d12e7f..a725c3d1 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -140,6 +140,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab case "ok": case "vk": case "tiktok": + case "newgrounds": params = { type: "proxy" }; break; diff --git a/api/src/processing/match.js b/api/src/processing/match.js index b0022d08..ec36aabb 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -29,6 +29,7 @@ import snapchat from "./services/snapchat.js"; import loom from "./services/loom.js"; import facebook from "./services/facebook.js"; import bluesky from "./services/bluesky.js"; +import newgrounds from "./services/newgrounds.js"; let freebind; @@ -243,6 +244,17 @@ export default async function({ host, patternMatch, params }) { }); break; + case "newgrounds": + r = await newgrounds({ + type: patternMatch.type, + method: patternMatch.method, + id: patternMatch.id, + quality: params.videoQuality, + isAudioOnly, + isAudioMuted + }); + 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 a2136ad0..02d8e769 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -62,6 +62,9 @@ export const services = { "url_shortener/:shortLink" ], }, + newgrounds: { + patterns: [":type/:method/:id"] + }, reddit: { patterns: [ "r/:sub/comments/:id/:title", diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index 7f8982b5..25f1ccec 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -73,4 +73,9 @@ export const testers = { "bsky": pattern => pattern.user?.length <= 128 && pattern.post?.length <= 128, + + "newgrounds": (patternMatch) => + (patternMatch.type == 'portal' && patternMatch.method == 'view') + || (patternMatch.type == 'audio' && patternMatch.method == 'listen') + && patternMatch.id?.length >= 1, } diff --git a/api/src/processing/services/newgrounds.js b/api/src/processing/services/newgrounds.js new file mode 100644 index 00000000..e7ee2228 --- /dev/null +++ b/api/src/processing/services/newgrounds.js @@ -0,0 +1,157 @@ +import { genericUserAgent } from "../../config.js"; +import { cleanString } from "../../misc/utils.js"; + +const qualities = ["4k", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p"]; + +const qualityMatch = { + 2160: "4k", + 1440: "1440p", + 1080: "1080p", + 720: "720p", + 480: "480p", + 360: "360p", + 240: "240p", + 144: "144p" +} + +function getQuality(sources, requestedQuality) { + if (requestedQuality == "max") { + for (let quality of qualities) { + if (sources[quality]) { + return { + src: sources[quality][0].src, + quality: quality, + type: sources[quality][0].type, + } + } + } + } + + let videoData = sources[qualityMatch[requestedQuality]]; + if (videoData) { + return { + src: videoData[0].src, + quality: requestedQuality + "p", + type: videoData[0].type, + } + } + + const qualityIndex = qualities.indexOf(qualityMatch[requestedQuality]); + if (qualityIndex !== -1) { + for (let i = qualityIndex; i >= 0; i--) { + if (sources[qualities[i]]) { + return { + src: sources[qualities[i]][0].src, + quality: qualities[i], + type: sources[qualities[i]][0].type, + } + } + } + for (let i = qualityIndex + 1; i < qualities.length; i++) { + if (sources[qualities[i]]) { + return { + src: sources[qualities[i]][0].src, + quality: qualities[i], + type: sources[qualities[i]][0].type, + } + } + } + } + + return null; +} + +async function getVideo(obj) { + let req = await fetch(`https://www.newgrounds.com/portal/video/${obj.id}`, { + headers: { + 'User-Agent': genericUserAgent, + 'X-Requested-With': 'XMLHttpRequest', + } + }) + .then(request => request.text()) + .catch(() => {}); + + if (!req) return { error: 'ErrorCouldntFetch' }; + + let json; + try { + json = JSON.parse(req); + } catch { return { error: 'ErrorEmptyDownload' }; } + + const videoData = getQuality(json.sources, obj.quality); + if (videoData == null) { + return { error: 'ErrorCouldntFetch' }; + } + if (!videoData.type.includes('mp4')) { + return { error: 'ErrorCouldntFetch' }; + } + + let fileMetadata = { + title: cleanString(decodeURIComponent(json.title)), + artist: cleanString(decodeURIComponent(json.author)), + } + + return { + urls: videoData.src, + filenameAttributes: { + service: "newgrounds", + id: obj.id, + title: fileMetadata.title, + author: fileMetadata.artist, + extension: 'mp4', + qualityLabel: videoData.quality, + resolution: videoData.quality + }, + fileMetadata, + } +} + +async function getMusic(obj) { + let req = await fetch(`https://www.newgrounds.com/audio/listen/${obj.id}`, { + headers: { + 'User-Agent': genericUserAgent, + } + }) + .then(request => request.text()) + .catch(() => {}); + + if (!req) return { error: 'ErrorCouldntFetch' }; + + const titleMatch = req.match(/"name"\s*:\s*"([^"]+)"/); + const artistMatch = req.match(/"artist"\s*:\s*"([^"]+)"/); + const urlMatch = req.match(/"filename"\s*:\s*"([^"]+)"/); + + if (!titleMatch || !artistMatch || !urlMatch) { + return { error: 'ErrorCouldntFetch' }; + } + + const title = titleMatch[1]; + const artist = artistMatch[1]; + const url = urlMatch[1].replace(/\\\//g, '/'); + let fileMetadata = { + title: cleanString(decodeURIComponent(title.trim())), + artist: cleanString(decodeURIComponent(artist.trim())), + } + + return { + urls: url, + filenameAttributes: { + service: "newgrounds", + id: obj.id, + title: fileMetadata.title, + author: fileMetadata.artist, + }, + fileMetadata, + isAudioOnly: true + } +} + +export default function(obj) { + if (obj.type == 'portal') { + return getVideo(obj); + } + if (obj.type == 'audio') { + return getMusic(obj); + } + return { error: 'ErrorUnsupported' }; +} \ No newline at end of file diff --git a/api/src/util/tests.json b/api/src/util/tests.json index 402a2baf..dc8a2669 100644 --- a/api/src/util/tests.json +++ b/api/src/util/tests.json @@ -1493,5 +1493,42 @@ "status": "error" } } - ] -} + ], + "newgrounds": [{ + "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" + } + }] +} \ No newline at end of file