Skip to content

Instantly share code, notes, and snippets.

@dansleboby
Created March 2, 2026 16:16
Show Gist options
  • Select an option

  • Save dansleboby/4c838abfeb4e40ac2e5a82e861ca5803 to your computer and use it in GitHub Desktop.

Select an option

Save dansleboby/4c838abfeb4e40ac2e5a82e861ca5803 to your computer and use it in GitHub Desktop.
A Tampermonkey userscript to automatically and sequentially download all videos from a ScreenPal content page. It handles pagination, resolves direct CDN URLs, formats filenames (YYYY-MM-DD-ID-Title.mp4), and uses GM_download to save files directly to the browser's default download directory.
// ==UserScript==
// @name ScreenPal Video Downloader (Sequential)
// @namespace https://screenpal.com/
// @version 3.0.0
// @description Download all videos sequentially across pages
// @author You
// @match https://screenpal.com/content/video*
// @match https://screenpal.com/content/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const KEY_STATE = 'sp_dl_state';
function gmGet(key, def = null) {
try { return JSON.parse(GM_getValue(key, JSON.stringify(def))); }
catch { return def; }
}
function gmSet(key, val) { GM_setValue(key, JSON.stringify(val)); }
// --- Parser ---
function parseCards() {
const cards = document.querySelectorAll('#grid-items .card[data-type="video"]');
const results = [];
cards.forEach(card => {
const playerUrl = card.dataset.playerAppearanceScriptUrl || '';
const id = playerUrl.split('/').pop() || '';
if (!id) return;
const title = card.querySelector('.card-title-text')?.textContent?.trim() || 'Untitled';
const metaText = card.querySelector('.card-text small')?.textContent?.trim() || '';
const metaParts = metaText.split('|').map(s => s.replace(/[^\w\s:/,.-]/g, '').trim());
const rawDate = metaParts[0] || '';
// Date formatting DD/MM/YYYY -> YYYY-MM-DD
let safeDate = '0000-00-00';
if (rawDate.includes('/')) {
const parts = rawDate.split('/');
if (parts.length === 3) safeDate = `${parts[2]}-${parts[1]}-${parts[0]}`;
}
// Title cleanup
const safeTitle = title.replace(/[\\/*?:"<>|]/g, '').replace(/\s+/g, '_');
results.push({
id,
filename: `${safeDate}-${id}-${safeTitle}.mp4`,
downloadUrl: `https://screenpal.com/content/video/download/${id}`
});
});
return results;
}
// --- Network ---
function resolveDirectUrl(downloadUrl) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'HEAD',
url: downloadUrl,
onload: (res) => resolve(res.finalUrl && res.finalUrl !== downloadUrl ? res.finalUrl : downloadUrl),
onerror: () => resolve(null),
ontimeout: () => resolve(null)
});
});
}
function downloadFile(url, filename) {
return new Promise(resolve => {
GM_download({
url: url,
name: filename,
saveAs: false,
onload: () => resolve(true),
onerror: (err) => {
console.error(`Download error for ${filename}:`, err);
resolve(false);
},
ontimeout: () => resolve(false)
});
});
}
// --- UI ---
if (!document.getElementById('sp-dl-ui')) {
const style = document.createElement('style');
style.textContent = `
#sp-dl-ui { position:fixed; bottom:24px; right:24px; z-index:999999; font-family:sans-serif; font-size:13px; width:300px; color:#e2e8f0; }
#sp-dl-ui .sp-card { background:#0f0f1a; border:1px solid #2d2d5e; border-radius:14px; padding:18px; box-shadow:0 12px 48px rgba(0,0,0,.7); }
#sp-dl-ui .sp-status { font-size:12px; margin:10px 0; line-height:1.5; color:#c4b5fd; }
#sp-dl-ui button { background:#6d28d9; color:#fff; border:none; border-radius:8px; cursor:pointer; font-weight:bold; padding:9px 14px; width:100%; margin-top:10px; }
#sp-dl-ui button:disabled { background:#475569; cursor:not-allowed; }
#sp-btn-stop { background:#ef4444; margin-top:5px; display:none; }
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'sp-dl-ui';
panel.innerHTML = `
<div class="sp-card">
<div style="font-weight:bold; color:#a78bfa; margin-bottom:5px;">ScreenPal Auto-Downloader</div>
<div class="sp-status" id="sp-status">Ready.</div>
<button id="sp-btn-start">Start Download</button>
<button id="sp-btn-stop">Stop</button>
</div>
`;
document.body.appendChild(panel);
}
const elStatus = document.getElementById('sp-status');
const btnStart = document.getElementById('sp-btn-start');
const btnStop = document.getElementById('sp-btn-stop');
// --- Logic ---
async function processQueue() {
const state = gmGet(KEY_STATE);
if (!state || !state.active) return;
btnStart.disabled = true;
btnStop.style.display = 'block';
const cards = parseCards();
// If current page is finished
if (state.index >= cards.length) {
const nextBtn = document.querySelector('#next-page');
const liParent = nextBtn ? nextBtn.closest('li') : null;
if (nextBtn && (!liParent || !liParent.classList.contains('disabled'))) {
elStatus.textContent = 'Changing page...';
gmSet(KEY_STATE, { active: true, index: 0, totalPages: state.totalPages + 1 });
nextBtn.click();
return; // Page reload will restart the script
} else {
elStatus.textContent = 'Done! All pages downloaded.';
gmSet(KEY_STATE, null);
btnStart.disabled = false;
btnStop.style.display = 'none';
return;
}
}
// Process current video
const video = cards[state.index];
elStatus.innerHTML = `Current page: Video ${state.index + 1} / ${cards.length}<br>Resolving: ${video.id}...`;
const directUrl = await resolveDirectUrl(video.downloadUrl);
if (directUrl) {
elStatus.innerHTML = `Downloading:<br>${video.filename}`;
await downloadFile(directUrl, video.filename);
} else {
console.warn(`Failed to resolve URL for ${video.id}`);
}
// 2-second pause to avoid spamming the server
await new Promise(r => setTimeout(r, 2000));
// Update state and proceed to next
const currentState = gmGet(KEY_STATE);
if (currentState && currentState.active) {
currentState.index += 1;
gmSet(KEY_STATE, currentState);
processQueue();
}
}
// --- Events ---
btnStart.addEventListener('click', () => {
gmSet(KEY_STATE, { active: true, index: 0, totalPages: 1 });
processQueue();
});
btnStop.addEventListener('click', () => {
gmSet(KEY_STATE, null);
elStatus.textContent = 'Stopped by user.';
btnStart.disabled = false;
btnStop.style.display = 'none';
});
// --- Auto-resume ---
const initialState = gmGet(KEY_STATE);
if (initialState && initialState.active) {
setTimeout(processQueue, 1500); // Delay to let the DOM load after a page change
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment