Skill-Capped Video Downloader

Download videos from Skill-Capped

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name        Skill-Capped Video Downloader
// @namespace   http://tampermonkey.net/
// @version     1.0
// @description Download videos from Skill-Capped
// @author      xizas
// @match       https://www.skill-capped.com/*
// @grant       none
// @run-at      document-idle
// ==/UserScript==

(function() {
    'use strict';

    const CLOUDFRONT_URL = 'https://d13z5uuzt1wkbz.cloudfront.net';
    const RESOLUTION = '2500';
    let ffmpegLoaded = false;
    let ffmpegLoading = false;

    // Load FFmpeg.wasm for MP4 conversion
    function loadFFmpeg() {
        if (ffmpegLoaded || ffmpegLoading) return;
        ffmpegLoading = true;

        const script1 = document.createElement('script');
        script1.src = 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd/ffmpeg.js';
        document.head.appendChild(script1);

        const script2 = document.createElement('script');
        script2.src = 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd/index.js';
        script2.onload = () => {
            ffmpegLoaded = true;
            console.log('[SC Downloader] FFmpeg loaded successfully');
        };
        document.head.appendChild(script2);
    }

    // Extract video ID from thumbnail
    function getVideoId() {
        const thumbnail = document.querySelector('img[data-name="Video Poster"]');
        if (thumbnail) {
            const match = thumbnail.src.match(/thumbnails\/([a-z0-9]+)\//i);
            if (match) return match[1];
        }

        // Fallback: try URL
        const urlMatch = window.location.pathname.match(/\/video\/([a-z0-9]+)/i);
        if (urlMatch) return urlMatch[1];

        return null;
    }

    // Extract video title for filename
    function getVideoTitle() {
        const selectors = [
            '[data-name="CourseTitleBar"]',
            '[data-name="Vid Info Row"] h1',
            '[data-name="Vid Info Row"] h2',
            'h1'
        ];

        for (const selector of selectors) {
            const el = document.querySelector(selector);
            if (el && el.textContent.trim()) {
                return el.textContent.trim()
                    .replace(/[<>:"/\\|?*]/g, '')
                    .substring(0, 80);
            }
        }
        return null;
    }

    // Download all .ts segments
    async function downloadSegments(videoId, updateStatus) {
        const chunks = [];
        let consecutiveFails = 0;

        for (let i = 1; i <= 2000; i++) {
            const url = `${CLOUDFRONT_URL}/${videoId}/HIDDEN${RESOLUTION}-${String(i).padStart(5, '0')}.ts`;

            try {
                const resp = await fetch(url);
                if (resp.ok) {
                    chunks.push(await resp.arrayBuffer());
                    consecutiveFails = 0;
                    const sizeMB = (chunks.reduce((a, b) => a + b.byteLength, 0) / 1024 / 1024).toFixed(1);
                    updateStatus(`Downloading: segment ${i} (${sizeMB} MB)`);
                } else {
                    consecutiveFails++;
                    if (consecutiveFails >= 3) break;
                }
            } catch (e) {
                consecutiveFails++;
                if (consecutiveFails >= 3) break;
            }
        }

        return chunks;
    }

    // Convert .ts to .mp4 using FFmpeg.wasm
    async function convertToMp4(chunks, updateStatus) {
        if (typeof FFmpegWASM === 'undefined' || typeof FFmpegUtil === 'undefined') {
            throw new Error('FFmpeg not loaded');
        }

        const { FFmpeg } = FFmpegWASM;
        const { fetchFile } = FFmpegUtil;

        const ffmpeg = new FFmpeg();

        updateStatus('Loading FFmpeg...');
        await ffmpeg.load({
            coreURL: 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd/ffmpeg-core.js',
        });

        updateStatus('Preparing conversion...');
        const tsBlob = new Blob(chunks, { type: 'video/mp2t' });
        const tsData = await fetchFile(tsBlob);

        await ffmpeg.writeFile('input.ts', tsData);

        updateStatus('Converting to MP4... (please wait)');
        await ffmpeg.exec(['-i', 'input.ts', '-c', 'copy', 'output.mp4']);

        const mp4Data = await ffmpeg.readFile('output.mp4');
        return new Blob([mp4Data.buffer], { type: 'video/mp4' });
    }

    // Trigger file download
    function triggerDownload(blob, filename) {
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(a.href);
    }

    // Main download function
    async function startDownload(btn) {
        const videoId = getVideoId();
        if (!videoId) {
            btn.innerHTML = 'ERROR: No video found';
            return;
        }

        const videoTitle = getVideoTitle() || videoId;
        btn.disabled = true;

        const updateStatus = (msg) => { btn.innerHTML = msg; };

        try {
            // Download segments
            const chunks = await downloadSegments(videoId, updateStatus);

            if (!chunks.length) {
                updateStatus('ERROR: No data downloaded');
                btn.disabled = false;
                return;
            }

            // Try MP4 conversion
            try {
                const mp4Blob = await convertToMp4(chunks, updateStatus);
                triggerDownload(mp4Blob, `${videoTitle}.mp4`);
                updateStatus('✓ MP4 DOWNLOAD COMPLETE');
            } catch (e) {
                console.warn('[SC Downloader] FFmpeg failed, falling back to .ts:', e);
                updateStatus('Converting failed, downloading as .ts...');

                const tsBlob = new Blob(chunks, { type: 'video/mp2t' });
                triggerDownload(tsBlob, `${videoTitle}.ts`);
                updateStatus('✓ DOWNLOADED AS .TS');
            }
        } catch (e) {
            console.error('[SC Downloader] Error:', e);
            updateStatus('ERROR: ' + e.message);
        }

        setTimeout(() => {
            btn.innerHTML = '⬇ DOWNLOAD MP4';
            btn.disabled = false;
        }, 5000);
    }

    // Create download button
    function createButton() {
        // Remove existing button
        document.querySelectorAll('.sc-dl-btn').forEach(e => e.remove());

        // Check if we're on a video page
        if (!document.querySelector('[data-name="Video Player Container"]')) {
            return;
        }

        const videoId = getVideoId();
        if (!videoId) return;

        console.log('[SC Downloader] Video detected:', videoId);

        const btn = document.createElement('button');
        btn.className = 'sc-dl-btn';
        btn.innerHTML = '⬇ DOWNLOAD MP4';
        btn.title = 'Video ID: ' + videoId;
        btn.setAttribute('style', `
            position: fixed !important;
            top: 10px !important;
            left: 10px !important;
            z-index: 2147483647 !important;
            padding: 15px 30px !important;
            background: linear-gradient(135deg, #d55051 0%, #b33a3b 100%) !important;
            color: white !important;
            border: 2px solid rgba(255,255,255,0.3) !important;
            border-radius: 8px !important;
            font-family: "Roboto Condensed", Arial, sans-serif !important;
            font-size: 14px !important;
            font-weight: bold !important;
            text-transform: uppercase !important;
            cursor: pointer !important;
            box-shadow: 0 4px 15px rgba(0,0,0,0.4) !important;
            transition: all 0.2s ease !important;
        `);

        btn.onmouseenter = () => {
            btn.style.transform = 'translateY(-2px)';
            btn.style.boxShadow = '0 6px 20px rgba(0,0,0,0.5)';
        };
        btn.onmouseleave = () => {
            btn.style.transform = 'translateY(0)';
            btn.style.boxShadow = '0 4px 15px rgba(0,0,0,0.4)';
        };

        btn.onclick = () => startDownload(btn);

        document.body.appendChild(btn);

        // Load FFmpeg in background
        loadFFmpeg();
    }

    // Initialize
    function init() {
        createButton();

        // Watch for SPA navigation
        let lastUrl = location.href;
        const observer = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                setTimeout(createButton, 1500);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Run when ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(init, 1000));
    } else {
        setTimeout(init, 1000);
    }

    // Also try on full load
    window.addEventListener('load', () => setTimeout(createButton, 2000));

})();