From 9a7a5b69ee356061834b4195aee1a94ab25c04b4 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 26 Jun 2024 16:48:05 -0700 Subject: [PATCH 1/3] Repo maintenance & add nowplaying --- .gitignore | 4 ++- config.example.json | 27 +++++++++++--------- liberals/nowplaying.js | 26 ++++++++++++++++++++ package-lock.json | 56 ++++++++++++++++++++++++++++-------------- package.json | 2 +- routes/nowplaying.js | 28 ++++++++++++++++++--- 6 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 liberals/nowplaying.js diff --git a/.gitignore b/.gitignore index b068519..c4978fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ config.json bun.lockb -GITVERSION \ No newline at end of file +GITVERSION +todo.txt +proto.js \ No newline at end of file diff --git a/config.example.json b/config.example.json index 2192d6e..7725a1f 100644 --- a/config.example.json +++ b/config.example.json @@ -8,12 +8,6 @@ "host": "hazeldale:5984", "authorization": "" }, - - "cider": { - "targetHosts": ["localhost:10769"], - - "authKeysDoc": "cider" - }, "mailjet": { "apiKey": "", @@ -23,14 +17,25 @@ "authKeysDoc": "mailjet" }, - "etyd": { - "randomHexLength": 6, - "authKeyInDb": "apiAuthKeys.etyd" - }, - "blog": { "postsDirectory": "C:/Users/natha/Downloads/proto/posts", "postsDirUrl": "/posts" + }, + + "nowplaying": { + "lastfm": { + "apiKey": "", + "target": "enstrayed" + }, + "jellyfin": { + "apiKey": "", + "host": "", + "target": "" + }, + "cider": { + "apiKeys": [], + "hosts": [] + } } } \ No newline at end of file diff --git a/liberals/nowplaying.js b/liberals/nowplaying.js new file mode 100644 index 0000000..ab7ec30 --- /dev/null +++ b/liberals/nowplaying.js @@ -0,0 +1,26 @@ +const { globalConfig } = require("../index.js") + +async function queryLastfm() { + return await fetch(`https://ws.audioscrobbler.com/2.0/?format=json&method=user.getrecenttracks&limit=1&api_key=${globalConfig.nowplaying.lastfm.apiKey}&user=${globalConfig.nowplaying.lastfm.target}`).then(response => response.json()).then(response => { + if (response["recenttracks"] == undefined) { + return 1 + } else { + if (response.recenttracks.track[0]["@attr"] == undefined) { + return 1 + } else { + return { + "json": { + "songName": response.recenttracks.track[0].name, + "artistName": response.recenttracks.track[0].artist["#text"], + "albumName": response.recenttracks.track[0].album["#text"], + "artUrl": response.recenttracks.track[0].image[3]["#text"], + "link": response.recenttracks.track[0].url + }, + "html": `Album Art

I'm listening to

${response.recenttracks.track[0].name} by ${response.recenttracks.track[0].artist["#text"]}

from ${response.recenttracks.track[0].album["#text"]}

View on Last.fm
` + } + } + } + }) +} + +module.exports = { queryLastfm } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 43da3b4..0b770df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,22 +9,43 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { - "express": "^4.18.2", - "typescript": "^5.4.3" + "express": "^4.18.2" }, "devDependencies": { + "@types/bun": "^1.0.12", "@types/node": "^20.12.3" } }, - "node_modules/@types/node": { - "version": "20.12.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.3.tgz", - "integrity": "sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==", + "node_modules/@types/bun": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.1.5.tgz", + "integrity": "sha512-7RprVDMF+1o+EWSo7F1+iJpkfNz+Ikw9K//vwambcY+D1QHXfb9l7jWY1hSBfuFEkW9yFAhkMzP2uTi1pQXoqw==", "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.1.14" + } + }, + "node_modules/@types/node": { + "version": "20.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz", + "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==", + "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -65,6 +86,17 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bun-types": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.14.tgz", + "integrity": "sha512-esfxOvECTkjEuUEHBOoOo590Qggf4b9cz5h29AOB2SKt3yZwG3LbAX4iIYwWZX7GnO7vaY5hIdcQygwN0xGdNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "~20.12.8", + "@types/ws": "~8.5.10" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -679,18 +711,6 @@ "node": ">= 0.6" } }, - "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 01392d7..633d388 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "dependencies": { "express": "^4.18.2" }, - "name": "api", + "name": "enstrayedapi", "version": "1.0.0", "description": "api.enstrayed.com", "main": "index.js", diff --git a/routes/nowplaying.js b/routes/nowplaying.js index 2254ab3..ab7e75b 100644 --- a/routes/nowplaying.js +++ b/routes/nowplaying.js @@ -1,11 +1,33 @@ const { app, globalConfig } = require("../index.js") +const { queryLastfm } = require("../liberals/nowplaying.js") + +var timeSinceLastLastfmQuery = Date.now()-5000 +var cachedLastfmResult = {} + +const notPlayingAnythingPlaceholder = { + "json": { + "playing": false + }, + "html": `I'm not currently listening to anything.` +} app.get("/nowplaying", (rreq,rres) => { - if (rreq.query.format === "html") { - rres.send("The /nowplaying endpoint is currently under construction.") + + if (Date.now() < timeSinceLastLastfmQuery+5000) { + rres.send(cachedLastfmResult[rreq.query.format] ?? cachedLastfmResult.json) } else { - rres.send({"message":"The /nowplaying endpoint is currently under construction."}) + timeSinceLastLastfmQuery = Date.now() + queryLastfm().then(response => { + if (response == 1) { + cachedLastfmResult = notPlayingAnythingPlaceholder + } else { + cachedLastfmResult = response + } + + rres.send(cachedLastfmResult[rreq.query.format] ?? cachedLastfmResult.json) + }) } + }) module.exports = {app} \ No newline at end of file From 6ea66c04070050c981e9fe1c5f172f0faf70f2ac Mon Sep 17 00:00:00 2001 From: Enstrayed <48845980+Enstrayed@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:16:01 -0700 Subject: [PATCH 2/3] More repo maintenance & add Readme --- Dockerfile | 9 ---- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++ config.example.json | 41 ----------------- deprecated/cider.js | 101 ------------------------------------------ docker-compose.yml | 10 ----- package.json | 6 +-- 6 files changed, 106 insertions(+), 166 deletions(-) delete mode 100644 Dockerfile create mode 100644 README.md delete mode 100644 config.example.json delete mode 100644 deprecated/cider.js delete mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index fbcad3c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM node:20 -WORKDIR /app - -RUN git clone https://github.com/enstrayed/enstrayedapi . -RUN git config --global --add safe.directory /app -RUN git show --oneline -s >> GITVERSION -RUN npm install - -ENTRYPOINT [ "node", "index.js" ] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..de1fa55 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# Enstrayed API +This repository contains the code for my personal web API written in JavaScript using the Express framework. + +## Documentation +This file contains documentation relevant for development and deployment, but not necessarily usage. Information for all endpoints is available [on my website](https://music.youtube.com/watch?v=n2LxBXS4jJM&si=ZpzGNBGvQp1cFicW). + +## Issues +If you would like to report a bug or security issue, please open a GitHub issue. If you are the operator of a service this application accesses, use the contact information provided during registration with your service to contact me directly. + +## Configuration +On startup, this application will look for two files. If either cannot be read, it will exit with an error code. +1. `config.json` contains settings required for operation and API keys used for calling external services. +2. `GITVERSION` contains the commit that was cloned when the container was built. + +
Configuration Example + +* `couchdb.host`: Hostname/IP address and port of a CouchDB server. +* `couchdb.authorization`: Username & password used to access the CouchDB server, in HTTP Basic authentication format, e.g. `username:password`. +* `blog.postsDirectory`: Directory that will be parsed when calling /blogposts. If running in Docker this directory will need to be mounted to the container. +* `blog.postsDirUrl`: Location of the posts directory on the web server. +* `nowplaying.*.target`: Set to the Last.fm/Jellyfin username to query for playback information. + +```json +{ + "startup": { + "apiPort": 8081, + "routesDir": "./routes" + }, + + "couchdb": { + "host": "hazeldale:5984", + "authorization": "" + }, + + "mailjet": { + "apiKey": "", + "senderAddress": "apinotifications@enstrayed.com", + "senderName": "API Notifications", + + "authKeysDoc": "mailjet" + }, + + "blog": { + "postsDirectory": "C:/Users/natha/Downloads/proto/posts", + "postsDirUrl": "/posts" + }, + + "nowplaying": { + "lastfm": { + "apiKey": "", + "target": "enstrayed" + }, + "jellyfin": { + "apiKey": "", + "host": "", + "target": "" + }, + "cider": { + "apiKeys": [], + "hosts": [] + } + } + +} +``` + +
+ +## Docker +In production, this application is designed to be run in Docker, and the container built by pulling the latest commit from the main branch. As such, deploying this application is just a matter of creating a directory and copying the Dockerfile: + +> [!IMPORTANT] +> Please review the Configuration section of this document for important information. By default, the `config.json` file is expected to be mounted into the container at `/app/config.json`. + +```dockerfile +FROM node:20 +WORKDIR /app + +RUN git clone https://github.com/enstrayed/enstrayedapi . +RUN git config --global --add safe.directory /app +RUN git show --oneline -s >> GITVERSION +RUN npm install + +ENTRYPOINT [ "node", "index.js" ] +``` + +
Docker Compose File + +```yaml +--- +services: + enstrayedapi: + build: + context: . + image: enstrayedapi + container_name: enstrayedapi + restart: unless-stopped + volumes: + - ./config.json:/app/config.json +``` + +
+ +## License +If for whatever reason you want to, you are free to adapt this code for your own projects or as reference. However, this software is provided as-is with no warranty or agreement to support it. \ No newline at end of file diff --git a/config.example.json b/config.example.json deleted file mode 100644 index 7725a1f..0000000 --- a/config.example.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "startup": { - "apiPort": 8081, - "routesDir": "./routes" - }, - - "couchdb": { - "host": "hazeldale:5984", - "authorization": "" - }, - - "mailjet": { - "apiKey": "", - "senderAddress": "apinotifications@enstrayed.com", - "senderName": "API Notifications", - - "authKeysDoc": "mailjet" - }, - - "blog": { - "postsDirectory": "C:/Users/natha/Downloads/proto/posts", - "postsDirUrl": "/posts" - }, - - "nowplaying": { - "lastfm": { - "apiKey": "", - "target": "enstrayed" - }, - "jellyfin": { - "apiKey": "", - "host": "", - "target": "" - }, - "cider": { - "apiKeys": [], - "hosts": [] - } - } - -} \ No newline at end of file diff --git a/deprecated/cider.js b/deprecated/cider.js deleted file mode 100644 index 048566c..0000000 --- a/deprecated/cider.js +++ /dev/null @@ -1,101 +0,0 @@ -const { app, db, globalConfig } = require("../index.js") // Get globals from index - -var timeSinceLastCiderQuery = Date.now()-2000; -var currentListening = {} -var currentListeningHtml = "" - -app.get("/cider", (rreq,rres) => { - - rres.send("Cider endpoint is temporarily unavailable.") -}) - -// app.get("/cider", (rreq,rres) => { // GET current listening from target - -// if (Date.now() < timeSinceLastCiderQuery+2000) { -// rres.send(currentListening); // If it has been <2 seconds since the last request, return the cached result. -// } else { -// getCurrentListening(globalConfig.cider.targetHosts[0],"json").then(funcRes => { -// if (funcRes == 1) { -// rres.sendStatus(503) // If there was a problem getting the upstream JSON, return 503 Service Unavailable. -// } else { -// // Required (I think?) because of CORS. -// currentListening = funcRes -// rres.send(funcRes) -// } -// }) -// } - -// }) - -// app.post("/cider", (rreq,rres) => { // POST stop listening on cider target - -// fetch(`http://${globalConfig.couchdb.host}/apiauthkeys/${globalConfig.cider.authKeysDoc}`, { -// headers: { -// "Authorization": `Basic ${btoa(globalConfig.couchdb.authorization)}` -// } -// }).then(dbRes => dbRes.json()).then(dbRes => { - -// if (dbRes.status == 404) { // If document containing cider auth keys does not exist -// console.log(`ERROR: Could not find apiauthkeys/${globalConfig.mailjet.authKeysDoc}`) -// rres.sendStatus(500) // Refuse request -// } else { -// if (dbRes["content"][rreq.get("Authorization").split("_")[0]] === rreq.get("Authorization").split("_")[1]) { - -// fetch(`http://${globalConfig.cider.targetHosts[0]}/stop`).then(fres => { // send GET /stop to cider target -// if (fres.status == 204) { -// console.log(`${rreq.get("cf-connecting-ip")} POST /cider returned 200 KEY:${rreq.get("Authorization")}`) -// rres.sendStatus(200) // if that works then 200 -// } else { -// rres.sendStatus(500) // otherwise lol -// } -// }).catch(ferror => { -// rres.sendStatus(503) // and if a problem happens its probably cause cider target is unavailable -// }) - -// } else { -// console.log(`${rreq.get("cf-connecting-ip")} POST /cider returned 401`) // log ip of unauthorized requests -// rres.sendStatus(401) // received auth key was not in database -// } -// } -// }) - -// }) - -// 2024-04-10: Retrieves currentPlayingSong JSON from specified Cider host and -// returns JSON/HTML containing the useful bits if successful, returning 1 if not. - -// async function getCurrentListening(host,contentType) { // Host should be hostname/ip & port only. -// timeSinceLastCiderQuery = Date.now(); // Save last time function was run, used to indicate when the cache needs refreshed. -// return await fetch(`http://${host}/currentPlayingSong`).then(fetchRes => { -// if (fetchRes.status == 502) { -// return 1 // If the upstream server returns 502 (Bad Gateway) then internally return 1, indicating error. -// } else { -// return fetchRes.json().then(jsonRes => { -// if (jsonRes.info.name == undefined) { -// return 1 // If Cider is running but not playing a song this check prevents an undefined variable error. -// } else { -// if (contentType === "json") { -// return { -// "songName": jsonRes.info.name, -// "artistName": jsonRes.info.artistName, -// "albumName": jsonRes.info.albumName, -// "songLinkUrl": jsonRes.info.url.songLink, -// "endtimeEpochInMs": jsonRes.info.endTime, -// "artworkUrl": jsonRes.info.artwork.url.replace("{w}", jsonRes.info.artwork.width).replace("{h}", jsonRes.info.artwork.height) -// } -// } else if (contentType === "html") { -// return `Album Art

I'm listening to

${`${jsonRes.info.name} by ${jsonRes.info.artistName}`}

from ${jsonRes.info.albumName}

song.link
` -// } else { -// return 1 -// } - -// } -// }) -// } -// }).catch(fetchError => { -// console.error("Error fetch()ing upstream Cider host: "+fetchError) -// return 1 // If something else happens then log it and return 1, indicating error. -// }) -// } - -module.exports = {app} // export routes to be imported by index for execution \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4f58d54..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -services: - enstrayedapi: - build: - context: . - image: enstrayedapi - container_name: enstrayedapi - restart: unless-stopped - volumes: - - ./config.json:/app/config.json \ No newline at end of file diff --git a/package.json b/package.json index 633d388..6794ec1 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,8 @@ }, "name": "enstrayedapi", "version": "1.0.0", - "description": "api.enstrayed.com", + "description": "EnstrayedAPI", "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, "repository": { "type": "git", "url": "git+https://github.com/enstrayed/enstrayedapi.git" @@ -18,7 +15,6 @@ "bugs": { "url": "https://github.com/enstrayed/enstrayedapi/issues" }, - "homepage": "https://api.enstrayed.com", "devDependencies": { "@types/bun": "^1.0.12", "@types/node": "^20.12.3" From 49f9927ab94724ff266b2637947dac5d48e93b6c Mon Sep 17 00:00:00 2001 From: Enstrayed <48845980+Enstrayed@users.noreply.github.com> Date: Sat, 6 Jul 2024 10:52:00 -0700 Subject: [PATCH 3/3] quick fix for couchdb --- README.md | 2 +- liberals/authorization.js | 6 +----- routes/etyd.js | 20 ++++---------------- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index de1fa55..8319009 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains the code for my personal web API written in JavaScript using the Express framework. ## Documentation -This file contains documentation relevant for development and deployment, but not necessarily usage. Information for all endpoints is available [on my website](https://music.youtube.com/watch?v=n2LxBXS4jJM&si=ZpzGNBGvQp1cFicW). +This file contains documentation relevant for development and deployment, but not necessarily usage. Information for all endpoints is available [on my website](https://enstrayed.com/posts/20240409-API-Documentation.html). ## Issues If you would like to report a bug or security issue, please open a GitHub issue. If you are the operator of a service this application accesses, use the contact information provided during registration with your service to contact me directly. diff --git a/liberals/authorization.js b/liberals/authorization.js index 4ca6348..49a14ed 100644 --- a/liberals/authorization.js +++ b/liberals/authorization.js @@ -1,11 +1,7 @@ const { globalConfig } = require("../index.js") async function checkAuthorization(documentToUse,keyToCheck) { - return await fetch(`http://${globalConfig.couchdb.host}/apiauthkeys/${documentToUse}`, { - headers: { - "Authorization": `Basic ${btoa(globalConfig.couchdb.authorization)}` - } - }).then(fetchRes => { + return await fetch(`${globalConfig.couchdbHost}/apiauthkeys/${documentToUse}`).then(fetchRes => { if (fetchRes.status === 404) { // If document doesnt exist fail gracefully diff --git a/routes/etyd.js b/routes/etyd.js index ca39d10..60b9cd8 100644 --- a/routes/etyd.js +++ b/routes/etyd.js @@ -2,11 +2,7 @@ const { app, globalConfig } = require("../index.js") // Get globals from index const { checkAuthorization } = require("../liberals/authorization.js") app.get("/etyd*", (rreq,rres) => { - fetch(`http://${globalConfig.couchdb.host}/etyd${rreq.path.replace("/etyd","")}`, { - headers: { - "Authorization": `Basic ${btoa(globalConfig.couchdb.authorization)}` - } - }).then(dbRes => { + fetch(`${globalConfig.couchdbHost}/etyd${rreq.path.replace("/etyd","")}`).then(dbRes => { if (dbRes.status == 404) { rres.sendStatus(404) } else { @@ -36,21 +32,16 @@ app.delete("/etyd*", (rreq,rres) => { rres.sendStatus(401) } else if (authRes === true) { // Authorization successful - fetch(`http://${globalConfig.couchdb.host}/etyd${rreq.path.replace("/etyd", "")}`, { - headers: { - "Authorization": `Basic ${btoa(globalConfig.couchdb.authorization)}` - } - }).then(dbRes => { + fetch(`${globalConfig.couchdbHost}/etyd${rreq.path.replace("/etyd", "")}`).then(dbRes => { if (dbRes.status == 404) { rres.sendStatus(404) } else { dbRes.json().then(dbRes => { - fetch(`http://${globalConfig.couchdb.host}/etyd${rreq.path.replace("/etyd", "")}`, { + fetch(`${globalConfig.couchdbHost}/etyd${rreq.path.replace("/etyd", "")}`, { method: "DELETE", headers: { - "Authorization": `Basic ${btoa(globalConfig.couchdb.authorization)}`, "If-Match": dbRes["_rev"] // Using the If-Match header is easiest for deleting entries in couchdb } }).then(fetchRes => { @@ -92,10 +83,7 @@ app.post("/etyd*", (rreq,rres) => { console.log(`${rreq.get("cf-connecting-ip")} POST ${rreq.path} returned 400 KEY: ${rreq.get("Authorization")}`) rres.sendStatus(400) } else { - fetch(`http://${globalConfig.couchdb.host}/etyd${rreq.path.replace("/etyd", "")}`, { - headers: { - "Authorization": `Basic ${btoa(globalConfig.couchdb.authorization)}` - }, + fetch(`${globalConfig.couchdbHost}/etyd${rreq.path.replace("/etyd", "")}`, { method: "PUT", body: JSON.stringify({ "content": {