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.
This commit is contained in:
Cadence Ember 2020-12-29 01:42:25 +13:00
parent c3afe77e2d
commit e0bc0d2e81
No known key found for this signature in database
GPG Key ID: BC1C2C61CF521B17
12 changed files with 106 additions and 34 deletions

View File

@ -22,6 +22,8 @@ module.exports = [
channels = db.prepare(`SELECT * FROM Channels WHERE ucid IN (${template}) ORDER BY name`).all(subscriptions) channels = db.prepare(`SELECT * FROM Channels WHERE ucid IN (${template}) ORDER BY name`).all(subscriptions)
// get refreshed status // get refreshed status
refreshed = db.prepare(`SELECT min(refreshed) as min, max(refreshed) as max, count(refreshed) as count FROM Channels WHERE ucid IN (${template})`).get(subscriptions) refreshed = db.prepare(`SELECT min(refreshed) as min, max(refreshed) as max, count(refreshed) as count FROM Channels WHERE ucid IN (${template})`).get(subscriptions)
// get watched videos
const watchedVideos = user.getWatchedVideos()
// get videos // get videos
if (subscriptions.length) { if (subscriptions.length) {
hasSubscriptions = true hasSubscriptions = true
@ -29,12 +31,15 @@ module.exports = [
videos = db.prepare(`SELECT * FROM Videos WHERE authorId IN (${template}) ORDER BY published DESC LIMIT 60`).all(subscriptions) videos = db.prepare(`SELECT * FROM Videos WHERE authorId IN (${template}) ORDER BY published DESC LIMIT 60`).all(subscriptions)
.map(video => { .map(video => {
video.publishedText = timeToPastText(video.published * 1000) video.publishedText = timeToPastText(video.published * 1000)
console.log(watchedVideos, video.videoId)
video.watched = watchedVideos.includes(video.videoId)
return video return video
}) })
} }
} }
const instanceOrigin = user.getSettingsOrDefaults().instance const settings = user.getSettingsOrDefaults()
return render(200, "pug/subscriptions.pug", {hasSubscriptions, videos, channels, refreshed, timeToPastText, instanceOrigin}) const instanceOrigin = settings.instance
return render(200, "pug/subscriptions.pug", {settings, hasSubscriptions, videos, channels, refreshed, timeToPastText, instanceOrigin})
} }
} }
] ]

View File

