From c9adc5e7e02afbabf883db3536e1a683fd63bb0f Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 7 Nov 2025 00:25:16 +0100 Subject: [PATCH 1/6] feat: mark video as watched after a certain duration --- assets/js/player.js | 79 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index ecdc0448..790aaa83 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -2,6 +2,8 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent); var video_data = JSON.parse(document.getElementById('video_data').textContent); +const STORAGE_MARK_WATCHED_AFTER_DURATION = "mark_watched_after_duration"; + var options = { liveui: true, playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], @@ -126,13 +128,42 @@ function addCurrentTimeToURL(url, base) { */ var timeupdate_last_ts = 5; +/** + * Global variable to save the total video time watched (in seconds). + */ +let time_watched = 0; + /** - * Callback that updates the timestamp on all external links + * The duration of a short video (in seconds). + * This value is used to determine whether the video should be watched fully before + * being marked as watched. + * + * @default 30 + */ +const SHORT_VIDEO_DURATION = 30; + +/** + * The duration (in seconds) after which a video should be marked as watched. + * + * @default 30 + */ +const MARK_WATCHED_AFTER_DURATION = SHORT_VIDEO_DURATION; + +/** + * Callback that updates the timestamp on all external links and marks the video as watched after: + * - fully watching short videos (<=30 seconds) + * - time watched reaches 30 seconds for long videos (>30 seconds) */ player.on('timeupdate', function () { // Only update once every second let current_ts = Math.floor(player.currentTime()); - if (current_ts > timeupdate_last_ts) timeupdate_last_ts = current_ts; + const last_player_time = timeupdate_last_ts; + if ( + // Seek forward + current_ts > timeupdate_last_ts || + // Seek backward + current_ts < timeupdate_last_ts + ) timeupdate_last_ts = current_ts; else return; // YouTube links @@ -166,6 +197,50 @@ player.on('timeupdate', function () { let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); } + + // Only increase time watched when the time difference is one second or the video is not already marked as watched + const isOneSecondDifference = current_ts - last_player_time === 1; + const exceedsMarkWatchedAfterDuration = time_watched > MARK_WATCHED_AFTER_DURATION; + const markWatchedAfterDuration = helpers.storage.get(STORAGE_MARK_WATCHED_AFTER_DURATION) ?? false; + + if (!isOneSecondDifference || exceedsMarkWatchedAfterDuration || markWatchedAfterDuration === false) return; + + time_watched += 1 + + // Check if time watched exceeds 30 seconds or the video is fully watched + const absolute_video_duration = Math.floor(player.duration()); + const watched_at_timestamp = absolute_video_duration > SHORT_VIDEO_DURATION + ? MARK_WATCHED_AFTER_DURATION + : absolute_video_duration; + + if (time_watched !== watched_at_timestamp) return; + + const video_id = document.querySelector('[name="id"]').value; + const $csrfToken = document.querySelector('[name="csrf_token"]'); + + // User is not logged in + if ($csrfToken === null) return; + + // Mark the video as watched + const csrf_token = $csrfToken.value; + + const params = new URLSearchParams({ + action: "mark_watched", + redirect: false, + id: video_id + }); + + const url = `/watch_ajax?${params}` + + helpers.xhr('POST', url, { payload: `csrf_token=${csrf_token}` }, { + on200: () => { + console.info(`Marked video ${video_id} as watched`); + }, + onNon200: ({ response }) => { + console.error(`Something went wrong while marking video ${video_id} as watched:`, response); + } + }); + }); From 089e0f9a9442a0adb006620e7cbb669b2547f035 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:37:08 +0100 Subject: [PATCH 2/6] refactor: prevent server from marking the video as "watched" when the page loads This will be handled at the front-end with JavaScript. --- src/invidious/routes/watch.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 8a4fa246..b4db2c47 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -68,10 +68,6 @@ module Invidious::Routes::Watch end env.params.query.delete_all("iv_load_policy") - if watched && preferences.watch_history - Invidious::Database::Users.mark_watched(user.as(User), id) - end - if CONFIG.enable_user_notifications && notifications && notifications.includes? id Invidious::Database::Users.remove_notification(user.as(User), id) env.get("user").as(User).notifications.delete(id) From 9a3735f4f4c9eae2510d5ecd0675396bac74cf83 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:45:17 +0100 Subject: [PATCH 3/6] refactor: add `mark_watched_after_duration` configuration option to backend --- src/invidious/config.cr | 1 + src/invidious/routes/preferences.cr | 5 +++ src/invidious/user/preferences.cr | 1 + src/invidious/videos/video_preferences.cr | 54 +++++++++++++---------- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 92c510d0..ce602cc4 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -52,6 +52,7 @@ struct ConfigPreferences property vr_mode : Bool = true property show_nick : Bool = true property save_player_pos : Bool = false + property mark_watched_after_duration : Bool = false @[YAML::Field(ignore: true)] property default_playlist : String? = nil diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 9936e523..81500e55 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -82,6 +82,10 @@ module Invidious::Routes::PreferencesRoute save_player_pos ||= "off" save_player_pos = save_player_pos == "on" + mark_watched_after_duration = env.params.body["mark_watched_after_duration"]?.try &.as(String) + mark_watched_after_duration ||= "off" + mark_watched_after_duration = mark_watched_after_duration == "on" + show_nick = env.params.body["show_nick"]?.try &.as(String) show_nick ||= "off" show_nick = show_nick == "on" @@ -182,6 +186,7 @@ module Invidious::Routes::PreferencesRoute vr_mode: vr_mode, show_nick: show_nick, save_player_pos: save_player_pos, + mark_watched_after_duration: mark_watched_after_duration, default_playlist: default_playlist, }.to_json) diff --git a/src/invidious/user/preferences.cr b/src/invidious/user/preferences.cr index df195dd6..1894bfdb 100644 --- a/src/invidious/user/preferences.cr +++ b/src/invidious/user/preferences.cr @@ -56,6 +56,7 @@ struct Preferences property extend_desc : Bool = CONFIG.default_user_preferences.extend_desc property volume : Int32 = CONFIG.default_user_preferences.volume property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos + property mark_watched_after_duration : Bool = CONFIG.default_user_preferences.mark_watched_after_duration property default_playlist : String? = nil module BoolToString diff --git a/src/invidious/videos/video_preferences.cr b/src/invidious/videos/video_preferences.cr index 48177bd8..9e0e2f64 100644 --- a/src/invidious/videos/video_preferences.cr +++ b/src/invidious/videos/video_preferences.cr @@ -25,6 +25,7 @@ struct VideoPreferences property volume : Int32 property vr_mode : Bool property save_player_pos : Bool + property mark_watched_after_duration : Bool end def process_video_params(query, preferences) @@ -48,6 +49,7 @@ def process_video_params(query, preferences) volume = query["volume"]?.try &.to_i? vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe } save_player_pos = query["save_player_pos"]?.try { |q| (q == "true" || q == "1").to_unsafe } + mark_watched_after_duration = query["mark_watched_after_duration"]?.try { |q| (q == "true" || q == "1").to_unsafe } if preferences # region ||= preferences.region @@ -70,6 +72,7 @@ def process_video_params(query, preferences) volume ||= preferences.volume vr_mode ||= preferences.vr_mode.to_unsafe save_player_pos ||= preferences.save_player_pos.to_unsafe + mark_watched_after_duration ||= preferences.mark_watched_after_duration.to_unsafe end annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe @@ -91,6 +94,7 @@ def process_video_params(query, preferences) volume ||= CONFIG.default_user_preferences.volume vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe + mark_watched_after_duration ||= CONFIG.default_user_preferences.mark_watched_after_duration.to_unsafe annotations = annotations == 1 preload = preload == 1 @@ -104,6 +108,7 @@ def process_video_params(query, preferences) extend_desc = extend_desc == 1 vr_mode = vr_mode == 1 save_player_pos = save_player_pos == 1 + mark_watched_after_duration = mark_watched_after_duration == 1 if CONFIG.disabled?("dash") && quality == "dash" quality = "high" @@ -132,30 +137,31 @@ def process_video_params(query, preferences) controls = controls >= 1 params = VideoPreferences.new({ - annotations: annotations, - preload: preload, - autoplay: autoplay, - comments: comments, - continue: continue, - continue_autoplay: continue_autoplay, - controls: controls, - listen: listen, - local: local, - player_style: player_style, - preferred_captions: preferred_captions, - quality: quality, - quality_dash: quality_dash, - raw: raw, - region: region, - related_videos: related_videos, - speed: speed, - video_end: video_end, - video_loop: video_loop, - extend_desc: extend_desc, - video_start: video_start, - volume: volume, - vr_mode: vr_mode, - save_player_pos: save_player_pos, + annotations: annotations, + preload: preload, + autoplay: autoplay, + comments: comments, + continue: continue, + continue_autoplay: continue_autoplay, + controls: controls, + listen: listen, + local: local, + player_style: player_style, + preferred_captions: preferred_captions, + quality: quality, + quality_dash: quality_dash, + raw: raw, + region: region, + related_videos: related_videos, + speed: speed, + video_end: video_end, + video_loop: video_loop, + extend_desc: extend_desc, + video_start: video_start, + volume: volume, + vr_mode: vr_mode, + save_player_pos: save_player_pos, + mark_watched_after_duration: mark_watched_after_duration, }) return params From 0320b8a5a4b6e2053b6026d836da00f369b5c039 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:49:01 +0100 Subject: [PATCH 4/6] refactor: add "mark watched after duration" preference --- src/invidious/views/user/preferences.ecr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 23cb89f6..1ebbf48a 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -229,6 +229,11 @@ checked<% end %>> +
+ + checked<% end %>> +
+
checked<% end %>> From 1db6bd6c0ee1193888dc15e44b02cfdfc6432ded Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:18:44 +0100 Subject: [PATCH 5/6] refactor: pass `mark_watched_after_duration` preference to frontend --- assets/js/player.js | 4 +++- src/invidious/views/template.ecr | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/assets/js/player.js b/assets/js/player.js index 790aaa83..594f447f 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -201,7 +201,9 @@ player.on('timeupdate', function () { // Only increase time watched when the time difference is one second or the video is not already marked as watched const isOneSecondDifference = current_ts - last_player_time === 1; const exceedsMarkWatchedAfterDuration = time_watched > MARK_WATCHED_AFTER_DURATION; - const markWatchedAfterDuration = helpers.storage.get(STORAGE_MARK_WATCHED_AFTER_DURATION) ?? false; + + const $markWatchedAfterDuration = document.getElementById(`${STORAGE_MARK_WATCHED_AFTER_DURATION}_pref`); + const markWatchedAfterDuration = $markWatchedAfterDuration.innerText === "true" ?? false; if (!isOneSecondDifference || exceedsMarkWatchedAfterDuration || markWatchedAfterDuration === false) return; diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 9904b4fc..572c1ea8 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -1,6 +1,7 @@ <% locale = env.get("preferences").as(Preferences).locale dark_mode = env.get("preferences").as(Preferences).dark_mode + mark_watched_after_duration = env.get("preferences").as(Preferences).mark_watched_after_duration %> @@ -27,6 +28,9 @@ -theme"> + <% if env.get? "user" %> + + <% end %>