Skill-Capped Video Downloader

Download videos from Skill-Capped

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();