diff --git a/api/channels.js b/api/channels.js
index 5c3aff5..c1af36e 100644
--- a/api/channels.js
+++ b/api/channels.js
@@ -1,7 +1,7 @@
 const {render} = require("pinski/plugins")
-const constants = require("./utils/constants")
-const {fetchChannel} = require("./utils/youtube")
-const {getUser} = require("./utils/getuser")
+const constants = require("../utils/constants")
+const {fetchChannel} = require("../utils/youtube")
+const {getUser} = require("../utils/getuser")
 
 module.exports = [
 	{
diff --git a/api/formapi.js b/api/formapi.js
index dc95364..8115135 100644
--- a/api/formapi.js
+++ b/api/formapi.js
@@ -1,10 +1,10 @@
 const {redirect} = require("pinski/plugins")
-const db = require("./utils/db")
-const constants = require("./utils/constants")
-const {getUser} = require("./utils/getuser")
-const validate = require("./utils/validate")
+const db = require("../utils/db")
+const constants = require("../utils/constants")
+const {getUser} = require("../utils/getuser")
+const validate = require("../utils/validate")
 const V = validate.V
-const {fetchChannel} = require("./utils/youtube")
+const {fetchChannel} = require("../utils/youtube")
 
 module.exports = [
 	{
diff --git a/api/settings.js b/api/settings.js
index 21ed293..d2cdf76 100644
--- a/api/settings.js
+++ b/api/settings.js
@@ -1,8 +1,8 @@
 const {render, redirect} = require("pinski/plugins")
-const db = require("./utils/db")
-const {getToken, getUser} = require("./utils/getuser")
-const constants = require("./utils/constants")
-const validate = require("./utils/validate")
+const db = require("../utils/db")
+const {getToken, getUser} = require("../utils/getuser")
+const constants = require("../utils/constants")
+const validate = require("../utils/validate")
 const V = validate.V
 
 module.exports = [
diff --git a/api/subscriptions.js b/api/subscriptions.js
index 7c01566..00d1227 100644
--- a/api/subscriptions.js
+++ b/api/subscriptions.js
@@ -1,7 +1,8 @@
 const {render} = require("pinski/plugins")
-const db = require("./utils/db")
-const {fetchChannelLatest} = require("./utils/youtube")
-const {getUser} = require("./utils/getuser")
+const db = require("../utils/db")
+const {fetchChannelLatest} = require("../utils/youtube")
+const {getUser} = require("../utils/getuser")
+const converters = require("../utils/converters")
 
 module.exports = [
 	{
@@ -22,6 +23,10 @@ module.exports = [
 					hasSubscriptions = true
 					const template = Array(subscriptions.length).fill("?").join(", ")
 					videos = db.prepare(`SELECT * FROM Videos WHERE authorId IN (${template}) ORDER BY published DESC LIMIT 60`).all(subscriptions)
+						.map(video => {
+							video.publishedText = converters.timeToPastText(video.published * 1000)
+							return video
+						})
 				}
 			}
 			return render(200, "pug/subscriptions.pug", {hasSubscriptions, videos, channels})
diff --git a/api/video.js b/api/video.js
index 8aab11d..c517551 100644
--- a/api/video.js
+++ b/api/video.js
@@ -1,7 +1,7 @@
 const fetch = require("node-fetch")
 const {render} = require("pinski/plugins")
-const db = require("./utils/db")
-const {getToken, getUser} = require("./utils/getuser")
+const db = require("../utils/db")
+const {getToken, getUser} = require("../utils/getuser")
 const pug = require("pug")
 
 class InstanceError extends Error {
diff --git a/api/youtube.js b/api/youtube.js
deleted file mode 100644
index 33bd76f..0000000
--- a/api/youtube.js
+++ /dev/null
@@ -1,596 +0,0 @@
-const fetch = require("node-fetch")
-const {render} = require("pinski/plugins")
-const fs = require("fs").promises
-
-class TTLCache {
-	constructor(ttl) {
-		this.backing = new Map()
-		this.ttl = ttl
-	}
-
-	set(key, value) {
-		this.backing.set(key, {time: Date.now(), value})
-		return value
-	}
-
-	clean(key) {
-		if (this.backing.has(key) && Date.now() - this.backing.get(key).time > this.ttl) {
-			this.delete(key)
-		}
-	}
-
-	has(key) {
-		this.clean(key)
-		return this.backing.has(key)
-	}
-
-	get(key) {
-		this.clean(key)
-		if (this.has(key)) {
-			return this.backing.get(key).value
-		} else {
-			return null
-		}
-	}
-
-	async getAs(key, callback) {
-		return this.get(key) || callback().then(value => this.set(key, value))
-	}
-}
-
-const videoCache = new TTLCache(Infinity)
-
-const channelCacheTimeout = 4*60*60*1000;
-
-let shareWords = [];
-fs.readFile("util/words.txt", "utf8").then(words => {
-	shareWords = words.split("\n");
-})
-const IDLetterIndex = []
-		.concat(Array(26).fill().map((_, i) => String.fromCharCode(i+65)))
-		.concat(Array(26).fill().map((_, i) => String.fromCharCode(i+97)))
-		.concat(Array(10).fill().map((_, i) => i.toString()))
-		.join("")
-	+"-_"
-
-function getShareWords(id) {
-    if (shareWords.length == 0) {
-        console.error("Tried to get share words, but they aren't loaded yet!");
-        return "";
-    }
-    // Convert ID string to binary number string
-    let binaryString = "";
-    for (let letter of id) {
-        binaryString += IDLetterIndex.indexOf(letter).toString(2).padStart(6, "0");
-    }
-    binaryString = binaryString.slice(0, 64);
-    // Convert binary string to words
-    let words = [];
-    for (let i = 0; i < 6; i++) {
-        let bitFragment = binaryString.substr(i*11, 11).padEnd(11, "0");
-        let number = parseInt(bitFragment, 2);
-        let word = shareWords[number];
-        words.push(word);
-    }
-    return words;
-}
-function getIDFromWords(words) {
-    // Convert words to binary number string
-    let binaryString = "";
-    for (let word of words) {
-        binaryString += shareWords.indexOf(word).toString(2).padStart(11, "0")
-    }
-    binaryString = binaryString.slice(0, 64);
-    // Convert binary string to ID
-    let id = "";
-    for (let i = 0; i < 11; i++) {
-        let bitFragment = binaryString.substr(i*6, 6).padEnd(6, "0");
-        let number = parseInt(bitFragment, 2);
-        id += IDLetterIndex[number];
-    }
-    return id;
-}
-function validateShareWords(words) {
-    if (words.length != 6) throw new Error("Expected 6 words, got "+words.length);
-    for (let word of words) {
-        if (!shareWords.includes(word)) throw new Error(word+" is not a valid share word");
-    }
-}
-function findShareWords(string) {
-    if (string.includes(" ")) {
-        return string.toLowerCase().split(" ");
-    } else {
-        let words = [];
-        let currentWord = "";
-        for (let i = 0; i < string.length; i++) {
-            if (string[i] == string[i].toUpperCase()) {
-                if (currentWord) words.push(currentWord);
-                currentWord = string[i].toLowerCase();
-            } else {
-                currentWord += string[i];
-            }
-        }
-        words.push(currentWord);
-        return words;
-    }
-}
-
-let channelCache = new Map();
-
-function refreshCache() {
-    for (let e of channelCache.entries()) {
-        if (Date.now()-e[1].refreshed > channelCacheTimeout) channelCache.delete(e[0]);
-    }
-}
-
-function fetchChannel(channelID, ignoreCache) {
-    refreshCache();
-    let cache = channelCache.get(channelID);
-    if (cache && !ignoreCache) {
-        if (cache.constructor.name == "Promise") {
-            //cf.log("Waiting on promise for "+channelID, "info");
-            return cache;
-        } else {
-            //cf.log("Using cache for "+channelID+", expires in "+Math.floor((channelCacheTimeout-Date.now()+cache.refreshed)/1000/60)+" minutes", "spam");
-            return Promise.resolve(cache.data);
-        }
-    } else {
-        //cf.log("Setting new cache for "+channelID, "spam");
-        let promise = new Promise(resolve => {
-            let channelType = channelID.startsWith("UC") && channelID.length == 24 ? "channel_id" : "user";
-            Promise.all([
-                rp(`${getInvidiousHost("channel")}/api/v1/channels/${channelID}`),
-                rp(`https://www.youtube.com/feeds/videos.xml?${channelType}=${channelID}`)
-            ]).then(([body, xml]) => {
-                let data = JSON.parse(body);
-                if (data.error) throw new Error("Couldn't refresh "+channelID+": "+data.error);
-                let feedItems = fxp.parse(xml).feed.entry;
-                //console.log(feedItems.slice(0, 2))
-                data.latestVideos.forEach(v => {
-                    v.author = data.author;
-                    let gotDateFromFeed = false;
-                    if (Array.isArray(feedItems)) {
-                        let feedItem = feedItems.find(i => i["yt:videoId"] == v.videoId);
-                        if (feedItem) {
-                            const date = new Date(feedItem.published)
-                            v.published = date.getTime();
-                            v.publishedText = date.toUTCString().split(" ").slice(1, 4).join(" ")
-                            gotDateFromFeed = true;
-                        }
-                    }
-                    if (!gotDateFromFeed) v.published = v.published * 1000;
-                });
-                //console.log(data.latestVideos.slice(0, 2))
-                channelCache.set(channelID, {refreshed: Date.now(), data: data});
-                //cf.log("Set new cache for "+channelID, "spam");
-                resolve(data);
-            }).catch(error => {
-                cf.log("Error while refreshing "+channelID, "error");
-                cf.log(error, "error");
-                channelCache.delete(channelID);
-                resolve(null);
-            });
-        });
-        channelCache.set(channelID, promise);
-        return promise;
-    }
-}
-
-module.exports = [
-	/*{
-		route: "/watch", methods: ["GET"], code: async ({url}) => {
-			const id = url.searchParams.get("v")
-			const video = await videoCache.getAs(id, () => fetch(`http://localhost:3000/api/v1/videos/${id}`).then(res => res.json()))
-			return render(200, "pug/video.pug", {video})
-		}
-	},
-
-    {
-        route: "/v/(.*)", methods: ["GET"], code: async ({fill}) => {
-            let id;
-            let wordsString = fill[0];
-            wordsString = wordsString.replace(/%20/g, " ")
-            if (wordsString.length == 11) {
-                id = wordsString
-            } else {
-                let words = findShareWords(wordsString);
-                try {
-                    validateShareWords(words);
-                } catch (e) {
-                    return [400, e.message];
-                }
-                id = getIDFromWords(words);
-            }
-            return {
-                statusCode: 301,
-                contentType: "text/html",
-                content: "Redirecting...",
-                headers: {
-                    "Location": "/cloudtube/video/"+id
-                }
-            }
-        }
-    },
-    {
-        route: "/cloudtube/video/([\\w-]+)", methods: ["GET"], code: ({req, fill}) => new Promise(resolve => {
-            rp(`${getInvidiousHost("video")}/api/v1/videos/${fill[0]}`).then(body => {
-                try {
-                    let data = JSON.parse(body);
-                    let page = pugCache.get("pug/old/cloudtube-video.pug").web()
-                    page = page.replace('""', () => body);
-                    let shareWords = getShareWords(fill[0]);
-                    page = page.replace('""', () => JSON.stringify(shareWords));
-                    page = page.replace("
", () => `${data.title} — CloudTube video`);
-                    while (page.includes("yt.www.watch.player.seekTo")) page = page.replace("yt.www.watch.player.seekTo", "seekTo");
-                    let metaOGTags =
-                        `\n`+
-                        `\n`+
-                        `\n`+
-                        `\n`+
-                        `\n`
-                    page = page.replace("", () => metaOGTags);
-                    resolve({
-                        statusCode: 200,
-                        contentType: "text/html",
-                        content: page
-                    });
-                } catch (e) {
-                    resolve([400, "Error parsing data from Invidious"]);
-                }
-            }).catch(err => {
-                resolve([500, "Error requesting data from Invidious"]);
-            });
-        })
-    },
-    {
-        route: "/cloudtube/channel/([\\w-]+)", methods: ["GET"], code: ({req, fill}) => new Promise(resolve => {
-            fetchChannel(fill[0]).then(data => {
-                try {
-                    let page = pugCache.get("pug/old/cloudtube-channel.pug").web()
-                    page = page.replace('""', () => JSON.stringify(data));
-                    page = page.replace("", () => `${data.author} — CloudTube channel`);
-                    let metaOGTags =
-                        `\n`+
-                        `\n`+
-                        // `\n`+
-                        `\n`+
-                        `\n`
-                    page = page.replace("", () => metaOGTags);
-                    resolve({
-                        statusCode: 200,
-                        contentType: "text/html",
-                        content: page
-                    });
-                } catch (e) {
-                    resolve([400, "Error parsing data from Invidious"]);
-                }
-            }).catch(err => {
-                resolve([500, "Error requesting data from Invidious"]);
-            });
-        })
-    },
-    {
-        route: "/cloudtube/playlist/([\\w-]+)", methods: ["GET"], code: ({req, fill}) => new Promise(resolve => {
-            rp(`${getInvidiousHost("playlist")}/api/v1/playlists/${fill[0]}`).then(body => {
-                try {
-                    let data = JSON.parse(body);
-                    let page = pugCache.get("pug/old/cloudtube-playlist.pug").web()
-                    page = page.replace('""', () => body);
-                    page = page.replace("", () => `${data.title} — CloudTube playlist`);
-                    while (page.includes("yt.www.watch.player.seekTo")) page = page.replace("yt.www.watch.player.seekTo", "seekTo");
-                    let metaOGTags =
-                        `\n`+
-                        `\n`+
-                        `\n`+
-                        `\n`
-                    if (data.videos[0]) metaOGTags += `\n`;
-                    page = page.replace("", () => metaOGTags);
-                    resolve({
-                        statusCode: 200,
-                        contentType: "text/html",
-                        content: page
-                    });
-                } catch (e) {
-                    resolve([400, "Error parsing data from Invidious"]);
-                }
-            }).catch(err => {
-                resolve([500, "Error requesting data from Invidious"]);
-            });
-        })
-    },
-    {
-        route: "/cloudtube/search", methods: ["GET"], upload: "json", code: ({req, url}) => new Promise(resolve => {
-            const params = url.searchParams
-            console.log("URL:", req.url)
-            console.log("Headers:", req.headers)
-            let page = pugCache.get("pug/old/cloudtube-search.pug").web()
-            if (params.has("q")) { // search terms were entered
-                let sort_by = params.get("sort_by") || "relevance";
-                rp(`${getInvidiousHost("search")}/api/v1/search?q=${encodeURIComponent(decodeURIComponent(params.get("q")))}&sort_by=${sort_by}`).then(body => {
-                    try {
-                        // json.parse?
-                        page = page.replace('""', () => body);
-                        page = page.replace("", () => `${decodeURIComponent(params.get("q"))} — CloudTube search`);
-                        let metaOGTags =
-                            `\n`+
-                            `\n`+
-                            `\n`+
-                            `\n`
-                        page = page.replace("", () => metaOGTags);
-                        resolve({
-                            statusCode: 200,
-                            contentType: "text/html",
-                            content: page
-                        });
-                    } catch (e) {
-                        resolve([400, "Error parsing data from Invidious"]);
-                    }
-                }).catch(err => {
-                    resolve([500, "Error requesting data from Invidious"]);
-                });
-            } else { // no search terms
-                page = page.replace("", "");
-                page = page.replace("", `CloudTube search`);
-                let metaOGTags =
-                    `\n`+
-                    `\n`+
-                    `\n`+
-                    `\n`
-                page = page.replace("", () => metaOGTags);
-                resolve({
-                    statusCode: 200,
-                    contentType: "text/html",
-                    content: page
-                });
-            }
-        })
-    },
-    {
-        route: "/api/youtube/subscribe", methods: ["POST"], upload: "json", code: async ({data}) => {
-            if (!data.channelID) return [400, 1];
-            if (!data.token) return [400, 8];
-            let userRow = await db.get("SELECT userID FROM AccountTokens WHERE token = ?", data.token);
-            if (!userRow || userRow.expires <= Date.now()) return [401, 8];
-            let subscriptions = (await db.all("SELECT channelID FROM AccountSubscriptions WHERE userID = ?", userRow.userID)).map(r => r.channelID);
-            let nowSubscribed;
-            if (subscriptions.includes(data.channelID)) {
-                await db.run("DELETE FROM AccountSubscriptions WHERE userID = ? AND channelID = ?", [userRow.userID, data.channelID]);
-                nowSubscribed = false;
-            } else {
-                await db.run("INSERT INTO AccountSubscriptions VALUES (?, ?)", [userRow.userID, data.channelID]);
-                nowSubscribed = true;
-            }
-            return [200, {channelID: data.channelID, nowSubscribed}];
-        }
-    },
-    {
-        route: "/api/youtube/subscriptions", methods: ["POST"], upload: "json", code: async ({data}) => {
-            let subscriptions;
-            if (data.token) {
-                let userRow = await db.get("SELECT userID FROM AccountTokens WHERE token = ?", data.token);
-                if (!userRow || userRow.expires <= Date.now()) return [401, 8];
-                subscriptions = (await db.all("SELECT channelID FROM AccountSubscriptions WHERE userID = ?", userRow.userID)).map(r => r.channelID);
-            } else {
-                if (data.subscriptions && data.subscriptions.constructor.name == "Array" && data.subscriptions.every(i => typeof(i) == "string")) subscriptions = data.subscriptions;
-                else return [400, 4];
-            }
-            if (data.force) {
-                for (let channelID of subscriptions) channelCache.delete(channelID);
-                return [204, ""];
-            } else {
-                let videos = [];
-                let channels = [];
-                let failedCount = 0
-                await Promise.all(subscriptions.map(s => fetchChannel(s).then(data => {
-                    if (data) {
-                        videos = videos.concat(data.latestVideos);
-                        channels.push({author: data.author, authorID: data.authorId, authorThumbnails: data.authorThumbnails});
-                    } else {
-                        failedCount++
-                    }
-                })));
-                videos = videos.sort((a, b) => (b.published - a.published))
-                let limit = 60;
-                if (data.limit && !isNaN(+data.limit) && (+data.limit > 0)) limit = +data.limit;
-                videos = videos.slice(0, limit);
-                channels = channels.sort((a, b) => (a.author.toLowerCase() < b.author.toLowerCase() ? -1 : 1));
-                return [200, {videos, channels, failedCount}];
-            }
-        }
-    },
-    {
-        route: "/api/youtube/subscriptions/import", methods: ["POST"], upload: "json", code: async ({data}) => {
-            if (!data) return [400, 3];
-            if (!typeof(data) == "object") return [400, 5];
-            if (!data.token) return [401, 8];
-            let userRow = await db.get("SELECT userID FROM AccountTokens WHERE token = ?", data.token);
-            if (!userRow || userRow.expires <= Date.now()) return [401, 8];
-            if (!data.subscriptions) return [400, 4];
-            if (!data.subscriptions.every(v => typeof(v) == "string")) return [400, 5];
-            await db.run("BEGIN TRANSACTION");
-            await db.run("DELETE FROM AccountSubscriptions WHERE userID = ?", userRow.userID);
-            await Promise.all(data.subscriptions.map(v =>
-                db.run("INSERT OR IGNORE INTO AccountSubscriptions VALUES (?, ?)", [userRow.userID, v])
-            ))
-            await db.run("END TRANSACTION");
-            return [204, ""];
-        }
-    },
-    {
-        route: "/api/youtube/channels/([\\w-]+)/info", methods: ["GET"], code: ({fill}) => {
-            return rp(`${getInvidiousHost("channel")}/api/v1/channels/${fill[0]}`).then(body => {
-                return {
-                    statusCode: 200,
-                    contentType: "application/json",
-                    content: body
-                }
-            }).catch(e => {
-                console.error(e);
-                return [500, "Unknown request error, check console"]
-            });
-        }
-    },
-    {
-        route: "/api/youtube/alternate/.*", methods: ["GET"], code: async ({req}) => {
-            return [404, "Please leave me alone. This endpoint has been removed and it's never coming back. Why not try youtube-dl instead? https://github.com/ytdl-org/youtube-dl/\nIf you own a bot that accesses this endpoint, please send me an email: https://cadence.moe/about/contact\nHave a nice day.\n"];
-            return null
-            return [400, {error: `/api/youtube/alternate has been removed. The page will be reloaded.
 `}]
-        }
-    },
-    {
-        route: "/api/youtube/dash/([\\w-]+)", methods: ["GET"], code: ({fill}) => new Promise(resolve => {
-            let id = fill[0];
-            let sentReq = rp({
-                url: `http://localhost:3000/api/manifest/dash/id/${id}?local=true`,
-                timeout: 8000
-            });
-            sentReq.catch(err => {
-                if (err.code == "ETIMEDOUT" || err.code == "ESOCKETTIMEDOUT" || err.code == "ECONNRESET") resolve([502, "Request to Invidious timed out"]);
-                else {
-                    console.log(err);
-                    resolve([500, "Unknown request error, check console"]);
-                }
-            });
-            sentReq.then(body => {
-                let data = fxp.parse(body, {ignoreAttributes: false});
-                resolve([200, data]);
-            }).catch(err => {
-                if (err.code == "ETIMEDOUT" || err.code == "ESOCKETTIMEDOUT" || err.code == "ECONNRESET") resolve([502, "Request to Invidious timed out"]);
-                else {
-                    console.log(err);
-                    resolve([500, "Unknown parse error, check console"]);
-                }
-            });
-        })
-    },
-    {
-        route: "/api/youtube/get_endscreen", methods: ["GET"], code: async ({params}) => {
-            if (!params.v) return [400, 1];
-            let data = await rp("https://www.youtube.com/get_endscreen?v="+params.v);
-            data = data.toString();
-            try {
-                if (data == `""`) {
-                    return {
-                        statusCode: 204,
-                        content: "",
-                        contentType: "text/html",
-                        headers: {"Access-Control-Allow-Origin": "*"}
-                    }
-                } else {
-                    let json = JSON.parse(data.slice(data.indexOf("\n")+1));
-                    let promises = [];
-                    for (let e of json.elements.filter(e => e.endscreenElementRenderer.style == "WEBSITE")) {
-                        for (let thb of e.endscreenElementRenderer.image.thumbnails) {
-                            let promise = rp(thb.url, {encoding: null});
-                            promise.then(image => {
-                                let base64 = image.toString("base64");
-                                thb.url = "data:image/jpeg;base64,"+base64;
-                            });
-                            promises.push(promise);
-                        }
-                    }
-                    await Promise.all(promises);
-                    return {
-                        statusCode: 200,
-                        content: json,
-                        contentType: "application/json",
-                        headers: {"Access-Control-Allow-Origin": "*"}
-                    }
-                }
-            } catch (e) {
-                return [500, "Couldn't parse endscreen data\n\n"+data];
-            }
-        }
-    },
-    {
-        route: "/api/youtube/video/([\\w-]+)", methods: ["GET"], code: ({fill}) => {
-            return new Promise(resolve => {
-                ytdl.getInfo(fill[0]).then(info => {
-                    resolve([200, Object.assign(info, {constructor: new Object().constructor})]);
-                }).catch(err => {
-                    resolve([400, err]);
-                });
-            });
-        }
-    },
-    {
-        route: "/api/youtube/channel/(\\S+)", methods: ["GET"], code: ({fill}) => {
-            return new Promise(resolve => {
-                rp(
-                    "https://www.googleapis.com/youtube/v3/channels?part=contentDetails"+
-                    `&id=${fill[0]}&key=${auth.yt_api_key}`
-                ).then(channelText => {
-                    let channel = JSON.parse(channelText);
-                    let playlistIDs = channel.items.map(i => i.contentDetails.relatedPlaylists.uploads);
-                    Promise.all(playlistIDs.map(pid => rp(
-                        "https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails"+
-                        `&playlistId=${pid}&maxResults=50&key=${auth.yt_api_key}`
-                    ))).then(playlistsText => {
-                        let playlists = playlistsText.map(pt => JSON.parse(pt));;
-                        let items = [].concat(...playlists.map(p => p.items))
-                        .map(i => i.contentDetails)
-                        .sort((a, b) => (a.videoPublishedAt > b.videoPublishedAt ? -1 : 1))
-                        .slice(0, 50);
-                        rp(
-                            "https://www.googleapis.com/youtube/v3/videos?part=contentDetails,snippet"+
-                            `&id=${items.map(i => i.videoId).join(",")}&key=${auth.yt_api_key}`
-                        ).then(videosText => {
-                            let videos = JSON.parse(videosText);
-                            videos.items.forEach(v => {
-                                let duration = v.contentDetails.duration.slice(2).replace(/\D/g, ":").slice(0, -1).split(":")
-                                .map((t, i) => {
-                                    if (i) t = t.padStart(2, "0");
-                                    return t;
-                                });
-                                if (duration.length == 1) duration.splice(0, 0, "0");
-                                v.duration = duration.join(":");
-                            });
-                            resolve([200, videos.items]);
-                        });
-                    });
-                }).catch(err => {
-                    resolve([500, "Unexpected promise rejection error. This should not happen. Contact Cadence as soon as possible."]);
-                    console.log("Unexpected promise rejection error!");
-                    console.log(err);
-                });
-            });
-        }
-    },
-    {
-        route: "/api/youtube/search", methods: ["GET"], code: ({params}) => {
-            return new Promise(resolve => {
-                if (!params || !params.q) return resolve([400, "Missing ?q parameter"]);
-                let searchObject = {
-                    maxResults: +params.maxResults || 20,
-                    key: auth.yt_api_key,
-                    type: "video"
-                };
-                if (params.order) searchObject.order = params.order;
-                yts(params.q, searchObject, (err, search) => {
-                    if (err) {
-                        resolve([500, "YouTube API error. This should not happen. Contact Cadence as soon as possible."]);
-                        console.log("YouTube API error!");
-                        console.log(search);
-                    } else {
-                        rp(
-                            "https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id="+
-                            search.map(r => r.id).join(",")+
-                            "&key="+auth.yt_api_key
-                        ).then(videos => {
-                            JSON.parse(videos).items.forEach(v => {
-                                let duration = v.contentDetails.duration.slice(2).replace(/\D/g, ":").slice(0, -1).split(":")
-                                .map((t, i) => {
-                                    if (i) t = t.padStart(2, "0");
-                                    return t;
-                                });
-                                if (duration.length == 1) duration.splice(0, 0, "0");
-                                search.find(r => r.id == v.id).duration = duration.join(":");
-                            });
-                            resolve([200, search]);
-                        });
-                    }
-                });
-            });
-        }
-    }*/
-]
diff --git a/background/feed-update.js b/background/feed-update.js
index 86fb4b8..9d7bb51 100644
--- a/background/feed-update.js
+++ b/background/feed-update.js
@@ -1,14 +1,14 @@
 const Denque = require("denque")
 const fetch = require("node-fetch")
-const constants = require("../api/utils/constants")
-const db = require("../api/utils/db")
+const constants = require("../utils/constants")
+const db = require("../utils/db")
 
 const prepared = {
 	video_insert: db.prepare(
 		"INSERT OR IGNORE INTO Videos"
-			+ " ( videoId,  title,  author,  authorId,  published,  publishedText,  viewCountText,  descriptionHtml)"
+			+ " ( videoId,  title,  author,  authorId,  published,  viewCountText,  descriptionHtml)"
 			+ " VALUES"
-			+ " (@videoId, @title, @author, @authorId, @published, @publishedText, @viewCountText, @descriptionHtml)"
+			+ " (@videoId, @title, @author, @authorId, @published, @viewCountText, @descriptionHtml)"
 	)
 }
 
@@ -48,6 +48,10 @@ class RefreshQueue {
 	}
 
 	next() {
+		if (this.isEmpty()) {
+			throw new Error("Cannot get next of empty refresh queue")
+		}
+
 		const item = this.queue.shift()
 		this.set.delete(item)
 		return item
diff --git a/server.js b/server.js
index 091eb40..d3b22dd 100644
--- a/server.js
+++ b/server.js
@@ -2,7 +2,7 @@ const {Pinski} = require("pinski")
 const {setInstance} = require("pinski/plugins")
 
 ;(async () => {
-	await require("./api/utils/upgradedb")()
+	await require("./utils/upgradedb")()
 
 	const server = new Pinski({
 		port: 10412,
diff --git a/api/utils/constants.js b/utils/constants.js
similarity index 100%
rename from api/utils/constants.js
rename to utils/constants.js
diff --git a/utils/converters.js b/utils/converters.js
new file mode 100644
index 0000000..cf78c34
--- /dev/null
+++ b/utils/converters.js
@@ -0,0 +1,21 @@
+function timeToPastText(timestamp) {
+	const difference = Date.now() - timestamp
+	return [
+		["year", 365 * 24 * 60 * 60 * 1000],
+		["month", 30 * 24 * 60 * 60 * 1000],
+		["week", 7 * 24 * 60 * 60 * 1000],
+		["day", 24 * 60 * 60 * 1000],
+		["hour", 60 * 60 * 1000],
+		["minute", 60 * 1000],
+		["second", 1 * 1000]
+	].reduce((acc, [unitName, unitValue]) => {
+		if (acc) return acc
+		if (difference > unitValue) {
+			const number = Math.floor(difference / unitValue)
+			const pluralUnit = unitName + (number == 1 ? "" : "s")
+			return `${number} ${pluralUnit} ago`
+		}
+	}, null) || "just now"
+}
+
+module.exports.timeToPastText = timeToPastText
diff --git a/api/utils/db.js b/utils/db.js
similarity index 85%
rename from api/utils/db.js
rename to utils/db.js
index 1ceab92..1f76de9 100644
--- a/api/utils/db.js
+++ b/utils/db.js
@@ -2,7 +2,7 @@ const sqlite = require("better-sqlite3")
 const pj = require("path").join
 const fs = require("fs")
 
-const dir = pj(__dirname, "../../db")
+const dir = pj(__dirname, "../db")
 fs.mkdirSync(pj(dir, "backups"), {recursive: true})
 const db = new sqlite(pj(dir, "cloudtube.db"))
 module.exports = db
diff --git a/api/utils/getuser.js b/utils/getuser.js
similarity index 100%
rename from api/utils/getuser.js
rename to utils/getuser.js
diff --git a/api/utils/upgradedb.js b/utils/upgradedb.js
similarity index 100%
rename from api/utils/upgradedb.js
rename to utils/upgradedb.js
diff --git a/api/utils/validate.js b/utils/validate.js
similarity index 100%
rename from api/utils/validate.js
rename to utils/validate.js
diff --git a/util/words.txt b/utils/words.txt
similarity index 100%
rename from util/words.txt
rename to utils/words.txt
diff --git a/api/utils/youtube.js b/utils/youtube.js
similarity index 100%
rename from api/utils/youtube.js
rename to utils/youtube.js
`}]
-        }
-    },
-    {
-        route: "/api/youtube/dash/([\\w-]+)", methods: ["GET"], code: ({fill}) => new Promise(resolve => {
-            let id = fill[0];
-            let sentReq = rp({
-                url: `http://localhost:3000/api/manifest/dash/id/${id}?local=true`,
-                timeout: 8000
-            });
-            sentReq.catch(err => {
-                if (err.code == "ETIMEDOUT" || err.code == "ESOCKETTIMEDOUT" || err.code == "ECONNRESET") resolve([502, "Request to Invidious timed out"]);
-                else {
-                    console.log(err);
-                    resolve([500, "Unknown request error, check console"]);
-                }
-            });
-            sentReq.then(body => {
-                let data = fxp.parse(body, {ignoreAttributes: false});
-                resolve([200, data]);
-            }).catch(err => {
-                if (err.code == "ETIMEDOUT" || err.code == "ESOCKETTIMEDOUT" || err.code == "ECONNRESET") resolve([502, "Request to Invidious timed out"]);
-                else {
-                    console.log(err);
-                    resolve([500, "Unknown parse error, check console"]);
-                }
-            });
-        })
-    },
-    {
-        route: "/api/youtube/get_endscreen", methods: ["GET"], code: async ({params}) => {
-            if (!params.v) return [400, 1];
-            let data = await rp("https://www.youtube.com/get_endscreen?v="+params.v);
-            data = data.toString();
-            try {
-                if (data == `""`) {
-                    return {
-                        statusCode: 204,
-                        content: "",
-                        contentType: "text/html",
-                        headers: {"Access-Control-Allow-Origin": "*"}
-                    }
-                } else {
-                    let json = JSON.parse(data.slice(data.indexOf("\n")+1));
-                    let promises = [];
-                    for (let e of json.elements.filter(e => e.endscreenElementRenderer.style == "WEBSITE")) {
-                        for (let thb of e.endscreenElementRenderer.image.thumbnails) {
-                            let promise = rp(thb.url, {encoding: null});
-                            promise.then(image => {
-                                let base64 = image.toString("base64");
-                                thb.url = "data:image/jpeg;base64,"+base64;
-                            });
-                            promises.push(promise);
-                        }
-                    }
-                    await Promise.all(promises);
-                    return {
-                        statusCode: 200,
-                        content: json,
-                        contentType: "application/json",
-                        headers: {"Access-Control-Allow-Origin": "*"}
-                    }
-                }
-            } catch (e) {
-                return [500, "Couldn't parse endscreen data\n\n"+data];
-            }
-        }
-    },
-    {
-        route: "/api/youtube/video/([\\w-]+)", methods: ["GET"], code: ({fill}) => {
-            return new Promise(resolve => {
-                ytdl.getInfo(fill[0]).then(info => {
-                    resolve([200, Object.assign(info, {constructor: new Object().constructor})]);
-                }).catch(err => {
-                    resolve([400, err]);
-                });
-            });
-        }
-    },
-    {
-        route: "/api/youtube/channel/(\\S+)", methods: ["GET"], code: ({fill}) => {
-            return new Promise(resolve => {
-                rp(
-                    "https://www.googleapis.com/youtube/v3/channels?part=contentDetails"+
-                    `&id=${fill[0]}&key=${auth.yt_api_key}`
-                ).then(channelText => {
-                    let channel = JSON.parse(channelText);
-                    let playlistIDs = channel.items.map(i => i.contentDetails.relatedPlaylists.uploads);
-                    Promise.all(playlistIDs.map(pid => rp(
-                        "https://www.googleapis.com/youtube/v3/playlistItems?part=contentDetails"+
-                        `&playlistId=${pid}&maxResults=50&key=${auth.yt_api_key}`
-                    ))).then(playlistsText => {
-                        let playlists = playlistsText.map(pt => JSON.parse(pt));;
-                        let items = [].concat(...playlists.map(p => p.items))
-                        .map(i => i.contentDetails)
-                        .sort((a, b) => (a.videoPublishedAt > b.videoPublishedAt ? -1 : 1))
-                        .slice(0, 50);
-                        rp(
-                            "https://www.googleapis.com/youtube/v3/videos?part=contentDetails,snippet"+
-                            `&id=${items.map(i => i.videoId).join(",")}&key=${auth.yt_api_key}`
-                        ).then(videosText => {
-                            let videos = JSON.parse(videosText);
-                            videos.items.forEach(v => {
-                                let duration = v.contentDetails.duration.slice(2).replace(/\D/g, ":").slice(0, -1).split(":")
-                                .map((t, i) => {
-                                    if (i) t = t.padStart(2, "0");
-                                    return t;
-                                });
-                                if (duration.length == 1) duration.splice(0, 0, "0");
-                                v.duration = duration.join(":");
-                            });
-                            resolve([200, videos.items]);
-                        });
-                    });
-                }).catch(err => {
-                    resolve([500, "Unexpected promise rejection error. This should not happen. Contact Cadence as soon as possible."]);
-                    console.log("Unexpected promise rejection error!");
-                    console.log(err);
-                });
-            });
-        }
-    },
-    {
-        route: "/api/youtube/search", methods: ["GET"], code: ({params}) => {
-            return new Promise(resolve => {
-                if (!params || !params.q) return resolve([400, "Missing ?q parameter"]);
-                let searchObject = {
-                    maxResults: +params.maxResults || 20,
-                    key: auth.yt_api_key,
-                    type: "video"
-                };
-                if (params.order) searchObject.order = params.order;
-                yts(params.q, searchObject, (err, search) => {
-                    if (err) {
-                        resolve([500, "YouTube API error. This should not happen. Contact Cadence as soon as possible."]);
-                        console.log("YouTube API error!");
-                        console.log(search);
-                    } else {
-                        rp(
-                            "https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id="+
-                            search.map(r => r.id).join(",")+
-                            "&key="+auth.yt_api_key
-                        ).then(videos => {
-                            JSON.parse(videos).items.forEach(v => {
-                                let duration = v.contentDetails.duration.slice(2).replace(/\D/g, ":").slice(0, -1).split(":")
-                                .map((t, i) => {
-                                    if (i) t = t.padStart(2, "0");
-                                    return t;
-                                });
-                                if (duration.length == 1) duration.splice(0, 0, "0");
-                                search.find(r => r.id == v.id).duration = duration.join(":");
-                            });
-                            resolve([200, search]);
-                        });
-                    }
-                });
-            });
-        }
-    }*/
-]
diff --git a/background/feed-update.js b/background/feed-update.js
index 86fb4b8..9d7bb51 100644
--- a/background/feed-update.js
+++ b/background/feed-update.js
@@ -1,14 +1,14 @@
 const Denque = require("denque")
 const fetch = require("node-fetch")
-const constants = require("../api/utils/constants")
-const db = require("../api/utils/db")
+const constants = require("../utils/constants")
+const db = require("../utils/db")
 
 const prepared = {
 	video_insert: db.prepare(
 		"INSERT OR IGNORE INTO Videos"
-			+ " ( videoId,  title,  author,  authorId,  published,  publishedText,  viewCountText,  descriptionHtml)"
+			+ " ( videoId,  title,  author,  authorId,  published,  viewCountText,  descriptionHtml)"
 			+ " VALUES"
-			+ " (@videoId, @title, @author, @authorId, @published, @publishedText, @viewCountText, @descriptionHtml)"
+			+ " (@videoId, @title, @author, @authorId, @published, @viewCountText, @descriptionHtml)"
 	)
 }
 
@@ -48,6 +48,10 @@ class RefreshQueue {
 	}
 
 	next() {
+		if (this.isEmpty()) {
+			throw new Error("Cannot get next of empty refresh queue")
+		}
+
 		const item = this.queue.shift()
 		this.set.delete(item)
 		return item
diff --git a/server.js b/server.js
index 091eb40..d3b22dd 100644
--- a/server.js
+++ b/server.js
@@ -2,7 +2,7 @@ const {Pinski} = require("pinski")
 const {setInstance} = require("pinski/plugins")
 
 ;(async () => {
-	await require("./api/utils/upgradedb")()
+	await require("./utils/upgradedb")()
 
 	const server = new Pinski({
 		port: 10412,
diff --git a/api/utils/constants.js b/utils/constants.js
similarity index 100%
rename from api/utils/constants.js
rename to utils/constants.js
diff --git a/utils/converters.js b/utils/converters.js
new file mode 100644
index 0000000..cf78c34
--- /dev/null
+++ b/utils/converters.js
@@ -0,0 +1,21 @@
+function timeToPastText(timestamp) {
+	const difference = Date.now() - timestamp
+	return [
+		["year", 365 * 24 * 60 * 60 * 1000],
+		["month", 30 * 24 * 60 * 60 * 1000],
+		["week", 7 * 24 * 60 * 60 * 1000],
+		["day", 24 * 60 * 60 * 1000],
+		["hour", 60 * 60 * 1000],
+		["minute", 60 * 1000],
+		["second", 1 * 1000]
+	].reduce((acc, [unitName, unitValue]) => {
+		if (acc) return acc
+		if (difference > unitValue) {
+			const number = Math.floor(difference / unitValue)
+			const pluralUnit = unitName + (number == 1 ? "" : "s")
+			return `${number} ${pluralUnit} ago`
+		}
+	}, null) || "just now"
+}
+
+module.exports.timeToPastText = timeToPastText
diff --git a/api/utils/db.js b/utils/db.js
similarity index 85%
rename from api/utils/db.js
rename to utils/db.js
index 1ceab92..1f76de9 100644
--- a/api/utils/db.js
+++ b/utils/db.js
@@ -2,7 +2,7 @@ const sqlite = require("better-sqlite3")
 const pj = require("path").join
 const fs = require("fs")
 
-const dir = pj(__dirname, "../../db")
+const dir = pj(__dirname, "../db")
 fs.mkdirSync(pj(dir, "backups"), {recursive: true})
 const db = new sqlite(pj(dir, "cloudtube.db"))
 module.exports = db
diff --git a/api/utils/getuser.js b/utils/getuser.js
similarity index 100%
rename from api/utils/getuser.js
rename to utils/getuser.js
diff --git a/api/utils/upgradedb.js b/utils/upgradedb.js
similarity index 100%
rename from api/utils/upgradedb.js
rename to utils/upgradedb.js
diff --git a/api/utils/validate.js b/utils/validate.js
similarity index 100%
rename from api/utils/validate.js
rename to utils/validate.js
diff --git a/util/words.txt b/utils/words.txt
similarity index 100%
rename from util/words.txt
rename to utils/words.txt
diff --git a/api/utils/youtube.js b/utils/youtube.js
similarity index 100%
rename from api/utils/youtube.js
rename to utils/youtube.js