Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||||||
Expand Down
1 change: 1 addition & 0 deletions api/src/processing/match-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default function({
case "vk":
case "tiktok":
case "xiaohongshu":
case "newgrounds":
params = { type: "proxy" };
break;

Expand Down
12 changes: 12 additions & 0 deletions api/src/processing/match.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -249,6 +250,17 @@ export default async function({ host, patternMatch, params, isSession }) {
});
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"
Expand Down
3 changes: 3 additions & 0 deletions api/src/processing/service-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export const services = {
"url_shortener/:shortLink"
],
},
newgrounds: {
patterns: [":type/:method/:id"]
},
reddit: {
patterns: [
"comments/:id",
Expand Down
7 changes: 6 additions & 1 deletion api/src/processing/service-patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,9 @@ export const testers = {
"xiaohongshu": pattern =>
pattern.id?.length <= 24 && pattern.token?.length <= 64
|| pattern.shareId?.length <= 24,
}

"newgrounds": (patternMatch) =>
(patternMatch.type == 'portal' && patternMatch.method == 'view')
|| (patternMatch.type == 'audio' && patternMatch.method == 'listen')
&& patternMatch.id?.length >= 1,
}
156 changes: 156 additions & 0 deletions api/src/processing/services/newgrounds.js
Original file line number Diff line number Diff line change
@@ -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' };
}
42 changes: 42 additions & 0 deletions api/src/util/tests/newgrounds.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
Loading