您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save and restore video playback time using Firestore
// ==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(); })();