// Import all the things require('dotenv').config(); const axios = require('axios'); const express = require('express'); const fs = require('node:fs'); const path = require('node:path'); const readline = require('node:readline/promises'); const http = require('node:http'); // yes, it is `new new require('ytmusic-api')`, i didnt fuck up // this is literally black magic i have no idea how or why it works const ytmusic = new new require('ytmusic-api'); // i love me a new new // Create interface for console const rl = readline.createInterface({input:process.stdin, output:process.stdout}); // Global constants /** * Should always be `/prod/portfolio`, dynamically caching it in case i decide to move it to a different path sometime in the future */ const baseDir = process.cwd(); /** * The ID for my favorite moosic playlist :D */ const playlistId = 'PLnlTMS4cxfx3-et_L8APzpEgy_eCf18-U'; /** * Port that nginx proxies to public */ const port = process.env.PORT || 48916; /** * Are we prod or dev */ const dev = process.env.DEV || "true"; // Helper functions I've made to do things and stuff :P /** * Loops a thing every hour * @param {Function} something * @param {Boolean} skipFirst * @returns jack shit */ // I know, I know, I am just the most bestest and awesomest coder everrrrr const loopHourly = (something, skipFirst = false) => { // If skipFirst is false, we do the thing immediately if (!skipFirst) setTimeout(() => { let cancel = false; something(); if (!cancel) loopHourly(something); return; }, 3600000 - new Date().getTime() % 3600000); // 3600000 ms = 1000 ms (one second) * 60 seconds (one minute) * 60 minutes (one hour) // If skipFirst is true, wait a hour then execute else setTimeout(() => { loopHourly(something) }, 3600000 - new Date().getTime() % 3600000); } /** * Pulls all the pictures for the songs and stores them in a base64 key:value array where the key is the song id and the value is the thumbnail * @param {*} song */ const populateThumbnails = (async song => { let id = song.videoId; let thumbnail; const response = await axios.get(song.thumbnails[song.thumbnails.length - 1].url, { responseType: 'arraybuffer' }); // We now check if Youtube moosic has yelled back a thing we want to hear if (response.status == 200) { thumbnail = Buffer.from(response.data, "utf-8").toString('base64'); thumbnails.push({ id, thumbnail }); } else { console.error("Failed to pull thumbnail: " + response); } }); // Updates the list of songs by yelling at youtube moosic to gimme the playlist async function updateSongs() { // Yell at youtube moosic to get a list of songs playlistSongs = await ytmusic.getPlaylistVideos(playlistId); if (typeof playlistSongs == 'object') { // For each song that youtube moosic has yelled back at us, give them individually to populateThumbnails() await Promise.all(playlistSongs.map(async song => await populateThumbnails(song))); // Write all of this to cache fs.writeFileSync(path.join(baseDir + '/ytmusic_cache'), JSON.stringify({playlistSongs, thumbnails})); } else { // like legit this shouldnt even be possible console.log("somehow i have managed to get this code into an area i thought impossible"); console.log("*various anxiousness noises*"); console.log("help"); throw TypeError('playlistSongs is not a object!'); // this should never ever happen, if it does that is very very bad } } var blog; // Setup the blog variable // Blog stuffs async function generateBlogIndex() { var blogfolder = fs.readdirSync(path.join(baseDir, 'blog/')); var tmpobj = {}; blogfolder.forEach(thing => { if (fs.statSync(path.join(baseDir, `blog/${thing}`)).isDirectory()) { var files = fs.readdirSync(path.join(baseDir, `blog/${thing}`)); tmpobj[thing] = {}; files.forEach(file => { tmpobj[thing][file] = true; }); } }); blog = tmpobj; } async function send404(req, res) { // TODO: actual 404 page res.status(404).send("
404 Not found
An internal server error has occurred. Please bother me if you see this."); // Give the user something that they can read }); // 404 app.use(function(req, res) { send404(req, res); }); function printHelp() { rl.write("\n\n-------------------------------HELP-----------------------------------------\n"); rl.write("> help [?] - prints this message\n"); rl.write("> updateMusic [um] - runs updateSongs()\n"); rl.write("> eval [exec] - runs raw JS code, returns output (if any)\n"); rl.write("> quit [exit] - stops the server, and quits\n"); rl.write("-------------------------------HELP-----------------------------------------\n\n") } function quit() { server.close(); rl.write('\n\nI do declare - end broadcast\n\n'); // Yoinked Lunduke Journal's signoff :> rl.close(); process.exit(0); } async function customConsole() { while (1) { // Pull commands const cmd_raw = await rl.question("> "); const cmd_raw_args = cmd_raw.split(" "); const cmd_args = cmd_raw_args.slice(1); const cmd = cmd_raw_args[0]; const cmd_body = cmd_raw.substring(cmd.length).trim(); switch(cmd) { case "?": case "help": printHelp(); break; case "um": case "updateMusic": updateSongs(); break; case "exec": case "eval": // TODO: Fix this not actually using the correct context for whatever reason it just evals in an empty context // ...... i dont know what or how or why but magically it is working now so i am just not gonna touch it and hope // whatever black magic made it work keeps working .-. try { rl.write(eval(cmd_body) + "\n"); } catch (err) { console.error(err); } break; case "quit": case "exit": quit(); return; case "": break; default: rl.write(`Command not found: ${cmd}`) printHelp(); break; } } } async function main() { if (dev == "true") console.log("Starting custom console"); // Start console customConsole(); // DO NOT CALL `await` ON THIS!! THIS WILL CAUSE IT TO NEVER RETURN!! if (dev == "true") console.log("Console started"); if (dev == "true") console.log("Loading ytmusic stuffs from cache file"); // Load from cache await (async () => { let cachefile; cachefile = fs.readFileSync(path.join(baseDir, '/ytmusic_cache')); let parsed_cachefile = JSON.parse(cachefile); playlistSongs = parsed_cachefile.playlistSongs; thumbnails = parsed_cachefile.thumbnails; if (dev == "true") console.log("Finished loading from ytmusic stuffs from cache"); })(); // Wrap in async but don't await as to not delay server boot, we can load from cache faster then we can load from yt music (async () => { if (dev == "true") console.log("ytmusic init begining"); // Init the moosics stuff await ytmusic.initialize(); if (dev == "true") console.log("ytmusic initialized"); // Populate playlistSongs and thumbnails await updateSongs(); if (dev == "true") console.log("Loaded new data from YTMusic!\n"); })(); if (dev == "true") console.log("populating blog index") // Populate the blog index await generateBlogIndex(); if (dev == "true") console.log("populated blog index"); server = http.createServer(app); if (dev == "true") console.log("Starting server"); await (async () => { server.listen(port, () => { console.log(`Listening to ${port}\n`); // Start hourly loop to update playlist loopHourly(async () => await updateSongs()); }); })(); } // Handle a few signals rl.on('SIGINT', () => { quit(); }); rl.on('SIGTERM', () => { quit(); }); main();
Contact me at @oddbyte.11 on Signal, or contact@oddbyte.dev if you are a caveman and are stuck in the last decade.