Created
March 2, 2026 16:16
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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