Initial Commit
build Details
--> Linted: DOCKERFILE_HADOLINT No errors were found in the linting process Details
--> Linted: EDITORCONFIG No errors were found in the linting process Details
--> Linted: GITHUB_ACTIONS No errors were found in the linting process Details
--> Linted: GITLEAKS No errors were found in the linting process Details
--> Linted: JSON No errors were found in the linting process Details
--> Linted: YAML No errors were found in the linting process Details
lint Details

This commit is contained in:
Chase 2023-08-21 19:34:27 -05:00
commit 752f7109fd
Signed by: chase
GPG Key ID: 9EC29E797878008C
31 changed files with 6850 additions and 0 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
**/.git
**/.node_modules
.env

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_size = 2
[*.json]
insert_final_newline = unset
[*.{md,markdown}]
indent_size = unset
[{Makefile,**.mk}]
# Use tabs for indentation (Makefiles require tabs)
indent_style = tab

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
NODE_ENV=development # development, production
PORT=5000
LASTFM_API_KEY=
LASTFM_USER=
TAUTULLI_BASEURL=
TAUTULLI_API_KEY=
TAUTULLI_USER=

16
.eslintrc.json Normal file
View File

@ -0,0 +1,16 @@
{
"env": {
"es2021": true,
"node": true,
"browser": true
},
"extends": [
"chase"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {},
"globals": {}
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

28
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: Bug Report
about: Report incorrect or unexpected behavior
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
<!--
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files into it.
-->
**To Reproduce**
Steps to reproduce the behavior:
1. Do '...'
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

2
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,2 @@
---
blank_issues_enabled: true

View File

@ -0,0 +1,14 @@
---
name: Feature Request
about: Suggest an idea for the project
title: ''
labels: enhancement
assignees: ''
---
**Describe the feature request you'd like**
A clear and concise description of what you want to have added.
**Additional context**
Add any other context or screenshots about the feature request here.

9
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,9 @@
**Please describe the changes this PR makes and why it should be merged:**
<!--
Please move lines that apply to you out of the comment:
- Code changes have been fully tested, or there are no code changes
- This PR includes breaking changes (methods removed or renamed, parameters moved or removed)
- This PR **only** includes non-code changes, like changes to documentation, README, etc.
- I have linted this PR and it passes all tests
-->

15
.github/SECURITY.md vendored Normal file
View File

@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
The only supported version of this project is the latest commit. Nothing else is supported by us.
## Testing
If you believe you have found a vulnerability, be sure you test it in the latest commit.
## Reporting a Vulnerability
If you believe you have discovered a vulnerability or exploit and ensured it happens on the latest commit, please reach out to the project owner's email.
**Do not make a GitHub Issue for a security vulnerability.**

2
.github/linters/.hadolint.yaml vendored Normal file
View File

@ -0,0 +1,2 @@
---
ignored: [DL3018]

12
.github/linters/.htmlhintrc vendored Normal file
View File

@ -0,0 +1,12 @@
{
"tagname-lowercase": true,
"attr-lowercase": false,
"attr-value-double-quotes": true,
"doctype-first": true,
"tag-pair": true,
"spec-char-escape": false,
"id-unique": true,
"src-not-empty": true,
"attr-no-duplication": true,
"title-require": true
}

6
.github/linters/.stylelintrc.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"rules": {
"selector-class-pattern": null,
"no-missing-end-of-source-newline": null
}
}

4
.github/linters/.yaml-lint.yml vendored Normal file
View File

@ -0,0 +1,4 @@
---
rules:
line-length:
max: 260

28
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: "📦 Publish Docker Container"
on:
push:
branches: [master, main]
env:
PAT: ${{ secrets.PAT }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build the Docker Image
run: docker build . --file Dockerfile --tag chase/life-api:latest --tag git.chse.dev/chase/life-api:latest
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: git.chse.dev
username: chase
password: ${{ secrets.PAT }}
- name: Push the Docker Image to Container Registry
run: docker push git.chse.dev/chase/life-api:latest

39
.github/workflows/linter.yml vendored Normal file
View File

@ -0,0 +1,39 @@
---
name: "🎨 Lint"
on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
jobs:
lint:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip lint]')
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: "📦️ Install Dependencies"
run: |
npm install
- name: "🎨 ESLint"
run: npx eslint . --ext .js,.jsx,.ts,.tsx
- name: "🎨 Super-Linter"
uses: github/super-linter/slim@v5
env:
VALIDATE_ALL_CODEBASE: true
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_JAVASCRIPT_ES: false
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_TYPESCRIPT_ES: false
VALIDATE_TYPESCRIPT_STANDARD: false
VALIDATE_JSCPD: false
VALIDATE_MARKDOWN: false
VALIDATE_NATURAL_LANGUAGE: false

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Packages
node_modules/
# Private Data
config.json
.env
*.db
# Misc
*.bak
.DS_Store

9
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"github.vscode-pull-request-github",
"eamodio.gitlens",
"aaron-bond.better-comments",
"EditorConfig.EditorConfig"
]
}

