350 lines
10 KiB
JavaScript
Executable File
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();
|