Switch to main #1
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
15
compose.yml
Normal 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
BIN
consumer/ComicSans.ttf
Normal file
Binary file not shown.
BIN
consumer/arrow.png
Normal file
BIN
consumer/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
consumer/background.png
Normal file
BIN
consumer/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
271
dist/start.js
vendored
Normal file
271
dist/start.js
vendored
Normal 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
|
||||||
10
package.json
10
package.json
@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"name": "api.jdf2.org",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.2"
|
"body-parser": "^1.19.0",
|
||||||
}
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user