diff --git a/api/video.js b/api/video.js index 5651248..3db5cc4 100644 --- a/api/video.js +++ b/api/video.js @@ -14,6 +14,9 @@ class InstanceError extends Error { } } +class MessageError extends Error { +} + function formatOrder(format) { // most significant to least significant // key, max, order, transform @@ -90,96 +93,11 @@ function sortFormats(video, preference) { return formats } -async function renderVideo(video, {user, settings, id, instanceOrigin}, locals = {}) { - try { - if (!video) throw new Error("The instance returned null.") - if (video.error) throw new InstanceError(video.error, video.identifier) - - // 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) - } - - // 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) - } - - // rewrite description - video.descriptionHtml = converters.rewriteVideoDescription(video.descriptionHtml, id) - - return render(200, "pug/video.pug", Object.assign(locals, {video, formats, 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. -` - message = pug.render(template, {instanceOrigin}) - } else if (e instanceof InstanceError) { - if (e.identifier === "RATE_LIMITED_BY_YOUTUBE" || e.message === "Could not extract video info. Instance is likely blocked.") { - 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 a working NewLeaf/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}) => { + // Prepare data needed to render video page + const user = getUser(req) const settings = user.getSettingsOrDefaults() const id = url.searchParams.get("v") @@ -196,23 +114,84 @@ module.exports = [ 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 video = await request(outURL).then(res => res.json()) + var videoFuture = request(outURL).then(res => res.json()) } else { // req.method === "POST" var instanceOrigin = "http://localhost:3000" - var video = JSON.parse(new URLSearchParams(body.toString()).get("video")) + var videoFuture = JSON.parse(new URLSearchParams(body.toString()).get("video")) } - return renderVideo(video, { - user, settings, id, instanceOrigin - }, { - mediaFragment, autoplay, continuous, sessionWatched, sessionWatchedNext - }) + 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) + + // 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) + } + + // 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) + } + + // rewrite description + video.descriptionHtml = converters.rewriteVideoDescription(video.descriptionHtml, id) + + return render(200, "pug/video.pug", { + video, formats, subscribed, instanceOrigin, mediaFragment, autoplay, continuous, + sessionWatched, sessionWatchedNext + }) + + } 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.pug", {video: {videoId: id}, error: true, message}) + } } } ] diff --git a/pug/errors/fetch-error.pug b/pug/errors/fetch-error.pug new file mode 100644 index 0000000..d763020 --- /dev/null +++ b/pug/errors/fetch-error.pug @@ -0,0 +1,5 @@ +p The selected instance, #[code= instanceOrigin], was unreachable. +details + summary If the instance is on a private network + p The instance URL was resolved by the server. If the instance is on a private network, CloudTube will not be able to connect back to it. + p To get around this error, you may be able to use local mode, or you can run your own CloudTube within the network. diff --git a/pug/errors/instance-error.pug b/pug/errors/instance-error.pug new file mode 100644 index 0000000..b171e03 --- /dev/null +++ b/pug/errors/instance-error.pug @@ -0,0 +1,4 @@ +p #[strong= error.message] +if error.identifier + p #[code= error.identifier] +p That error was generated by #[code= instanceOrigin]. diff --git a/pug/errors/message-error.pug b/pug/errors/message-error.pug new file mode 100644 index 0000000..2bafe60 --- /dev/null +++ b/pug/errors/message-error.pug @@ -0,0 +1 @@ +pre= error.toString() \ No newline at end of file diff --git a/pug/errors/rate-limited.pug b/pug/errors/rate-limited.pug new file mode 100644 index 0000000..f3cd3b3 --- /dev/null +++ b/pug/errors/rate-limited.pug @@ -0,0 +1,25 @@ +.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 a working NewLeaf/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! diff --git a/pug/errors/unrecognised-error.pug b/pug/errors/unrecognised-error.pug new file mode 100644 index 0000000..811b1a9 --- /dev/null +++ b/pug/errors/unrecognised-error.pug @@ -0,0 +1 @@ +pre= error.stack || error.toString() diff --git a/server.js b/server.js index 3ce5d04..3ccd8f4 100644 --- a/server.js +++ b/server.js @@ -16,6 +16,7 @@ const {setInstance} = require("pinski/plugins") server.addRoute("/static/css/main.css", "sass/main.sass", "sass") server.addPugDir("pug", ["pug/includes"]) + server.addPugDir("pug/errors") server.addRoute("/cant-think", "pug/cant-think.pug", "pug") server.addRoute("/privacy", "pug/privacy.pug", "pug") server.addRoute("/js-licenses", "pug/js-licenses.pug", "pug")