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/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..8319009
--- /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://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.
+
+## 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 2192d6e..0000000
--- a/config.example.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "startup": {
- "apiPort": 8081,
- "routesDir": "./routes"
- },
-
- "couchdb": {
- "host": "hazeldale:5984",
- "authorization": ""
- },
-
- "cider": {
- "targetHosts": ["localhost:10769"],
-
- "authKeysDoc": "cider"
- },
-
- "mailjet": {
- "apiKey": "",
- "senderAddress": "apinotifications@enstrayed.com",
- "senderName": "API Notifications",
-
- "authKeysDoc": "mailjet"
- },
-
- "etyd": {
- "randomHexLength": 6,
- "authKeyInDb": "apiAuthKeys.etyd"
- },
-
- "blog": {
- "postsDirectory": "C:/Users/natha/Downloads/proto/posts",
- "postsDirUrl": "/posts"
- }
-
-}
\ 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 `
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/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/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": `
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..6794ec1 100644
--- a/package.json
+++ b/package.json
@@ -2,13 +2,10 @@
"dependencies": {
"express": "^4.18.2"
},
- "name": "api",
+ "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"
diff --git a/routes/etyd.js b/routes/etyd.js
index 87ed09a..b381c2c 100644
--- a/routes/etyd.js
+++ b/routes/etyd.js
@@ -2,11 +2,7 @@ const { app, globalConfig } = require("../index.js") // Get globals from index
const { checkToken } = require("../liberals/auth.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 {
@@ -35,21 +31,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 => {
@@ -89,10 +80,7 @@ app.post("/etyd*", (rreq,rres) => {
if (rreq.body["url"] == undefined) {
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": {
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