您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Downloader for pixel sites
// ==UserScript== // @name Pixel Downloader // @namespace 🫡 pxu // @version 2025-05-30 // @description Downloader for pixel sites // @author small bee // @match *://*.fun/* // @grant none // @license MIT // ==/UserScript== (async () => { const apiBase = window.location.origin; const hostname = window.location.hostname; const backupBase = `${window.location.protocol}//backup.${hostname}`; let me; try { const response = await fetch(`${apiBase}/api/me`, { credentials: 'include' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); me = await response.json(); } catch (error) { console.warn('Could not fetch /api/me from this site:', error); return; } const parts = hostname.split('.'); const domain = parts.length >= 2 ? parts[parts.length - 2] : parts[0]; const siteName = domain.charAt(0).toUpperCase() + domain.slice(1); const ui = document.createElement('div'); Object.assign(ui.style, { position: 'fixed', top: '10px', right: '10px', width: '260px', background: 'rgba(0,0,0,0.8)', color: '#fff', borderRadius: '8px', fontFamily: 'sans-serif', zIndex: 999999, userSelect: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.5)' }); ui.innerHTML = ` <div id="pv_header" style="cursor:move;padding:8px;background:rgba(255,255,255,0.1);display:flex;justify-content:space-between;border-top-left-radius:8px;border-top-right-radius:8px"> <strong>${siteName} DL</strong><span id="pv_close" style="cursor:pointer">×</span> </div> <div style="padding:8px"> <label>Canvas:<select id="pv_canvas" style="width:100%"></select></label><br><br> <label>Mode:<select id="pv_mode" style="width:100%"><option value="area">Area</option><option value="history">History (1 fps, laggy)</option></select></label><br><br> <label>Top-Left (x_y):<br><input id="pv_tl" style="width:100%" placeholder="-128_64"></label><br><br> <label>Bot-Right (x_y):<br><input id="pv_br" style="width:100%" placeholder="128_0"></label><br><br> <div id="pv_dates" style="display:none"> <label>Start Date:<br><input type="date" id="pv_start" style="width:100%"></label><br><br> <label>End Date:<br><input type="date" id="pv_end" style="width:100%"></label><br><br> </div> <button id="pv_btn" style="width:100%;padding:6px">Download</button> <div id="pv_msg" style="margin-top:6px;font-size:12px;min-height:16px"></div> </div> `; document.body.appendChild(ui); const select = ui.querySelector('#pv_canvas'); if (me.canvases) { Object.entries(me.canvases).forEach(([cid, canvas]) => { const opt = document.createElement('option'); opt.value = cid; opt.textContent = `${cid} - ${canvas.title}`; select.append(opt); }); if (select.options.length) select.selectedIndex = 0; } ui.querySelector('#pv_close').onclick = () => ui.remove(); (() => { const header = ui.querySelector('#pv_header'); let dx, dy, ox, oy, dragging = false; header.onmousedown = e => { dragging = true; ox = ui.offsetLeft; oy = ui.offsetTop; dx = e.clientX; dy = e.clientY; e.preventDefault(); }; document.onmousemove = e => { if (!dragging) return; ui.style.left = ox + (e.clientX - dx) + 'px'; ui.style.top = oy + (e.clientY - dy) + 'px'; ui.style.right = 'auto'; }; document.onmouseup = () => dragging = false; })(); ui.querySelector('#pv_mode').onchange = e => ui.querySelector('#pv_dates').style.display = e.target.value === 'history' ? '' : 'none'; ui.querySelector('#pv_btn').onclick = async () => { const msg = ui.querySelector('#pv_msg'); const displayMsg = m => msg.textContent = m; displayMsg('Loading…'); try { const cid = select.value; if (!cid || !me.canvases[cid]) throw 'Invalid canvas'; const canvas = me.canvases[cid]; const parseXY = str => { const [x, y] = str.split('_').map(Number); if (isNaN(x) || isNaN(y)) throw 'Invalid coordinates'; return [x, y]; }; const [x1, y1] = parseXY(ui.querySelector('#pv_tl').value); const [x2, y2] = parseXY(ui.querySelector('#pv_br').value); const width = x2 - x1 + 1, height = y2 - y1 + 1; const getOffset = s => -Math.sqrt(s) * Math.sqrt(s) / 2 | 0; const toTile = (v, off) => Math.floor((v - off) / 256); if (ui.querySelector('#pv_mode').value === 'area') { displayMsg('Fetching tiles…'); const off = getOffset(canvas.size); const ix0 = toTile(x1, off), ix1 = toTile(x2, off); const iy0 = toTile(y1, off), iy1 = toTile(y2, off); const cvs = document.createElement('canvas'); cvs.width = width; cvs.height = height; const ctx = cvs.getContext('2d'); const imgData = ctx.createImageData(width, height); const data = imgData.data; const jobs = []; for (let ty = iy0; ty <= iy1; ty++) for (let tx = ix0; tx <= ix1; tx++) { const path = `/chunks/${cid}/${tx}/${ty}.bmp`; jobs.push( fetch(`${apiBase}${path}`) .catch(() => fetch(`${backupBase}${path}`)) .then(r => r.arrayBuffer()) .then(buf => ({ buf, tx, ty })) ); } const parts = await Promise.all(jobs); displayMsg('Stitching image…'); parts.forEach(({ buf, tx, ty }) => { const bytes = new Uint8Array(buf); if (!bytes.length) return; const bx = tx * 256 + off; const by = ty * 256 + off; bytes.forEach((b, i) => { const gx = bx + (i % 256), gy = by + (i / 256 | 0); if (gx < x1 || gx > x2 || gy < y1 || gy > y2) return; const color = (Array.isArray(canvas.colors) ? canvas.colors : Object.values(canvas.colors))[b & 0x7F]; if (!color) return; const idx = ((gy - y1) * width + (gx - x1)) * 4; data[idx] = color[0]; data[idx+1] = color[1]; data[idx+2] = color[2]; data[idx+3] = 255; }); }); ctx.putImageData(imgData, 0, 0); displayMsg('Preparing download…'); cvs.toBlob(blob => { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${cid}_${x1}_${y1}_${x2}_${y2}.png`; a.click(); displayMsg('Done!'); }); } else { const startDate = ui.querySelector('#pv_start').value; if (!startDate) throw 'Start date missing'; const endDate = ui.querySelector('#pv_end').value; let current = new Date(startDate); const end = endDate ? new Date(endDate) : new Date(); if (end < current) throw 'Invalid date range'; const fps = 5; displayMsg('Recording video…'); const vcan = document.createElement('canvas'); vcan.width = width; vcan.height = height; const vctx = vcan.getContext('2d'); const recorder = new MediaRecorder(vcan.captureStream(fps), { mimeType: 'video/webm' }); const chunks = []; recorder.ondataavailable = e => chunks.push(e.data); recorder.start(); while (current <= end) { const Y = current.getFullYear(), M = String(current.getMonth()+1).padStart(2,'0'), D = String(current.getDate()).padStart(2,'0'); const dayStr = `${Y}${M}${D}`; displayMsg(`Fetching full tiles for ${dayStr}…`); let size = canvas.size; if (canvas.historicalSizes) for (const [d, s] of canvas.historicalSizes) if (dayStr <= d) { size = s; break; } const off2 = getOffset(size); const ix0 = toTile(x1, off2), ix1 = toTile(x2, off2); const iy0 = toTile(y1, off2), iy1 = toTile(y2, off2); const fullJobs = []; for (let ty = iy0; ty <= iy1; ty++) for (let tx = ix0; tx <= ix1; tx++) { const path = `/${Y}/${M}/${D}/${cid}/tiles/${tx}/${ty}.png`; fullJobs.push( fetch(`${apiBase.replace('://','://storage.')}${path}`) .catch(() => fetch(`${backupBase}${path}`)) .then(r => r.blob()) .then(b => createImageBitmap(b)) .then(bmp => ({ bmp, tx, ty })) .catch(() => null) ); } const fullTiles = await Promise.all(fullJobs); vctx.clearRect(0,0,width,height); fullTiles.forEach(t => t && vctx.drawImage(t.bmp, t.tx*256+off2-x1, t.ty*256+off2-y1)); await new Promise(r => setTimeout(r, 1000/fps)); displayMsg(`Applying incremental updates for ${dayStr}…`); const times = await fetch(`${apiBase}/history?day=${dayStr}&id=${cid}`).then(r => r.json()); for (const t of times) { if (t === '0000') continue; const incJobs = []; for (let ty = iy0; ty <= iy1; ty++) for (let tx = ix0; tx <= ix1; tx++) { const incPath = `/${Y}/${M}/${D}/${cid}/${t}/${tx}/${ty}.png`; incJobs.push( fetch(`${apiBase.replace('://','://storage.')}${incPath}`) .catch(() => fetch(`${backupBase}${incPath}`)) .then(r => r.blob()) .then(b => createImageBitmap(b)) .then(bmp => ({ bmp, tx, ty })) .catch(() => null) ); } const incTiles = await Promise.all(incJobs); incTiles.forEach(t2 => t2 && vctx.drawImage(t2.bmp, t2.tx*256+off2-x1, t2.ty*256+off2-y1)); await new Promise(r => setTimeout(r, 1000/fps)); } current.setDate(current.getDate()+1); } recorder.onstop = () => { displayMsg('Finalizing video…'); const blob = new Blob(chunks, { type: 'video/webm' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${cid}_${x1}_${y1}_${x2}_${y2}_timelapse.webm`; a.click(); displayMsg('Done!'); }; recorder.stop(); } } catch (err) { displayMsg('Error: ' + err); console.error(err); } }; })();