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

"); } // Begin the server-ing things const app = express(); var server; // Set at app.listen (bottom), used to kill server on ctrl-c // YT Music stuff /** * An object that stores a Map of songs (also objects) */ let playlistSongs = new Map; /** * Key:Value array of songs. The song ID is the key, and a base64 encoded image is the value */ let thumbnails = []; // Other middlewares app.set('view engine', 'ejs'); app.use(express.static('public')); app.use(express.static('private')); app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { res.setHeader("Content-Security-Policy","default-src 'self' 'unsafe-inline'; script-src-elem 'self' 'unsafe-inline'; img-src 'self' data:; frame-src 'self';"); next(); }); // Cache the favicon cause I'm probably not gonna change that for a while const favicon = fs.readFileSync('public/favicon.ico'); // Also cache a base64 version of it to pass to my pages const faviconb64 = favicon.toString('base64'); // --------------------- // - Routes begin here - // --------------------- // Sends the cached favicon.ico to prevent an unneeded read on a static file :P app.get('/favicon.ico', (req, res) => { res.status(200).send(favicon); }); // Home page app.get('/', (req, res) => { res.render('home', { faviconb64 }); }); // Fun Fact page app.get('/funfact', (req, res) => { res.render('funfact', { }); }); // The page with all my art :> app.get('/art', (req, res) => { res.render('art', { faviconb64 }); }); // Moosic page app.get('/music', (req, res) => { res.render('moosic', { faviconb64, playlistId, playlistSongs, thumbnails }); }); // Blog app.get('/blog', (req, res) => { if (req.query && req.query.sample == "1") { res.render('blog', { faviconb64, type: "sample", page: null, blog }); } else res.render('blog', { faviconb64, type: "main", page: null, blog }); }); // Individual blog pages / topic pages app.get(/^\/blog\/.*/, (req, res) => { let requestedTopic = req.path.split('/blog/')[1].split('/')[0]; let requestedPage = req.path.split('/blog/')[1].split('/')[1]; let topic = blog[requestedTopic]; let page; if (typeof topic != "object") { send404(req, res); return; } if (topic[requestedPage] == true) { page = fs.readFileSync(path.join(baseDir, 'blog', requestedTopic, requestedPage)); res.render('blog', { faviconb64, type: "individual", page, blog }) } else send404(req, res); }); app.get('/editor', (req, res) => { res.render('editor', { faviconb64 }) }); // Error handling (must be 2nd to last last app.use to make sure it catches all the errorings) app.use(function(err, req, res, next) { if(!err) { next(); return; } console.error('-------------------------------ERROR---------------------------------------\n'); console.error(err); // Log da errors console.error('-----------------------------USER-INFO-------------------------------------\n'); console.error("Body:\n"); console.error(req.body); console.error("\n"); console.error("Cookies:\n"); console.error(req.cookies); console.error("\n"); console.error("Host:\n"); console.error(req.host); console.error("\n"); console.error("Path:\n"); console.error(req.path); console.error("\n"); console.error('----------------------------------------------------------------------------\n'); res.status(400).send("
An internal server error has occurred. Please bother me if you see this.
Contact me at @oddbyte.11 on Signal, or contact@oddbyte.dev if you are a caveman and are stuck in the last decade.
"); // 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]; 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_args.toString()) + "\n"); } catch (err) { console.error(err); } break; case "quit": case "exit": quit(); return; case "": break; default: rl.write(`Command not found: ${cmd}`) printHelp(); break; } } } // Wrap in a async function to wait for youtube music response before starting http server // (to prevent a race condition where people can view the moosic page be4 it is ready) async function main() { // Init the moosics stuff (black magic) await ytmusic.initialize(); // Populate playlistSongs and thumbnails await updateSongs(); // Populate the blog index await generateBlogIndex(); server = http.createServer(app); await (async () => { server.listen(port, () => { console.log(`Listening to ${port}`); // Start hourly loop to update playlist loopHourly(async () => await updateSongs()); }); })(); // Start console customConsole(); } // Handle a few signals rl.on('SIGINT', () => { quit(); }); rl.on('SIGTERM', () => { quit(); }); main();