1
0
mirror of https://git.sr.ht/~cadence/cloudtube synced 2024-11-10 02:27:29 +00:00
cloudtube/api/video.js
Cadence Ember e0bc0d2e81
Implement watched videos
Watched videos on your subscriptions feed will be darkened, and the
option to hide all of them has been added.

This only takes effect if you have enabled saving watched videos on the
server in the settings menu - default is off.
2020-12-29 01:45:02 +13:00

143 lines
4.6 KiB
JavaScript

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")
class InstanceError extends Error {
constructor(error, identifier) {
super(error)
this.identifier = identifier
}
}
function formatOrder(format) {
// most significant to least significant
// key, max, order, transform
// asc: lower number comes first, desc: higher number comes first
const spec = [
["second__height", 8000, "desc", x => x ? Math.floor(x/96) : 0],
["fps", 100, "desc", x => x ? Math.floor(x/10) : 0],
["type", " ".repeat(60), "asc", x => x.length],
]
let total = 0
for (let i = 0; i < spec.length; i++) {
const s = spec[i]
let diff = s[3](format[s[0]])
if (s[2] === "asc") diff = s[3](s[1]) - diff
total += diff
if (i+1 < spec.length) {
s2 = spec[i+1]
total *= s2[3](s2[1])
}
}
return -total
}
async function renderVideo(videoPromise, {user, id, instanceOrigin}) {
try {
// resolve video
const video = await videoPromise
if (!video) throw new Error("The instance returned null.")
if (video.error) throw new InstanceError(video.error, video.identifier)
// process stream list ordering
for (const format of video.formatStreams.concat(video.adaptiveFormats)) {
if (!format.second__height && format.resolution) format.second__height = +format.resolution.slice(0, -1)
if (!format.second__order) format.second__order = formatOrder(format)
}
// process length text
for (const rec of video.recommendedVideos) {
if (!rec.second__lengthText && rec.lengthSeconds > 0) {
rec.second__lengthText = converters.lengthSecondsToLengthText(rec.lengthSeconds)
}
}
// 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)
}
}
return render(200, "pug/video.pug", {video, subscribed, instanceOrigin})
} catch (e) {
// show an appropriate error message
// these should probably be split out to their own files
let message = pug.render("pre= error", {error: e.stack || e.toString()})
if (e instanceof fetch.FetchError) {
const template = `
p The selected instance, #[code= instanceOrigin], did not respond correctly.
p Requested URL: #[a(href=url)= url]
`
message = pug.render(template, {instanceOrigin, url: outURL})
} else if (e instanceof InstanceError) {
if (e.identifier === "RATE_LIMITED_BY_YOUTUBE") {
const template = `
.blocked-explanation
img(src="/static/images/instance-blocked.svg" width=552 height=96)
.rows
.row
h3.actor You
| Working
.row
h3.actor CloudTube
| Working
.row
h3.actor Instance
| Blocked by YouTube
.row
h3.actor YouTube
| Working
p.
CloudTube needs to a working Second/Invidious instance in order to get data about videos.
However, the selected instance, #[code= instanceOrigin], has been temporarily blocked by YouTube.
p.
You will be able to watch this video if you select a working instance in settings.
#[br]#[a(href="/settings") Go to settings →]
p.
(Tip: Try #[code https://invidious.snopyta.org] or #[code https://invidious.site].)
p.
This situation #[em will] be improved in the future!
`
message = pug.render(template, {instanceOrigin})
} else {
const template = `
p #[strong= error.message]
if error.identifier
p #[code= error.identifier]
p That error was generated by #[code= instanceOrigin].
`
message = pug.render(template, {instanceOrigin, error: e})
}
}
return render(500, "pug/video.pug", {video: {videoId: id}, error: true, message})
}
}
module.exports = [
{
route: "/watch", methods: ["GET", "POST"], upload: true, code: async ({req, url, body}) => {
const user = getUser(req)
const settings = user.getSettingsOrDefaults()
if (req.method === "GET") {
const id = url.searchParams.get("v")
if (!settings.local) {
const instanceOrigin = settings.instance
const outURL = `${instanceOrigin}/api/v1/videos/${id}`
const videoPromise = fetch(outURL).then(res => res.json())
return renderVideo(videoPromise, {user, id, instanceOrigin})
} else {
return render(200, "pug/local-video.pug", {id})
}
} else { // req.method === "POST"
const video = JSON.parse(new URLSearchParams(body.toString()).get("video"))
const videoPromise = Promise.resolve(video)
return renderVideo(videoPromise, {user})
}
}
}
]