Skill-Capped Video Downloader

Download videos from Skill-Capped

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        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));

})();