Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v14.16.0
v18.17.1
7,776 changes: 5,012 additions & 2,764 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jetsetradio-api",
"version": "1.1.3",
"version": "1.1.4",
"description": "A Data Provider relating to the JSR/JSRF universe",
"type": "module",
"main": "src/app.js",
Expand All @@ -15,12 +15,12 @@
"author": "RazzNBlue",
"license": "Apache 2.0",
"dependencies": {
"axios": "^1.3.4",
"axios": "^1.11.0",
"bcrypt": "^5.1.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"ejs": "^3.1.9",
"express": "^4.18.2",
"ejs": "^3.1.10",
"express": "^4.21.2",
"express-list-endpoints": "^6.0.0",
"express-rate-limit": "^6.7.0",
"memory-cache": "^0.2.0",
Expand Down
122 changes: 122 additions & 0 deletions src/controllers/audioController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Constants from "../constants/dbConstants.js";
import {Actions} from "../config/dbActions.js";
import {performDBAction} from "../config/db.js";
import {sortObjects} from "../utils/utility.js";
import {fetchRandom} from "./utilController.js";
import LOGGER from "../utils/logger.js";

const Audio = "Audio";
const {JSR_DB, JSRF_DB} = Constants;

export const getAudio = async (req, res) => {
try {
const sortByValue = req?.query?.sortBy ? req?.query?.sortBy : undefined;
const sortOrder = req?.query?.orderBy ? req?.query?.orderBy : "asc";
const audio = await fetchAudio(req, "ALL");
if (sortByValue) {
return res.send(audio.sort(sortObjects(sortByValue, sortOrder)));
}
res.send(audio);
} catch (err) {
LOGGER.error(`Could not fetch ALL ${Audio}`, err);
res.status(500).send(`Could not fetch ALL ${Audio} due to error:`, err);
}
};

export const getRandomAudio = async (req, res) => {
try {
res.send(await fetchRandom(req, Audio));
} catch (err) {
LOGGER.error(`Could not fetch random ${Audio}`, err);
res.status(500).json({error: `Failed to fetch random ${Audio}`});
}
};

export const getJSRAudio = async (req, res) => {
try {
const jsrAudio = await fetchAudio(req, JSR_DB);
if (jsrAudio) {
return res.send(jsrAudio);
}
res.status(404).send();
} catch (err) {
LOGGER.error(`Could not fetch JSR ${Audio}`, err);
res.status(500).send(`Could not fetch JSR ${Audio}`, err);
}
};

export const getJSRAudioById = async (req, res) => {
try {
const id = req?.params?.id;
const jsrAudio = await performDBAction(
Actions.fetchById,
JSR_DB,
Audio,
id
);
if (jsrAudio) {
return res.send(jsrAudio);
}
res
.status(404)
.send(`JSR ${Audio} Resource could not be found at requested location`);
} catch (err) {
LOGGER.error(`Could not fetch JSR ${Audio} by ID ${req?.params?.id}`, err);
res
.status(500)
.send(`Could not fetch JSR ${Audio} with ID: ${req.params.id}`, err);
}
};

export const getJSRFAudio = async (req, res) => {
try {
const jsrfAudio = await fetchAudio(req, JSRF_DB);
if (jsrfAudio) {
return res.send(jsrfAudio);
}
res.status(404).send();
} catch (err) {
LOGGER.error(`Could not fetch JSRF ${Audio}`, err);
res.status(500).send(`Could not fetch JSRF ${Audio}`, err);
}
};

export const getJSRFAudioById = async (req, res) => {
try {
const id = req?.params?.id;
const jsrfAudio = await performDBAction(
Actions.fetchById,
JSRF_DB,
Audio,
id
);
if (jsrfAudio) {
return res.send(jsrfAudio);
}
res
.status(404)
.send(`JSRF ${Audio} Resource could not be found at requested location`);
} catch (err) {
LOGGER.error(`Could not fetch JSRF ${Audio} by ID`, err);
res
.status(500)
.send(`Could not fetch JSRF ${Audio} with ID: ${req.params.id}`, err);
}
};

export const fetchAudio = async (req, dbName) => {
if (dbName === "ALL") {
const jsrAudio = await fetchAudio(req, JSR_DB);
const jsrfAudio = await fetchAudio(req, JSRF_DB);
const audio = [...jsrAudio, ...jsrfAudio];
return audio;
}

return await performDBAction(
Actions.fetchWithQuery,
dbName,
Audio,
null,
req?.query
);
};
3 changes: 2 additions & 1 deletion src/controllers/utilController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const {JSR_DB, JSRF_DB, BRC_DB, gameMap} = Constants;
/* Helper Functions to support all other Controllers */
export const fetchRandom = async (req, resource, game) => {
try {
const games = [JSR_DB, JSRF_DB, BRC_DB];
const games =
resource === "Audio" ? [JSR_DB, JSRF_DB] : [JSR_DB, JSRF_DB, BRC_DB];
const selectedGame = req?.query?.game;
const count = Number(req?.query?.count);
const safeCount = Number.isFinite(count) && count > 0 ? count : 1;
Expand Down
10 changes: 8 additions & 2 deletions src/managers/MiddlewareManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,18 @@ const filterPipeRoutes = async (req, endpoints) => {
filteredEndpoints.push(endpoint);
}
if (
(jsrCollections.includes(model) && endpoint.includes("jsr")) ||
((jsrCollections.includes(model) ||
jsrCollections.includes(model + "s")) &&
endpoint.includes("jsr")) ||
endpoint.includes("levels")
) {
filteredEndpoints.push(endpoint);
}
if (jsrfCollections.includes(model) && endpoint.includes("jsrf")) {
if (
(jsrfCollections.includes(model) ||
jsrCollections.includes(model + "s")) &&
endpoint.includes("jsrf")
) {
filteredEndpoints.push(endpoint);
}
if (
Expand Down
43 changes: 33 additions & 10 deletions src/public/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ <h2>Graffiti-Tags</h2>
<pre id="graffiti-tag-response" class="expandable code-snippet" style="display: none;"></pre>
</div>

<div class="main-container">
<h2>Collectibles</h2>
<p>A Collectible is a resource describing a location or place from a specific Game. The only supported game currently is Bomb Rush Cyberfunk.</p>
<p>Endpoints:</p>
<ul>
<li><code class="code-snippet">/collectibles</code> ==> Returns all Collectibles</li>
<li><code class="code-snippet">/collectibles/random</code> ==> Returns a Random Collectible</li>
<li><code class="code-snippet">/collectibles/random?count=10</code> ==> Returns 10 random Collectibles</li>
<li><code class="code-snippet">/collectibles/:id</code> ==> Returns a single Collectible by ID</li>
<li><code class="code-snippet">/collectibles?type=CD</code> ==> Returns all CD Collectibles</li>
<li><code class="code-snippet">/collectibles?character.name=Shine</code> ==> Returns Shine's Collectibles</li>
</ul>
<p>Example Request:</p>
<code class="code-snippet">https://jetsetradio-api.onrender.com/v1/api/collectibles?sortBy=name&limit=5</code>
<p>Example Response: <button class="expandable-button">Expand</button> </p>
<pre id="collectible-response" class="expandable code-snippet" style="display: none;"></pre>
</div>

<div class="main-container">
<h2>Songs</h2>
<p>A Song is a resource describing a song from a specific Game.</p>
Expand Down Expand Up @@ -194,22 +212,27 @@ <h2>Artists</h2>
</div>

<div class="main-container">
<h2>Collectibles</h2>
<p>A Collectible is a resource describing a location or place from a specific Game. The only supported game currently is Bomb Rush Cyberfunk.</p>
<h2>Audio/Quotes</h2>
<p>An Audio is a resource describing a audio/quote by a character from a specific Game.</p>
<p>Endpoints:</p>
<ul>
<li><code class="code-snippet">/collectibles</code> ==> Returns all Collectibles</li>
<li><code class="code-snippet">/collectibles/random</code> ==> Returns a Random Collectible</li>
<li><code class="code-snippet">/collectibles/random?count=10</code> ==> Returns 10 random Collectibles</li>
<li><code class="code-snippet">/collectibles/:id</code> ==> Returns a single Collectible by ID</li>
<li><code class="code-snippet">/collectibles?type=CD</code> ==> Returns all CD Collectibles</li>
<li><code class="code-snippet">/collectibles?character.name=Shine</code> ==> Returns Shine's Collectibles</li>
<li><code class="code-snippet">/audio</code> ==> Returns all Audio</li>
<li><code class="code-snippet">/audio/random</code> ==> Returns a Random Audio From any Game</li>
<li><code class="code-snippet">/audio/random?game=jsr</code> ==> Returns a Random JSR Audio</li>
<li><code class="code-snippet">/audio/random?game=jsrf</code> ==> Returns a Random JSRF Audio</li>
<li><code class="code-snippet">/audio/random?count=10&game=jsrf</code> ==> Returns 10 random JSRF Audio</li>
<li><code class="code-snippet">/audio/jsr</code> ==> Returns all Jet Set Radio Audio</li>
<li><code class="code-snippet">/audio/jsr/:id</code> ==> Returns a single JSR Audio by ID</li>
<li><code class="code-snippet">/audio/jsrf</code> ==> Returns all Jet Set Radio Future Audio</li>
<li><code class="code-snippet">/audio/jsrf/:id</code> ==> Returns a single JSRF Audio by ID</li>
<li><code class="code-snippet">/audio/jsrf?speaker=DJ Professor K</code> ==> Returns DJ Professor K quotes from JSRF</li>
</ul>
<p>Example Request:</p>
<code class="code-snippet">https://jetsetradio-api.onrender.com/v1/api/collectibles?sortBy=name&limit=5</code>
<code class="code-snippet">https://jetsetradio-api.onrender.com/v1/api/songs?gameId=64285b7918c8a0231136dc5a</code>
<p>Example Response: <button class="expandable-button">Expand</button> </p>
<pre id="collectible-response" class="expandable code-snippet" style="display: none;"></pre>
<pre id="audio-response" class="expandable code-snippet" style="display: none;"></pre>
</div>
___________________________________________________________

<div class="main-container">
<h2>Base URL</h2>
Expand Down
5 changes: 5 additions & 0 deletions src/public/js/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import graffitiTagResource from "./examples/graffitiTagExample.js";
import songResource from "./examples/songExample.js";
import artistResource from "./examples/artistExample.js";
import collectibleResource from "./examples/collectibleExample.js";
import audioResource from "./examples/audioExample.js";

const gameResponse = document.querySelector('#game-response');
if (gameResponse) {
Expand Down Expand Up @@ -39,6 +40,10 @@ const collectibleResponse = document.querySelector('#collectible-response');
if (collectibleResponse) {
collectibleResponse.textContent = JSON.stringify(collectibleResource, null, 4);
}
const audioResponse = document.querySelector('#audio-response');
if (audioResponse) {
audioResponse.textContent = JSON.stringify(audioResource, null, 4);
}

const expandableButtons = document.querySelectorAll(".expandable-button");
if (expandableButtons) {
Expand Down
27 changes: 27 additions & 0 deletions src/public/js/examples/audioExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const audioResource = {
_id: "68be5885b4a1c9bd51b61a2c",
key: "dj-professor-k-643c71c18cabe0dcede8691c-https://stat...",
ordinality: 15,
audioUrl: "https://static.wikia.nocookie.net/jetsetradio/image...",
metadata: {
duration: 5.87319727891156,
codec: "PCM",
mimeType: "audio/wave",
sampleRate: 22050,
bitrate: 352800,
numberOfChannels: 1,
},
transcription:
"\"Whoa! Someone's kidnapped the goddess?! That's bad karma, man. Baaaaad..\"",
usage: "When the player reaches the Goddess Statue plinth....",
category: "Chapter 1",
language: "en",
speaker: "DJ Professor K",
gameId: "64285b7918c8a0231136dc5d",
characterId: "643c71c18cabe0dcede8691c",
source: "Jet Set Radio Future",
createdAt: "2025-09-08T04:16:05.199Z",
updatedAt: "2025-09-08T04:16:05.199Z",
};

export default audioResource;
13 changes: 7 additions & 6 deletions src/public/style/docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,16 @@ a {
.main-container p .expandable-button.expanded::after {
content: "-";
vertical-align: middle;
}
}

@font-face {
font-family: jetSet;
src: url('/font/Jet-Set.ttf');
src: url("/font/Jet-Set.ttf");
}

@font-face {
font-family: jetSetItalic;
src: url('/font/Jet-Set-Italic.ttf');
src: url("/font/Jet-Set-Italic.ttf");
}

#img-container {
Expand All @@ -63,12 +63,13 @@ a {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
transform: translate(-50%, -50%);
font-size: 5.5vw;
text-align: center;
}

#home, #swagger {
#home,
#swagger {
position: absolute;
right: 3%;
top: 3%;
Expand Down Expand Up @@ -152,4 +153,4 @@ footer a img {
width: 100%;
height: auto;
min-width: 50px;
}
}
14 changes: 14 additions & 0 deletions src/routes/audioRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import express from 'express';

import { getAudio, getJSRAudio, getJSRAudioById, getJSRFAudio, getJSRFAudioById, getRandomAudio } from '../controllers/audioController.js';

const audio = express.Router();

audio.get('/', async (req, res) => /* #swagger.tags = ['Audio'] */ await getAudio(req, res));
audio.get('/random', async (req, res) => /* #swagger.tags = ['Audio'] */ await getRandomAudio(req, res));
audio.get('/jsr', async (req, res) => /* #swagger.tags = ['Audio'] */ await getJSRAudio(req, res));
audio.get('/jsr/:id', async (req, res) => /* #swagger.tags = ['Audio'] */ await getJSRAudioById(req, res));
audio.get('/jsrf', async (req, res) => /* #swagger.tags = ['Audio'] */ await getJSRFAudio(req, res));
audio.get('/jsrf/:id', async (req, res) => /* #swagger.tags = ['Audio'] */ await getJSRFAudioById(req, res));

export default audio;
2 changes: 2 additions & 0 deletions src/routes/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import characters from "./characterRouter.js";
import locations from "./locationRouter.js";
import levels from "./levelRouter.js";
import collectibles from "./collectibleRouter.js";
import audio from "./audioRouter.js";
import dotenv from "dotenv";
dotenv.config();

Expand All @@ -21,5 +22,6 @@ router.use("/characters", characters);
router.use("/locations", locations);
router.use("/levels", levels);
router.use("/collectibles", collectibles);
router.use("/audio", audio);

export default router;
Loading