Parcourir la source

bump version, add ?format=xml, add TS types

Sv443 il y a 3 ans
Parent
commit
165f3d5079
7 fichiers modifiés avec 118 ajouts et 16 suppressions
  1. 12 4
      README.md
  2. 18 0
      changelog.md
  3. 27 0
      package-lock.json
  4. 2 1
      package.json
  5. 25 11
      src/server.js
  6. 7 0
      src/songMeta.js
  7. 27 0
      src/types.d.ts

+ 12 - 4
README.md

@@ -29,10 +29,14 @@ All routes support gzip and deflate compression.
 > <br>
 >
 > **Parameters:**  
-> `?q=search%20query`  
+> `?q=search%20query` (required)  
 > This parameter should contain both song and artist name(s) if possible (order doesn't matter, separate with a whitespace).  
 > Sometimes the song name alone might be enough but the results may vary.  
-> If the search query contains special characters, they need to be [percent/URL-encoded.](https://en.wikipedia.org/wiki/Percent-encoding)
+> If the search query contains special characters, they need to be [percent/URL-encoded.](https://en.wikipedia.org/wiki/Percent-encoding)  
+>   
+> `?format=json/xml` (optional)  
+> Use this parameter to change the response format from the default (`json`) to `xml`  
+> The structure of the data closely resembles that of the shown JSON data.
 >
 > <br>
 > <details><summary><b>Successful response (click to view)</b></summary>
@@ -91,10 +95,14 @@ All routes support gzip and deflate compression.
 > <br>
 >
 > **Parameters:**  
-> `?q=search%20query`  
+> `?q=search%20query` (required)  
 > This parameter should contain both song and artist name(s) if possible (order doesn't matter, separate with a whitespace).  
 > Sometimes the song name alone might be enough but the results may vary.  
-> If the search query contains special characters, they need to be [percent/URL-encoded.](https://en.wikipedia.org/wiki/Percent-encoding)
+> If the search query contains special characters, they need to be [percent/URL-encoded.](https://en.wikipedia.org/wiki/Percent-encoding)  
+>   
+> `?format=json/xml` (optional)  
+> Use this parameter to change the response format from the default (`json`) to `xml`  
+> The structure of the data closely resembles that of the shown JSON data.
 >
 > <br>
 > <details><summary><b>Successful response (click to view)</b></summary>

+ 18 - 0
changelog.md

@@ -0,0 +1,18 @@
+## Version History:
+- **[0.2.0](#v020)**
+- [0.1.0](#v010)
+
+<br><br>
+
+## v0.2.0
+- Added XML format
+
+<br><br>
+
+## v0.1.0
+- Added endpoints
+    - `/search` to search for the top result and the 10 best matches
+    - `/search/top` to only search for the top result
+- Added gzip and brotli encoding
+
+<br><br>

+ 27 - 0
package-lock.json

@@ -14,6 +14,7 @@
         "cors": "^2.8.5",
         "express": "^4.17.3",
         "helmet": "^5.0.2",
+        "js2xmlparser": "^4.0.2",
         "kleur": "^4.1.4",
         "rate-limiter-flexible": "^2.3.6",
         "svcorelib": "^1.14.2",
@@ -1069,6 +1070,14 @@
         "js-yaml": "bin/js-yaml.js"
       }
     },
+    "node_modules/js2xmlparser": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
+      "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
+      "dependencies": {
+        "xmlcreate": "^2.0.4"
+      }
+    },
     "node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -1803,6 +1812,11 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
+    },
+    "node_modules/xmlcreate": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
+      "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="
     }
   },
   "dependencies": {
@@ -2604,6 +2618,14 @@
         "argparse": "^2.0.1"
       }
     },
+    "js2xmlparser": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
+      "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
+      "requires": {
+        "xmlcreate": "^2.0.4"
+      }
+    },
     "json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3161,6 +3183,11 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
+    },
+    "xmlcreate": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
+      "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="
     }
   }
 }

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "geniurl",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "description": "Simple \"REST proxy\" to search for lyrics on genius.com",
   "main": "src/index.js",
   "scripts": {
@@ -32,6 +32,7 @@
     "cors": "^2.8.5",
     "express": "^4.17.3",
     "helmet": "^5.0.2",
+    "js2xmlparser": "^4.0.2",
     "kleur": "^4.1.4",
     "rate-limiter-flexible": "^2.3.6",
     "svcorelib": "^1.14.2",

+ 25 - 11
src/server.js

@@ -5,13 +5,16 @@ const helmet = require("helmet");
 const { RateLimiterMemory } = require("rate-limiter-flexible");
 const k = require("kleur");
 const cors = require("cors");
+const jsonToXml = require("js2xmlparser");
 
 const packageJson = require("../package.json");
 const error = require("./error");
-const { getMeta } = require("./lyrics");
+const { getMeta } = require("./songMeta");
 
 /** @typedef {import("svcorelib").JSONCompatible} JSONCompatible */
 /** @typedef {import("express").Response} Response */
+/** @typedef {import("./types").ResponseType} ResponseType */
+/** @typedef {import("./types").ResponseFormat} ResponseFormat */
 
 const app = express();
 
@@ -36,7 +39,7 @@ async function init()
     // on error
     app.use((err, req, res, next) => {
         if(typeof err === "string" || err instanceof Error)
-            return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`);
+            return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`, req?.params?.format);
         else
             return next();
     });
