Merge 670a4562d0e09bac626a13ff75f9aab1bc61fa17 into c250b9c0b1f947c822a4e0905975eb600352d7d3

This commit is contained in:
Goestav 2025-11-11 01:08:27 +00:00 committed by GitHub
commit 7139927523
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 126 additions and 30 deletions

View File

@ -2,6 +2,8 @@
var player_data = JSON.parse(document.getElementById('player_data').textContent); var player_data = JSON.parse(document.getElementById('player_data').textContent);
var video_data = JSON.parse(document.getElementById('video_data').textContent); var video_data = JSON.parse(document.getElementById('video_data').textContent);
const STORAGE_MARK_WATCHED_AFTER_DURATION = "mark_watched_after_duration";
var options = { var options = {
liveui: true, liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
@ -127,12 +129,41 @@ function addCurrentTimeToURL(url, base) {
var timeupdate_last_ts = 5; var timeupdate_last_ts = 5;
/** /**
* Callback that updates the timestamp on all external links * Global variable to save the total video time watched (in seconds).
*/
let time_watched = 0;
/**
* 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 () { player.on('timeupdate', function () {
// Only update once every second // Only update once every second
let current_ts = Math.floor(player.currentTime()); 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; else return;
// YouTube links // YouTube links
@ -166,6 +197,52 @@ player.on('timeupdate', function () {
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); 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 = document.getElementById(`${STORAGE_MARK_WATCHED_AFTER_DURATION}_pref`);
const markWatchedAfterDuration = $markWatchedAfterDuration.innerText === "true" ?? 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);
}
});
}); });

View File

@ -77,6 +77,7 @@
"preferences_listen_label": "Listen by default: ", "preferences_listen_label": "Listen by default: ",
"preferences_local_label": "Proxy videos: ", "preferences_local_label": "Proxy videos: ",
"preferences_watch_history_label": "Enable watch history: ", "preferences_watch_history_label": "Enable watch history: ",
"preferences_mark_watched_after_duration_label": "Mark video as watched after watching for some time: ",
"preferences_speed_label": "Default speed: ", "preferences_speed_label": "Default speed: ",
"preferences_quality_label": "Preferred video quality: ", "preferences_quality_label": "Preferred video quality: ",
"preferences_quality_option_dash": "DASH (adaptive quality)", "preferences_quality_option_dash": "DASH (adaptive quality)",

View File

@ -52,6 +52,7 @@ struct ConfigPreferences
property vr_mode : Bool = true property vr_mode : Bool = true
property show_nick : Bool = true property show_nick : Bool = true
property save_player_pos : Bool = false property save_player_pos : Bool = false
property mark_watched_after_duration : Bool = false
@[YAML::Field(ignore: true)] @[YAML::Field(ignore: true)]
property default_playlist : String? = nil property default_playlist : String? = nil

View File

@ -82,6 +82,10 @@ module Invidious::Routes::PreferencesRoute
save_player_pos ||= "off" save_player_pos ||= "off"
save_player_pos = save_player_pos == "on" 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 = env.params.body["show_nick"]?.try &.as(String)
show_nick ||= "off" show_nick ||= "off"
show_nick = show_nick == "on" show_nick = show_nick == "on"
@ -182,6 +186,7 @@ module Invidious::Routes::PreferencesRoute
vr_mode: vr_mode, vr_mode: vr_mode,
show_nick: show_nick, show_nick: show_nick,
save_player_pos: save_player_pos, save_player_pos: save_player_pos,
mark_watched_after_duration: mark_watched_after_duration,
default_playlist: default_playlist, default_playlist: default_playlist,
}.to_json) }.to_json)

View File

@ -68,10 +68,6 @@ module Invidious::Routes::Watch
end end
env.params.query.delete_all("iv_load_policy") 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 if CONFIG.enable_user_notifications && notifications && notifications.includes? id
Invidious::Database::Users.remove_notification(user.as(User), id) Invidious::Database::Users.remove_notification(user.as(User), id)
env.get("user").as(User).notifications.delete(id) env.get("user").as(User).notifications.delete(id)

View File

@ -56,6 +56,7 @@ struct Preferences
property extend_desc : Bool = CONFIG.default_user_preferences.extend_desc property extend_desc : Bool = CONFIG.default_user_preferences.extend_desc
property volume : Int32 = CONFIG.default_user_preferences.volume property volume : Int32 = CONFIG.default_user_preferences.volume
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos 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 property default_playlist : String? = nil
module BoolToString module BoolToString

View File

@ -25,6 +25,7 @@ struct VideoPreferences
property volume : Int32 property volume : Int32
property vr_mode : Bool property vr_mode : Bool
property save_player_pos : Bool property save_player_pos : Bool
property mark_watched_after_duration : Bool
end end
def process_video_params(query, preferences) def process_video_params(query, preferences)
@ -48,6 +49,7 @@ def process_video_params(query, preferences)
volume = query["volume"]?.try &.to_i? volume = query["volume"]?.try &.to_i?
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe } 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 } 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 if preferences
# region ||= preferences.region # region ||= preferences.region
@ -70,6 +72,7 @@ def process_video_params(query, preferences)
volume ||= preferences.volume volume ||= preferences.volume
vr_mode ||= preferences.vr_mode.to_unsafe vr_mode ||= preferences.vr_mode.to_unsafe
save_player_pos ||= preferences.save_player_pos.to_unsafe save_player_pos ||= preferences.save_player_pos.to_unsafe
mark_watched_after_duration ||= preferences.mark_watched_after_duration.to_unsafe
end end
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
@ -91,6 +94,7 @@ def process_video_params(query, preferences)
volume ||= CONFIG.default_user_preferences.volume volume ||= CONFIG.default_user_preferences.volume
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.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 annotations = annotations == 1
preload = preload == 1 preload = preload == 1
@ -104,6 +108,7 @@ def process_video_params(query, preferences)
extend_desc = extend_desc == 1 extend_desc = extend_desc == 1
vr_mode = vr_mode == 1 vr_mode = vr_mode == 1
save_player_pos = save_player_pos == 1 save_player_pos = save_player_pos == 1
mark_watched_after_duration = mark_watched_after_duration == 1
if CONFIG.disabled?("dash") && quality == "dash" if CONFIG.disabled?("dash") && quality == "dash"
quality = "high" quality = "high"
@ -132,30 +137,31 @@ def process_video_params(query, preferences)
controls = controls >= 1 controls = controls >= 1
params = VideoPreferences.new({ params = VideoPreferences.new({
annotations: annotations, annotations: annotations,
preload: preload, preload: preload,
autoplay: autoplay, autoplay: autoplay,
comments: comments, comments: comments,
continue: continue, continue: continue,
continue_autoplay: continue_autoplay, continue_autoplay: continue_autoplay,
controls: controls, controls: controls,
listen: listen, listen: listen,
local: local, local: local,
player_style: player_style, player_style: player_style,
preferred_captions: preferred_captions, preferred_captions: preferred_captions,
quality: quality, quality: quality,
quality_dash: quality_dash, quality_dash: quality_dash,
raw: raw, raw: raw,
region: region, region: region,
related_videos: related_videos, related_videos: related_videos,
speed: speed, speed: speed,
video_end: video_end, video_end: video_end,
video_loop: video_loop, video_loop: video_loop,
extend_desc: extend_desc, extend_desc: extend_desc,
video_start: video_start, video_start: video_start,
volume: volume, volume: volume,
vr_mode: vr_mode, vr_mode: vr_mode,
save_player_pos: save_player_pos, save_player_pos: save_player_pos,
mark_watched_after_duration: mark_watched_after_duration,
}) })
return params return params

View File

@ -1,6 +1,7 @@
<% <%
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode dark_mode = env.get("preferences").as(Preferences).dark_mode
mark_watched_after_duration = env.get("preferences").as(Preferences).mark_watched_after_duration
%> %>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= locale %>"> <html lang="<%= locale %>">
@ -27,6 +28,9 @@
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme"> <body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
<span style="display:none" id="dark_mode_pref"><%= dark_mode %></span> <span style="display:none" id="dark_mode_pref"><%= dark_mode %></span>
<% if env.get? "user" %>
<span style="display:none" id="mark_watched_after_duration_pref"><%= mark_watched_after_duration %></span>
<% end %>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1 pure-u-xl-20-24" id="contents"> <div class="pure-u-1 pure-u-xl-20-24" id="contents">
<div class="pure-g navbar h-box"> <div class="pure-g navbar h-box">

View File

@ -229,6 +229,11 @@
<input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>> <input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>>
</div> </div>
<div class="pure-control-group">
<label for="mark_watched_after_duration"><%= translate(locale, "preferences_mark_watched_after_duration_label") %></label>
<input name="mark_watched_after_duration" id="mark_watched_after_duration" type="checkbox" <% if preferences.mark_watched_after_duration %>checked<% end %>>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="annotations_subscribed"><%= translate(locale, "preferences_annotations_subscribed_label") %></label> <label for="annotations_subscribed"><%= translate(locale, "preferences_annotations_subscribed_label") %></label>
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>> <input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>