Switch to main #1

Merged
jdf2 merged 28 commits from prod into main 2025-04-26 21:04:34 +00:00
8 changed files with 304 additions and 2 deletions

View File

10
Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:23.6-bullseye-slim
RUN apt-get update && apt-get install curl -y
WORKDIR /opt/prod/app
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "node", "dist/start.js" ]

15
compose.yml Normal file
View File

@ -0,0 +1,15 @@
services:
api.jdf2.org:
build:
context: .
dockerfile: ./Dockerfile
volumes:
- storage:/opt/prod/storage/
healthcheck:
test:
- CMD
- curl
- 'http://127.0.0.1:3000/getMostRecentSpotifyTrack'
interval: 2s
timeout: 10s
retries: 15

BIN
consumer/ComicSans.ttf Normal file

Binary file not shown.

BIN
consumer/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
consumer/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

271
dist/start.js vendored Normal file
View File

@ -0,0 +1,271 @@
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const https = require("https");
const fs = require("fs/promises");
const agent = new https.Agent({
rejectUnauthorized: false,
});
const app = express();
app.use(cors());
app.use(bodyParser.json());
const apiCache = {
_: {},
get(key) {
if (
this._[key] &&
this._[key].timestamp + (this._[key].expire ?? 180000) > Date.now()
) {
return this._[key].value;
} else {
return undefined;
}
},
set(key, value, expire) {
this._[key] = {
timestamp: Date.now(),
expire: expire,
value: value,
};
},
};
app.get("/getMostRecentSpotifyTrack", async function (req, res) {
console.log(apiCache.get("getMostRecentSpotifyTrack"));
if (apiCache.get("getMostRecentSpotifyTrack")) {
res.send(JSON.stringify(apiCache.get("getMostRecentSpotifyTrack")));
} else {
console.log("new")
const lastfmResponse = await fetch(
`http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=jdf221&api_key=${process.env.LASTFM_API_KEY}&format=json`
).then((res) => res.json());
const response = {
track: undefined,
artist: undefined,
};
const track = lastfmResponse?.["recenttracks"]?.["track"]?.[0];
response.track = track?.name;
response.artist = track?.artist?.["#text"];
console.log(response)
apiCache.set("getMostRecentSpotifyTrack", response);
res.send(JSON.stringify(response));
}
});
app.listen(3000);
async function getSpotifyApi() {
return {
token: await fs
.readFile("/opt/prod/storage/spotifySeasons/tokenData.json")
.then((b) => JSON.parse(b.toString())),
application: {
id: process.env.SPOTIFY_API_ID,
secret: process.env.SPOTIFY_API_SECRET,
},
async sendRequest(url, method = "get", data, asJson = true) {
await new Promise((r) => setTimeout(r, 500));
if (
url !== "https://accounts.spotify.com/api/token" &&
Date.now() > this.token.expiresAfterTimestamp
) {
await this.refreshToken();
}
return await fetch(url, {
method: method,
body: data
? asJson
? JSON.stringify(data)
: Object.keys(data)
.map(
(key) =>
encodeURIComponent(key) +
"=" +
encodeURIComponent(data[key])
)
.join("&")
: undefined,
headers: {
"Content-Type": asJson
? "application/json"
: "application/x-www-form-urlencoded;charset=UTF-8",
Authorization:
url == "https://accounts.spotify.com/api/token"
? "Basic " +
new Buffer.from(
this.application.id + ":" + this.application.secret
).toString("base64")
: "Bearer " + this.token.access_token,
},
}).then((b) => b.json());
},
async refreshToken() {
const response = await this.sendRequest(
"https://accounts.spotify.com/api/token",
"post",
{
grant_type: "refresh_token",
refresh_token: this.token.refresh_token,
},
false
);
this.token = response;
this.token.expiresAfterTimestamp =
Date.now() + this.token.expires_in * 1000;
this.token.refresh_token = process.env.SPOTIFY_API_REFRESH_TOKEN
await fs.writeFile(
"/opt/prod/storage/spotifySeasons/tokenData.json",
JSON.stringify(this.token)
);
},
async createPlaylist(name) {
return (
await this.sendRequest(
"https://api.spotify.com/v1/users/jdf221/playlists",
"post",
{
name: name,
public: false,
}
)
).id;
},
async search(query) {
const queryObject = { q: query, type: ["track"] };
const queryString = Object.keys(queryObject)
.map(
(key) =>
encodeURIComponent(key) + "=" + encodeURIComponent(queryObject[key])
)
.join("&");
return this.sendRequest(
"https://api.spotify.com/v1/search?" + queryString
);
},
async addTracksToPlaylist(playlistId, tracksArray) {
return this.sendRequest(
`https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
"post",
{ uris: tracksArray },
true
);
},
};
}
async function weeklySpotifyHandler() {
const spotifyApi = await getSpotifyApi();
const seasons = [
"winter", // 0 Jan
"winter", // 1 February
"spring", // 2 March
"spring", // 3 April
"spring", // 4 May
"summer", // 5 June
"summer", // 6 July
"summer", // 7 August
"fall", // 8 September
"fall", // 9 October
"fall", // 10 November
"winter", // 11 December
];
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() - 7);
const currentSeason = seasons[currentDate.getUTCMonth()];
const currentPlaylistInternalId = `${currentSeason}-${currentDate.getUTCFullYear()}`;
const createdPlaylists = await fs
.readFile("/opt/prod/storage/spotifySeasons/playlists.json")
.then((b) => JSON.parse(b.toString()));
if (!createdPlaylists[currentPlaylistInternalId]) {
const newPlaylistId = await spotifyApi.createPlaylist(
currentDate.getUTCFullYear() +
" " +
(currentSeason[0].toUpperCase() + currentSeason.slice(1))
);
createdPlaylists[currentPlaylistInternalId] = {
spotifyPlaylistId: newPlaylistId,
tracks: [],
};
}
const playlistId =
createdPlaylists[currentPlaylistInternalId].spotifyPlaylistId;
const topTracks = await fetch(
`https://ws.audioscrobbler.com/2.0/?method=user.getTopTracks&user=jdf221&period=7day&api_key=${process.env.LASTFM_API_KEY}&format=json`
).then((b) => b.json());
const filteredTopTracks = topTracks.toptracks.track.filter(
(t) => parseInt(t.playcount) > 2
);
const newTracksToAdd = [];
for (const lastfmTrack of filteredTopTracks) {
const spotifySearch = await spotifyApi.search(
`track:${lastfmTrack.name} artist:${lastfmTrack.artist.name}`
);
const tracksButExplicitFirst = spotifySearch.tracks.items.sort((a, b) => {
if (a.explicit && b.explicit) return 0;
if (a.explicit) return -1;
if (b.explicit) return 1;
});
for (const track of tracksButExplicitFirst) {
if (
lastfmTrack.artist.name === track.artists[0].name &&
lastfmTrack.name === track.name
) {
if (
!createdPlaylists[currentPlaylistInternalId].tracks.includes(
track.uri
)
) {
createdPlaylists[currentPlaylistInternalId].tracks.push(track.uri);
console.log(`Adding ${lastfmTrack.name}} to ${currentPlaylistInternalId}`)
newTracksToAdd.push(track.uri);
}
else {
console.log(`Skipping ${lastfmTrack.name}}, already added to ${currentPlaylistInternalId}`)
}
break;
}
}
}
spotifyApi.addTracksToPlaylist(playlistId, newTracksToAdd);
await fs.writeFile(
"/opt/prod/storage/spotifySeasons/playlists.json",
JSON.stringify(createdPlaylists)
);
}
setInterval(() => {
const currentDate = new Date();
if (
currentDate.getDay() === 0 &&
currentDate.getUTCHours() === 0 &&
currentDate.getUTCMinutes() < 29
) {
weeklySpotifyHandler();
}
}, 1000 * 60 * 30); // Every 30 minutes

View File

@ -1,5 +1,11 @@
{
"name": "api.jdf2.org",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.21.2"
}
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"form-data": "^4.0.0"
}
}