14
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": true,
"javascript.format.placeOpenBraceOnNewLineForFunctions": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.detectIndentation": false,
"editor.tabSize": 4,
"files.eol": "\n",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

6
Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD [ "node", "." ]

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 chase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
.DEFAULT_GOAL:=help
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
update: ## Bumps installed deps.
@npx npm-check-updates -u && npm install

29
README.md Normal file
View File

@ -0,0 +1,29 @@
<div align="center">
<h1>life-api<br>
<a href="https://chse.dev/donate"><img alt="Donate" src="https://img.shields.io/badge/Donate_To_This_Project-brightgreen"></a>
</h1></div>
An API to show what I'm up to.
## Running
```bash
docker run -d \
--name life-api \
-p 3000:3000 \
-e NODE_ENV=production \
-e LASTFM_API_KEY= \
-e LASTFM_USER= \
-e TAUTULLI_BASEURL= \
-e TAUTULLI_API_KEY= \
-e TAUTULLI_USER= \
git.chse.dev/chase/life-api:latest
```
## Development
```bash
git clone https://git.chse.dev/chase/life-api.git
cd life-api
npm i
cp .env.example .env # add creds.
node .
```

6310
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "life-api",
"version": "1.0.0",
"description": "An API to show what I'm up to.",
"main": "./src/app.js",
"engines": {
"node": ">=16.14.0"
},
"type": "module",
"scripts": {
"start": "node ./src/app.js",
"lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src"
},
"repository": {
"type": "git",
"url": "git+https://github.com/chase/life-api.git"
},
"keywords": [],
"author": "Chase <c@chse.dev> (https://chse.dev)",
"contributors": [
""
],
"license": "MIT",
"bugs": {
"url": "https://github.com/chase/life-api/issues"
},
"homepage": "https://github.com/chase/life-api#readme",
"dependencies": {
"dotenv": "16.3.1",
"express": "4.18.2",
"express-rate-limit": "6.9.0",
"helmet": "7.0.0",
"node-fetch": "3.3.2"
},
"devDependencies": {
"eslint": "8.47.0",
"eslint-config-chase": "1.0.11",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jsdoc": "46.5.0",
"eslint-plugin-unicorn": "48.0.1"
}
}

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

38
src/app.js Normal file
View File

@ -0,0 +1,38 @@
/* eslint-disable import/no-extraneous-dependencies */
import express from "express";
import { config } from "dotenv";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import router from "./router.js";
config();
const port = process.env.PORT || 3000;
// file deepcode ignore UseCsurfForExpress: don't care.
const app = express();
app.set(`trust proxy`, 1); // trust first proxy
app.use(helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
"img-src": [`'self'`, `data:`, `blob:`, `https://*.chse.dev`],
"script-src": [`'self'`, `blob:`, `https://*.chse.dev`],
"default-src": [`'self'`, `blob:`, `https://*.chse.dev`],
},
},
})); // Apply helmet to all requests
const limiter = rateLimit({
windowMs: 5 * 60 * 1000, // First Number = Time in Minutes.
max: 15, // Limit each IP to X requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
skipFailedRequests: true, // Do not count failed requests (status >= 400)
});
app.use(limiter); // Apply rate limiter to all requests
app.use(router); // Apply router from src/router.js
app.listen(port, () =>
{
console.log(`Server listening on http://localhost:${ port }!`);
});

12
src/router.js Normal file
View File

@ -0,0 +1,12 @@
import express from "express";
import index from "./routes/index.js";
import badge from "./routes/badge.js";
import fourOhFour from "./routes/404.js";
const router = new express.Router();
router.use(`/`, index);
router.use(`/badge`, badge);
router.use(`/*`, fourOhFour);
export default router;

10
src/routes/404.js Normal file
View File

@ -0,0 +1,10 @@
import express from "express";
const router = new express.Router();
router.get(`/`, (request, response) =>
{
response.redirect(`/`);
});
export default router;

30
src/routes/badge.js Normal file
View File

@ -0,0 +1,30 @@
import express from "express";
import { config } from "dotenv";
import fetch from "node-fetch";
const router = new express.Router();
config(); // Load environment variables from .env
router.get(`/`, async (request, response) =>
{
const port = process.env.PORT || 3000;
try
{
const apiCall = await fetch(
`http://127.0.0.1:${ port }`
);
const apiResponse = await apiCall.json();
if (apiResponse.error) throw new Error(apiResponse.error);
const currentActivity = apiResponse.currentActivity;
const leftText = currentActivity.split(`: `)[0].replaceAll(` `, `_`).replaceAll(`-`, `--`);
const rightText = currentActivity.split(`: `)[1].replaceAll(` `, `_`).replaceAll(`-`, `--`);
const rightColor = `blue`;
return response.redirect(`https://img.shields.io/badge/${ leftText }-${ rightText }-${ rightColor }`);
}
catch
{
return response.redirect(`https://img.shields.io/badge/Error-Something_went_wrong._:(-red`);
}
});
export default router;