@ -37,22 +37,35 @@ function formatOrder(format) {
async function renderVideo(videoPromise, {user, id, instanceOrigin}) { async function renderVideo(videoPromise, {user, id, instanceOrigin}) {
try { try {
// 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)
// video data additional processing // process stream list ordering
for (const format of video.formatStreams.concat(video.adaptiveFormats)) { 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__height && format.resolution) format.second__height = +format.resolution.slice(0, -1)
if (!format.second__order) format.second__order = formatOrder(format) if (!format.second__order) format.second__order = formatOrder(format)
} }
// process length text
for (const rec of video.recommendedVideos) { for (const rec of video.recommendedVideos) {
if (!rec.second__lengthText && rec.lengthSeconds > 0) { if (!rec.second__lengthText && rec.lengthSeconds > 0) {
rec.second__lengthText = converters.lengthSecondsToLengthText(rec.lengthSeconds) rec.second__lengthText = converters.lengthSecondsToLengthText(rec.lengthSeconds)
} }
} }
// get subscription data
const subscribed = user.isSubscribed(video.authorId) 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}) return render(200, "pug/video.pug", {video, subscribed, instanceOrigin})
} catch (e) { } 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()}) let message = pug.render("pre= error", {error: e.stack || e.toString()})
if (e instanceof fetch.FetchError) { if (e instanceof fetch.FetchError) {
const template = ` const template = `

View File

@ -28,5 +28,4 @@ block content
.videos .videos
each video in data.latestVideos each video in data.latestVideos
.channel-video +video_list_item("channel-video", video, instanceOrigin)
+video_list_item(video, instanceOrigin)

View File

@ -1,19 +1,20 @@
mixin video_list_item(video, instanceOrigin) mixin video_list_item(className, video, instanceOrigin)
- let link = `/watch?v=${video.videoId}` div(class={[className]: true, "video-list-item--watched": video.watched})
a(href=link tabindex="-1").thumbnail - let link = `/watch?v=${video.videoId}`
img(src=`/vi/${video.videoId}/mqdefault.jpg` width=320 height=180 alt="").image a(href=link tabindex="-1").thumbnail
if video.second__lengthText != undefined img(src=`/vi/${video.videoId}/mqdefault.jpg` width=320 height=180 alt="").image
span.duration= video.second__lengthText if video.second__lengthText != undefined
.info span.duration= video.second__lengthText
div.title: a(href=link).title-link= video.title .info
div.author-line div.title: a(href=link).title-link= video.title
a(href=`/channel/${video.authorId}`).author= video.author div.author-line
- const views = video.viewCountText || video.second__viewCountText a(href=`/channel/${video.authorId}`).author= video.author
if views - const views = video.viewCountText || video.second__viewCountText
= ` • ` if views
span.views= views = ` • `
if video.publishedText span.views= views
= ` • ` if video.publishedText
span.published= video.publishedText = ` • `
if video.descriptionHtml span.published= video.publishedText
div.description!= video.descriptionHtml if video.descriptionHtml
div.description!= video.descriptionHtml

View File

@ -8,5 +8,4 @@ block head
block content block content
main.search-page main.search-page
each result in results each result in results
.search-result +video_list_item("search-result", result, instanceOrigin)
+video_list_item(result, instanceOrigin)

View File

@ -37,9 +37,9 @@ block content
"https://invidious.fdn.fr" "https://invidious.fdn.fr"
]) ])
+select("save_history", "Watch history", false, [ +select("save_history", "Watched videos history", false, [
{value: "0", text: "Don't save"}, {value: "0", text: "Don't store"},
{value: "1", text: "Save"} {value: "1", text: "Store on server"}
]) ])
+select("local", "Fetch videos", false, [ +select("local", "Fetch videos", false, [

View File

@ -27,9 +27,13 @@ block content
if notLoaded if notLoaded
div #{notLoaded} subscriptions have not been refreshed at all div #{notLoaded} subscriptions have not been refreshed at all
if settings.save_history
input(type="checkbox" id="watched-videos-display")
.watched-videos-display-container
label(for="watched-videos-display").watched-videos-display-label Hide watched videos
each video in videos each video in videos
.subscriptions-video +video_list_item("subscriptions-video", video, instanceOrigin)
+video_list_item(video, instanceOrigin)
else else
.no-subscriptions .no-subscriptions
h2 You have no subscriptions. h2 You have no subscriptions.

View File

@ -66,8 +66,7 @@ block content
aside.related-videos aside.related-videos
h2.related-header Related videos h2.related-header Related videos
each r in video.recommendedVideos each r in video.recommendedVideos
.related-video +video_list_item("related-video", r, instanceOrigin)
+video_list_item(r, instanceOrigin)
else else
//- error //- error

View File

@ -54,7 +54,6 @@ details
border-radius: 8px border-radius: 8px
summary summary
text-decoration: underline
cursor: pointer cursor: pointer
line-height: 1 line-height: 1
margin-bottom: 0 margin-bottom: 0

View File

@ -33,3 +33,35 @@
.name .name
font-size: 22px font-size: 22px
color: c.$fg-main color: c.$fg-main
#watched-videos-display
position: relative
left: 10px
display: block
z-index: 1
height: 42px
margin: 0
.watched-videos-display-container
position: relative
display: grid // why does the default not work???
top: -42px
background: c.$bg-accent-x
line-height: 1
border-radius: 8px
margin-bottom: -18px
.watched-videos-display-label
padding: 12px 0px 12px 32px
cursor: pointer
#watched-videos-display:checked ~ .video-list-item--watched
display: none
.video-list-item--watched
background: c.$bg-darker
padding: 8px 8px 0px
.thumbnail .image
opacity: 0.4

View File

@ -57,6 +57,22 @@ class User {
return false return false
} }
} }
getWatchedVideos() {
const settings = this.getSettingsOrDefaults()
if (this.token && settings.save_history) {
return db.prepare("SELECT videoID FROM WatchedVideos WHERE token = ?").pluck().all(this.token)
} else {
return []
}
}
addWatchedVideoMaybe(videoID) {
const settings = this.getSettingsOrDefaults()
if (videoID && this.token && settings.save_history) {
db.prepare("INSERT OR IGNORE INTO WatchedVideos (token, videoID) VALUES (?, ?)").run([this.token, videoID])
}
}
} }
/** /**

View File

@ -28,6 +28,11 @@ const deltas = [
function() { function() {
db.prepare("ALTER TABLE Settings ADD COLUMN local INTEGER DEFAULT 0") db.prepare("ALTER TABLE Settings ADD COLUMN local INTEGER DEFAULT 0")
.run() .run()
},
// 3: +WatchedVideos
function() {
db.prepare("CREATE TABLE WatchedVideos (token TEXT NOT NULL, videoID TEXT NOT NULL, PRIMARY KEY (token, videoID))")
.run()
} }
] ]