From 55fae2b0d43eb617da58729b79932523a492a28b Mon Sep 17 00:00:00 2001 From: Jared Furlow Date: Sun, 26 Jan 2025 01:26:54 -0600 Subject: [PATCH] Test --- .Dockerfile | 1 + dist/start.js | 348 ++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 11 +- 3 files changed, 346 insertions(+), 14 deletions(-) diff --git a/.Dockerfile b/.Dockerfile index d114fe5..8825d4d 100644 --- a/.Dockerfile +++ b/.Dockerfile @@ -3,4 +3,5 @@ WORKDIR /opt/prod/app COPY . . RUN npm install EXPOSE 3000 +RUN mkdir /opt/prod/storage/spotifySeasons CMD [ "node", "dist/start.js" ] \ No newline at end of file diff --git a/dist/start.js b/dist/start.js index 02185a0..46a12f6 100644 --- a/dist/start.js +++ b/dist/start.js @@ -1,19 +1,343 @@ -console.log("bob test") +const fetch = require("node-fetch"); +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 express = require('express'); const app = express(); -const port = 3000; +app.use(cors()); +app.use(bodyParser.json()); -// Define a simple route -app.get('/', (req, res) => { - res.send('Hello World from Express testing!'); +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) { + if (apiCache.get("getMostRecentSpotifyTrack")) { + res.send(JSON.stringify(apiCache.get("getMostRecentSpotifyTrack"))); + } else { + 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"]; + + apiCache.set("getMostRecentSpotifyTrack", response); + res.send(JSON.stringify(response)); + } }); -app.get('/*', (req, res) => { - res.send('Hello World from '+req.originalUrl+'!'); +const CanvasApi = require("canvas"); +CanvasApi.registerFont("./consumer/ComicSans.ttf", { family: "Comic Sans MS" }); +const backgroundImage = CanvasApi.loadImage("./consumer/background.png"); +const arrowImage = CanvasApi.loadImage("./consumer/arrow.png"); +app.get("/consoomer/getLineGraphImage", async (request, response) => { + const config = { + maxPoints: 60, + lowestX: -80, + highestX: 1630, + }; + + const params = { + userId: request.query.userId ?? "-1", + nickname: request.query.nickname ?? "Idiot", + profileImageUrl: + request.query.profileImageUrl ?? "https://i.imgur.com/E7JL7yp.png", + points: parseInt(request.query.points) ?? 0, + }; + if (!Object.values(params).every((v) => typeof v !== "undefined")) { + response.send("Bad params"); + return; + } + if (Number.isNaN(params.points)) { + response.send("Bad points"); + return; + } else { + params.points = Math.min(Math.abs(params.points), config.maxPoints); + } + + const step = Math.floor( + (Math.abs(config.lowestX) + config.highestX) / config.maxPoints + ); + const xPosition = Math.floor(step * params.points) + config.lowestX; + + //min x = -80 + //max x = 1630 + //y = 135 + + const canvas = CanvasApi.createCanvas(1920, 737); + const context = canvas.getContext("2d"); + + try { + context.drawImage(await backgroundImage, 0, 0); + + let profileImage = apiCache.get( + "consoomer.profilePic." + params.profileImageUrl + ); + if (!profileImage) { + profileImage = await CanvasApi.loadImage( + await fetch(params.profileImageUrl).then(async (r) => + Buffer.from(await r.arrayBuffer()) + ) + ); + apiCache.set( + "consoomer.profilePic." + params.profileImageUrl, + profileImage, + 300000 + ); // 5 minutes + } + context.drawImage(profileImage, xPosition + 115, 432); + + context.drawImage(await arrowImage, xPosition, 135); + + context.font = "48px 'Comic Sans MS'"; + context.fillText("Consoomer Report", 700, 75); + context.font = "34px 'Comic Sans MS'"; + context.fillText(`Results for: ${params.nickname}`, 700, 115); + context.fillText(`Total: ${params.points}`, 698, 150); + + response.header("Content-Type", "image/png"); + response.send(canvas.toBuffer("image/png")); + } catch (error) { + //throw error + response.send("Uknown error"); + return; + } }); -// Start the server -app.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); \ No newline at end of file +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=5d7730f5457bb3e794f69d7713b884ad&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); + newTracksToAdd.push(track.uri); + } + 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 diff --git a/package.json b/package.json index abdc2a8..dc197f7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,12 @@ { + "name": "api.jdf2.org", + "version": "1.0.0", + "main": "index.js", "dependencies": { - "express": "^4.21.2" - } + "body-parser": "^1.19.0", + "canvas": "^2.11.2", + "cors": "^2.8.5", + "express": "^4.17.1", + "form-data": "^4.0.0" + } }