97
src/routes/index.js Normal file
View File

@ -0,0 +1,97 @@
import express from "express";
import { config } from "dotenv";
import fetch from "node-fetch";
const router = new express.Router();
config(); // Load environment variables from .env
router.get(`/`, async (request, response) =>
{
response.header(`Content-Type`, `application/json`);
try
{
let useMockData = false;
if (process.env.NODE_ENV === `development`)
useMockData = true;
let lastFmCall;
let lastFmResponse;
let tautulliCall;
let tautulliResponse;
if (useMockData)
{
// eslint-disable-next-line no-useless-escape
lastFmCall = `{"recenttracks":{"track":[{"artist":{"mbid":"b28ac005-27fe-4bb9-86a2-dbf55ef21469","#text":"Hoodie Allen"},"streamable":"0","image":[{"size":"small","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/34s\/ddfc343f935ca65f55027a0aee6ebe79.jpg"},{"size":"medium","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/64s\/ddfc343f935ca65f55027a0aee6ebe79.jpg"},{"size":"large","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/174s\/ddfc343f935ca65f55027a0aee6ebe79.jpg"},{"size":"extralarge","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/300x300\/ddfc343f935ca65f55027a0aee6ebe79.jpg"}],"mbid":"","album":{"mbid":"","#text":"Better Me"},"name":"Better Me","url":"https:\/\/www.last.fm\/music\/Hoodie+Allen\/_\/Better+Me"},{"artist":{"mbid":"b28ac005-27fe-4bb9-86a2-dbf55ef21469","#text":"Hoodie Allen"},"streamable":"0","image":[{"size":"small","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/34s\/ddfc343f935ca65f55027a0aee6ebe79.jpg"},{"size":"medium","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/64s\/ddfc343f935ca65f55027a0aee6ebe79.jpg"},{"size":"large","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/174s\/ddfc343f935ca65f55027a0aee6ebe79.jpg"},{"size":"extralarge","#text":"https:\/\/lastfm.freetls.fastly.net\/i\/u\/300x300\/ddfc343f935ca65f55027a0aee6ebe79.jpg"}],"mbid":"","album":{"mbid":"","#text":"Better Me"},"name":"Better Me","url":"https:\/\/www.last.fm\/music\/Hoodie+Allen\/_\/Better+Me","date":{"uts":"1692669085","#text":"22 Aug 2023, 01:51"}}],"@attr":{"user":"chxseh","totalPages":"22809","page":"1","perPage":"1","total":"22809"}}}`;
lastFmResponse = JSON.parse(lastFmCall);
tautulliCall = `{"response": {"result": "success", "message": null, "data": {"stream_count": "1", "sessions": [{"user": "${ process.env.TAUTULLI_USER }", "full_title": "House - House Training" }]}}}`;
tautulliResponse = JSON.parse(tautulliCall);
}
else
{
lastFmCall = await fetch(
`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${ process.env.LASTFM_USER }&api_key=${ process.env.LASTFM_API_KEY }&format=json&limit=1`
);
lastFmResponse = await lastFmCall.json();
tautulliCall = await fetch(
`${ process.env.TAUTULLI_BASEURL }/api/v2?apikey=${ process.env.TAUTULLI_API_KEY }&cmd=get_activity`
);
tautulliResponse = await tautulliCall.json();
}
const lastFmTrack = lastFmResponse.recenttracks.track[0];
const lastFmTrackName = lastFmTrack.name;
const lastFmTrackArtist = lastFmTrack.artist[`#text`];
const lastFmTrackAlbum = lastFmTrack.album[`#text`];
let lastFmNowPlaying = false;
if (lastFmTrack[`@attr`] && lastFmTrack[`@attr`].nowplaying === `true`)
lastFmNowPlaying = true;
let tautulliInfo = tautulliResponse.response.data.sessions;
let tautulliNowPlaying = false;
for (const stream of tautulliInfo)
{
if (stream.user === process.env.TAUTULLI_USER)
{
tautulliInfo = stream;
tautulliNowPlaying = true;
break;
}
tautulliInfo = undefined;
}
const tautulliShow = tautulliInfo.full_title;
let currentActivity = `Doing: Nothing`;
if (lastFmNowPlaying)
currentActivity = `Listening To: ${ lastFmTrackName } - ${ lastFmTrackArtist }`;
if (tautulliNowPlaying)
currentActivity = `Watching: ${ tautulliShow }`;
const object = {
currentActivity,
music: {
track: lastFmTrackName,
artist: lastFmTrackArtist,
album: lastFmTrackAlbum,
nowPlaying: lastFmNowPlaying,
},
tvOrMovie: {
content: tautulliShow,
nowPlaying: tautulliNowPlaying,
},
};
// file deepcode ignore XSS: don't care.
return response.send(JSON.stringify(object, undefined, 4));
}
catch (error)
{
const object = {
error: `Something went wrong. :(`,
};
console.error(error);
return response.send(JSON.stringify(object, undefined, 4));
}
});
export default router;