Video time tracker (Firestore)

Save and restore video playback time using Firestore

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Video time tracker (Firestore)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Save and restore video playback time using Firestore
// @author       Bui Quoc Dung
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function () {
    "use strict";

    const FIRESTORE_URL = "PASTE YOUR FIRESTORE LINK"; // Firestore API endpoint URL
    const SAVE_INTERVAL = 30 * 1000; // seconds *1000 - Time between saves in milliseconds
    const MIN_TRACK_TIME = 20 * 60; // minutes * 60 - Minimum video duration to track (seconds)
    const REMOVE_TIME_INTERVAL = 2; // Days before data removal
    const REMOVE_TIME = "08:30"; // Daily cleanup time (24h format)

    let video = null; // HTML video element reference
    let VIDEO_ID = ""; // Current video's unique identifier
    let lastSaveTime = 0; // Timestamp of last save operation

    function getVideoID() {
        const url = new URL(window.location.href);

        // Ưu tiên lấy ID từ tham số truy vấn dạng ?v= hoặc ?id=
        const queryID = url.searchParams.get("v") || url.searchParams.get("id");
        if (queryID) return queryID;

        // Nếu không có tham số, lấy phần cuối của URL nếu có dạng số hoặc chữ cái
        const pathSegments = url.pathname.split('/');
        const lastSegment = pathSegments.pop() || pathSegments.pop(); // Tránh dấu '/' ở cuối
        if (/^[a-zA-Z0-9]+$/.test(lastSegment)) return lastSegment;

        return null; // Không tìm thấy ID hợp lệ
    }


    function findVideo() { // Find video element and initialize tracking
        video = document.querySelector("video") || detectPlyrVideo();
        if (video) {
            if (video.duration < MIN_TRACK_TIME) return;
            initializeVideo();
        } else setTimeout(findVideo, 1000);
    }

    function detectPlyrVideo() { // Detect Plyr player video element
        if (typeof Plyr !== "undefined" && Plyr.instances.length > 0) {
            return Plyr.instances[0].elements.container.querySelector("video");
        }
        return null;
    }

    function loadSavedProgress() { // Load saved playback time from Firestore
        GM_xmlhttpRequest({
            method: "GET",
            url: `${FIRESTORE_URL}/${VIDEO_ID}`,
            onload: function (response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data?.fields?.time?.integerValue) {
                        video.currentTime = parseInt(data.fields.time.integerValue);
                    }
                } catch (error) {}
            }
        });
    }

    function savePlaybackProgress() { // Save current time to Firestore
        if (!video || video.paused || video.ended || video.duration < MIN_TRACK_TIME) return;

        const currentTime = Math.floor(video.currentTime);
        const currentDate = new Date().toISOString().split('T')[0];

        if (Date.now() - lastSaveTime >= SAVE_INTERVAL) {
            GM_xmlhttpRequest({
                method: "PATCH",
                url: `${FIRESTORE_URL}/${VIDEO_ID}`,
                headers: { "Content-Type": "application/json" },
                data: JSON.stringify({
                    fields: {
                        time: { integerValue: currentTime },
                        date: { stringValue: currentDate }
                    }
                }),
                onload: () => lastSaveTime = Date.now()
            });
        }
    }

    function monitorUrlChanges() { // Watch for URL changes to detect new videos
        let previousUrl = location.href;
        setInterval(() => {
            if (location.href !== previousUrl) {
                previousUrl = location.href;
                VIDEO_ID = getVideoID();
                findVideo();
            }
        }, 1000);
    }

    function initializeVideo() { // Set up video event listeners
        loadSavedProgress();
        video.addEventListener("timeupdate", savePlaybackProgress);
        video.addEventListener("seeked", savePlaybackProgress);
    }

    function shouldDeleteData(videoDate) { // Check if data exceeds retention period
        const today = new Date();
        const savedDate = new Date(videoDate);
        const timeDifference = (today - savedDate) / (1000 * 60 * 60 * 24);
        return timeDifference > REMOVE_TIME_INTERVAL;
    }

    function removeOldData() { // Delete expired documents from Firestore
        GM_xmlhttpRequest({
            method: "GET",
            url: `${FIRESTORE_URL}`,
            onload: function (response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.documents) {
                        data.documents.forEach((doc) => {
                            const videoDate = doc.fields?.date?.stringValue;
                            if (videoDate && shouldDeleteData(videoDate)) {
                                GM_xmlhttpRequest({
                                    method: "DELETE",
                                    url: `${FIRESTORE_URL}/${doc.name.split("/").pop()}`,
                                });
                            }
                        });
                    }
                } catch (error) {}
            }
        });
    }

    function scheduleDailyCleanup() { // Schedule daily data cleanup
        setInterval(() => {
            const now = new Date();
            const currentTime = now.getHours().toString().padStart(2, "0") + ":" + now.getMinutes().toString().padStart(2, "0");
            if (currentTime === REMOVE_TIME) {
                removeOldData();
            }
        }, 60 * 1000);
    }

    function init() { // Main initialization function
        VIDEO_ID = getVideoID();
        findVideo();
        monitorUrlChanges();
        scheduleDailyCleanup();
    }

    init();
})();