Compare commits
21 Commits
main
...
fork-maste
Author | SHA1 | Date |
---|---|---|
Olivier 'reivilibre' | d08d6932e4 | |
Cadence Ember | 9f7b1bbcae | |
Cadence Ember | 3d5927ab28 | |
jo! | 8d292cc200 | |
jo! | 17185c6b5e | |
Olivier 'reivilibre' | 9970c5e92f | |
Olivier 'reivilibre' | 6e87edad3b | |
Olivier 'reivilibre' | facb959cb8 | |
Cadence Ember | 61c8e205d8 | |
Cadence Ember | a2dfeb2edc | |
Lomanic | 6de9abd499 | |
Cadence Ember | 5e6b2bf31c | |
Cadence Ember | f04be0d3f9 | |
Cadence Ember | 25baf8c73b | |
Cadence Ember | 1333b990f6 | |
Cadence Ember | 893684c311 | |
Cadence Ember | 109dcd22de | |
Cadence Ember | 15e3f06ad6 | |
Cadence Ember | 0d23d66700 | |
Cadence Ember | 4e1f2b3607 | |
Olivier 'reivilibre' | 6d1f91b2fa |
|
@ -1,18 +1,28 @@
|
||||||
const {render} = require("pinski/plugins")
|
const {render} = require("pinski/plugins")
|
||||||
const constants = require("../utils/constants")
|
|
||||||
const {fetchChannel} = require("../utils/youtube")
|
const {fetchChannel} = require("../utils/youtube")
|
||||||
const {getUser} = require("../utils/getuser")
|
const {getUser} = require("../utils/getuser")
|
||||||
const converters = require("../utils/converters")
|
const converters = require("../utils/converters")
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
route: `/channel/(${constants.regex.ucid})`, methods: ["GET"], code: async ({req, fill, url}) => {
|
route: `/(c|channel|user)/(.+)`, methods: ["GET"], code: async ({req, fill, url}) => {
|
||||||
const id = fill[0]
|
const path = fill[0]
|
||||||
|
const id = fill[1]
|
||||||
const user = getUser(req)
|
const user = getUser(req)
|
||||||
const settings = user.getSettingsOrDefaults()
|
const settings = user.getSettingsOrDefaults()
|
||||||
const data = await fetchChannel(id, settings.instance)
|
const data = await fetchChannel(path, id, settings.instance)
|
||||||
const subscribed = user.isSubscribed(id)
|
|
||||||
const instanceOrigin = settings.instance
|
const instanceOrigin = settings.instance
|
||||||
|
|
||||||
|
// problem with the channel? fetchChannel has collected the necessary information for us.
|
||||||
|
// we can render a skeleton page, display the message, and provide the option to unsubscribe.
|
||||||
|
if (data.error) {
|
||||||
|
const statusCode = data.missing ? 410 : 500
|
||||||
|
const subscribed = user.isSubscribed(id)
|
||||||
|
return render(statusCode, "pug/channel-error.pug", {req, settings, data, subscribed, instanceOrigin})
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything is fine
|
||||||
|
|
||||||
// normalise info, apply watched status
|
// normalise info, apply watched status
|
||||||
if (!data.second__subCountText && data.subCount) {
|
if (!data.second__subCountText && data.subCount) {
|
||||||
data.second__subCountText = converters.subscriberCountToText(data.subCount)
|
data.second__subCountText = converters.subscriberCountToText(data.subCount)
|
||||||
|
@ -24,7 +34,8 @@ module.exports = [
|
||||||
video.watched = watchedVideos.includes(video.videoId)
|
video.watched = watchedVideos.includes(video.videoId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return render(200, "pug/channel.pug", {url, data, subscribed, instanceOrigin})
|
const subscribed = user.isSubscribed(data.authorId)
|
||||||
|
return render(200, "pug/channel.pug", {req, settings, data, subscribed, instanceOrigin})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,8 +9,7 @@ const {Matcher, PatternCompileError} = require("../utils/matcher")
|
||||||
const filterMaxLength = 160
|
const filterMaxLength = 160
|
||||||
const regexpEnabledText = constants.server_setup.allow_regexp_filters ? "" : "not"
|
const regexpEnabledText = constants.server_setup.allow_regexp_filters ? "" : "not"
|
||||||
|
|
||||||
function getCategories(req) {
|
function getCategories(user) {
|
||||||
const user = getUser(req)
|
|
||||||
const filters = user.getFilters()
|
const filters = user.getFilters()
|
||||||
|
|
||||||
// Sort filters into categories for display. Titles are already sorted.
|
// Sort filters into categories for display. Titles are already sorted.
|
||||||
|
@ -39,7 +38,9 @@ function getCategories(req) {
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
route: "/filters", methods: ["GET"], code: async ({req, url}) => {
|
route: "/filters", methods: ["GET"], code: async ({req, url}) => {
|
||||||
const categories = getCategories(req)
|
const user = getUser(req)
|
||||||
|
const categories = getCategories(user)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
let referrer = url.searchParams.get("referrer") || null
|
let referrer = url.searchParams.get("referrer") || null
|
||||||
|
|
||||||
let type = null
|
let type = null
|
||||||
|
@ -54,7 +55,7 @@ module.exports = [
|
||||||
label = url.searchParams.get("label")
|
label = url.searchParams.get("label")
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(200, "pug/filters.pug", {categories, type, contents, label, referrer, filterMaxLength, regexpEnabledText})
|
return render(200, "pug/filters.pug", {req, settings, categories, type, contents, label, referrer, filterMaxLength, regexpEnabledText})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -100,8 +101,10 @@ module.exports = [
|
||||||
return true
|
return true
|
||||||
}, state => {
|
}, state => {
|
||||||
const {type, contents, label, compileError} = state
|
const {type, contents, label, compileError} = state
|
||||||
const categories = getCategories(req)
|
const user = getUser(req)
|
||||||
return render(400, "pug/filters.pug", {categories, type, contents, label, compileError, filterMaxLength, regexpEnabledText})
|
const categories = getCategories(user)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
return render(400, "pug/filters.pug", {req, settings, categories, type, contents, label, compileError, filterMaxLength, regexpEnabledText})
|
||||||
})
|
})
|
||||||
.last(state => {
|
.last(state => {
|
||||||
const {type, contents, label} = state
|
const {type, contents, label} = state
|
||||||
|
|
|
@ -23,10 +23,9 @@ module.exports = [
|
||||||
const token = user.token
|
const token = user.token
|
||||||
|
|
||||||
if (add) {
|
if (add) {
|
||||||
await fetchChannel(ucid, settings.instance)
|
await fetchChannel("channel", ucid, settings.instance)
|
||||||
db.prepare(
|
db.prepare(
|
||||||
"INSERT INTO Subscriptions (token, ucid) VALUES (?, ?)"
|
"INSERT INTO Subscriptions (token, ucid) VALUES (?, ?)"
|
||||||
+ " ON CONFLICT (token, ucid) DO UPDATE SET channel_missing = 0"
|
|
||||||
).run(token, ucid)
|
).run(token, ucid)
|
||||||
} else {
|
} else {
|
||||||
db.prepare("DELETE FROM Subscriptions WHERE token = ? AND ucid = ?").run(token, ucid)
|
db.prepare("DELETE FROM Subscriptions WHERE token = ? AND ucid = ?").run(token, ucid)
|
||||||
|
@ -41,7 +40,6 @@ module.exports = [
|
||||||
}),
|
}),
|
||||||
content: "Success, redirecting..."
|
content: "Success, redirecting..."
|
||||||
}
|
}
|
||||||
return redirect(params.get("referrer"), 303)
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|
25
api/pages.js
|
@ -1,16 +1,35 @@
|
||||||
const {render} = require("pinski/plugins")
|
const {render} = require("pinski/plugins")
|
||||||
|
const {getUser} = require("../utils/getuser")
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
route: "/", methods: ["GET"], code: async ({req}) => {
|
route: "/", methods: ["GET"], code: async ({req}) => {
|
||||||
const userAgent = req.headers["user-agent"] || ""
|
const userAgent = req.headers["user-agent"] || ""
|
||||||
const mobile = userAgent.toLowerCase().includes("mobile")
|
const mobile = userAgent.toLowerCase().includes("mobile")
|
||||||
return render(200, "pug/home.pug", {mobile})
|
const user = getUser(req)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
return render(200, "pug/home.pug", {req, settings, mobile})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
route: "/js-licenses", methods: ["GET"], code: async () => {
|
route: "/(?:js-)?licenses", methods: ["GET"], code: async ({req}) => {
|
||||||
return render(200, "pug/js-licenses.pug")
|
const user = getUser(req)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
return render(200, "pug/licenses.pug", {req, settings})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/cant-think", methods: ["GET"], code: async ({req}) => {
|
||||||
|
const user = getUser(req)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
return render(200, "pug/cant-think.pug", {req, settings})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/privacy", methods: ["GET"], code: async ({req}) => {
|
||||||
|
const user = getUser(req)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
return render(200, "pug/privacy.pug", {req, settings})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -26,7 +26,7 @@ module.exports = [
|
||||||
const filters = user.getFilters()
|
const filters = user.getFilters()
|
||||||
results = converters.applyVideoFilters(results, filters).videos
|
results = converters.applyVideoFilters(results, filters).videos
|
||||||
|
|
||||||
return render(200, "pug/search.pug", {url, query, results, instanceOrigin})
|
return render(200, "pug/search.pug", {req, settings, url, query, results, instanceOrigin})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,12 +7,23 @@ const validate = require("../utils/validate")
|
||||||
const V = validate.V
|
const V = validate.V
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
{
|
||||||
|
route: "/api/settings", methods: ["GET"], code: async ({req}) => {
|
||||||
|
const user = getUser(req)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
content: settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
route: "/settings", methods: ["GET"], code: async ({req}) => {
|
route: "/settings", methods: ["GET"], code: async ({req}) => {
|
||||||
const user = getUser(req)
|
const user = getUser(req)
|
||||||
const settings = user.getSettings()
|
const settings = user.getSettings()
|
||||||
const instances = instancesList.get()
|
const instances = instancesList.get()
|
||||||
return render(200, "pug/settings.pug", {constants, user, settings, instances})
|
return render(200, "pug/settings.pug", {req, constants, user, settings, instances})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,12 +11,14 @@ module.exports = [
|
||||||
let hasSubscriptions = false
|
let hasSubscriptions = false
|
||||||
let videos = []
|
let videos = []
|
||||||
let channels = []
|
let channels = []
|
||||||
|
let missingChannelCount = 0
|
||||||
let refreshed = null
|
let refreshed = null
|
||||||
if (user.token) {
|
if (user.token) {
|
||||||
// trigger a background refresh, needed if they came back from being inactive
|
// trigger a background refresh, needed if they came back from being inactive
|
||||||
refresher.skipWaiting()
|
refresher.skipWaiting()
|
||||||
// get channels
|
// get channels
|
||||||
channels = db.prepare(`SELECT Channels.* FROM Channels INNER JOIN Subscriptions ON Channels.ucid = Subscriptions.ucid WHERE token = ? ORDER BY name`).all(user.token)
|
channels = db.prepare(`SELECT Channels.* FROM Channels INNER JOIN Subscriptions ON Channels.ucid = Subscriptions.ucid WHERE token = ? ORDER BY name`).all(user.token)
|
||||||
|
missingChannelCount = channels.reduce((a, c) => a + c.missing, 0)
|
||||||
// get refreshed status
|
// get refreshed status
|
||||||
refreshed = db.prepare(`SELECT min(refreshed) as min, max(refreshed) as max, count(refreshed) as count FROM Channels INNER JOIN Subscriptions ON Channels.ucid = Subscriptions.ucid WHERE token = ?`).get(user.token)
|
refreshed = db.prepare(`SELECT min(refreshed) as min, max(refreshed) as max, count(refreshed) as count FROM Channels INNER JOIN Subscriptions ON Channels.ucid = Subscriptions.ucid WHERE token = ?`).get(user.token)
|
||||||
// get watched videos
|
// get watched videos
|
||||||
|
@ -37,7 +39,7 @@ module.exports = [
|
||||||
}
|
}
|
||||||
const settings = user.getSettingsOrDefaults()
|
const settings = user.getSettingsOrDefaults()
|
||||||
const instanceOrigin = settings.instance
|
const instanceOrigin = settings.instance
|
||||||
return render(200, "pug/subscriptions.pug", {url, settings, hasSubscriptions, videos, channels, refreshed, timeToPastText, instanceOrigin})
|
return render(200, "pug/subscriptions.pug", {req, url, settings, hasSubscriptions, videos, channels, missingChannelCount, refreshed, timeToPastText, instanceOrigin})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,8 +3,8 @@ const {render} = require("pinski/plugins")
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
route: "/takedown", methods: ["GET"], code: async () => {
|
route: "/takedown", methods: ["GET"], code: async ({req}) => {
|
||||||
return render(200, "pug/takedown.pug", {constants})
|
return render(200, "pug/takedown.pug", {req, constants})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
10
api/video.js
|
@ -111,7 +111,7 @@ module.exports = [
|
||||||
// Check if playback is allowed
|
// Check if playback is allowed
|
||||||
const videoTakedownInfo = db.prepare("SELECT id, org, url FROM TakedownVideos WHERE id = ?").get(id)
|
const videoTakedownInfo = db.prepare("SELECT id, org, url FROM TakedownVideos WHERE id = ?").get(id)
|
||||||
if (videoTakedownInfo) {
|
if (videoTakedownInfo) {
|
||||||
return render(451, "pug/takedown-video.pug", videoTakedownInfo)
|
return render(451, "pug/takedown-video.pug", Object.assign({req, settings}, videoTakedownInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media fragment
|
// Media fragment
|
||||||
|
@ -129,7 +129,7 @@ module.exports = [
|
||||||
// Work out how to fetch the video
|
// Work out how to fetch the video
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
if (settings.local) { // skip to the local fetching page, which will then POST video data in a moment
|
if (settings.local) { // skip to the local fetching page, which will then POST video data in a moment
|
||||||
return render(200, "pug/local-video.pug", {id})
|
return render(200, "pug/local-video.pug", {req, settings, id})
|
||||||
}
|
}
|
||||||
var instanceOrigin = settings.instance
|
var instanceOrigin = settings.instance
|
||||||
var outURL = `${instanceOrigin}/api/v1/videos/${id}`
|
var outURL = `${instanceOrigin}/api/v1/videos/${id}`
|
||||||
|
@ -153,7 +153,7 @@ module.exports = [
|
||||||
// automatically add the entry to the videos list, so it won't be fetched again
|
// automatically add the entry to the videos list, so it won't be fetched again
|
||||||
const args = {id, ...channelTakedownInfo}
|
const args = {id, ...channelTakedownInfo}
|
||||||
db.prepare("INSERT INTO TakedownVideos (id, org, url) VALUES (@id, @org, @url)").run(args)
|
db.prepare("INSERT INTO TakedownVideos (id, org, url) VALUES (@id, @org, @url)").run(args)
|
||||||
return render(451, "pug/takedown-video.pug", channelTakedownInfo)
|
return render(451, "pug/takedown-video.pug", Object.assign({req, settings}, channelTakedownInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
// process stream list ordering
|
// process stream list ordering
|
||||||
|
@ -199,7 +199,7 @@ module.exports = [
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(200, "pug/video.pug", {
|
return render(200, "pug/video.pug", {
|
||||||
url, video, formats, subscribed, instanceOrigin, mediaFragment, autoplay, continuous,
|
req, url, video, formats, subscribed, instanceOrigin, mediaFragment, autoplay, continuous,
|
||||||
sessionWatched, sessionWatchedNext, settings
|
sessionWatched, sessionWatchedNext, settings
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ module.exports = [
|
||||||
// Create appropriate formatted message
|
// Create appropriate formatted message
|
||||||
const message = render(0, `pug/errors/${errorType}.pug`, locals).content
|
const message = render(0, `pug/errors/${errorType}.pug`, locals).content
|
||||||
|
|
||||||
return render(500, "pug/video.pug", {video: {videoId: id}, error: true, message})
|
return render(500, "pug/video.pug", {video: {videoId: id}, error: true, message, req, settings})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
const {request} = require("../utils/request")
|
||||||
|
/** @type {import("node-fetch").default} */
|
||||||
|
// @ts-ignore
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const {render} = require("pinski/plugins")
|
||||||
|
const db = require("../utils/db")
|
||||||
|
const {getToken, getUser} = require("../utils/getuser")
|
||||||
|
const pug = require("pug")
|
||||||
|
const converters = require("../utils/converters")
|
||||||
|
const constants = require("../utils/constants")
|
||||||
|
|
||||||
|
class InstanceError extends Error {
|
||||||
|
constructor(error, identifier) {
|
||||||
|
super(error)
|
||||||
|
this.identifier = identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageError extends Error {
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOrder(format) {
|
||||||
|
// most significant to least significant
|
||||||
|
// key, max, order, transform
|
||||||
|
// asc: lower number comes first, desc: higher number comes first
|
||||||
|
const spec = [
|
||||||
|
{key: "second__height", max: 8000, order: "desc", transform: x => x ? Math.floor(x/96) : 0},
|
||||||
|
{key: "fps", max: 100, order: "desc", transform: x => x ? Math.floor(x/10) : 0},
|
||||||
|
{key: "type", max: " ".repeat(60), order: "asc", transform: x => x.length}
|
||||||
|
]
|
||||||
|
let total = 0
|
||||||
|
for (let i = 0; i < spec.length; i++) {
|
||||||
|
const s = spec[i]
|
||||||
|
let diff = s.transform(format[s.key])
|
||||||
|
if (s.order === "asc") diff = s.transform(s.max) - diff
|
||||||
|
total += diff
|
||||||
|
if (i+1 < spec.length) { // not the last spec item?
|
||||||
|
const s2 = spec[i+1]
|
||||||
|
total *= s2.transform(s2.max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -total
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortFormats(video, preference) {
|
||||||
|
// Add second__ extensions to format objects, required if Invidious was the extractor
|
||||||
|
let formats = video.formatStreams.concat(video.adaptiveFormats)
|
||||||
|
for (const format of formats) {
|
||||||
|
if (!format.second__height && format.resolution) format.second__height = +format.resolution.slice(0, -1)
|
||||||
|
if (!format.second__order) format.second__order = formatOrder(format)
|
||||||
|
format.cloudtube__label = `${format.qualityLabel} ${format.container}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properly build and order format list
|
||||||
|
const standard = video.formatStreams.slice().sort((a, b) => b.second__height - a.second__height)
|
||||||
|
const adaptive = video.adaptiveFormats.filter(f => f.type.startsWith("video") && f.qualityLabel).sort((a, b) => a.second__order - b.second__order)
|
||||||
|
for (const format of adaptive) {
|
||||||
|
if (!format.cloudtube__label.endsWith("*")) format.cloudtube__label += " *"
|
||||||
|
}
|
||||||
|
formats = standard.concat(adaptive)
|
||||||
|
|
||||||
|
// Reorder fomats based on user preference
|
||||||
|
if (preference === 1) { // best dash
|
||||||
|
formats.sort((a, b) => {
|
||||||
|
const a1 = a.second__height + a.fps / 100
|
||||||
|
const b1 = b.second__height + b.fps / 100
|
||||||
|
return b1 - a1
|
||||||
|
})
|
||||||
|
} else if (preference === 2) { // best <=1080p
|
||||||
|
formats.sort((a, b) => {
|
||||||
|
const a1 = a.second__height + a.fps / 100
|
||||||
|
const b1 = b.second__height + b.fps / 100
|
||||||
|
if (b1 > 1081) {
|
||||||
|
if (a1 > 1081) return b1 - a1
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a1 > 1081) return 1
|
||||||
|
return b1 - a1
|
||||||
|
})
|
||||||
|
} else if (preference === 3) { // best low-fps
|
||||||
|
formats.sort((a, b) => {
|
||||||
|
if (b.fps > 30) {
|
||||||
|
if (a.fps < 30) return b.second__height - a.second__height
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a.fps > 30) return 1
|
||||||
|
return b.second__height - a.second__height
|
||||||
|
})
|
||||||
|
} else if (preference === 4) { // 360p only
|
||||||
|
formats.sort((a, b) => {
|
||||||
|
if (a.itag == 18) return -1
|
||||||
|
if (b.itag == 18) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
} else { // preference === 0, best combined
|
||||||
|
// should already be correct
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
route: `/embed/(${constants.regex.video_id})`, methods: ["GET"], upload: false, code: async ({req, url, body, fill}) => {
|
||||||
|
// Prepare data needed to render video page
|
||||||
|
|
||||||
|
const user = getUser(req)
|
||||||
|
const settings = user.getSettingsOrDefaults()
|
||||||
|
const id = fill[0];
|
||||||
|
|
||||||
|
// Check if playback is allowed
|
||||||
|
const videoTakedownInfo = db.prepare("SELECT id, org, url FROM TakedownVideos WHERE id = ?").get(id)
|
||||||
|
if (videoTakedownInfo) {
|
||||||
|
return render(451, "pug/takedown-video.pug", videoTakedownInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media fragment
|
||||||
|
const t = url.searchParams.get("t")
|
||||||
|
let mediaFragment = converters.tToMediaFragment(t)
|
||||||
|
|
||||||
|
// Continuous mode
|
||||||
|
const continuous = url.searchParams.get("continuous") === "1"
|
||||||
|
const autoplay = url.searchParams.get("autoplay") === "1"
|
||||||
|
const swp = url.searchParams.get("session-watched")
|
||||||
|
const sessionWatched = swp ? swp.split(" ") : []
|
||||||
|
const sessionWatchedNext = sessionWatched.concat([id]).join("+")
|
||||||
|
if (continuous) settings.quality = 0 // autoplay with synced streams does not work
|
||||||
|
|
||||||
|
// Work out how to fetch the video
|
||||||
|
if (req.method === "GET") {
|
||||||
|
if (settings.local) { // skip to the local fetching page, which will then POST video data in a moment
|
||||||
|
return render(200, "pug/local-video.pug", {id})
|
||||||
|
}
|
||||||
|
var instanceOrigin = settings.instance
|
||||||
|
var outURL = `${instanceOrigin}/api/v1/videos/${id}`
|
||||||
|
var videoFuture = request(outURL).then(res => res.json())
|
||||||
|
} else { // req.method === "POST"
|
||||||
|
var instanceOrigin = "http://localhost:3000"
|
||||||
|
var videoFuture = JSON.parse(new URLSearchParams(body.toString()).get("video"))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the video
|
||||||
|
const video = await videoFuture
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
if (!video) throw new MessageError("The instance returned null.")
|
||||||
|
if (video.error) throw new InstanceError(video.error, video.identifier)
|
||||||
|
|
||||||
|
// Check if channel playback is allowed
|
||||||
|
const channelTakedownInfo = db.prepare("SELECT ucid, org, url FROM TakedownChannels WHERE ucid = ?").get(video.authorId)
|
||||||
|
if (channelTakedownInfo) {
|
||||||
|
// automatically add the entry to the videos list, so it won't be fetched again
|
||||||
|
const args = {id, ...channelTakedownInfo}
|
||||||
|
db.prepare("INSERT INTO TakedownVideos (id, org, url) VALUES (@id, @org, @url)").run(args)
|
||||||
|
return render(451, "pug/takedown-video.pug", channelTakedownInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process stream list ordering
|
||||||
|
const formats = sortFormats(video, settings.quality)
|
||||||
|
|
||||||
|
// process length text and view count
|
||||||
|
for (const rec of video.recommendedVideos) {
|
||||||
|
converters.normaliseVideoInfo(rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter list
|
||||||
|
const {videos, filteredCount} = converters.applyVideoFilters(video.recommendedVideos, user.getFilters())
|
||||||
|
video.recommendedVideos = videos
|
||||||
|
|
||||||
|
// get subscription data
|
||||||
|
const subscribed = user.isSubscribed(video.authorId)
|
||||||
|
|
||||||
|
// process watched videos
|
||||||
|
user.addWatchedVideoMaybe(video.videoId)
|
||||||
|
const watchedVideos = user.getWatchedVideos()
|
||||||
|
if (watchedVideos.length) {
|
||||||
|
for (const rec of video.recommendedVideos) {
|
||||||
|
rec.watched = watchedVideos.includes(rec.videoId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalise view count
|
||||||
|
if (!video.second__viewCountText && video.viewCount) {
|
||||||
|
video.second__viewCountText = converters.viewCountToText(video.viewCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply media fragment to all sources
|
||||||
|
for (const format of formats) {
|
||||||
|
format.url += mediaFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite description
|
||||||
|
video.descriptionHtml = converters.rewriteVideoDescription(video.descriptionHtml, id)
|
||||||
|
|
||||||
|
// rewrite captions urls so they are served on the same domain via the /proxy route
|
||||||
|
for (const caption of video.captions) {
|
||||||
|
caption.url = `/proxy?${new URLSearchParams({"url": caption.url})}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(200, "pug/video_embed.pug", {
|
||||||
|
url, video, formats, subscribed, instanceOrigin, mediaFragment, autoplay, continuous,
|
||||||
|
sessionWatched, sessionWatchedNext, settings
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Something went wrong, somewhere! Find out where.
|
||||||
|
|
||||||
|
let errorType = "unrecognised-error"
|
||||||
|
const locals = {instanceOrigin, error}
|
||||||
|
|
||||||
|
// Sort error category
|
||||||
|
if (error instanceof fetch.FetchError) {
|
||||||
|
errorType = "fetch-error"
|
||||||
|
} else if (error instanceof MessageError) {
|
||||||
|
errorType = "message-error"
|
||||||
|
} else if (error instanceof InstanceError) {
|
||||||
|
if (error.identifier === "RATE_LIMITED_BY_YOUTUBE" || error.message === "Could not extract video info. Instance is likely blocked.") {
|
||||||
|
errorType = "rate-limited"
|
||||||
|
} else {
|
||||||
|
errorType = "instance-error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create appropriate formatted message
|
||||||
|
const message = render(0, `pug/errors/${errorType}.pug`, locals).content
|
||||||
|
|
||||||
|
return render(500, "pug/video_embed.pug", {video: {videoId: id}, error: true, message})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -15,8 +15,8 @@ const prepared = {
|
||||||
channel_refreshed_update: db.prepare(
|
channel_refreshed_update: db.prepare(
|
||||||
"UPDATE Channels SET refreshed = ? WHERE ucid = ?"
|
"UPDATE Channels SET refreshed = ? WHERE ucid = ?"
|
||||||
),
|
),
|
||||||
unsubscribe_all_from_channel: db.prepare(
|
channel_mark_as_missing: db.prepare(
|
||||||
"UPDATE Subscriptions SET channel_missing = 1 WHERE ucid = ?"
|
"UPDATE Channels SET missing = 1, missing_reason = ? WHERE ucid = ?"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ class RefreshQueue {
|
||||||
// get the next set of scheduled channels to refresh
|
// get the next set of scheduled channels to refresh
|
||||||
const afterTime = Date.now() - constants.caching.seen_token_subscriptions_eligible
|
const afterTime = Date.now() - constants.caching.seen_token_subscriptions_eligible
|
||||||
const channels = db.prepare(
|
const channels = db.prepare(
|
||||||
"SELECT DISTINCT Subscriptions.ucid FROM SeenTokens INNER JOIN Subscriptions ON SeenTokens.token = Subscriptions.token AND SeenTokens.seen > ? WHERE Subscriptions.channel_missing = 0 ORDER BY SeenTokens.seen DESC"
|
"SELECT DISTINCT Subscriptions.ucid FROM SeenTokens INNER JOIN Subscriptions ON SeenTokens.token = Subscriptions.token INNER JOIN Channels ON Channels.ucid = Subscriptions.ucid WHERE Channels.missing = 0 AND SeenTokens.seen > ? ORDER BY SeenTokens.seen DESC"
|
||||||
).pluck().all(afterTime)
|
).pluck().all(afterTime)
|
||||||
this.addLast(channels)
|
this.addLast(channels)
|
||||||
this.lastLoadTime = Date.now()
|
this.lastLoadTime = Date.now()
|
||||||
|
@ -72,11 +72,12 @@ class Refresher {
|
||||||
this.refreshQueue = new RefreshQueue()
|
this.refreshQueue = new RefreshQueue()
|
||||||
this.state = this.sym.ACTIVE
|
this.state = this.sym.ACTIVE
|
||||||
this.waitingTimeout = null
|
this.waitingTimeout = null
|
||||||
|
this.lastFakeNotFoundTime = 0
|
||||||
this.next()
|
this.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshChannel(ucid) {
|
refreshChannel(ucid) {
|
||||||
return fetch(`${constants.server_setup.local_instance_origin}/api/v1/channels/${ucid}/latest`).then(res => res.json()).then(root => {
|
return fetch(`${constants.server_setup.local_instance_origin}/api/v1/channels/${ucid}/latest`).then(res => res.json()).then(/** @param {any} root */ root => {
|
||||||
if (Array.isArray(root)) {
|
if (Array.isArray(root)) {
|
||||||
root.forEach(video => {
|
root.forEach(video => {
|
||||||
// organise
|
// organise
|
||||||
|
@ -89,11 +90,24 @@ class Refresher {
|
||||||
prepared.channel_refreshed_update.run(Date.now(), ucid)
|
prepared.channel_refreshed_update.run(Date.now(), ucid)
|
||||||
// console.log(`updated ${root.length} videos for channel ${ucid}`)
|
// console.log(`updated ${root.length} videos for channel ${ucid}`)
|
||||||
} else if (root.identifier === "PUBLISHED_DATES_NOT_PROVIDED") {
|
} else if (root.identifier === "PUBLISHED_DATES_NOT_PROVIDED") {
|
||||||
return [] // nothing we can do. skip this iteration.
|
// nothing we can do. skip this iteration.
|
||||||
} else if (root.identifier === "NOT_FOUND") {
|
} else if (root.identifier === "NOT_FOUND") {
|
||||||
// the channel does not exist. we should unsubscribe all users so we don't try again.
|
// YouTube sometimes returns not found for absolutely no reason.
|
||||||
// console.log(`channel ${ucid} does not exist, unsubscribing all users`)
|
// There is no way to distinguish between a fake missing channel and a real missing channel without requesting the real endpoint.
|
||||||
prepared.unsubscribe_all_from_channel.run(ucid)
|
// These fake missing channels often happen in bursts, which is why there is a cooldown.
|
||||||
|
const timeSinceLastFakeNotFound = Date.now() - this.lastFakeNotFoundTime
|
||||||
|
if (timeSinceLastFakeNotFound >= constants.caching.subscriptions_refesh_fake_not_found_cooldown) {
|
||||||
|
// We'll request the real endpoint to verify.
|
||||||
|
fetch(`${constants.server_setup.local_instance_origin}/api/v1/channels/${ucid}`).then(res => res.json()).then(/** @param {any} root */ root => {
|
||||||
|
if (root.error && (root.identifier === "NOT_FOUND" || root.identifier === "ACCOUNT_TERMINATED")) {
|
||||||
|
// The channel is really gone, and we should mark it as missing for everyone.
|
||||||
|
prepared.channel_mark_as_missing.run(root.error, ucid)
|
||||||
|
} else {
|
||||||
|
// The channel is not actually gone and YouTube is trolling us.
|
||||||
|
this.lastFakeNotFoundTime = Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} // else youtube is currently trolling us, skip this until later.
|
||||||
} else {
|
} else {
|
||||||
throw new Error(root.error)
|
throw new Error(root.error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/static/images/mstile-150x150.png"/>
|
||||||
|
<TileColor>#2b5797</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "CloudTube",
|
||||||
|
"short_name": "CloudTube",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/images/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/images/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/images/maskable-icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/images/maskable-icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#36393f",
|
||||||
|
"background_color": "#36393f",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 578 B |
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 225 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="8" viewBox="0 0 5.821 2.117"><path d="M1.269.53l.767.793h.161L2.964.53h.211v.265L2.117 1.852 1.058.794V.529z" fill="#202020" paint-order="markers stroke fill"/></svg>
|
After Width: | Height: | Size: 226 B |
After Width: | Height: | Size: 710 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 491 B |
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M2667 4020 c-115 -29 -296 -105 -387 -162 -54 -34 -182 -133 -207
|
||||||
|
-159 -12 -14 -33 -49 -47 -79 -13 -30 -48 -80 -78 -110 -46 -47 -170 -221
|
||||||
|
-180 -252 -3 -7 -39 1 -96 21 -50 17 -147 43 -214 57 -111 23 -137 24 -258 18
|
||||||
|
-87 -4 -154 -13 -190 -24 -30 -10 -82 -24 -115 -31 -90 -17 -163 -59 -231
|
||||||
|
-131 -184 -197 -233 -305 -250 -554 -6 -90 -3 -115 31 -279 31 -152 44 -193
|
||||||
|
81 -265 24 -47 59 -120 78 -162 19 -43 56 -106 82 -140 66 -88 173 -196 214
|
||||||
|
-217 126 -65 482 -83 711 -35 68 14 156 36 194 49 95 32 317 43 420 22 39 -8
|
||||||
|
104 -18 145 -22 41 -3 109 -12 150 -20 166 -31 322 -37 825 -33 420 4 509 7
|
||||||
|
555 21 30 9 89 24 131 35 156 39 312 154 413 306 20 30 41 58 46 61 5 3 34 52
|
||||||
|
64 108 43 82 58 123 75 206 26 126 27 278 5 387 -18 86 -32 121 -74 174 -15
|
||||||
|
19 -34 49 -43 66 -34 67 -174 199 -262 248 -16 9 -52 32 -78 51 -27 19 -67 39
|
||||||
|
-90 45 -23 6 -71 24 -107 39 -64 28 -340 91 -397 91 -28 0 -30 4 -56 88 -15
|
||||||
|
48 -40 108 -55 134 -38 64 -224 269 -288 318 -63 48 -182 104 -250 118 -75 16
|
||||||
|
-224 22 -267 12z m-428 -984 c7 -8 33 -16 59 -19 183 -20 276 -46 447 -127 61
|
||||||
|
-29 128 -58 149 -66 22 -8 67 -32 100 -53 34 -21 84 -50 113 -65 29 -14 68
|
||||||
|
-40 88 -57 31 -27 35 -37 35 -78 0 -40 -5 -51 -32 -77 -53 -49 -130 -100 -171
|
||||||
|
-114 -21 -6 -57 -26 -80 -44 -49 -37 -70 -49 -192 -106 -49 -23 -99 -51 -111
|
||||||
|
-61 -11 -10 -45 -32 -75 -48 -30 -16 -88 -47 -129 -70 -95 -52 -134 -54 -177
|
||||||
|
-8 -74 76 -121 287 -133 592 -4 121 -13 236 -19 255 -19 60 -14 101 16 132 30
|
||||||
|
31 91 39 112 14z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 6.615 6.615"><path d="M2.64 0s-.03.94-.424 1.215C1.823 1.49.885.894.885.894L.112 2.232.109 2.23l.003.002c.01.006.797.499.838.974.042.478-.944.992-.944.992l.77 1.34s.83-.442 1.265-.24c.435.204.387 1.314.387 1.314l1.546.003s.032-.94.425-1.215c.393-.275 1.331.321 1.331.321l.775-1.338s-.798-.496-.84-.974c-.041-.478.944-.993.944-.993l-.77-1.34s-.83.443-1.265.24C4.14 1.113 4.187.002 4.187.002zm.688 2.25a1.106 1.106 0 110 2.211 1.106 1.106 0 010-2.21z" fill="#c4c4c4" paint-order="fill markers stroke"/></svg>
|
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 6.615 6.615"><title>Settings</title><path d="M2.64 0s-.03.94-.424 1.215C1.823 1.49.885.894.885.894L.112 2.232.109 2.23l.003.002c.01.006.797.499.838.974.042.478-.944.992-.944.992l.77 1.34s.83-.442 1.265-.24c.435.204.387 1.314.387 1.314l1.546.003s.032-.94.425-1.215c.393-.275 1.331.321 1.331.321l.775-1.338s-.798-.496-.84-.974c-.041-.478.944-.993.944-.993l-.77-1.34s-.83.443-1.265.24C4.14 1.113 4.187.002 4.187.002zm.688 2.25a1.106 1.106 0 110 2.211 1.106 1.106 0 010-2.21z" paint-order="fill markers stroke"/></svg>
|
||||||
|
|
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 611 B |
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="25" viewBox="0 0 7.937 6.615"><path d="M2.373 0h3.191a.52.52 0 01.521.521H1.852A.52.52 0 012.373 0zm-.91.794h5.011c.371 0 .67.298.67.67v.123l-6.35-.016v-.108c0-.37.298-.67.67-.67zm-.405 1.058C.472 1.852 0 2.184 0 2.77v2.868c0 .586.472.977 1.058.977H6.88c.586 0 1.059-.39 1.059-.977V2.77c0-.586-.473-.918-1.059-.918zM5.3 4.017c.167.099.19.276.012.366l-2.098.985c-.131.077-.304-.002-.302-.19V3.29c0-.203.18-.333.34-.245z" fill="#c4c4c4" paint-order="fill markers stroke"/></svg>
|
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="30" height="25" viewBox="0 0 7.937 6.615"><title>Subscriptions</title><path d="M2.373 0h3.191a.52.52 0 01.521.521H1.852A.52.52 0 012.373 0zm-.91.794h5.011c.371 0 .67.298.67.67v.123l-6.35-.016v-.108c0-.37.298-.67.67-.67zm-.405 1.058C.472 1.852 0 2.184 0 2.77v2.868c0 .586.472.977 1.058.977H6.88c.586 0 1.059-.39 1.059-.977V2.77c0-.586-.473-.918-1.059-.918zM5.3 4.017c.167.099.19.276.012.366l-2.098.985c-.131.077-.304-.002-.302-.19V3.29c0-.203.18-.333.34-.245z" paint-order="fill markers stroke"/></svg>
|
||||||
|
|
Before Width: | Height: | Size: 535 B After Width: | Height: | Size: 569 B |
|
@ -10,11 +10,11 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^7.1.0",
|
"better-sqlite3": "^7.4.5",
|
||||||
"cookie": "^0.4.1",
|
"cookie": "^0.4.1",
|
||||||
"denque": "^1.4.1",
|
"denque": "^1.5.1",
|
||||||
"mixin-deep": "^2.0.1",
|
"mixin-deep": "^2.0.1",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.6",
|
||||||
"pinski": "git+https://git.sr.ht/~cadence/nodejs-pinski#210be3cfacbd93d44e104698a29abd39494a6271"
|
"pinski": "git+https://git.sr.ht/~cadence/nodejs-pinski#9653807f309aee34c8c63ce4e6ee760cccbfdf0d"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ mixin leave(index, prefix="No more?", final)
|
||||||
a(href="/search?q=cats+being+cute") [BLISS]
|
a(href="/search?q=cats+being+cute") [BLISS]
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
include includes/video-list-item
|
||||||
main.cant-think-page
|
main.cant-think-page
|
||||||
- let src = constants.server_setup.cant_think_narration_url
|
- let src = constants.server_setup.cant_think_narration_url
|
||||||
if src
|
if src
|
||||||
|
@ -71,3 +72,8 @@ block content
|
||||||
p You know what you must do.
|
p You know what you must do.
|
||||||
p.ultimatum: a(href="#i-understand").border-look I know what I must do.
|
p.ultimatum: a(href="#i-understand").border-look I know what I must do.
|
||||||
a#i-understand
|
a#i-understand
|
||||||
|
|
||||||
|
p.the-end Well, looks like you reached the end of the content.#[br]You know what happens now.
|
||||||
|
h3 Recommended videos
|
||||||
|
+video_list_item("related-video", {videoId: "jKKCF_Bqtw4", title: "dopamine review", author: "Jreg", authorId: "UCGSGPehp0RWfca-kENgBJ9Q", viewCountText: "84,651 views", second__lengthText: "3:10"})
|
||||||
|
+video_list_item("related-video", {videoId: "gLYWLobR248", title: "I Watch My YouTube Videos At 2x Speed", author: "Jreg", authorId: "UCGSGPehp0RWfca-kENgBJ9Q", viewCountText: "125,625 views", second__lengthText: "2:24"})
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
extends includes/layout
|
||||||
|
|
||||||
|
include includes/video-list-item
|
||||||
|
include includes/subscribe-button
|
||||||
|
|
||||||
|
block head
|
||||||
|
title= `${data.row ? data.row.name : "Deleted channel"} - CloudTube`
|
||||||
|
script(type="module" src=getStaticURL("html", "/static/js/channel.js"))
|
||||||
|
|
||||||
|
block content
|
||||||
|
main.channel-page
|
||||||
|
if data.row
|
||||||
|
.channel-data
|
||||||
|
.info
|
||||||
|
- const iconURL = data.row.icon_url
|
||||||
|
if iconURL
|
||||||
|
.logo
|
||||||
|
img(src=iconURL alt="").thumbnail-image
|
||||||
|
.about
|
||||||
|
h1.name= data.row.name
|
||||||
|
+subscribe_button(data.ucid, subscribed, `/channel/${data.ucid}`).subscribe-button.base-border-look
|
||||||
|
|
||||||
|
.channel-error
|
||||||
|
div= data.message
|
||||||
|
|
||||||
|
if data.missing && subscribed
|
||||||
|
.you-should-unsubscribe To remove this channel from your subscriptions list, click Unsubscribe.
|
|
@ -3,8 +3,21 @@ html
|
||||||
head
|
head
|
||||||
meta(charset="utf-8")
|
meta(charset="utf-8")
|
||||||
meta(name="viewport" content="width=device-width, initial-scale=1")
|
meta(name="viewport" content="width=device-width, initial-scale=1")
|
||||||
link(rel="stylesheet" type="text/css" href=getStaticURL("sass", "/main.sass"))
|
- const theme = settings && ["dark", "light", "edgeless-light"][settings.theme] || "dark"
|
||||||
|
link(rel="stylesheet" type="text/css" href=getStaticURL("sass", `/${theme}.sass`))
|
||||||
script(type="module" src=getStaticURL("html", "/static/js/focus.js"))
|
script(type="module" src=getStaticURL("html", "/static/js/focus.js"))
|
||||||
|
link(rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png")
|
||||||
|
link(rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png")
|
||||||
|
link(rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png")
|
||||||
|
link(rel="manifest" href="/site.webmanifest")
|
||||||
|
link(rel="mask-icon" href="/static/images/safari-pinned-tab.svg" color="#5bbad5")
|
||||||
|
link(rel="shortcut icon" href="/static/images/favicon.ico")
|
||||||
|
meta(name="apple-mobile-web-app-title" content="CloudTube")
|
||||||
|
meta(name="application-name" content="CloudTube")
|
||||||
|
meta(name="msapplication-TileColor" content="#2b5797")
|
||||||
|
meta(name="msapplication-config" content="/browserconfig.xml")
|
||||||
|
meta(name="theme-color" content="#36393f")
|
||||||
|
|
||||||
block head
|
block head
|
||||||
|
|
||||||
body.show-focus
|
body.show-focus
|
||||||
|
@ -13,15 +26,14 @@ html
|
||||||
if showNav
|
if showNav
|
||||||
nav.main-nav
|
nav.main-nav
|
||||||
.links
|
.links
|
||||||
|
if req && req.headers && "x-insecure" in req.headers
|
||||||
|
a(href="/").link.home CloudTube - Insecure
|
||||||
|
else
|
||||||
a(href="/").link.home CloudTube
|
a(href="/").link.home CloudTube
|
||||||
a(href="/subscriptions" title="Subscriptions").link.icon-link
|
a(href="/subscriptions" title="Subscriptions").link.icon-link
|
||||||
svg(width=30 height=25)
|
!= icons.get("subscriptions")
|
||||||
image(href=getStaticURL("html", "/static/images/subscriptions.svg") alt="Subscriptions.").icon
|
|
||||||
title Subscriptions
|
|
||||||
a(href="/settings" title="Settings").link.icon-link
|
a(href="/settings" title="Settings").link.icon-link
|
||||||
svg(width=25 height=25)
|
!= icons.get("settings")
|
||||||
image(href=getStaticURL("html", "/static/images/settings.svg") alt="Settings.").icon
|
|
||||||
title Settings
|
|
||||||
form(method="get" action="/search").search-form
|
form(method="get" action="/search").search-form
|
||||||
input(type="text" placeholder="Search" aria-label="Search a video" name="q" autocomplete="off" value=query).search
|
input(type="text" placeholder="Search" aria-label="Search a video" name="q" autocomplete="off" value=query).search
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
meta(charset="utf-8")
|
||||||
|
meta(name="viewport" content="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
|
||||||
|
div
|
||||||
|
block content
|
|
@ -36,6 +36,16 @@ block content
|
||||||
form(method="post" action="/settings")
|
form(method="post" action="/settings")
|
||||||
+fieldset("Settings")
|
+fieldset("Settings")
|
||||||
|
|
||||||
|
+select({
|
||||||
|
id: "theme",
|
||||||
|
label: "Theme",
|
||||||
|
options: [
|
||||||
|
{value: "0", text: "Standard dark"},
|
||||||
|
{value: "1", text: "Standard light"},
|
||||||
|
{value: "2", text: "Edgeless light"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
+input({
|
+input({
|
||||||
id: "instance",
|
id: "instance",
|
||||||
label: "Instance",
|
label: "Instance",
|
||||||
|
|
|
@ -11,12 +11,24 @@ block content
|
||||||
if hasSubscriptions
|
if hasSubscriptions
|
||||||
section
|
section
|
||||||
details.channels-details
|
details.channels-details
|
||||||
summary #{channels.length} subscriptions
|
summary
|
||||||
|
| #{channels.length} subscriptions
|
||||||
|
if missingChannelCount === 1
|
||||||
|
= ` - ${missingChannelCount} channel is gone`
|
||||||
|
else if missingChannelCount > 1
|
||||||
|
= ` - ${missingChannelCount} channels are gone`
|
||||||
.channels-list
|
.channels-list
|
||||||
for channel in channels
|
for channel in channels
|
||||||
a(href=`/channel/${channel.ucid}`).channel-item
|
a(href=`/channel/${channel.ucid}`).channel-item
|
||||||
img(src=channel.icon_url width=512 height=512 alt="").thumbnail
|
img(src=channel.icon_url width=512 height=512 alt="").thumbnail
|
||||||
span.name= channel.name
|
div
|
||||||
|
div.name= channel.name
|
||||||
|
if channel.missing
|
||||||
|
div.missing-reason
|
||||||
|
if channel.missing_reason
|
||||||
|
= channel.missing_reason
|
||||||
|
else
|
||||||
|
| This channel appears to be deleted or terminated. Click to check it.
|
||||||
|
|
||||||
if refreshed
|
if refreshed
|
||||||
section
|
section
|
||||||
|
@ -31,8 +43,8 @@ block content
|
||||||
|
|
||||||
if settings.save_history
|
if settings.save_history
|
||||||
input(type="checkbox" id="watched-videos-display")
|
input(type="checkbox" id="watched-videos-display")
|
||||||
.watched-videos-display-container
|
.checkbox-hider__container
|
||||||
label(for="watched-videos-display").watched-videos-display-label Hide watched videos
|
label(for="watched-videos-display").checkbox-hider__label Hide watched videos
|
||||||
|
|
||||||
each video in videos
|
each video in videos
|
||||||
+video_list_item("subscriptions-video", video, instanceOrigin, {showMarkWatched: settings.save_history && !video.watched})
|
+video_list_item("subscriptions-video", video, instanceOrigin, {showMarkWatched: settings.save_history && !video.watched})
|
||||||
|
|
|
@ -21,6 +21,7 @@ block content
|
||||||
noscript
|
noscript
|
||||||
meta(http-equiv="refresh" content=`${video.lengthSeconds+5};url=/watch?v=${first.videoId}&continuous=1&session-watched=${sessionWatchedNext}`)
|
meta(http-equiv="refresh" content=`${video.lengthSeconds+5};url=/watch?v=${first.videoId}&continuous=1&session-watched=${sessionWatchedNext}`)
|
||||||
.video-page(class={
|
.video-page(class={
|
||||||
|
"video-page--recommended-side": settings.recommended_mode === 0,
|
||||||
"video-page--recommended-below": settings.recommended_mode === 1,
|
"video-page--recommended-below": settings.recommended_mode === 1,
|
||||||
"video-page--recommended-hidden": settings.recommended_mode === 2
|
"video-page--recommended-hidden": settings.recommended_mode === 2
|
||||||
})
|
})
|
||||||
|
@ -76,7 +77,7 @@ block content
|
||||||
img(src="/static/images/search.svg" width=17 height=17 alt="").button-icon
|
img(src="/static/images/search.svg" width=17 height=17 alt="").button-icon
|
||||||
| Search
|
| Search
|
||||||
//- button.border-look#share Share
|
//- button.border-look#share Share
|
||||||
a(href=`https://www.youtube.com/watch?v=${video.videoId}#cloudtube`).border-look YouTube
|
a(href=`https://yewtu.be/watch?v=${video.videoId}`).border-look YewTube
|
||||||
a(href=`https://redirect.invidious.io/watch?v=${video.videoId}`).border-look Invidious
|
a(href=`https://redirect.invidious.io/watch?v=${video.videoId}`).border-look Invidious
|
||||||
|
|
||||||
.description#description!= video.descriptionHtml
|
.description#description!= video.descriptionHtml
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
extends includes/layout_embed
|
||||||
|
|
||||||
|
include includes/video-list-item
|
||||||
|
include includes/subscribe-button
|
||||||
|
|
||||||
|
block head
|
||||||
|
unless error
|
||||||
|
title= `${video.title} (embedded) - CloudTube`
|
||||||
|
else
|
||||||
|
title Error - CloudTube
|
||||||
|
script(type="module" src=getStaticURL("html", "/static/js/player.js"))
|
||||||
|
script const data = !{JSON.stringify({...video, continuous})}
|
||||||
|
|
||||||
|
block content
|
||||||
|
unless error
|
||||||
|
.video-embed-page(class={
|
||||||
|
"video-page--recommended-below": settings.recommended_mode === 1,
|
||||||
|
"video-page--recommended-hidden": settings.recommended_mode === 2
|
||||||
|
})
|
||||||
|
main.embed-video-section
|
||||||
|
- const format = formats[0]
|
||||||
|
if format
|
||||||
|
video(controls preload="auto" width=format.second__width height=format.second__height data-itag=format.itag autoplay=continuous||autoplay)#video.video
|
||||||
|
source(src=format.url type=format.type)
|
||||||
|
each t in video.captions
|
||||||
|
track(label=t.label kind="subtitles" srclang=t.languageCode src=t.url)
|
||||||
|
// fallback: flash player
|
||||||
|
- let flashvars = new URLSearchParams({skin: "/static/flash/skin.swf", video: format.url})
|
||||||
|
embed(type="application/x-shockwave-flash" src="/static/flash/player.swf" id="f4Player" width=1280 height=720 flashvars=flashvars.toString() allowscriptaccess="always" allowfullscreen="true" bgcolor="#000000")
|
||||||
|
else
|
||||||
|
video(src="")#video.video
|
||||||
|
.stream-notice The server provided no playback streams.
|
||||||
|
|
||||||
|
#current-time-container
|
||||||
|
#end-cards-container
|
||||||
|
|
||||||
|
audio(preload="auto")#audio
|
||||||
|
#live-event-notice
|
||||||
|
#audio-loading-display
|
||||||
|
|
||||||
|
.button-container
|
||||||
|
select(aria-label="Quality" autocomplete="off").border-look#quality-select
|
||||||
|
each f in formats
|
||||||
|
option(value=f.itag)= f.cloudtube__label
|
||||||
|
//-
|
||||||
|
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(target="_blank", href=`/watch?v=${video.videoId}`).border-look CloudTube (not embedded)
|
||||||
|
a(target="_blank", href=`https://yewtu.be/watch?v=${video.videoId}`).border-look YewTube
|
||||||
|
a(target="_blank", href=`https://redirect.invidious.io/watch?v=${video.videoId}`).border-look Invidious
|
||||||
|
else
|
||||||
|
//- error
|
||||||
|
main.video-error-page
|
||||||
|
h2 Error
|
||||||
|
!= message
|
||||||
|
p: a(href=`https://www.youtube.com/watch?v=${video.videoId}#cloudtube`) Watch on YouTube →
|
|
@ -0,0 +1,9 @@
|
||||||
|
@use "themes/dark" as *
|
||||||
|
@use "includes/main" with ($_theme: $theme)
|
||||||
|
|
||||||
|
@use "theme-modules/edgeless" with ($_theme: $theme)
|
||||||
|
|
||||||
|
// navigation shadow
|
||||||
|
.main-nav
|
||||||
|
position: relative // needed for box shadow to overlap related videos section
|
||||||
|
box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1)
|
|
@ -0,0 +1,4 @@
|
||||||
|
@use "themes/edgeless-light" as *
|
||||||
|
@use "includes/main" with ($_theme: $theme)
|
||||||
|
|
||||||
|
@use "theme-modules/edgeless" with ($_theme: $theme)
|
|
@ -1,8 +1,10 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
body
|
body
|
||||||
background-color: c.$bg-dark
|
background-color: map.get($_theme, "bg-2")
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
font-family: "Bariol", sans-serif
|
font-family: "Bariol", sans-serif
|
||||||
font-size: 18px
|
font-size: 18px
|
||||||
margin: 0
|
margin: 0
|
||||||
|
@ -13,13 +15,13 @@ body
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
a
|
a
|
||||||
color: c.$link
|
color: map.get($_theme, "link")
|
||||||
|
|
||||||
pre, code
|
pre, code
|
||||||
font-size: 0.88em
|
font-size: 0.88em
|
||||||
|
|
||||||
code
|
code
|
||||||
background: c.$bg-darker
|
background: map.get($_theme, "bg-1")
|
||||||
padding: 3px 5px
|
padding: 3px 5px
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ button
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
::placeholder
|
::placeholder
|
||||||
color: #c4c4c4
|
color: map.get($_theme, "placeholder")
|
||||||
opacity: 1
|
opacity: 1
|
||||||
|
|
||||||
// focus section
|
// focus section
|
||||||
|
@ -48,19 +50,20 @@ button
|
||||||
|
|
||||||
select:-moz-focusring
|
select:-moz-focusring
|
||||||
color: transparent
|
color: transparent
|
||||||
text-shadow: 0 0 0 c.$fg-bright
|
text-shadow: 0 0 0 map.get($_theme, "fg-bright")
|
||||||
|
|
||||||
body.show-focus
|
body.show-focus
|
||||||
a, select, button, input, video, summary
|
a, select, button, input, video, summary
|
||||||
&:focus
|
&:focus
|
||||||
outline: 2px dotted #ddd
|
outline: 2px dotted map.get($_theme, "fg-main")
|
||||||
|
|
||||||
video
|
video
|
||||||
background-color: black
|
background-color: black
|
||||||
|
|
||||||
details
|
details
|
||||||
background-color: c.$bg-accent-x
|
background-color: map.get($_theme, "bg-3")
|
||||||
padding: 12px
|
padding: 12px
|
||||||
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
border-radius: 8px
|
border-radius: 8px
|
||||||
|
|
||||||
summary
|
summary
|
||||||
|
@ -68,20 +71,22 @@ details
|
||||||
line-height: 1
|
line-height: 1
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
user-select: none
|
user-select: none
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
&[open] summary
|
&[open] summary
|
||||||
margin-bottom: 16px
|
padding-bottom: 12px
|
||||||
|
border-bottom: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
margin-bottom: 8px
|
||||||
|
|
||||||
table
|
table
|
||||||
background-color: c.$bg-darker
|
background-color: map.get($_theme, "bg-1")
|
||||||
|
|
||||||
table, td, th
|
table, td, th
|
||||||
border: 1px solid c.$edge-grey
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
border-collapse: collapse
|
border-collapse: collapse
|
||||||
|
|
||||||
td, th
|
td, th
|
||||||
padding: 4px 8px
|
padding: 4px 8px
|
||||||
|
|
||||||
thead, tr:nth-child(even)
|
thead, tr:nth-child(even)
|
||||||
background-color: c.$bg-darkest
|
background-color: map.get($_theme, "bg-0")
|
|
@ -1,10 +1,12 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
@use "sass:selector"
|
@use "sass:selector"
|
||||||
@use "colors.sass" as c
|
@use "sass:map"
|
||||||
|
|
||||||
@mixin button-base
|
@mixin button-base
|
||||||
-webkit-appearance: none
|
-webkit-appearance: none
|
||||||
-moz-appearance: none
|
-moz-appearance: none
|
||||||
color: c.$fg-bright
|
color: map.get($_theme, "fg-bright")
|
||||||
border: none
|
border: none
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
padding: 8px
|
padding: 8px
|
||||||
|
@ -14,7 +16,7 @@
|
||||||
|
|
||||||
@at-root #{selector.unify(&, "select")}
|
@at-root #{selector.unify(&, "select")}
|
||||||
padding: 8px 27px 8px 8px
|
padding: 8px 27px 8px 8px
|
||||||
background: url(/static/images/arrow-down-wide.svg) right 53% no-repeat c.$bg-accent-x
|
background: map.get($_theme, "image-dropdown") right 53% no-repeat map.get($_theme, "bg-4")
|
||||||
|
|
||||||
@at-root #{selector.unify(&, "a")}
|
@at-root #{selector.unify(&, "a")}
|
||||||
padding: 7px 8px
|
padding: 7px 8px
|
||||||
|
@ -31,12 +33,12 @@
|
||||||
@mixin button-bg
|
@mixin button-bg
|
||||||
@include button-base
|
@include button-base
|
||||||
|
|
||||||
background-color: c.$bg-accent-x
|
background-color: map.get($_theme, "bg-4")
|
||||||
|
|
||||||
@mixin border-button
|
@mixin border-button
|
||||||
@include button-bg
|
@include button-bg
|
||||||
|
|
||||||
border: 1px solid c.$edge-grey
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
|
||||||
@mixin button-size
|
@mixin button-size
|
||||||
margin: 4px
|
margin: 4px
|
||||||
|
@ -44,10 +46,10 @@
|
||||||
|
|
||||||
@mixin button-hover
|
@mixin button-hover
|
||||||
&:hover
|
&:hover
|
||||||
background-color: c.$bg-accent
|
background-color: map.get($_theme, "bg-3")
|
||||||
|
|
||||||
&:active
|
&:active
|
||||||
background-color: c.$bg-dark
|
background-color: map.get($_theme, "bg-2")
|
||||||
|
|
||||||
.base-border-look
|
.base-border-look
|
||||||
@include border-button
|
@include border-button
|
||||||
|
@ -62,13 +64,13 @@
|
||||||
@include button-size
|
@include button-size
|
||||||
-webkit-appearance: none
|
-webkit-appearance: none
|
||||||
-moz-appearance: none
|
-moz-appearance: none
|
||||||
color: c.$fg-bright
|
color: map.get($_theme, "fg-bright")
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
line-height: 1.25
|
line-height: 1.25
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 8px 20px
|
padding: 8px 20px
|
||||||
background: c.$bg-accent
|
background: map.get($_theme, "bg-3")
|
||||||
border: solid c.$bg-darker
|
border: solid map.get($_theme, "edge-grey")
|
||||||
border-width: 1px 0px 0px
|
border-width: 1px 0px 0px
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
||||||
|
@ -76,7 +78,7 @@
|
||||||
border-width: 1px 0px 1px
|
border-width: 1px 0px 1px
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
background: c.$bg-accent-x
|
background: map.get($_theme, "bg-4")
|
||||||
|
|
||||||
&:active
|
&:active
|
||||||
background: c.$bg-darker
|
background: map.get($_theme, "bg-1")
|
|
@ -1,5 +1,7 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
@use "sass:list"
|
@use "sass:list"
|
||||||
@use "colors.sass" as c
|
@use "sass:map"
|
||||||
|
|
||||||
.cant-think-page
|
.cant-think-page
|
||||||
.main-nav
|
.main-nav
|
||||||
|
@ -9,13 +11,14 @@
|
||||||
text-align: left
|
text-align: left
|
||||||
max-width: 572px
|
max-width: 572px
|
||||||
padding-top: 40px
|
padding-top: 40px
|
||||||
|
margin-bottom: 12vh
|
||||||
border-radius: 0px 0px 16px 16px
|
border-radius: 0px 0px 16px 16px
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
|
||||||
.page-narration
|
.page-narration
|
||||||
background-color: c.$bg-accent
|
background-color: map.get($_theme, "bg-3")
|
||||||
border: 1px solid #aaa
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
color: #fff
|
color: map.get($_theme, "fg-bright")
|
||||||
border-radius: 0
|
border-radius: 0
|
||||||
padding: 16px
|
padding: 16px
|
||||||
margin: 40px auto 60px
|
margin: 40px auto 60px
|
||||||
|
@ -28,7 +31,7 @@
|
||||||
|
|
||||||
.leave
|
.leave
|
||||||
margin: 26px 32px !important
|
margin: 26px 32px !important
|
||||||
color: #aaa
|
color: map.get($_theme, "fg-dim")
|
||||||
|
|
||||||
$sizes: 14px 16px 21px 30px 72px
|
$sizes: 14px 16px 21px 30px 72px
|
||||||
@each $size in $sizes
|
@each $size in $sizes
|
||||||
|
@ -37,7 +40,7 @@
|
||||||
|
|
||||||
&.leave__final
|
&.leave__final
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
color: #f2f2f2
|
color: map.get($_theme, "fg-bright")
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
.leave__actions
|
.leave__actions
|
||||||
|
@ -54,6 +57,12 @@
|
||||||
.ultimatum
|
.ultimatum
|
||||||
margin-top: 32px !important
|
margin-top: 32px !important
|
||||||
|
|
||||||
|
.the-end
|
||||||
|
margin: 120px 0px 1em !important
|
||||||
|
|
||||||
|
.thumbnail__more
|
||||||
|
display: none
|
||||||
|
|
||||||
#i-understand
|
#i-understand
|
||||||
display: flex
|
display: flex
|
||||||
height: 20px
|
height: 20px
|
||||||
|
@ -65,6 +74,7 @@
|
||||||
|
|
||||||
&:target
|
&:target
|
||||||
position: fixed
|
position: fixed
|
||||||
|
z-index: 1
|
||||||
top: 0
|
top: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
left: 0
|
left: 0
|
|
@ -1,6 +1,8 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
@use "video-list-item.sass" as *
|
|
||||||
@use "_dimensions.sass" as dimensions
|
@use "sass:map"
|
||||||
|
@use "_dimensions" as dimensions
|
||||||
|
@use "video-list-item" as *
|
||||||
|
|
||||||
.channel-page
|
.channel-page
|
||||||
padding: 40px 20px 20px
|
padding: 40px 20px 20px
|
||||||
|
@ -17,7 +19,7 @@
|
||||||
align-self: flex-start
|
align-self: flex-start
|
||||||
|
|
||||||
.channel-data
|
.channel-data
|
||||||
background-color: c.$bg-darker
|
background-color: map.get($_theme, "bg-1")
|
||||||
padding: 24px
|
padding: 24px
|
||||||
margin: 12px 0px 24px
|
margin: 12px 0px 24px
|
||||||
border-radius: 8px
|
border-radius: 8px
|
||||||
|
@ -44,11 +46,11 @@
|
||||||
.name
|
.name
|
||||||
font-size: 30px
|
font-size: 30px
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
color: c.$fg-bright
|
color: map.get($_theme, "fg-bright")
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
.subscribers
|
.subscribers
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
font-size: 18px
|
font-size: 18px
|
||||||
|
|
||||||
.subscribe-form
|
.subscribe-form
|
||||||
|
@ -61,7 +63,8 @@
|
||||||
line-height: 1
|
line-height: 1
|
||||||
border-radius: 8px
|
border-radius: 8px
|
||||||
font-size: 22px
|
font-size: 22px
|
||||||
background-color: c.$power-deep
|
background-color: map.get($_theme, "power-deep")
|
||||||
|
color: map.get($_theme, "power-fg")
|
||||||
border: none
|
border: none
|
||||||
|
|
||||||
.description
|
.description
|
||||||
|
@ -71,6 +74,19 @@
|
||||||
.channel-video
|
.channel-video
|
||||||
@include channel-video
|
@include channel-video
|
||||||
|
|
||||||
|
.channel-error
|
||||||
|
background-color: map.get($_theme, "bg-1")
|
||||||
|
padding: 24px
|
||||||
|
margin: 12px 0px 24px
|
||||||
|
border-radius: 8px
|
||||||
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
font-size: 20px
|
||||||
|
color: map.get($_theme, "fg-warning")
|
||||||
|
|
||||||
|
.you-should-unsubscribe
|
||||||
|
margin-top: 20px
|
||||||
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
.about-description // class provided by youtube
|
.about-description // class provided by youtube
|
||||||
pre
|
pre
|
||||||
font-size: inherit
|
font-size: inherit
|
|
@ -1,10 +1,12 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
@mixin filter-notice
|
@mixin filter-notice
|
||||||
margin-top: 24px
|
margin-top: 24px
|
||||||
padding: 12px
|
padding: 12px
|
||||||
border-radius: 8px
|
border-radius: 8px
|
||||||
background-color: c.$bg-darker
|
background-color: map.get($_theme, "bg-1")
|
||||||
white-space: pre-line
|
white-space: pre-line
|
||||||
|
|
||||||
.filters-page
|
.filters-page
|
||||||
|
@ -20,23 +22,23 @@
|
||||||
|
|
||||||
.filter-confirmation-notice
|
.filter-confirmation-notice
|
||||||
@include filter-notice
|
@include filter-notice
|
||||||
color: c.$fg-warning
|
color: map.get($_theme, "fg-warning")
|
||||||
|
|
||||||
.filter-compile-error
|
.filter-compile-error
|
||||||
@include filter-notice
|
@include filter-notice
|
||||||
|
|
||||||
&__header
|
&__header
|
||||||
color: c.$fg-warning
|
color: map.get($_theme, "fg-warning")
|
||||||
|
|
||||||
&__trace
|
&__trace
|
||||||
background-color: c.$bg-darkest
|
background-color: map.get($_theme, "bg-0")
|
||||||
padding: 6px
|
padding: 6px
|
||||||
|
|
||||||
.save-filter
|
.save-filter
|
||||||
margin-top: 12px
|
margin-top: 12px
|
||||||
|
|
||||||
.border-look
|
.border-look
|
||||||
background-color: c.$bg-darker
|
background-color: map.get($_theme, "bg-1")
|
||||||
font-size: 22px
|
font-size: 22px
|
||||||
padding: 7px 16px 8px
|
padding: 7px 16px 8px
|
||||||
font-size: 24px
|
font-size: 24px
|
||||||
|
@ -48,17 +50,17 @@
|
||||||
.filter
|
.filter
|
||||||
display: flex
|
display: flex
|
||||||
padding: 5px 0
|
padding: 5px 0
|
||||||
border-top: 1px solid c.$edge-grey
|
border-top: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
|
||||||
&:last-child
|
&:last-child
|
||||||
border-bottom: 1px solid c.$edge-grey
|
border-bottom: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
|
||||||
&__details
|
&__details
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
&__type
|
&__type
|
||||||
font-size: 15px
|
font-size: 15px
|
||||||
color: c.$fg-dim
|
color: map.get($_theme, "fg-dim")
|
||||||
|
|
||||||
&__remove
|
&__remove
|
||||||
flex-shrink: 0
|
flex-shrink: 0
|
|
@ -1,4 +1,6 @@
|
||||||
@use "./colors.sass" as c
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
.footer__container
|
.footer__container
|
||||||
flex: 1
|
flex: 1
|
||||||
|
@ -10,9 +12,10 @@
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
align-items: center
|
align-items: center
|
||||||
background-color: c.$bg-darkest
|
background-color: map.get($_theme, "bg-dim")
|
||||||
margin: 40px 0 0
|
margin: 40px 0 0
|
||||||
padding: 10px 10px 30px
|
padding: 10px 10px 30px
|
||||||
|
border-top: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
|
||||||
.footer__cols
|
.footer__cols
|
||||||
display: flex
|
display: flex
|
|
@ -1,7 +1,9 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
@mixin disabled
|
@mixin disabled
|
||||||
background-color: c.$bg-dark
|
background-color: map.get($_theme, "bg-2")
|
||||||
color: #808080
|
color: #808080
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
|
@ -20,7 +22,7 @@ fieldset
|
||||||
font-size: 28px
|
font-size: 28px
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
padding: 0
|
padding: 0
|
||||||
border-bottom: 1px solid #333
|
border-bottom: 1px solid map.get($_theme, "edge-grey") // TODO: originally contrasted more
|
||||||
line-height: 1.56
|
line-height: 1.56
|
||||||
|
|
||||||
@media screen and (max-width: 400px)
|
@media screen and (max-width: 400px)
|
||||||
|
@ -36,7 +38,7 @@ fieldset
|
||||||
position: relative
|
position: relative
|
||||||
padding-bottom: 5px
|
padding-bottom: 5px
|
||||||
margin-bottom: 5px
|
margin-bottom: 5px
|
||||||
border-bottom: 1px solid #999
|
border-bottom: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
|
||||||
@media screen and (max-width: 400px)
|
@media screen and (max-width: 400px)
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
@ -52,7 +54,7 @@ fieldset
|
||||||
&__label
|
&__label
|
||||||
grid-area: label
|
grid-area: label
|
||||||
padding: 8px 8px 8px 0px
|
padding: 8px 8px 8px 0px
|
||||||
color: #fff
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
&__input
|
&__input
|
||||||
grid-area: input
|
grid-area: input
|
||||||
|
@ -63,7 +65,7 @@ fieldset
|
||||||
white-space: pre-line
|
white-space: pre-line
|
||||||
margin: 12px 0px 18px
|
margin: 12px 0px 18px
|
||||||
font-size: 16px
|
font-size: 16px
|
||||||
color: #ccc
|
color: map.get($_theme, "fg-dim")
|
||||||
line-height: 1.2
|
line-height: 1.2
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -79,7 +81,7 @@ fieldset
|
||||||
width: 16px
|
width: 16px
|
||||||
height: 16px
|
height: 16px
|
||||||
padding: 0px
|
padding: 0px
|
||||||
border: 1px solid #666
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
margin-left: 8px
|
margin-left: 8px
|
||||||
position: relative
|
position: relative
|
||||||
|
@ -101,6 +103,20 @@ fieldset
|
||||||
@include acts-like-button
|
@include acts-like-button
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
|
.checkbox-hider__container
|
||||||
|
position: relative
|
||||||
|
display: grid // why does the default not work???
|
||||||
|
top: -42px
|
||||||
|
background: map.get($_theme, "bg-3")
|
||||||
|
line-height: 1
|
||||||
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
border-radius: 8px
|
||||||
|
margin-bottom: -18px
|
||||||
|
|
||||||
|
.checkbox-hider__label
|
||||||
|
padding: 12px 0px 12px 32px
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
@mixin checkbox-hider($base)
|
@mixin checkbox-hider($base)
|
||||||
##{$base}
|
##{$base}
|
||||||
position: relative
|
position: relative
|
||||||
|
@ -110,18 +126,17 @@ fieldset
|
||||||
height: 42px
|
height: 42px
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
|
/*
|
||||||
|
automatically add these styles too
|
||||||
|
this means that components based off this can either add the .checkbox-hider__container class, or they can add the .base-name-container class,
|
||||||
|
depending on which one is more reasonable in the moment
|
||||||
|
for example, .delete-confirm-container takes advantage of the @extend here.
|
||||||
|
|
||||||
.#{$base}-container
|
.#{$base}-container
|
||||||
position: relative
|
@extend .checkbox-hider__container
|
||||||
display: grid // why does the default not work???
|
|
||||||
top: -42px
|
|
||||||
background: c.$bg-accent-x
|
|
||||||
line-height: 1
|
|
||||||
border-radius: 8px
|
|
||||||
margin-bottom: -18px
|
|
||||||
|
|
||||||
.#{$base}-label
|
.#{$base}-label
|
||||||
padding: 12px 0px 12px 32px
|
@extend .checkbox-hider__label
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
@mixin single-button-form
|
@mixin single-button-form
|
||||||
display: inline-block
|
display: inline-block
|
|
@ -1,4 +1,6 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
.home-page
|
.home-page
|
||||||
padding: 40px
|
padding: 40px
|
||||||
|
@ -18,8 +20,8 @@
|
||||||
padding: 16px
|
padding: 16px
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
font-size: 20px
|
font-size: 20px
|
||||||
background-color: c.$bg-darker
|
background-color: map.get($_theme, "bg-1")
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
p
|
p
|
||||||
margin: 0 32px
|
margin: 0 32px
|
|
@ -1,3 +1,5 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
.js-licenses-page
|
.js-licenses-page
|
||||||
max-width: 800px
|
max-width: 800px
|
||||||
margin: 0 auto
|
margin: 0 auto
|
|
@ -0,0 +1,33 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:selector"
|
||||||
|
|
||||||
|
// preload second-level includes with the theme (there will be conflicts due to reconfiguration they are loaded individually)
|
||||||
|
// this isn't _exactly_ what @forward is supposed to be used for, but it's the best option here
|
||||||
|
@forward "video-list-item" show _ with ($_theme: $_theme)
|
||||||
|
@forward "forms" show _ with ($_theme: $_theme)
|
||||||
|
@forward "buttons" show _ with ($_theme: $_theme)
|
||||||
|
|
||||||
|
@use "base" with ($_theme: $_theme)
|
||||||
|
@use "video-page" with ($_theme: $_theme)
|
||||||
|
@use "video-embed" with ($_theme: $_theme)
|
||||||
|
@use "search-page" with ($_theme: $_theme)
|
||||||
|
@use "home-page" with ($_theme: $_theme)
|
||||||
|
@use "channel-page" with ($_theme: $_theme)
|
||||||
|
@use "subscriptions-page" with ($_theme: $_theme)
|
||||||
|
@use "settings-page" with ($_theme: $_theme)
|
||||||
|
@use "cant-think-page" with ($_theme: $_theme)
|
||||||
|
@use "privacy-page" with ($_theme: $_theme)
|
||||||
|
@use "licenses-page" with ($_theme: $_theme)
|
||||||
|
@use "filters-page" with ($_theme: $_theme)
|
||||||
|
@use "takedown-page" with ($_theme: $_theme)
|
||||||
|
@use "nav" with ($_theme: $_theme)
|
||||||
|
@use "footer" with ($_theme: $_theme)
|
||||||
|
|
||||||
|
@font-face
|
||||||
|
font-family: "Bariol"
|
||||||
|
src: url(/static/fonts/bariol.woff?statichash=1)
|
||||||
|
|
||||||
|
.button-container
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
|
@ -1,12 +1,14 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
@use "buttons.sass" as *
|
|
||||||
@use "_dimensions.sass" as dimensions
|
@use "sass:map"
|
||||||
|
@use "buttons" as *
|
||||||
|
@use "_dimensions" as dimensions
|
||||||
|
|
||||||
.main-nav
|
.main-nav
|
||||||
background-color: c.$bg-accent
|
background-color: map.get($_theme, "bg-nav")
|
||||||
display: flex
|
display: flex
|
||||||
padding: 8px
|
padding: 8px
|
||||||
box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1)
|
border-bottom: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
|
||||||
+dimensions.thin
|
+dimensions.thin
|
||||||
display: block
|
display: block
|
||||||
|
@ -30,10 +32,16 @@
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
&, &:visited
|
&, &:visited
|
||||||
color: #fff
|
color: map.get($_theme, "fg-bright")
|
||||||
|
|
||||||
&:focus, &:hover
|
&:focus, &:hover
|
||||||
background-color: c.$bg-accent-x
|
background-color: map.get($_theme, "bg-4")
|
||||||
|
|
||||||
|
&.icon-link
|
||||||
|
color: map.get($_theme, "fg-dim")
|
||||||
|
|
||||||
|
&:hover, &:focus
|
||||||
|
color: map.get($_theme, "fg-bright")
|
||||||
|
|
||||||
.search-form
|
.search-form
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -44,8 +52,7 @@
|
||||||
@include button-bg
|
@include button-bg
|
||||||
padding: 10px
|
padding: 10px
|
||||||
flex: 1
|
flex: 1
|
||||||
margin: 1px
|
border: 1px solid map.get($_theme, "bg-nav")
|
||||||
|
|
||||||
&:hover, &:focus
|
&:hover, &:focus
|
||||||
border: 1px solid c.$edge-grey
|
border-color: map.get($_theme, "edge-grey")
|
||||||
margin: 0px
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
.privacy-page
|
.privacy-page
|
||||||
max-width: 600px
|
max-width: 600px
|
||||||
margin: 0 auto
|
margin: 0 auto
|
|
@ -1,5 +1,7 @@
|
||||||
@use "video-list-item.sass" as *
|
$_theme: () !default
|
||||||
@use "colors.sass" as c
|
|
||||||
|
@use "sass:map"
|
||||||
|
@use "video-list-item" as *
|
||||||
|
|
||||||
.search-page
|
.search-page
|
||||||
padding: 40px 20px 20px
|
padding: 40px 20px 20px
|
|
@ -1,5 +1,7 @@
|
||||||
@use "forms.sass" as forms
|
$_theme: () !default
|
||||||
@use "colors.sass" as c
|
|
||||||
|
@use "sass:map"
|
||||||
|
@use "forms" as forms
|
||||||
|
|
||||||
.settings-page
|
.settings-page
|
||||||
padding: 40px 20px 20px
|
padding: 40px 20px 20px
|
||||||
|
@ -19,8 +21,9 @@
|
||||||
.more-settings
|
.more-settings
|
||||||
margin-top: 24px
|
margin-top: 24px
|
||||||
padding: 12px
|
padding: 12px
|
||||||
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
border-radius: 8px
|
border-radius: 8px
|
||||||
background-color: c.$bg-accent-x
|
background-color: map.get($_theme, "bg-3")
|
||||||
|
|
||||||
&__list
|
&__list
|
||||||
margin: 0
|
margin: 0
|
||||||
|
@ -34,7 +37,7 @@
|
||||||
margin-top: 24px
|
margin-top: 24px
|
||||||
|
|
||||||
.delete-confirm-container
|
.delete-confirm-container
|
||||||
background: c.$bg-darker
|
background: map.get($_theme, "bg-1")
|
||||||
margin-bottom: -36px
|
margin-bottom: -36px
|
||||||
|
|
||||||
@include forms.checkbox-hider("delete-confirm")
|
@include forms.checkbox-hider("delete-confirm")
|
|
@ -1,6 +1,8 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
@use "video-list-item.sass" as *
|
|
||||||
@use "forms.sass" as forms
|
@use "sass:map"
|
||||||
|
@use "forms" as forms
|
||||||
|
@use "video-list-item" as *
|
||||||
|
|
||||||
.subscriptions-page
|
.subscriptions-page
|
||||||
padding: 40px 20px 20px
|
padding: 40px 20px 20px
|
||||||
|
@ -33,7 +35,11 @@
|
||||||
|
|
||||||
.name
|
.name
|
||||||
font-size: 22px
|
font-size: 22px
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
|
.missing-reason
|
||||||
|
font-size: 16px
|
||||||
|
color: map.get($_theme, "fg-warning")
|
||||||
|
|
||||||
@include forms.checkbox-hider("watched-videos-display")
|
@include forms.checkbox-hider("watched-videos-display")
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
|
.takedown-page
|
||||||
|
max-width: 700px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.new-section
|
||||||
|
margin-top: 200px
|
||||||
|
|
||||||
|
.important-section
|
||||||
|
padding: 4px 20px
|
||||||
|
border: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
color: map.get($_theme, "fg-bright")
|
||||||
|
background-color: map.get($_theme, "bg-1")
|
|
@ -0,0 +1,22 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
@use "video-list-item" as *
|
||||||
|
|
||||||
|
.video-embed-page
|
||||||
|
display: block
|
||||||
|
|
||||||
|
.embed-video-section
|
||||||
|
width: 100%
|
||||||
|
height: 100vh
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.video
|
||||||
|
flex-grow: 1
|
||||||
|
display: block
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.stream-notice
|
||||||
|
background: map.get($_theme, "bg-0")
|
||||||
|
padding: 4px
|
|
@ -1,5 +1,7 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
@use "_dimensions.sass" as dimensions
|
|
||||||
|
@use "sass:map"
|
||||||
|
@use "_dimensions" as dimensions
|
||||||
|
|
||||||
// navigator hacks
|
// navigator hacks
|
||||||
.thumbnail > .thumbnail__options-container
|
.thumbnail > .thumbnail__options-container
|
||||||
|
@ -30,6 +32,7 @@
|
||||||
&__show-more
|
&__show-more
|
||||||
display: block
|
display: block
|
||||||
height: $more-size
|
height: $more-size
|
||||||
|
color: #fff
|
||||||
line-height: 16px
|
line-height: 16px
|
||||||
font-size: 25px
|
font-size: 25px
|
||||||
text-align: center
|
text-align: center
|
||||||
|
@ -52,7 +55,7 @@
|
||||||
&__options-list
|
&__options-list
|
||||||
pointer-events: auto
|
pointer-events: auto
|
||||||
display: grid
|
display: grid
|
||||||
background-color: c.$bg-accent
|
background-color: map.get($_theme, "bg-3")
|
||||||
padding: 8px 0px
|
padding: 8px 0px
|
||||||
border-radius: 8px
|
border-radius: 8px
|
||||||
box-shadow: 0 2px 6px 2px #000
|
box-shadow: 0 2px 6px 2px #000
|
||||||
|
@ -67,7 +70,7 @@
|
||||||
right: 0
|
right: 0
|
||||||
transform: translate(-6px, -1px) rotate(-45deg)
|
transform: translate(-6px, -1px) rotate(-45deg)
|
||||||
clip-path: polygon(-5% -20%, 120% -20%, 120% 125%)
|
clip-path: polygon(-5% -20%, 120% -20%, 120% 125%)
|
||||||
background-color: c.$bg-accent
|
background-color: map.get($_theme, "bg-3")
|
||||||
box-shadow: 0px 0px 4px 0px #000
|
box-shadow: 0px 0px 4px 0px #000
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|
||||||
|
@ -80,7 +83,7 @@
|
||||||
margin-bottom: 12px
|
margin-bottom: 12px
|
||||||
|
|
||||||
@at-root .video-list-item--watched#{&}
|
@at-root .video-list-item--watched#{&}
|
||||||
background: c.$bg-darker
|
background: map.get($_theme, "bg-dim")
|
||||||
padding: 4px 4px 0px
|
padding: 4px 4px 0px
|
||||||
margin: -4px -4px 8px
|
margin: -4px -4px 8px
|
||||||
|
|
||||||
|
@ -93,7 +96,7 @@
|
||||||
.thumbnail
|
.thumbnail
|
||||||
position: relative
|
position: relative
|
||||||
display: flex
|
display: flex
|
||||||
background: c.$bg-darkest
|
background: map.get($_theme, "bg-0")
|
||||||
|
|
||||||
&__link
|
&__link
|
||||||
font-size: 0 // remove whitespace around the image
|
font-size: 0 // remove whitespace around the image
|
||||||
|
@ -106,7 +109,7 @@
|
||||||
position: absolute
|
position: absolute
|
||||||
bottom: 3px
|
bottom: 3px
|
||||||
right: 3px
|
right: 3px
|
||||||
color: c.$fg-bright
|
color: #fff
|
||||||
font-size: 14px
|
font-size: 14px
|
||||||
background: rgba(20, 20, 20, 0.85)
|
background: rgba(20, 20, 20, 0.85)
|
||||||
line-height: 1
|
line-height: 1
|
||||||
|
@ -119,20 +122,20 @@
|
||||||
line-height: 1.2
|
line-height: 1.2
|
||||||
|
|
||||||
.title-link
|
.title-link
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
.author-line
|
.author-line
|
||||||
margin-top: 4px
|
margin-top: 4px
|
||||||
font-size: 15px
|
font-size: 15px
|
||||||
color: c.$fg-dim
|
color: map.get($_theme, "fg-dim")
|
||||||
|
|
||||||
.author
|
.author
|
||||||
color: c.$fg-dim
|
color: map.get($_theme, "fg-dim")
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
&:hover, &:active
|
&:hover, &:active
|
||||||
color: c.$fg-bright
|
color: map.get($_theme, "fg-bright")
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
|
|
||||||
@mixin recommendation-item
|
@mixin recommendation-item
|
||||||
|
@ -176,15 +179,15 @@
|
||||||
|
|
||||||
.author-line
|
.author-line
|
||||||
font-size: 15px
|
font-size: 15px
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
.author
|
.author
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
.description
|
.description
|
||||||
margin-top: 16px
|
margin-top: 16px
|
||||||
font-size: 15px
|
font-size: 15px
|
||||||
color: c.$fg-dim
|
color: map.get($_theme, "fg-dim")
|
||||||
|
|
||||||
+dimensions.thin
|
+dimensions.thin
|
||||||
.description
|
.description
|
||||||
|
@ -195,7 +198,7 @@
|
||||||
|
|
||||||
.description b
|
.description b
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
|
|
||||||
@mixin channel-video
|
@mixin channel-video
|
||||||
@include large-item
|
@include large-item
|
|
@ -1,11 +1,11 @@
|
||||||
@use "colors.sass" as c
|
$_theme: () !default
|
||||||
@use "video-list-item.sass" as *
|
|
||||||
|
@use "sass:map"
|
||||||
|
@use "video-list-item" as *
|
||||||
|
|
||||||
.video-page
|
.video-page
|
||||||
display: grid
|
display: grid
|
||||||
grid-auto-flow: row
|
grid-auto-flow: row
|
||||||
padding: 20px
|
|
||||||
grid-gap: 16px
|
|
||||||
|
|
||||||
@media screen and (min-width: 1000px)
|
@media screen and (min-width: 1000px)
|
||||||
grid-template-columns: 1fr 400px
|
grid-template-columns: 1fr 400px
|
||||||
|
@ -13,10 +13,25 @@
|
||||||
&--recommended-below, &--recommended-hidden
|
&--recommended-below, &--recommended-hidden
|
||||||
grid-template-columns: none
|
grid-template-columns: none
|
||||||
|
|
||||||
&--recommended-hidden .related-videos
|
&--recommended-side
|
||||||
|
.related-videos
|
||||||
|
border-left: 1px solid map.get($_theme, "edge-grey")
|
||||||
|
padding-left: 12px
|
||||||
|
padding-right: 20px
|
||||||
|
background-color: map.get($_theme, "bg-4")
|
||||||
|
padding-top: 12px
|
||||||
|
|
||||||
|
&--recommended-below
|
||||||
|
.related-videos
|
||||||
|
padding: 20px
|
||||||
|
|
||||||
|
&--recommended-hidden
|
||||||
|
.related-videos
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
.main-video-section
|
.main-video-section
|
||||||
|
padding: 20px
|
||||||
|
|
||||||
.video-container
|
.video-container
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
@ -27,7 +42,7 @@
|
||||||
max-height: 80vh
|
max-height: 80vh
|
||||||
|
|
||||||
.stream-notice
|
.stream-notice
|
||||||
background: c.$bg-darkest
|
background: map.get($_theme, "bg-0")
|
||||||
padding: 4px
|
padding: 4px
|
||||||
|
|
||||||
.info
|
.info
|
||||||
|
@ -45,15 +60,15 @@
|
||||||
margin: 0px 0px 4px
|
margin: 0px 0px 4px
|
||||||
font-size: 30px
|
font-size: 30px
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
color: c.$fg-bright
|
color: map.get($_theme, "fg-bright")
|
||||||
word-break: break-word
|
word-break: break-word
|
||||||
|
|
||||||
.author-link
|
.author-link
|
||||||
color: c.$fg-main
|
color: map.get($_theme, "fg-main")
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
&:hover, &:active
|
&:hover, &:active
|
||||||
color: c.$fg-bright
|
color: map.get($_theme, "fg-bright")
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
|
|
||||||
.info-secondary
|
.info-secondary
|
||||||
|
@ -75,7 +90,7 @@
|
||||||
margin: 16px 4px
|
margin: 16px 4px
|
||||||
padding: 12px
|
padding: 12px
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
background-color: c.$bg-darkest
|
background-color: map.get($_theme, "bg-0")
|
||||||
|
|
||||||
&__description
|
&__description
|
||||||
margin-left: 12px
|
margin-left: 12px
|
||||||
|
@ -85,7 +100,7 @@
|
||||||
|
|
||||||
&__script-warning
|
&__script-warning
|
||||||
font-size: 15px
|
font-size: 15px
|
||||||
color: c.$fg-warning
|
color: map.get($_theme, "fg-warning")
|
||||||
|
|
||||||
&__buttons
|
&__buttons
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -97,12 +112,12 @@
|
||||||
line-height: 1.4
|
line-height: 1.4
|
||||||
word-break: break-word
|
word-break: break-word
|
||||||
margin: 16px 4px 4px 4px
|
margin: 16px 4px 4px 4px
|
||||||
background-color: c.$bg-accent-area
|
background-color: map.get($_theme, "bg-5")
|
||||||
padding: 12px
|
padding: 12px
|
||||||
border-radius: 4px
|
border-radius: 4px
|
||||||
|
|
||||||
--regular-background: #{c.$bg-accent-area}
|
--regular-background: #{map.get($_theme, "bg-5")}
|
||||||
--highlight-background: #{c.$bg-darker}
|
--highlight-background: #{map.get($_theme, "bg-1")}
|
||||||
|
|
||||||
.subscribe-form
|
.subscribe-form
|
||||||
display: inline-block
|
display: inline-block
|
|
@ -1,17 +0,0 @@
|
||||||
$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
|
|
||||||
$fg-warning: #fdca6d
|
|
||||||
|
|
||||||
$edge-grey: #a0a0a0
|
|
||||||
|
|
||||||
$link: #8ac2f9
|
|
||||||
|
|
||||||
$power-deep: #c62727
|
|
|
@ -1,14 +0,0 @@
|
||||||
@use "colors.sass" as c
|
|
||||||
|
|
||||||
.takedown-page
|
|
||||||
max-width: 700px
|
|
||||||
margin: 0 auto
|
|
||||||
|
|
||||||
.new-section
|
|
||||||
margin-top: 200px
|
|
||||||
|
|
||||||
.important-section
|
|
||||||
padding: 4px 20px
|
|
||||||
border: 1px solid c.$edge-grey
|
|
||||||
color: c.$fg-bright
|
|
||||||
background-color: c.$bg-darker
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
@use "themes/light" as *
|
||||||
|
@use "includes/main" with ($_theme: $theme)
|
|
@ -1,30 +0,0 @@
|
||||||
@use "includes/colors.sass" as c
|
|
||||||
|
|
||||||
@use "includes/base.sass"
|
|
||||||
@use "sass:selector"
|
|
||||||
@use "includes/video-page.sass"
|
|
||||||
@use "includes/search-page.sass"
|
|
||||||
@use "includes/home-page.sass"
|
|
||||||
@use "includes/channel-page.sass"
|
|
||||||
@use "includes/subscriptions-page.sass"
|
|
||||||
@use "includes/settings-page.sass"
|
|
||||||
@use "includes/cant-think-page.sass"
|
|
||||||
@use "includes/privacy-page.sass"
|
|
||||||
@use "includes/licenses-page.sass"
|
|
||||||
@use "includes/filters-page.sass"
|
|
||||||
@use "includes/takedown-page.sass"
|
|
||||||
@use "includes/forms.sass"
|
|
||||||
@use "includes/nav.sass"
|
|
||||||
@use "includes/footer.sass"
|
|
||||||
|
|
||||||
@font-face
|
|
||||||
font-family: "Bariol"
|
|
||||||
src: url(/static/fonts/bariol.woff?statichash=1)
|
|
||||||
|
|
||||||
.icon-link:hover, .icon-link:focus
|
|
||||||
.icon
|
|
||||||
filter: brightness(2)
|
|
||||||
|
|
||||||
.button-container
|
|
||||||
display: flex
|
|
||||||
flex-wrap: wrap
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
$_theme: () !default
|
||||||
|
|
||||||
|
@use "sass:map"
|
||||||
|
|
||||||
|
// remove separating edges
|
||||||
|
.main-nav, .footer__center, .video-page--recommended-side .related-videos
|
||||||
|
border: none
|
||||||
|
|
||||||
|
// no background change to recommended videos sidebar
|
||||||
|
.video-page--recommended-side .related-videos
|
||||||
|
background-color: map.get($_theme, "bg-2")
|
||||||
|
|
||||||
|
// navigation shadow
|
||||||
|
.main-nav
|
||||||
|
position: relative // needed for box shadow to overlap related videos section
|
||||||
|
box-shadow: 0px 0px 20px 5px rgba(0, 0, 0, 0.1)
|
||||||
|
|
||||||
|
// thumbnail dropdown menu dividers
|
||||||
|
.menu-look
|
||||||
|
border-color: map.get($_theme, "bg-0")
|
||||||
|
|
||||||
|
// details areas
|
||||||
|
details, .checkbox-hider__container, .more-settings
|
||||||
|
border: none
|
||||||
|
details[open] summary
|
||||||
|
border: none
|
||||||
|
margin-bottom: 4px
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Defined in scss file instead of sass because indented syntax does not have multiline maps
|
||||||
|
// https://github.com/sass/sass/issues/216
|
||||||
|
|
||||||
|
@use "sass:map";
|
||||||
|
|
||||||
|
// This section is for colour shades
|
||||||
|
$theme: (
|
||||||
|
// darker
|
||||||
|
"bg-0": #252628,
|
||||||
|
"bg-1": #303336,
|
||||||
|
// regular
|
||||||
|
"bg-2": #36393f,
|
||||||
|
// lighter
|
||||||
|
"bg-3": #3f4247, // slightly
|
||||||
|
"bg-4": #44474b, // noticably
|
||||||
|
"bg-5": #4f5359, // brightly
|
||||||
|
|
||||||
|
"fg-bright": #fff,
|
||||||
|
"fg-main": #ddd,
|
||||||
|
"fg-dim": #bbb,
|
||||||
|
"fg-warning": #fdca6d,
|
||||||
|
|
||||||
|
"edge-grey": #a0a0a0,
|
||||||
|
"placeholder": #c4c4c4,
|
||||||
|
|
||||||
|
"link": #8ac2f9,
|
||||||
|
|
||||||
|
"power-deep": #c62727,
|
||||||
|
"power-fg": "#fff",
|
||||||
|
|
||||||
|
"image-dropdown": url(/static/images/arrow-down-wide-dark.svg)
|
||||||
|
);
|
||||||
|
|
||||||
|
// This section is for colour meanings
|
||||||
|
$theme: map.merge($theme, (
|
||||||
|
"bg-dim": map.get($theme, "bg-0"),
|
||||||
|
"bg-nav": map.get($theme, "bg-5"),
|
||||||
|
));
|
|
@ -0,0 +1,8 @@
|
||||||
|
// extend regular light theme to change a couple of shades
|
||||||
|
@use "light";
|
||||||
|
@use "sass:map";
|
||||||
|
|
||||||
|
// this section is for colour meanings
|
||||||
|
$theme: map.merge(light.$theme, (
|
||||||
|
"edge-grey": #c0c0c0,
|
||||||
|
));
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Defined in scss file instead of sass because indented syntax does not have multiline maps
|
||||||
|
// https://github.com/sass/sass/issues/216
|
||||||
|
|
||||||
|
@use "sass:map";
|
||||||
|
|
||||||
|
// this section is for colour shades
|
||||||
|
$theme: (
|
||||||
|
// lighter
|
||||||
|
"bg-0": #fff,
|
||||||
|
"bg-1": #fff,
|
||||||
|
// regular
|
||||||
|
"bg-2": #f2f2f2,
|
||||||
|
// darker
|
||||||
|
"bg-3": #e8e8e8, // slightly
|
||||||
|
"bg-4": #dadada, // noticably
|
||||||
|
"bg-5": #d0d0d0, // brightly
|
||||||
|
|
||||||
|
"fg-bright": #000,
|
||||||
|
"fg-main": #202020,
|
||||||
|
"fg-dim": #454545,
|
||||||
|
"fg-warning": #ce8600,
|
||||||
|
|
||||||
|
"edge-grey": #909090,
|
||||||
|
"placeholder": #636363,
|
||||||
|
|
||||||
|
"link": #0b51d4,
|
||||||
|
|
||||||
|
"power-deep": #c62727,
|
||||||
|
"power-fg": #fff,
|
||||||
|
|
||||||
|
"image-dropdown": url(/static/images/arrow-down-wide-light.svg)
|
||||||
|
);
|
||||||
|
|
||||||
|
// this section is for colour meanings
|
||||||
|
$theme: map.merge($theme, (
|
||||||
|
"bg-dim": map.get($theme, "bg-4"),
|
||||||
|
"bg-nav": map.get($theme, "bg-0")
|
||||||
|
));
|
12
server.js
|
@ -1,9 +1,11 @@
|
||||||
const {Pinski} = require("pinski")
|
const {Pinski} = require("pinski")
|
||||||
const {setInstance} = require("pinski/plugins")
|
const {setInstance} = require("pinski/plugins")
|
||||||
const constants = require("./utils/constants")
|
const constants = require("./utils/constants")
|
||||||
|
const iconLoader = require("./utils/icon-loader").icons
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
await require("./utils/upgradedb")()
|
await require("./utils/upgradedb")()
|
||||||
|
const icons = await iconLoader
|
||||||
|
|
||||||
const server = new Pinski({
|
const server = new Pinski({
|
||||||
port: 10412,
|
port: 10412,
|
||||||
|
@ -13,19 +15,19 @@ const constants = require("./utils/constants")
|
||||||
|
|
||||||
setInstance(server)
|
setInstance(server)
|
||||||
server.pugDefaultLocals.constants = constants
|
server.pugDefaultLocals.constants = constants
|
||||||
|
server.pugDefaultLocals.icons = icons
|
||||||
|
|
||||||
server.muteLogsStartingWith("/vi/")
|
server.muteLogsStartingWith("/vi/")
|
||||||
server.muteLogsStartingWith("/favicon")
|
server.muteLogsStartingWith("/favicon")
|
||||||
server.muteLogsStartingWith("/static")
|
server.muteLogsStartingWith("/static")
|
||||||
|
|
||||||
server.addSassDir("sass", ["sass/includes"])
|
server.addSassDir("sass", ["sass/includes", "sass/themes", "sass/theme-modules"])
|
||||||
server.addRoute("/static/css/main.css", "sass/main.sass", "sass")
|
server.addRoute("/static/css/dark.css", "sass/dark.sass", "sass")
|
||||||
|
server.addRoute("/static/css/light.css", "sass/light.sass", "sass")
|
||||||
|
server.addRoute("/static/css/edgeless-light.css", "sass/edgeless-light.sass", "sass")
|
||||||
|
|
||||||
server.addPugDir("pug", ["pug/includes"])
|
server.addPugDir("pug", ["pug/includes"])
|
||||||
server.addPugDir("pug/errors")
|
server.addPugDir("pug/errors")
|
||||||
server.addRoute("/cant-think", "pug/cant-think.pug", "pug")
|
|
||||||
server.addRoute("/privacy", "pug/privacy.pug", "pug")
|
|
||||||
server.addRoute("/licenses", "pug/licenses.pug", "pug")
|
|
||||||
|
|
||||||
server.addStaticHashTableDir("html/static/js")
|
server.addStaticHashTableDir("html/static/js")
|
||||||
server.addStaticHashTableDir("html/static/js/elemjs")
|
server.addStaticHashTableDir("html/static/js/elemjs")
|
||||||
|
|
|
@ -9,6 +9,10 @@ let constants = {
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "http://localhost:3000"
|
default: "http://localhost:3000"
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
type: "integer",
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
save_history: {
|
save_history: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false
|
default: false
|
||||||
|
@ -46,6 +50,7 @@ let constants = {
|
||||||
csrf_time: 4*60*60*1000,
|
csrf_time: 4*60*60*1000,
|
||||||
seen_token_subscriptions_eligible: 40*60*60*1000,
|
seen_token_subscriptions_eligible: 40*60*60*1000,
|
||||||
subscriptions_refresh_loop_min: 5*60*1000,
|
subscriptions_refresh_loop_min: 5*60*1000,
|
||||||
|
subscriptions_refesh_fake_not_found_cooldown: 10*60*1000,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Pattern matching.
|
// Pattern matching.
|
||||||
|
|
|
@ -54,7 +54,7 @@ class User {
|
||||||
|
|
||||||
getSubscriptions() {
|
getSubscriptions() {
|
||||||
if (this.token) {
|
if (this.token) {
|
||||||
return db.prepare("SELECT ucid FROM Subscriptions WHERE token = ? AND channel_missing = 0").pluck().all(this.token)
|
return db.prepare("SELECT ucid FROM Subscriptions WHERE token = ?").pluck().all(this.token)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ class User {
|
||||||
|
|
||||||
isSubscribed(ucid) {
|
isSubscribed(ucid) {
|
||||||
if (this.token) {
|
if (this.token) {
|
||||||
return !!db.prepare("SELECT * FROM Subscriptions WHERE token = ? AND ucid = ? AND channel_missing = 0").get([this.token, ucid])
|
return !!db.prepare("SELECT * FROM Subscriptions WHERE token = ? AND ucid = ?").get([this.token, ucid])
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
const fs = require("fs").promises
|
||||||
|
|
||||||
|
const names = ["subscriptions", "settings"]
|
||||||
|
const icons = names.map(name => fs.readFile(`html/static/images/${name}.svg`, "utf8"))
|
||||||
|
|
||||||
|
module.exports.icons = Promise.all(icons).then(resolvedIcons => {
|
||||||
|
return new Map(names.map((name, index) => [name, resolvedIcons[index]]))
|
||||||
|
})
|
|
@ -70,6 +70,31 @@ const deltas = [
|
||||||
.run()
|
.run()
|
||||||
db.prepare("CREATE TABLE TakedownChannels (ucid TEXT NOT NULL, org TEXT, url TEXT, PRIMARY KEY (ucid))")
|
db.prepare("CREATE TABLE TakedownChannels (ucid TEXT NOT NULL, org TEXT, url TEXT, PRIMARY KEY (ucid))")
|
||||||
.run()
|
.run()
|
||||||
|
},
|
||||||
|
// 11: Settings +theme
|
||||||
|
function() {
|
||||||
|
db.prepare("ALTER TABLE Settings ADD COLUMN theme INTEGER DEFAULT 0")
|
||||||
|
.run()
|
||||||
|
},
|
||||||
|
// 12: Channels +missing +missing_reason, Subscriptions -
|
||||||
|
// Better management for missing channels
|
||||||
|
// We totally discard the existing Subscriptions.channel_missing since it is unreliable.
|
||||||
|
function() {
|
||||||
|
db.prepare("ALTER TABLE Channels ADD COLUMN missing INTEGER NOT NULL DEFAULT 0")
|
||||||
|
.run()
|
||||||
|
db.prepare("ALTER TABLE Channels ADD COLUMN missing_reason TEXT")
|
||||||
|
.run()
|
||||||
|
// https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare("CREATE TABLE NEW_Subscriptions (token TEXT NOT NULL, ucid TEXT NOT NULL, PRIMARY KEY (token, ucid))")
|
||||||
|
.run()
|
||||||
|
db.prepare("INSERT INTO NEW_Subscriptions (token, ucid) SELECT token, ucid FROM Subscriptions")
|
||||||
|
.run()
|
||||||
|
db.prepare("DROP TABLE Subscriptions")
|
||||||
|
.run()
|
||||||
|
db.prepare("ALTER TABLE NEW_Subscriptions RENAME TO Subscriptions")
|
||||||
|
.run()
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -82,7 +107,7 @@ async function createBackup(entry) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} entry
|
* @param {number} entry
|
||||||
* @param {boolean} log
|
* @param {boolean} [log]
|
||||||
*/
|
*/
|
||||||
function runDelta(entry, log) {
|
function runDelta(entry, log) {
|
||||||
process.stdout.write(`Upgrading database to version ${entry}... `)
|
process.stdout.write(`Upgrading database to version ${entry}... `)
|
||||||
|
|
|
@ -1,15 +1,59 @@
|
||||||
const {request} = require("./request")
|
const {request} = require("./request")
|
||||||
const db = require("./db")
|
const db = require("./db")
|
||||||
|
|
||||||
async function fetchChannel(ucid, instance) {
|
async function fetchChannel(path, ucid, instance) {
|
||||||
if (!instance) throw new Error("No instance parameter provided")
|
function updateGoodData(channel) {
|
||||||
// fetch
|
|
||||||
const channel = await request(`${instance}/api/v1/channels/${ucid}`).then(res => res.json())
|
|
||||||
// update database
|
|
||||||
const bestIcon = channel.authorThumbnails.slice(-1)[0]
|
const bestIcon = channel.authorThumbnails.slice(-1)[0]
|
||||||
const iconURL = bestIcon ? bestIcon.url : null
|
const iconURL = bestIcon ? bestIcon.url : null
|
||||||
db.prepare("REPLACE INTO Channels (ucid, name, icon_url) VALUES (?, ?, ?)").run([channel.authorId, channel.author, iconURL])
|
db.prepare("REPLACE INTO Channels (ucid, name, icon_url, missing, missing_reason) VALUES (?, ?, ?, 0, NULL)").run(channel.authorId, channel.author, iconURL)
|
||||||
// return
|
}
|
||||||
|
|
||||||
|
function updateBadData(channel) {
|
||||||
|
if (channel.identifier === "NOT_FOUND" || channel.identifier === "ACCOUNT_TERMINATED") {
|
||||||
|
db.prepare("UPDATE Channels SET missing = 1, missing_reason = ? WHERE ucid = ?").run(channel.error, channel.authorId)
|
||||||
|
return {
|
||||||
|
missing: true,
|
||||||
|
message: channel.error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
missing: false,
|
||||||
|
message: channel.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instance) throw new Error("No instance parameter provided")
|
||||||
|
|
||||||
|
const row = db.prepare("SELECT * FROM Channels WHERE ucid = ?").get(ucid)
|
||||||
|
|
||||||
|
// handle the case where the channel has a known error
|
||||||
|
if (row && row.missing_reason) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
ucid,
|
||||||
|
row,
|
||||||
|
missing: true,
|
||||||
|
message: row.missing_reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {any} */
|
||||||
|
const channel = await request(`${instance}/api/v1/channels/${ucid}?second__path=${path}`).then(res => res.json())
|
||||||
|
|
||||||
|
// handle the case where the channel has a newly discovered error
|
||||||
|
if (channel.error) {
|
||||||
|
const missingData = updateBadData(channel)
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
ucid,
|
||||||
|
row,
|
||||||
|
...missingData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the case where the channel returns good data (this is the only remaining scenario)
|
||||||
|
updateGoodData(channel)
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|