mirror of
https://git.sr.ht/~cadence/cloudtube
synced 2024-11-14 04:17:29 +00:00
Implement preferred quality selection
The list is subject to change as I collect more feedback. I just want to get this initial change out for everyone to use and appreciate.
This commit is contained in:
parent
fd854ec222
commit
ac3de4b4e6
68
api/video.js
68
api/video.js
@ -37,6 +37,53 @@ function formatOrder(format) {
|
|||||||
return -total
|
return -total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortFormats(video, preference) {
|
||||||
|
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)
|
||||||
|
let formats = standard.concat(adaptive)
|
||||||
|
|
||||||
|
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}`
|
||||||
|
}
|
||||||
|
for (const format of adaptive) {
|
||||||
|
format.cloudtube__label += " *"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference === 1) { // best dash
|
||||||
|
formats.sort((a, b) => (b.second__height - a.second__height))
|
||||||
|
} else if (preference === 2) { // best <=1080p
|
||||||
|
formats.sort((a, b) => {
|
||||||
|
if (b.second__height > 1080) {
|
||||||
|
if (a.second__height > 1080) return b.second__height - a.second__height
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a.second__height > 1080) return 1
|
||||||
|
return b.second__height - a.second__height
|
||||||
|
})
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
|
||||||
function rewriteVideoDescription(descriptionHtml, id) {
|
function rewriteVideoDescription(descriptionHtml, id) {
|
||||||
// replace timestamps to clickable links and rewrite youtube links to stay on the instance instead of pointing to YouTube
|
// replace timestamps to clickable links and rewrite youtube links to stay on the instance instead of pointing to YouTube
|
||||||
// test cases
|
// test cases
|
||||||
@ -71,23 +118,24 @@ function rewriteVideoDescription(descriptionHtml, id) {
|
|||||||
return descriptionHtml
|
return descriptionHtml
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderVideo(videoPromise, {user, id, instanceOrigin}, locals = {}) {
|
async function renderVideo(videoPromise, {user, settings, id, instanceOrigin}, locals = {}) {
|
||||||
try {
|
try {
|
||||||
// resolve video
|
// resolve video
|
||||||
const video = await videoPromise
|
const video = await videoPromise
|
||||||
if (!video) throw new Error("The instance returned null.")
|
if (!video) throw new Error("The instance returned null.")
|
||||||
if (video.error) throw new InstanceError(video.error, video.identifier)
|
if (video.error) throw new InstanceError(video.error, video.identifier)
|
||||||
|
|
||||||
// process stream list ordering
|
// process stream list ordering
|
||||||
for (const format of video.formatStreams.concat(video.adaptiveFormats)) {
|
const formats = sortFormats(video, settings.quality)
|
||||||
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 and view count
|
// process length text and view count
|
||||||
for (const rec of video.recommendedVideos) {
|
for (const rec of video.recommendedVideos) {
|
||||||
converters.normaliseVideoInfo(rec)
|
converters.normaliseVideoInfo(rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get subscription data
|
// get subscription data
|
||||||
const subscribed = user.isSubscribed(video.authorId)
|
const subscribed = user.isSubscribed(video.authorId)
|
||||||
|
|
||||||
// process watched videos
|
// process watched videos
|
||||||
user.addWatchedVideoMaybe(video.videoId)
|
user.addWatchedVideoMaybe(video.videoId)
|
||||||
const watchedVideos = user.getWatchedVideos()
|
const watchedVideos = user.getWatchedVideos()
|
||||||
@ -96,12 +144,16 @@ async function renderVideo(videoPromise, {user, id, instanceOrigin}, locals = {}
|
|||||||
rec.watched = watchedVideos.includes(rec.videoId)
|
rec.watched = watchedVideos.includes(rec.videoId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalise view count
|
// normalise view count
|
||||||
if (!video.second__viewCountText && video.viewCount) {
|
if (!video.second__viewCountText && video.viewCount) {
|
||||||
video.second__viewCountText = converters.viewCountToText(video.viewCount)
|
video.second__viewCountText = converters.viewCountToText(video.viewCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rewrite description
|
||||||
video.descriptionHtml = rewriteVideoDescription(video.descriptionHtml, id)
|
video.descriptionHtml = rewriteVideoDescription(video.descriptionHtml, id)
|
||||||
return render(200, "pug/video.pug", Object.assign(locals, {video, subscribed, instanceOrigin}))
|
|
||||||
|
return render(200, "pug/video.pug", Object.assign(locals, {video, formats, subscribed, instanceOrigin}))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// show an appropriate error message
|
// show an appropriate error message
|
||||||
// these should probably be split out to their own files
|
// these should probably be split out to their own files
|
||||||
@ -168,7 +220,7 @@ module.exports = [
|
|||||||
const instanceOrigin = settings.instance
|
const instanceOrigin = settings.instance
|
||||||
const outURL = `${instanceOrigin}/api/v1/videos/${id}`
|
const outURL = `${instanceOrigin}/api/v1/videos/${id}`
|
||||||
const videoPromise = request(outURL).then(res => res.json())
|
const videoPromise = request(outURL).then(res => res.json())
|
||||||
return renderVideo(videoPromise, {user, id, instanceOrigin}, {mediaFragment})
|
return renderVideo(videoPromise, {user, settings, id, instanceOrigin}, {mediaFragment})
|
||||||
} else {
|
} else {
|
||||||
return render(200, "pug/local-video.pug", {id})
|
return render(200, "pug/local-video.pug", {id})
|
||||||
}
|
}
|
||||||
@ -176,7 +228,7 @@ module.exports = [
|
|||||||
const video = JSON.parse(new URLSearchParams(body.toString()).get("video"))
|
const video = JSON.parse(new URLSearchParams(body.toString()).get("video"))
|
||||||
const videoPromise = Promise.resolve(video)
|
const videoPromise = Promise.resolve(video)
|
||||||
const instanceOrigin = "http://localhost:3000"
|
const instanceOrigin = "http://localhost:3000"
|
||||||
return renderVideo(videoPromise, {user, id, instanceOrigin}, {mediaFragment})
|
return renderVideo(videoPromise, {user, settings, id, instanceOrigin}, {mediaFragment})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,10 +87,11 @@ const playManagers = {
|
|||||||
class QualitySelect extends ElemJS {
|
class QualitySelect extends ElemJS {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(q("#quality-select"))
|
super(q("#quality-select"))
|
||||||
this.on("input", this.onInput.bind(this))
|
this.on("input", this.setFormat.bind(this))
|
||||||
|
this.setFormat()
|
||||||
}
|
}
|
||||||
|
|
||||||
onInput() {
|
setFormat() {
|
||||||
const itag = this.element.value
|
const itag = this.element.value
|
||||||
formatLoader.play(itag)
|
formatLoader.play(itag)
|
||||||
video.focus()
|
video.focus()
|
||||||
|
@ -32,6 +32,14 @@ block content
|
|||||||
|
|
||||||
+input("instance", "Instance", "url", constants.user_settings.instance.default, false, instances)
|
+input("instance", "Instance", "url", constants.user_settings.instance.default, false, instances)
|
||||||
|
|
||||||
|
+select("quality", "Preferred qualities", false, [
|
||||||
|
{value: "0", text: "Best combined"},
|
||||||
|
{value: "1", text: "Best DASH"},
|
||||||
|
{value: "2", text: "Best <=1080p"},
|
||||||
|
{value: "3", text: "Best low-fps"},
|
||||||
|
{value: "4", text: "360p"}
|
||||||
|
])
|
||||||
|
|
||||||
+select("save_history", "Watched videos history", false, [
|
+select("save_history", "Watched videos history", false, [
|
||||||
{value: "0", text: "Don't store"},
|
{value: "0", text: "Don't store"},
|
||||||
{value: "1", text: "Store on server"}
|
{value: "1", text: "Store on server"}
|
||||||
|
@ -14,12 +14,9 @@ block head
|
|||||||
block content
|
block content
|
||||||
unless error
|
unless error
|
||||||
main.video-page
|
main.video-page
|
||||||
- const sortedFormatStreams = video.formatStreams.slice().sort((a, b) => b.second__height - a.second__height)
|
|
||||||
- const sortedVideoAdaptiveFormats = video.adaptiveFormats.filter(f => f.type.startsWith("video") && f.qualityLabel).sort((a, b) => a.second__order - b.second__order)
|
|
||||||
|
|
||||||
.main-video-section
|
.main-video-section
|
||||||
.video-container
|
.video-container
|
||||||
- const format = sortedFormatStreams[0]
|
- const format = formats[0]
|
||||||
if format
|
if format
|
||||||
video(controls preload="auto" width=format.second__width height=format.second__height data-itag=format.itag)#video.video
|
video(controls preload="auto" width=format.second__width height=format.second__height data-itag=format.itag)#video.video
|
||||||
source(src=format.url+mediaFragment type=format.type)
|
source(src=format.url+mediaFragment type=format.type)
|
||||||
@ -49,10 +46,8 @@ block content
|
|||||||
+subscribe_button(video.authorId, subscribed, `/watch?v=${video.videoId}`).border-look
|
+subscribe_button(video.authorId, subscribed, `/watch?v=${video.videoId}`).border-look
|
||||||
//- button.border-look#theatre Theatre
|
//- button.border-look#theatre Theatre
|
||||||
select(autocomplete="off").border-look#quality-select
|
select(autocomplete="off").border-look#quality-select
|
||||||
each f in sortedFormatStreams
|
each f in formats
|
||||||
option(value=f.itag)= `${f.qualityLabel} ${f.container}`
|
option(value=f.itag)= f.cloudtube__label
|
||||||
each f in sortedVideoAdaptiveFormats
|
|
||||||
option(value=f.itag)= `${f.qualityLabel} ${f.container} *`
|
|
||||||
//-
|
//-
|
||||||
a(href="/subscriptions").border-look
|
a(href="/subscriptions").border-look
|
||||||
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
|
||||||
|
@ -16,6 +16,10 @@ let constants = {
|
|||||||
local: {
|
local: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
quality: {
|
||||||
|
type: "integer",
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -56,7 +60,8 @@ try {
|
|||||||
const overrides = require("../config/config.js")
|
const overrides = require("../config/config.js")
|
||||||
constants = mixin(constants, overrides)
|
constants = mixin(constants, overrides)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Note: overrides file `config/config.js` ignored, file not found.")
|
console.error("Missing config file /config/config.js\nDocumentation: https://git.sr.ht/~cadence/tube-docs/tree/main/item/docs")
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = constants
|
module.exports = constants
|
||||||
|
@ -38,6 +38,11 @@ const deltas = [
|
|||||||
function() {
|
function() {
|
||||||
db.prepare("UPDATE Settings SET instance = REPLACE(REPLACE(instance, '/', ''), ':', '://') WHERE instance LIKE '%/'")
|
db.prepare("UPDATE Settings SET instance = REPLACE(REPLACE(instance, '/', ''), ':', '://') WHERE instance LIKE '%/'")
|
||||||
.run()
|
.run()
|
||||||
|
},
|
||||||
|
// 5: Settings +quality
|
||||||
|
function() {
|
||||||
|
db.prepare("ALTER TABLE Settings ADD COLUMN quality INTEGER DEFAULT 0")
|
||||||
|
.run()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user