From cbc3a2bf6775f7ff09d8ca4a934ff16bb21b56d1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 23 Aug 2020 01:17:31 +1200 Subject: [PATCH] First working video page --- .gitignore | 8 + api/youtube.js | 596 +++++++ html/static/fonts/bariol.woff | Bin 0 -> 69584 bytes html/static/images/arrow-down-wide.svg | 3 + html/static/images/search.svg | 7 + html/static/js/elemjs/elemjs.js | 137 ++ html/static/js/elemjs/elemjs.mjs | 78 - html/static/js/focus.js | 9 + html/static/js/player.js | 117 ++ jsconfig.json | 9 + package-lock.json | 622 +++++++ package.json | 16 + pug/includes/head.pug | 1 + pug/includes/layout.pug | 15 + pug/video.pug | 68 + sass/includes/base.sass | 34 + sass/includes/colors.sass | 14 + sass/includes/video-page.sass | 117 ++ sass/main.sass | 88 + server.js | 26 + util/words.txt | 2048 ++++++++++++++++++++++++ 21 files changed, 3935 insertions(+), 78 deletions(-) create mode 100644 .gitignore create mode 100644 api/youtube.js create mode 100755 html/static/fonts/bariol.woff create mode 100644 html/static/images/arrow-down-wide.svg create mode 100644 html/static/images/search.svg create mode 100644 html/static/js/elemjs/elemjs.js delete mode 100644 html/static/js/elemjs/elemjs.mjs create mode 100644 html/static/js/focus.js create mode 100644 html/static/js/player.js create mode 100644 jsconfig.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pug/includes/head.pug create mode 100644 pug/includes/layout.pug create mode 100644 pug/video.pug create mode 100644 sass/includes/base.sass create mode 100644 sass/includes/colors.sass create mode 100644 sass/includes/video-page.sass create mode 100644 sass/main.sass create mode 100644 server.js create mode 100644 util/words.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32c7cda --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Editor crud files +*~ +\#*# +.#* +.vscode + +# Auto-generated files +node_modules \ No newline at end of file diff --git a/api/youtube.js b/api/youtube.js new file mode 100644 index 0000000..2dafdc2 --- /dev/null +++ b/api/youtube.js @@ -0,0 +1,596 @@ +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/html/static/fonts/bariol.woff b/html/static/fonts/bariol.woff new file mode 100755 index 0000000000000000000000000000000000000000..79981c21be2a5d059d9cc713ad502e3587a24ccc GIT binary patch literal 69584 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�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 + + diff --git a/html/static/images/search.svg b/html/static/images/search.svg new file mode 100644 index 0000000..b00640b --- /dev/null +++ b/html/static/images/search.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/html/static/js/elemjs/elemjs.js b/html/static/js/elemjs/elemjs.js new file mode 100644 index 0000000..62be34e --- /dev/null +++ b/html/static/js/elemjs/elemjs.js @@ -0,0 +1,137 @@ +/** + * Shortcut for querySelector. + * @template {HTMLElement} T + * @returns {T} + */ +const q = s => document.querySelector(s); +/** + * Shortcut for querySelectorAll. + * @template {HTMLElement} T + * @returns {T[]} + */ +const qa = s => document.querySelectorAll(s); + +/** + * An easier, chainable, object-oriented way to create and update elements + * and children according to related data. Subclass ElemJS to create useful, + * advanced data managers, or just use it inline to quickly make a custom element. + */ +class ElemJS { + constructor(type) { + if (type instanceof HTMLElement) { + // If passed an existing element, bind to it + this.bind(type); + } else if (typeof type === "string") { + // Otherwise, create a new detached element to bind to + this.bind(document.createElement(type)); + } else { + throw new Error("Cannot create an element of type ${type}") + } + this.children = []; + } + + /** Bind this construct to an existing element on the page. */ + bind(element) { + this.element = element; + this.element.js = this; + return this; + } + + /** Add a class. */ + class() { + for (let name of arguments) if (name) this.element.classList.add(name); + return this; + } + + /** Remove a class. */ + removeClass() { + for (let name of arguments) if (name) this.element.classList.remove(name); + return this; + } + + /** Set a JS property on the element. */ + direct(name, value) { + if (name) this.element[name] = value; + return this; + } + + /** Set an attribute on the element. */ + attribute(name, value) { + if (name) this.element.setAttribute(name, value); + return this; + } + + /** Set a style on the element. */ + style(name, value) { + if (name) this.element.style[name] = value; + return this; + } + + /** Set the element's ID. */ + id(name) { + if (name) this.element.id = name; + return this; + } + + /** Attach a callback function to an event on the element. */ + on(name, callback) { + this.element.addEventListener(name, callback); + return this; + } + + /** Set the element's text. */ + text(name) { + this.element.innerText = name; + return this; + } + + /** Create a text node and add it to the element. */ + addText(name) { + const node = document.createTextNode(name); + this.element.appendChild(node); + return this; + } + + /** Set the element's HTML content. */ + html(name) { + this.element.innerHTML = name; + return this; + } + + /** + * Add children to the element. + * Children can either be an instance of ElemJS, in + * which case the element will be appended as a child, + * or a string, in which case the string will be added as a text node. + * Each child should be a parameter to this method. + */ + child(...children) { + for (const toAdd of children) { + if (typeof toAdd === "object" && toAdd !== null) { + // Should be an instance of ElemJS, so append as child + toAdd.parent = this; + this.element.appendChild(toAdd.element); + this.children.push(toAdd); + } else if (typeof toAdd === "string") { + // Is a string, so add as text node + this.addText(toAdd); + } + } + return this; + } + + /** + * Remove all children from the element. + */ + clearChildren() { + this.children.length = 0; + while (this.element.lastChild) this.element.removeChild(this.element.lastChild); + } +} + +/** Shortcut for `new ElemJS`. */ +function ejs(tag) { + return new ElemJS(tag); +} + +export {q, qa, ElemJS, ejs}; diff --git a/html/static/js/elemjs/elemjs.mjs b/html/static/js/elemjs/elemjs.mjs deleted file mode 100644 index 563d921..0000000 --- a/html/static/js/elemjs/elemjs.mjs +++ /dev/null @@ -1,78 +0,0 @@ -/** @returns {HTMLElement} */ -export function q(s) { - return document.querySelector(s) -} - -export class ElemJS { - constructor(type) { - if (type instanceof HTMLElement) this.bind(type) - else this.bind(document.createElement(type)) - this.children = []; - } - bind(element) { - /** @type {HTMLElement} */ - this.element = element - // @ts-ignore - this.element.js = this - return this - } - class() { - for (let name of arguments) if (name) this.element.classList.add(name); - return this; - } - removeClass() { - for (let name of arguments) if (name) this.element.classList.remove(name); - return this; - } - direct(name, value) { - if (name) this.element[name] = value; - return this; - } - attribute(name, value) { - if (name) this.element.setAttribute(name, value); - return this; - } - style(name, value) { - if (name) this.element.style[name] = value; - return this; - } - id(name) { - if (name) this.element.id = name; - return this; - } - text(name) { - this.element.innerText = name; - return this; - } - addText(name) { - const node = document.createTextNode(name) - this.element.appendChild(node) - return this - } - html(name) { - this.element.innerHTML = name; - return this; - } - event(name, callback) { - this.element.addEventListener(name, event => callback(event)) - } - child(toAdd, position) { - if (typeof(toAdd) == "object") { - toAdd.parent = this; - if (typeof(position) == "number" && position >= 0) { - this.element.insertBefore(toAdd.element, this.element.children[position]); - this.children.splice(position, 0, toAdd); - } else { - this.element.appendChild(toAdd.element); - this.children.push(toAdd); - } - } else if (typeof toAdd === "string") { - this.text(toAdd) - } - return this; - } - clearChildren() { - this.children.length = 0; - while (this.element.lastChild) this.element.removeChild(this.element.lastChild); - } -} diff --git a/html/static/js/focus.js b/html/static/js/focus.js new file mode 100644 index 0000000..12b2dc3 --- /dev/null +++ b/html/static/js/focus.js @@ -0,0 +1,9 @@ +document.addEventListener("mousedown", () => { + document.body.classList.remove("show-focus") +}) + +document.addEventListener("keydown", event => { + if (event.key === "Tab") { + document.body.classList.add("show-focus") + } +}) diff --git a/html/static/js/player.js b/html/static/js/player.js new file mode 100644 index 0000000..69b016d --- /dev/null +++ b/html/static/js/player.js @@ -0,0 +1,117 @@ +import {q, ElemJS} from "/static/js/elemjs/elemjs.js" + +const video = q("#video") +const audio = q("#audio") + +const videoFormats = new Map() +const audioFormats = new Map() +for (const f of [].concat( + data.formatStreams.map(f => (f.isAdaptive = false, f)), + data.adaptiveFormats.map(f => (f.isAdaptive = true, f)) +)) { + if (f.type.startsWith("video")) { + videoFormats.set(f.itag, f) + } else { + audioFormats.set(f.itag, f) + } +} + +function getBestAudioFormat() { + let best = null + for (const f of audioFormats.values()) { + if (best === null || f.bitrate > best.bitrate) { + best = f + } + } + return best +} + +class FormatLoader { + constructor() { + this.npv = videoFormats.get(q("#video").getAttribute("data-itag")) + this.npa = null + } + + play(itag) { + this.npv = videoFormats.get(itag) + if (this.npv.isAdaptive) { + this.npa = getBestAudioFormat() + } else { + this.npa = null + } + this.update() + } + + update() { + const lastTime = video.currentTime + video.src = this.npv.url + video.currentTime = lastTime + if (this.npa) { + audio.src = this.npa.url + audio.currentTime = lastTime + } + } +} + +const formatLoader = new FormatLoader() + +class QualitySelect extends ElemJS { + constructor() { + super(q("#quality-select")) + this.on("input", this.onInput.bind(this)) + } + + onInput() { + const itag = this.element.value + formatLoader.play(itag) + } +} + +const qualitySelect = new QualitySelect() + +function playbackIntervention(event) { + console.log(event.target.tagName.toLowerCase(), event.type) + if (audio.src) { + let target = event.target + let targetName = target.tagName.toLowerCase() + let other = (event.target === video ? audio : video) + switch (event.type) { + case "durationchange": + target.ready = false; + break; + case "seeked": + target.ready = false; + target.pause(); + other.currentTime = target.currentTime; + break; + case "play": + other.currentTime = target.currentTime; + other.play(); + break; + case "pause": + other.currentTime = target.currentTime; + other.pause(); + case "playing": + other.currentTime = target.currentTime; + break; + case "ratechange": + other.rate = target.rate; + break; + // case "stalled": + // case "waiting": + // target.pause(); + // break; + } + } else { + // @ts-ignore this does exist + // if (event.type == "canplaythrough" && !video.manualPaused) video.play(); + } +} + +for (let eventName of ["pause", "play", "seeked"]) { + video.addEventListener(eventName, playbackIntervention) +} +for (let eventName of ["canplaythrough", "waiting", "stalled"]) { + video.addEventListener(eventName, playbackIntervention) + audio.addEventListener(eventName, playbackIntervention) +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..834a9ea --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "checkJs": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d93bc22 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,622 @@ +{ + "name": "cloudtube", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "pinski": { + "version": "file:../pinski", + "requires": { + "mime": "^2.4.4", + "pug": "^2.0.3", + "sass": "^1.26.5", + "ws": "^7.1.1" + }, + "dependencies": { + "@types/babel-types": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", + "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" + }, + "@types/babylon": { + "version": "6.16.5", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", + "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", + "requires": { + "@types/babel-types": "*" + } + }, + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, + "chokidar": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", + "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.2.tgz", + "integrity": "sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "sass": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.5.tgz", + "integrity": "sha512-FG2swzaZUiX53YzZSjSakzvGtlds0lcbF+URuU9mxOv7WBh7NhXEVDa4kPKN4hN6fC2TkOTOKqiqp6d53N9X5Q==", + "requires": { + "chokidar": ">=2.0.0 <4.0.0" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "ws": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.1.tgz", + "integrity": "sha512-o41D/WmDeca0BqYhsr3nJzQyg9NF5X8l/UdnFNux9cS3lwB+swm8qGWX5rn+aD6xfBU3rGmtHij7g7x6LxFU3A==", + "requires": { + "async-limiter": "^1.0.0" + } + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dd38fd1 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "cloudtube", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "AGPL-3.0", + "dependencies": { + "node-fetch": "^2.6.0", + "pinski": "file:../pinski" + } +} diff --git a/pug/includes/head.pug b/pug/includes/head.pug new file mode 100644 index 0000000..bc0fb7a --- /dev/null +++ b/pug/includes/head.pug @@ -0,0 +1 @@ +doctype html diff --git a/pug/includes/layout.pug b/pug/includes/layout.pug new file mode 100644 index 0000000..438574a --- /dev/null +++ b/pug/includes/layout.pug @@ -0,0 +1,15 @@ +doctype html +html + head + meta(charset="utf-8") + meta(name="viewport" value="width=device-width, initial-scale=1") + link(rel="stylesheet" type="text/css" href=getStaticURL("sass", "/main.sass")) + script(type="module" src=getStaticURL("html", "/static/js/focus.js")) + block head + + body.show-focus + nav.main-nav + a(href="/").link.home CloudTube + input(type="text" placeholder="Search" name="q" autocomplete="off").search + + block content diff --git a/pug/video.pug b/pug/video.pug new file mode 100644 index 0000000..b16718f --- /dev/null +++ b/pug/video.pug @@ -0,0 +1,68 @@ +extends includes/layout.pug + +block head + title= video.title + script(type="module" src=getStaticURL("html", "/static/js/player.js")) + script const data = !{JSON.stringify(video)} + +block content + - const sortedFormatStreams = video.formatStreams.slice().sort((a, b) => b.second__height - a.second__height) + - const sortedVideoAdaptiveFormats = video.adaptiveFormats.filter(f => f.type.startsWith("video")).sort((a, b) => b.second__height - a.second__height) + main.video-page + .main-video-section + .video-container + - const format = sortedFormatStreams[0] + video(controls preload="auto" width=format.second__width height=format.second__height data-itag=format.itag)#video.video + source(src=format.url type=format.type) + + #current-time-container + #end-cards-container + .info + header.info-main + h1.title= video.title + .author + a(href=`/channel/${video.authorId}`).author-link= `Uploaded by ${video.author}` + .info-secondary + - const date = new Date(video.published*1000) + - const month = new Intl.DateTimeFormat("en-US", {month: "short"}).format(date.getTime()) + div= `Uploaded ${date.getUTCDate()} ${month} ${date.getUTCFullYear()}` + div= video.second__viewCountText + div(style=`--rating: ${video.rating*20}%`)#rating-bar.rating-bar + + audio(preload="auto")#audio + #live-event-notice + #audio-loading-display + + .video-button-container + button.border-look#subscribe Subscribe + button.border-look#theatre Theatre + select(autocomplete="off").border-look#quality-select + each f in sortedFormatStreams + option(value=f.itag)= `${f.qualityLabel} ${f.container}` + each f in sortedVideoAdaptiveFormats + option(value=f.itag)= `${f.qualityLabel} ${f.container} *` + + .video-button-container + a(href="/subscriptions").border-look + img(src="/static/images/search.svg" width=17 height=17 alt="").button-icon + | Search + button.border-look#share Share + a.border-look YouTube + a.border-look Iv: Snopyta + + .description!= video.descriptionHtml + + aside.related-videos + h2.related-header Related videos + each r in video.recommendedVideos + .related-video + - let link = `/watch?v=${r.videoId}` + a(href=link).thumbnail + img(src=`https://i.ytimg.com/vi/${r.videoId}/mqdefault.jpg` width=320 height=180 alt="").image + span.duration= r.second__lengthText + .info + div.title: a(href=link).title-link= r.title + div.author-line + a(href=`/channel/${authorId}`).author= r.author + = ` • ` + span.views= r.viewCountText diff --git a/sass/includes/base.sass b/sass/includes/base.sass new file mode 100644 index 0000000..33eeef1 --- /dev/null +++ b/sass/includes/base.sass @@ -0,0 +1,34 @@ +@use "colors.sass" as c + +body + background-color: c.$bg-dark + color: c.$fg-main + font-family: "Bariol", sans-serif + font-size: 18px + margin: 0 + padding: 0 + +a + color: c.$link + +input, select, button + font-family: inherit + font-size: 16px + +button + cursor: pointer + +:-moz-focusring + outline: none + +::-moz-focus-inner + border: 0 + +select:-moz-focusring + color: transparent + text-shadow: 0 0 0 c.$fg-bright + +body.show-focus + a, select, button, input, video + &:focus + outline: 2px dotted #ddd diff --git a/sass/includes/colors.sass b/sass/includes/colors.sass new file mode 100644 index 0000000..b0eec04 --- /dev/null +++ b/sass/includes/colors.sass @@ -0,0 +1,14 @@ +$bg-darkest: #202123 +$bg-darker: #303336 +$bg-dark: #36393f +$bg-accent: #4f5359 +$bg-accent-x: #3f4247 +$bg-accent-area: #44474b + +$fg-bright: #fff +$fg-main: #ddd +$fg-dim: #bbb + +$edge-grey: #808080 + +$link: #72b4f6 diff --git a/sass/includes/video-page.sass b/sass/includes/video-page.sass new file mode 100644 index 0000000..51afc9b --- /dev/null +++ b/sass/includes/video-page.sass @@ -0,0 +1,117 @@ +@use "colors.sass" as c + +.video-page + display: grid + grid-auto-flow: row + padding: 20px + grid-gap: 16px + + @media screen and (min-width: 1000px) + grid-template-columns: 1fr 400px + +.main-video-section + .video-container + text-align: center + + .video + display: inline-block + width: 100% + height: auto + max-height: 80vh + + .info + display: flex + margin: 8px 4px 16px + font-size: 17px + + .info-main + flex: 1 + + .title + margin: 0px 0px 4px + font-size: 30px + font-weight: normal + color: c.$fg-bright + + .author-link + color: c.$fg-main + text-decoration: none + + &:hover, &:active + color: c.$fg-bright + text-decoration: underline + + .info-secondary + display: flex + flex-direction: column + align-items: end + margin-top: 6px + margin-left: 6px + + .rating-bar + margin-top: 8px + width: 140px + height: 8px + border-radius: 3px + background: linear-gradient(to right, #1a1 var(--rating), #bbb var(--rating)) + + .description + font-size: 17px + margin: 16px 4px 4px 4px + background-color: c.$bg-accent-area + padding: 12px + border-radius: 4px + +.related-header + margin: 4px 0px 12px 2px + font-weight: normal + font-size: 26px + +.related-video + display: grid + grid-template-columns: 160px 1fr + grid-gap: 8px + align-items: start + align-content: start + margin-bottom: 16px + + .thumbnail + position: relative + display: flex + background: c.$bg-darkest + + .image + width: 160px + height: 90px + + .duration + position: absolute + bottom: 3px + right: 3px + color: c.$fg-bright + font-size: 14px + background: rgba(20, 20, 20, 0.85) + line-height: 1 + padding: 3px 5px 4px + border-radius: 4px + + .title + font-size: 15px + line-height: 1.2 + + .title-link + color: c.$fg-main + text-decoration: none + + .author-line + margin-top: 4px + font-size: 15px + color: c.$fg-dim + + .author + color: c.$fg-dim + text-decoration: none + + &:hover, &:active + color: c.$fg-bright + text-decoration: underline diff --git a/sass/main.sass b/sass/main.sass new file mode 100644 index 0000000..787d6b7 --- /dev/null +++ b/sass/main.sass @@ -0,0 +1,88 @@ +@use "includes/base.sass" +@use "includes/colors.sass" as c +@use "sass:selector" +@use "includes/video-page.sass" + +@font-face + font-family: "Bariol" + src: url(/static/fonts/bariol.woff) + +@mixin button-base + appearance: none + -moz-appearance: none + color: c.$fg-bright + border: none + border-radius: 4px + padding: 8px + margin: 0 + text-decoration: none + line-height: 1.25 + + @at-root #{selector.unify(&, "select")} + padding: 7px 27px 7px 8px + background: url(/static/images/arrow-down-wide.svg) right 53% no-repeat c.$bg-accent-x + + @at-root #{selector.unify(&, "a")} + padding: 7px 8px + + .button-icon + position: relative + top: 3px + margin-right: 8px + margin-left: 2px + +@mixin button-bg + @include button-base + + background-color: c.$bg-accent-x + +@mixin border-button + @include button-bg + + border: 1px solid c.$edge-grey + +@mixin button-size + margin: 4px + font-size: 16px + +@mixin button-hover + &:hover + background-color: c.$bg-accent + + &:active + background-color: c.$bg-dark + +.border-look + @include border-button + @include button-size + @include button-hover + +.main-nav + background-color: c.$bg-accent + display: flex + padding: 8px + box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1) + + .link + @include button-base + text-decoration: none + margin: 1px 8px 1px 0px + font-size: 20px + + &.home + font-weight: bold + + &, &:visited + color: #fff + + &:focus, &:hover + background-color: c.$bg-accent-x + + .search + @include button-bg + flex: 1 + margin: 1px + + &:hover, &:focus + border: 1px solid c.$edge-grey + margin: 0px diff --git a/server.js b/server.js new file mode 100644 index 0000000..01551a4 --- /dev/null +++ b/server.js @@ -0,0 +1,26 @@ +const {Pinski} = require("pinski") +const {setInstance} = require("pinski/plugins") + +const server = new Pinski({ + port: 8080, + relativeRoot: __dirname, + filesDir: "html" +}) + +setInstance(server) + +server.addSassDir("sass", ["sass/includes"]) +server.addRoute("/static/css/main.css", "sass/main.sass", "sass") + +server.addPugDir("pug", ["pug/includes"]) + +server.addStaticHashTableDir("html/static/js") +server.addStaticHashTableDir("html/static/js/elemjs") + +server.addAPIDir("api") + +server.startServer() + +setTimeout(() => { + console.log(server.staticFileTable, server.pageHandlers) +}, 2000) diff --git a/util/words.txt b/util/words.txt new file mode 100644 index 0000000..f78ccaf --- /dev/null +++ b/util/words.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo \ No newline at end of file