Vector LMS Video Auto-Completer

Auto-complete videos on Vector LMS training pages with timer

// ==UserScript==
// @name         Vector LMS Video Auto-Completer
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Auto-complete videos on Vector LMS training pages with timer
// @author       savetheplanet07, wu5bocheng, Updated by: Mehdi Mamas
// @match        *://*.vectorlmsedu.com/*
// @license      MIT
// @match        *://*.vectorlmsedu.com/training/launch/course_work/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @run-at       document-end
// ==/UserScript==

console.log("████████████████████████████████████████████████████████");
console.log("██ VECTOR LMS AUTO-COMPLETER SCRIPT LOADED ██");
console.log("████████████████████████████████████████████████████████");
console.log("Script version: 2.4");
console.log("Current URL:", window.location.href);
console.log("Document ready state:", document.readyState);

(function() {
    'use strict';

    console.log("IIFE started");

    const STORAGE_KEY = 'vectorlms_autocomplete_active';
    const COURSE_KEY = 'vectorlms_current_course';

    let timerInterval = null;
    let remainingSeconds = 0;

    function asyncWaitSeconds(seconds) {
        console.log(`asyncWaitSeconds called with ${seconds} seconds`);
        return new Promise((resolve, reject) => {
            remainingSeconds = seconds;
            updateTimerDisplay();

            timerInterval = setInterval(() => {
                remainingSeconds--;
                updateTimerDisplay();

                if (remainingSeconds <= 0) {
                    clearInterval(timerInterval);
                }
            }, 1000);

            setTimeout(() => {
                clearInterval(timerInterval);
                resolve();
            }, seconds * 1000);
        });
    }

    function updateTimerDisplay() {
        let timerDiv = document.getElementById('vectorlms-timer');
        if (!timerDiv) {
            return;
        }

        if (remainingSeconds > 0) {
            let minutes = Math.floor(remainingSeconds / 60);
            let seconds = remainingSeconds % 60;
            timerDiv.innerHTML = `⏱️ Time remaining: ${minutes}m ${seconds}s`;
            timerDiv.style.display = 'block';
        } else {
            timerDiv.style.display = 'none';
        }
    }

    function getCourseId() {
        let match = window.location.pathname.match(/\/course_work\/([^\/]+)/);
        let courseId = match ? match[1] : null;
        console.log("Course ID:", courseId);
        return courseId;
    }

    function isAutoCompleteActive() {
        try {
            let active = GM_getValue(STORAGE_KEY, false);
            let storedCourseId = GM_getValue(COURSE_KEY, null);
            let currentCourseId = getCourseId();

            console.log(`Auto-complete status check:`, {
                active: active,
                storedCourseId: storedCourseId,
                currentCourseId: currentCourseId,
                result: active && storedCourseId === currentCourseId
            });

            return active && storedCourseId === currentCourseId;
        } catch (e) {
            console.error("Error checking auto-complete status:", e);
            return false;
        }
    }

    function startAutoComplete() {
        try {
            let courseId = getCourseId();
            GM_setValue(STORAGE_KEY, true);
            GM_setValue(COURSE_KEY, courseId);
            console.log(`✓ Auto-complete activated for course: ${courseId}`);
        } catch (e) {
            console.error("Error starting auto-complete:", e);
        }
    }

    function stopAutoComplete() {
        try {
            GM_deleteValue(STORAGE_KEY);
            GM_deleteValue(COURSE_KEY);
            if (timerInterval) {
                clearInterval(timerInterval);
            }
            console.log('✓ Auto-complete stopped');
        } catch (e) {
            console.error("Error stopping auto-complete:", e);
        }
    }

    async function main() {
        console.log("════════════════════════════════════════════════");
        console.log("MAIN FUNCTION STARTED");
        console.log("════════════════════════════════════════════════");

        // Wait for page to fully load
        console.log("Waiting 2 seconds for page to load...");
        await asyncWaitSeconds(2);

        let TOC_items = document.getElementsByClassName("TOC_item");
        console.log(`Found ${TOC_items.length} TOC items`);

        if (TOC_items.length === 0) {
            console.log("⚠️ No TOC items found - may not be on course overview page");
            return;
        }

        let TOC_unwatched_videos = [];

        // Scrape the page for video data
        for (let i = 0; i < TOC_items.length; i++) {
            try {
                let data_entry = {};
                data_entry.element = TOC_items[i];

                // Check if it's a video by looking for the play icon
                data_entry.isVideo = TOC_items[i].querySelector(".fa-play") != null;

                // Get the href - locked items won't have one
                let href = TOC_items[i].getAttribute("href");

                // Skip locked items (no href) or non-videos
                if (!href || !data_entry.isVideo) {
                    continue;
                }

                data_entry.href = href;
                data_entry.title = TOC_items[i].querySelector(".lead").innerText;

                let len = href.split("?")[0].split("/").length;
                data_entry.work_id = href.split("?")[0].split("/")[len - 1];
                data_entry.item_id = href.split("?")[0].split("/")[len - 2];

                // Extract video duration
                let timeText = TOC_items[i].querySelector(".span_link").innerText;
                let timeMatch = timeText.match(/(\d+)\s*Minutes?/i);
                data_entry.time_min = timeMatch ? parseInt(timeMatch[1]) + 0.5 : 1;

                data_entry.completed = false;
                TOC_unwatched_videos.push(data_entry);
                console.log(`✓ Added video: ${data_entry.title} (${data_entry.time_min} min)`);
            } catch (err) {
                console.error("Error scraping TOC item:", err);
            }
        }

        console.log(`════════════════════════════════════════════════`);
        console.log(`📊 SUMMARY: Found ${TOC_unwatched_videos.length} unwatched videos`);
        console.log(`════════════════════════════════════════════════`);

        if (TOC_unwatched_videos.length === 0) {
            console.log("🎉 All videos completed! Stopping auto-complete.");
            stopAutoComplete();
            updateButton(true);
            return;
        }

        // Process the first unwatched video
        let unwatched_video = TOC_unwatched_videos[0];
        let school_host = window.location.host;

        console.log(`▶️ Processing video: ${unwatched_video.title}`);
        updateStatusDisplay(`Processing: ${unwatched_video.title}`, `${TOC_unwatched_videos.length} videos remaining`);

        // Request the tracking start
        let tracking_start_url = `https://${school_host}/rpc/v2/json/training/tracking_start?course_item_id=${unwatched_video.item_id}&course_work_id=${unwatched_video.work_id}`;
        console.log(`📡 Calling tracking_start...`);

        const tracking_start_response = await fetch(tracking_start_url);
        let tracking_start_data = await tracking_start_response.json();
        unwatched_video.work_hist_id = tracking_start_data.course_work_hist_id;

        console.log(`✓ Video tracking started. Work hist ID: ${unwatched_video.work_hist_id}`);

        // Delay for video length
        let waitTime = unwatched_video.time_min * 60;
        console.log(`⏱️ Waiting ${waitTime} seconds (${unwatched_video.time_min} minutes)...`);
        updateStatusDisplay(`Watching: ${unwatched_video.title}`, `${TOC_unwatched_videos.length} videos remaining`);
        await asyncWaitSeconds(waitTime);

        // Request the tracking finish
        let tracking_finish_url = `https://${school_host}/rpc/v2/json/training/tracking_finish?course_work_hist_id=${unwatched_video.work_hist_id}&_=${(Date.now() + unwatched_video.time_min * 60 * 1000).toString()}`;
        console.log(`📡 Calling tracking_finish...`);

        const tracking_finish_response = await fetch(tracking_finish_url);
        let tracking_finish_data = await tracking_finish_response.json();

        console.log(`Tracking finish response:`, tracking_finish_data);

        // 0 is completed, 1 is not completed
        unwatched_video.completed = !(tracking_finish_data.tracking_status);

        if (unwatched_video.completed) {
            console.log(`✅ COMPLETED: ${unwatched_video.title}`);
            console.log("🔄 Reloading page to continue with next video...");
            updateStatusDisplay(`✓ Completed: ${unwatched_video.title}`, 'Reloading...');
            setTimeout(() => location.reload(), 1000);
        } else {
            console.log(`❌ FAILED: ${unwatched_video.title}`);
            console.log("Stopping auto-complete due to failure.");
            stopAutoComplete();
            updateButton(false);
            updateStatusDisplay('Failed to complete video', 'Auto-complete stopped');
        }
    }

    function updateStatusDisplay(mainText, subText) {
        let statusDiv = document.getElementById('vectorlms-status');
        if (!statusDiv) {
            return;
        }

        statusDiv.innerHTML = `
            <div style="font-weight: bold; margin-bottom: 5px;">${mainText}</div>
            <div style="font-size: 12px; opacity: 0.9;">${subText}</div>
        `;
    }

    function updateButton(completed) {
        let button = document.getElementById('vectorlms-autocomplete-btn');
        if (!button) {
            return;
        }

        if (isAutoCompleteActive()) {
            button.innerHTML = '⏸ Stop Auto-Complete';
            button.style.background = '#dc3545';
        } else if (completed) {
            button.innerHTML = '✓ All Videos Completed!';
            button.style.background = '#28a745';
            button.disabled = false;
        } else {
            button.innerHTML = '▶ Start Auto-Complete';
            button.style.background = '#007bff';
            button.disabled = false;
        }
    }

    function createUI() {
        console.log("════════════════════════════════════════════════");
        console.log("CREATING UI ELEMENTS");
        console.log("════════════════════════════════════════════════");

        if (!document.body) {
            console.error("❌ document.body is null! Retrying in 500ms...");
            setTimeout(createUI, 500);
            return;
        }

        // Check if UI already exists
        if (document.getElementById('vectorlms-container')) {
            console.log("⚠️ UI already exists, skipping creation");
            return;
        }

        // Create container div
        console.log("Creating container div...");
        let container = document.createElement('div');
        container.id = 'vectorlms-container';
        container.style.cssText = `
            position: fixed !important;
            top: 10px !important;
            right: 10px !important;
            z-index: 999999 !important;
            font-family: Arial, sans-serif !important;
        `;

        // Create a button to start/stop the process
        let button = document.createElement('button');
        button.id = 'vectorlms-autocomplete-btn';
        button.innerHTML = isAutoCompleteActive() ? '⏸ Stop Auto-Complete' : '▶ Start Auto-Complete';
        button.style.cssText = `
            width: 100% !important;
            padding: 12px 20px !important;
            background: ${isAutoCompleteActive() ? '#dc3545' : '#007bff'} !important;
            color: white !important;
            border: none !important;
            border-radius: 5px !important;
            cursor: pointer !important;
            font-weight: bold !important;
            font-size: 14px !important;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important;
            margin-bottom: 10px !important;
        `;

        // Create status display
        let statusDiv = document.createElement('div');
        statusDiv.id = 'vectorlms-status';
        statusDiv.style.cssText = `
            padding: 10px !important;
            background: white !important;
            color: #333 !important;
            border-radius: 5px !important;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important;
            margin-bottom: 10px !important;
            font-size: 13px !important;
            display: none !important;
            min-width: 250px !important;
        `;

        // Create timer display
        let timerDiv = document.createElement('div');
        timerDiv.id = 'vectorlms-timer';
        timerDiv.style.cssText = `
            padding: 10px !important;
            background: #fff3cd !important;
            color: #856404 !important;
            border-radius: 5px !important;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important;
            font-size: 14px !important;
            font-weight: bold !important;
            text-align: center !important;
            display: none !important;
            min-width: 250px !important;
        `;

        button.onclick = function() {
            console.log("🖱️ BUTTON CLICKED!");
            if (isAutoCompleteActive()) {
                stopAutoComplete();
                button.innerHTML = '▶ Start Auto-Complete';
                button.style.background = '#007bff';
                statusDiv.style.display = 'none';
                timerDiv.style.display = 'none';
            } else {
                startAutoComplete();
                button.disabled = true;
                button.innerHTML = '⏳ Starting...';
                statusDiv.style.display = 'block';
                main().then(() => {
                    console.log("✓ Main function completed");
                }).catch(err => {
                    console.error("❌ Error in main function:", err);
                    stopAutoComplete();
                    updateButton(false);
                    statusDiv.style.display = 'none';
                    timerDiv.style.display = 'none';
                });
            }
        };

        container.appendChild(button);
        container.appendChild(statusDiv);
        container.appendChild(timerDiv);
        document.body.appendChild(container);
        console.log("✅ UI CREATION COMPLETE!");

        // If auto-complete is active, automatically continue
        if (isAutoCompleteActive()) {
            console.log("🔄 Auto-complete is active, continuing automatically...");
            button.disabled = true;
            button.innerHTML = '⏳ Processing...';
            statusDiv.style.display = 'block';
            main().then(() => {
                console.log("✓ Main function completed");
            }).catch(err => {
                console.error("❌ Error in main function:", err);
                stopAutoComplete();
                updateButton(false);
                statusDiv.style.display = 'none';
                timerDiv.style.display = 'none';
            });
        } else {
            console.log("💡 Click the button to start auto-completing videos.");
        }
    }

    // Multiple attempts to ensure UI is created
    if (document.readyState === 'loading') {
        console.log("Document is loading, waiting for DOMContentLoaded...");
        document.addEventListener('DOMContentLoaded', function() {
            console.log("🎯 DOMContentLoaded event fired!");
            setTimeout(createUI, 500);
        });
    } else {
        console.log("Document already loaded");
        setTimeout(createUI, 500);
    }

    // Backup - also try on window.load
    window.addEventListener('load', function() {
        console.log("🎯 Window load event fired!");
        if (!document.getElementById('vectorlms-container')) {
            console.log("UI not found, creating now...");
            createUI();
        }
    });

})();