Video time tracker (Firestore)

Save and restore video playback time using Firestore

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();