Files
website/index.js
2026-01-19 14:57:08 -05:00

350 lines
10 KiB
JavaScript
Executable File

// 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("<h1><pre>404 Not found</pre></h1>");
}
// 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("<pre>An internal server error has occurred. Please bother me if you see this.<br />Contact me at @oddbyte.11 on Signal, or <a href='mailto:contact@oddbyte.dev'>contact@oddbyte.dev</a> if you are a caveman and are stuck in the last decade.</pre>"); // 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;
}
}
}
async function main() {
// 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;
});
// 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 () => {
// Init the moosics stuff
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();