Explorar o código

feat: /album route

Sv443 %!s(int64=2) %!d(string=hai) anos
pai
achega
c4dc28e6e5
Modificáronse 8 ficheiros con 165 adicións e 13 borrados
  1. 66 0
      README.md
  2. 2 1
      changelog.md
  3. 35 0
      src/routes/album.ts
  4. 2 0
      src/routes/index.ts
  5. 15 6
      src/server.ts
  6. 34 1
      src/songData.ts
  7. 9 0
      src/types.d.ts
  8. 2 5
      src/utils.ts

+ 66 - 0
README.md

@@ -31,6 +31,7 @@ All routes support gzip and deflate compression.
 - [Search](#get-search)
     - [Search (only top result)](#get-searchtop)
 - [Translations](#get-translationssongid)
+- [Associated Album](#get-albumsongid)
 
 <br>
 
@@ -338,6 +339,71 @@ All routes support gzip and deflate compression.
 >
 > </details><br>
 
+<br><br>
+
+> ### GET `/album/:songId`
+>
+> This endpoint returns any associated album for a specified song.  
+> Example: `/translations/3093344`
+> 
+> <br>
+>
+> **Optional URL Parameters:**  
+> `?format=json/xml`  
+> Use this optional parameter to change the response format from the default (`json`) to `xml`  
+> The structure of the XML data is similar to the shown JSON data.
+> 
+> <br>
+> 
+> **Response:**  
+> 
+> <details><summary><b>Successful response (click to view)</b></summary>
+>
+> ```jsonc
+> {
+>     "error": false,
+>     "matches": 1,
+>     "translations": [
+>         {
+>             "language": "es",
+>             "title": "Artist - Song (Traducción al Español)",
+>             "url": "https://genius.com/Genius-traducciones-al-espanol-artist-song-al-espanol-lyrics",
+>             "path": "/Genius-traducciones-al-espanol-artist-song-al-espanol-lyrics",
+>             "id": 6942
+>         }
+>     ],
+>     "timestamp": 1234567890123
+> }
+> ```
+>
+> </details>
+> <br>
+> <details><summary>Errored response (click to view)</summary>
+>
+> ```json
+> {
+>     "error": true,
+>     "matches": null,
+>     "message": "Something went wrong",
+>     "timestamp": 1234567890123
+> }
+> ```
+>
+> </details>
+> <br>
+> <details><summary>Response when no result found (click to view)</summary>
+>
+> ```json
+> {
+>     "error": false,
+>     "matches": 0,
+>     "translations": [],
+>     "timestamp": 1234567890123
+> }
+> ```
+>
+> </details><br>
+
 <br><br><br>
 
 <div align="center" style="text-align:center;">

+ 2 - 1
changelog.md

@@ -10,7 +10,8 @@
 <br><br>
 
 ### v1.3.0
-- Added endpoint `/translations/:songId` to receive info about a song's translations
+- Added route `/translations/:songId` to receive info about a song's translation pages
+- Added route `/album/:songId` to get info about the album that the provided song is in
 - Added parameter `?preferLang=en` to always rank results of a certain language higher than the rest
 
 <br>

+ 35 - 0
src/routes/album.ts

@@ -0,0 +1,35 @@
+import { Router } from "express";
+import { paramValid, respond } from "../utils";
+import { getAlbum } from "../songData";
+
+export function initAlbumRoutes(router: Router) {
+    router.get("/album", (req, res) => {
+        const format: string = req.query.format ? String(req.query.format) : "json";
+
+        return respond(res, "clientError", "No song ID provided", format);
+    });
+
+    router.get("/album/:songId", async (req, res) => {
+        try
+        {
+            const { songId } = req.params;
+            const { format: fmt } = req.query;
+
+            const format: string = fmt ? String(fmt) : "json";
+
+            if(!paramValid(songId) || isNaN(Number(songId)))
+                return respond(res, "clientError", "Provided song ID is invalid", format);
+
+            const album = await getAlbum(Number(songId));
+
+            if(!album)
+                return respond(res, "clientError", "Couldn't find any associated album for this song", format);
+
+            return respond(res, "success", { album }, format);
+        }
+        catch(err)
+        {
+            return respond(res, "serverError", `Encountered an internal server error: ${err instanceof Error ? err.message : ""}`, "json");
+        }
+    });
+}

+ 2 - 0
src/routes/index.ts

@@ -1,5 +1,6 @@
 import { Application, Router } from "express";
 import packageJson from "../../package.json";
+import { initAlbumRoutes } from "./album";
 
 import { initSearchRoutes } from "./search";
 import { initTranslationsRoutes } from "./translations";
@@ -7,6 +8,7 @@ import { initTranslationsRoutes } from "./translations";
 const routeFuncs: ((router: Router) => unknown)[] = [
     initSearchRoutes,
     initTranslationsRoutes,
+    initAlbumRoutes,
 ];
 
 const router = Router();

+ 15 - 6
src/server.ts

@@ -55,16 +55,25 @@ export async function init()
         if(authHeader && authTokens.has(authHeader))
             return next();
 
+        const setRateLimitHeaders = (rateLimiterRes: RateLimiterRes) => {
+            res.setHeader("Retry-After", rateLimiterRes.msBeforeNext / 1000);
+            res.setHeader("X-RateLimit-Limit", rateLimiter.points);
+            res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
+            res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
+        };
+
         rateLimiter.consume(req.ip)
+            .then((rateLimiterRes: RateLimiterRes) => {
+                setRateLimitHeaders(rateLimiterRes);
+                return next();
+            })
             .catch((err) => {
                 if(err instanceof RateLimiterRes) {
-                    res.set("Retry-After", String(Math.ceil(err.msBeforeNext / 1000)));
-                    return respond(res, 429, { message: "You are being rate limited" }, fmt);
+                    setRateLimitHeaders(err);
+                    return respond(res, 429, { message: "You are being rate limited. Please try again a little later." }, fmt);
                 }
-                else
-                    return respond(res, 500, { message: "Internal error in rate limiting middleware. Please try again later." }, fmt);
-            })
-            .finally(next);
+                else return respond(res, 500, { message: "Encountered an internal error. Please try again a little later." }, fmt);
+            });
     });
 
     const listener = app.listen(port, host, () => {

+ 34 - 1
src/songData.ts

@@ -4,7 +4,7 @@ import { axios } from "./axios";
 import Fuse from "fuse.js";
 import { nanoid } from "nanoid";
 import { clamp, Stringifiable } from "svcorelib";
-import type { ApiSearchResult, ApiSongResult, GetMetaArgs, GetMetaResult, GetTranslationsArgs, MetaSearchHit, SongMeta, SongTranslation } from "./types";
+import type { Album, ApiSearchResult, ApiSongResult, GetMetaArgs, GetMetaResult, GetTranslationsArgs, MetaSearchHit, SongMeta, SongTranslation } from "./types";
 
 const defaultFuzzyThreshold = 0.65;
 
@@ -195,6 +195,39 @@ export async function getTranslations(songId: number, { preferLang }: GetTransla
     }
 }
 
+export async function getAlbum(songId: number): Promise<Album | null> {
+    try {
+        const accessToken = process.env.GENIUS_ACCESS_TOKEN ?? "ERR_NO_ENV";
+
+        const { data, status } = await axios.get<ApiSongResult>(`https://api.genius.com/songs/${songId}`, {
+            headers: { "Authorization": `Bearer ${accessToken}` },
+        });
+
+        if(status >= 200 && status < 300 && data?.response?.song?.album?.id)
+        {
+            const { response: { song: { album } } } = data;
+
+            return {
+                coverArt: album.cover_art_url ?? null,
+                fullTitle: album.full_title,
+                id: album.id,
+                name: album.name,
+                url: album.url,
+                artist: {
+                    name: album.artist.name ?? null,
+                    url: album.artist.url ?? null,
+                    image: album.artist.image_url ?? null,
+                    headerImage: album.artist.header_image_url ?? null,
+                }
+            };
+        }
+        return null;
+    }
+    catch(e) {
+        return null;
+    }
+}
+
 /**
  * Removes invisible characters and control characters from a string  
  * @throws Throws TypeError if the input is not a string

+ 9 - 0
src/types.d.ts

@@ -148,5 +148,14 @@ type ArtistObj = {
     url: string;
 }
 
+export interface Album {
+    coverArt: string | null;
+    fullTitle: string;
+    id: number;
+    name: string;
+    url: string;
+    artist: Artist;
+}
+
 //#SECTION internal
 export type SupportedMethods = "GET";

+ 2 - 5
src/utils.ts

@@ -58,8 +58,6 @@ export function respond(res: Response, type: ResponseType | number, data: String
         break;
     }
 
-    const mimeType = format !== "xml" ? "application/json" : "application/xml";
-
     resData = {
         error,
         ...(matches === undefined ? {} : { matches }),
@@ -69,7 +67,6 @@ export function respond(res: Response, type: ResponseType | number, data: String
 
     const finalData = format === "xml" ? jsonToXml.parse("data", resData) : resData;
 
-    res.setHeader("Content-Type", mimeType);
-    res.status(statusCode)
-        .send(finalData);
+    res.setHeader("Content-Type", format === "xml" ? "application/xml" : "application/json");
+    res.status(statusCode).send(finalData);
 }