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 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) { 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)); } }); 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; } }); 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