Greasy Fork is available in English.
Download videos from Skill-Capped
// ==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));
})();