Video time tracker (Firestore)

Save and restore video playback time using Firestore

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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();
})();