@@ -53,7 +56,7 @@ async function init()
             catch(rlRejected)
             {
                 res.set("Retry-After", rlRejected?.msBeforeNext ? String(Math.round(rlRejected.msBeforeNext / 1000)) || 1 : 1);
-                return respond(res, 429, { message: "You are being rate limited" });
+                return respond(res, 429, { message: "You are being rate limited" }, req?.params?.format);
             }
 
             return next();
@@ -76,25 +79,28 @@ function registerEndpoints()
         });
 
         app.get("/search", async (req, res) => {
-            const { q } = req.query;
+            const { q, format } = req.query;
 
             if(typeof q !== "string" || q.length === 0)
-                return respond(res, "clientError", "No query parameter (?q=...) provided or it is invalid");
+                return respond(res, "clientError", "No query parameter (?q=...) provided or it is invalid", req?.params?.format);
 
             const meta = await getMeta(q);
 
-            return respond(res, "success", meta);
+            // js2xmlparser needs special treatment when using arrays to produce a good XML structure
+            const response = format === "xml" ? { top: meta.top, all: { "result": meta.all } } : meta;
+
+            return respond(res, "success", response, req?.params?.format);
         });
 
         app.get("/search/top", async (req, res) => {
             const { q } = req.query;
 
             if(typeof q !== "string" || q.length === 0)
-                return respond(res, "clientError", "No query parameter (?q=...) provided or it is invalid");
+                return respond(res, "clientError", "No query parameter (?q=...) provided or it is invalid", req?.params?.format);
 
             const meta = await getMeta(q);
 
-            return respond(res, "success", meta.top);
+            return respond(res, "success", meta.top, req?.params?.format);
         });
     }
     catch(err)
@@ -105,16 +111,22 @@ function registerEndpoints()
 
 /**
  * @param {Response} res
- * @param {"serverError"|"clientError"|"success"|number} type Set to number for custom status code
+ * @param {ResponseType|number} type Specifies the type of response and thus a predefined status code - overload: set to number for custom status code
  * @param {JSONCompatible} data JSON object for "success", else an error message string
+ * @param {ResponseFormat} [format]
  */
-function respond(res, type, data)
+function respond(res, type, data, format)
 {
     let statusCode = 500;
     let error = true;
 
     let resData = {};
 
+    if(typeof format !== "string" || !["json", "xml"].includes(format.toLowerCase()))
+        format = "json";
+
+    format = format.toLowerCase();
+
     switch(type)
     {
         case "success":
@@ -148,7 +160,9 @@ function respond(res, type, data)
         timestamp: Date.now(),
     };
 
-    res.status(statusCode).send(resData);
+    const finalData = format === "xml" ? jsonToXml.parse("data", resData) : resData;
+
+    res.status(statusCode).send(finalData);
 }
 
 module.exports = { init };

+ 7 - 0
src/lyrics.js → src/songMeta.js

@@ -1,7 +1,14 @@
 const { default: axios } = require("axios");
 
+/** @typedef {import("./types").SongMeta} SongMeta */
+
 const accessToken = process.env.GENIUS_ACCESS_TOKEN || "ERR_NO_ENV";
 
+/**
+ * Returns meta information about the top 10 results of a search through the genius API
+ * @param {string} search
+ * @returns {Promise<{ top: SongMeta, all: SongMeta[] }>}
+ */
 async function getMeta(search)
 {
     const { data: { response } } = await axios.get(`https://api.genius.com/search?q=${encodeURIComponent(search)}`, {

+ 27 - 0
src/types.d.ts

@@ -0,0 +1,27 @@
+//#SECTION meta
+
+export interface SongMeta {
+    url: string;
+    path: string;
+    meta: {
+        title: string;
+        fullTitle: string;
+        artists: string;
+        primaryArtist: {
+            name: string;
+            url: string;
+        },
+    },
+    resources: {
+        thumbnail: string;
+        image: string;
+    },
+    lyricsState: string;
+    id: number;
+}
+
+//#SECTION server
+
+export type ResponseType = "serverError" | "clientError" | "success";
+
+export type ResponseFormat = "json" | "xml";