diff --git a/api/README.md b/api/README.md index d740fb21..96a9769d 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 262b3acf..92831ed1 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -141,6 +141,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 fe587f09..b9a00ddb 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -28,6 +28,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; @@ -238,6 +239,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 7e9dfaa4..9c80c2a2 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 0c3d63d4..607f8c95 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -70,4 +70,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