diff --git a/api/README.md b/api/README.md index 70d85de67..3684b02af 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 363cb4031..6fb99feb2 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -146,6 +146,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab case "vk": case "tiktok": case "xiaohongshu": + case "newgrounds": params = { type: "proxy" }; break; diff --git a/api/src/processing/match.js b/api/src/processing/match.js index b7be84567..ec7cb7a9e 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; @@ -248,6 +249,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 1dc8bf30d..010e4c720 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -74,6 +74,9 @@ export const services = { "url_shortener/:shortLink" ], }, + newgrounds: { + patterns: [":type/:method/:id"] + }, reddit: { patterns: [ "comments/:id", diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index 8735f1238..5c45b5767 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -78,4 +78,9 @@ export const testers = { "xiaohongshu": pattern => pattern.id?.length <= 24 && pattern.token?.length <= 64 || pattern.shareId?.length <= 12, -} + + "newgrounds": (patternMatch) => + (patternMatch.type == 'portal' && patternMatch.method == 'view') + || (patternMatch.type == 'audio' && patternMatch.method == 'listen') + && patternMatch.id?.length >= 1, +} \ No newline at end of file diff --git a/api/src/processing/services/newgrounds.js b/api/src/processing/services/newgrounds.js new file mode 100644 index 000000000..44039cf74 --- /dev/null +++ b/api/src/processing/services/newgrounds.js @@ -0,0 +1,156 @@ +import { genericUserAgent } from "../../config.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: 'fetch.fail' }; + + let json; + try { + json = JSON.parse(req); + } catch { return { error: 'fetch.empty' }; } + + const videoData = getQuality(json.sources, obj.quality); + if (videoData == null) { + return { error: 'fetch.empty' }; + } + if (!videoData.type.includes('mp4')) { + return { error: 'fetch.empty' }; + } + + let fileMetadata = { + title: decodeURIComponent(json.title), + artist: 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: 'fetch.fail' }; + + 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: 'fetch.empty' }; + } + + const title = titleMatch[1]; + const artist = artistMatch[1]; + const url = urlMatch[1].replace(/\\\//g, '/'); + let fileMetadata = { + title: decodeURIComponent(title.trim()), + artist: 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: 'link.unsupported' }; +} \ No newline at end of file diff --git a/api/src/util/tests/newgrounds.json b/api/src/util/tests/newgrounds.json new file mode 100644 index 000000000..b0101c569 --- /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" + } + } +] \ No newline at end of file