Captures Sketchfab's actual model package (zip/osgjs/glb) from network traffic and exports as a proper .glb or .obj file you can open in Blender, Maya, etc.
Ezt a szkriptet nem ajánlott közvetlenül telepíteni. Ez egy könyvtár más szkriptek számára, amik tartalmazzák a // @require https://update.greasyfork.org/scripts/572682/1791305/Sketchfab%20Model%20Downloader%20%28View-Only%20Fix%29.js hivatkozást.
// ==UserScript==
// @name Sketchfab Model Downloader (View-Only Fix)
// @namespace https://sketchfab.com/
// @version 4.0
// @description Captures Sketchfab's actual model package (zip/osgjs/glb) from network traffic and exports as a proper .glb or .obj file you can open in Blender, Maya, etc.
// @author UserScript
// @match https://sketchfab.com/3d-models/*
// @match https://sketchfab.com/models/*
// @match https://sketchfab.com/show/*
// @grant GM_download
// @grant GM_addStyle
// @grant unsafeWindow
// @run-at document-start
// @connect *
// ==/UserScript==
(function () {
'use strict';
/* ══════════════════════════════════════════════════════
STATE — we collect every promising response by URL
══════════════════════════════════════════════════════ */
const files = new Map(); // url → { url, buf, size, label, ext }
let statusMsg = 'Waiting — let the viewer load the model…';
/* ══════════════════════════════════════════════════════
WHAT TO INTERCEPT
Sketchfab's viewer fetches:
• model_file — a ZIP containing osgjs + geometry bins
• file.osgjs.gz — compressed scene graph
• model_file_geo*.bin — geometry chunks
• *.glb — binary glTF (newer models)
• *.bin — binary buffers referenced by glTF
══════════════════════════════════════════════════════ */
function isWanted(url) {
if (!url) return false;
const u = url.toLowerCase();
// Explicit model extensions
if (/\.(glb|gltf|osgjs|osgb|obj|fbx|stl)($|\?|\.gz)/.test(u)) return true;
// Sketchfab CDN model paths
if (/sketchfab\.com.*(model_file|file\.osgjs|geometry|mesh|_geo\b)/.test(u)) return true;
if (/media\.sketchfab\.com|cdn\.sketchfab\.com/.test(u) && /\.(zip|bin|gz)($|\?)/.test(u)) return true;
// Large binary blobs from sketchfab domains
if (/sketchfab\.com/.test(u) && /\.(zip|bin\.gz|osgjs\.gz)($|\?)/.test(u)) return true;
return false;
}
function extFor(url, buf) {
const u = url.toLowerCase().split('?')[0];
// Check GLB magic bytes: "glTF" = 0x46546C67
if (buf && buf.byteLength >= 4) {
const v = new DataView(buf);
if (v.getUint32(0, true) === 0x46546C67) return 'glb';
}
// Check ZIP magic: PK\x03\x04
if (buf && buf.byteLength >= 4) {
const v = new DataView(buf);
if (v.getUint32(0, true) === 0x04034B50) return 'zip';
}
// Check gzip magic: 0x1f8b
if (buf && buf.byteLength >= 2) {
const v = new DataView(buf);
if (v.getUint16(0, true) === 0x8B1F) return 'gz';
}
if (u.endsWith('.glb')) return 'glb';
if (u.endsWith('.gltf')) return 'gltf';
if (u.endsWith('.zip')) return 'zip';
if (u.endsWith('.gz') || u.endsWith('.osgjs.gz')) return 'gz';
if (u.endsWith('.osgjs')) return 'osgjs';
if (u.endsWith('.obj')) return 'obj';
if (u.endsWith('.fbx')) return 'fbx';
return 'bin';
}
function labelFor(url, ext) {
if (ext === 'glb') return '✅ GLB (ready to import)';
if (ext === 'gltf') return '✅ glTF JSON';
if (ext === 'zip') return '📦 Model ZIP (extract → Blender)';
if (ext === 'gz') return '📦 Compressed model (rename to .zip)';
if (ext === 'osgjs') return '📄 OSGJS scene (Blender plugin needed)';
if (ext === 'obj') return '✅ OBJ (ready to import)';
if (ext === 'fbx') return '✅ FBX (ready to import)';
return '⚠️ Binary chunk';
}
function addFile(url, buf) {
if (files.has(url)) return;
const ext = extFor(url, buf);
files.set(url, {
url,
buf,
size: buf.byteLength,
ext,
label: labelFor(url, ext),
name: makeName(url, ext)
});
setStatus(`Captured ${files.size} file(s) — click a file to download`);
refreshPanel();
}
function makeName(url, ext) {
const title = document.title.replace(/\s*[-|]\s*Sketchfab.*/i, '').trim()
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') || 'sketchfab_model';
const urlPart = url.split('/').pop().split('?')[0].replace(/[^a-z0-9._-]/gi, '_');
return `${title}__${urlPart}`.substring(0, 120) + '.' + ext;
}
/* ══════════════════════════════════════════════════════
XHR INTERCEPT — patch at document-start
══════════════════════════════════════════════════════ */
const NativeXHR = unsafeWindow.XMLHttpRequest;
function PatchedXHR() {
const xhr = new NativeXHR();
let _url = '';
const origOpen = xhr.open.bind(xhr);
xhr.open = function (method, url) {
_url = String(url);
// Force arraybuffer response for wanted URLs so we can copy the bytes
if (isWanted(_url)) {
origOpen.apply(xhr, arguments);
xhr.responseType = 'arraybuffer';
return;
}
return origOpen.apply(xhr, arguments);
};
xhr.addEventListener('load', function () {
if (isWanted(_url) && xhr.response instanceof ArrayBuffer && xhr.response.byteLength > 500) {
addFile(_url, xhr.response.slice(0));
}
});
return xhr;
}
PatchedXHR.prototype = NativeXHR.prototype;
unsafeWindow.XMLHttpRequest = PatchedXHR;
/* ══════════════════════════════════════════════════════
FETCH INTERCEPT
══════════════════════════════════════════════════════ */
const origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function (input, init) {
const url = typeof input === 'string' ? input : (input?.url || String(input));
const resp = await origFetch.apply(this, arguments);
if (isWanted(url)) {
try {
const clone = resp.clone();
clone.arrayBuffer().then(buf => {
if (buf.byteLength > 500) addFile(url, buf.slice(0));
}).catch(() => {});
} catch (_) {}
}
return resp;
};
/* ══════════════════════════════════════════════════════
SERVICE WORKER / OSGJS JSON SNIFF
The viewer also embeds the model URL in a JSON config.
We scan the page DOM for it.
══════════════════════════════════════════════════════ */
function scanDOM() {
// Look for model URLs embedded in page scripts or data attributes
const patterns = [
/["'](https?:\/\/[^"']+\.(glb|osgjs|zip|gz|fbx|obj|bin)(\?[^"']*)?)['"]/gi
];
const html = document.documentElement.innerHTML;
for (const re of patterns) {
let m;
while ((m = re.exec(html)) !== null) {
const url = m[1];
if (isWanted(url) && !files.has(url)) {
// Try fetching it directly
origFetch(url, { credentials: 'include' })
.then(r => r.arrayBuffer())
.then(buf => { if (buf.byteLength > 500) addFile(url, buf); })
.catch(() => {});
}
}
}
}
/* ══════════════════════════════════════════════════════
DOWNLOAD
══════════════════════════════════════════════════════ */
function downloadFile(entry) {
const blob = new Blob([entry.buf], { type: 'application/octet-stream' });
const blobUrl = URL.createObjectURL(blob);
setStatus('Downloading ' + entry.name + '…');
GM_download({
url: blobUrl,
name: entry.name,
onload() { URL.revokeObjectURL(blobUrl); setStatus('✅ Saved: ' + entry.name); },
onerror() {
const a = document.createElement('a');
a.href = blobUrl; a.download = entry.name;
document.body.appendChild(a); a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(blobUrl), 4000);
setStatus('✅ Saved (fallback): ' + entry.name);
}
});
}
/* ══════════════════════════════════════════════════════
HELPERS
══════════════════════════════════════════════════════ */
function fmtSize(b) {
if (b > 1048576) return (b / 1048576).toFixed(1) + ' MB';
if (b > 1024) return (b / 1024).toFixed(1) + ' KB';
return b + ' B';
}
function setStatus(msg) {
statusMsg = msg;
const el = document.getElementById('sfm-status-txt');
if (el) el.textContent = msg;
}
/* ══════════════════════════════════════════════════════
UI
══════════════════════════════════════════════════════ */
GM_addStyle(`
#sfm-fab {
position:fixed; bottom:22px; right:22px; z-index:2147483647;
width:48px; height:48px; border-radius:50%;
background:#0F1117; border:2px solid #6366F1;
box-shadow:0 0 0 4px rgba(99,102,241,.18);
cursor:pointer; display:flex; align-items:center; justify-content:center;
transition:transform .18s;
font-family:system-ui,sans-serif;
}
#sfm-fab:hover{transform:scale(1.1);}
#sfm-badge{
position:absolute; top:-4px; right:-4px;
background:#10B981; color:#fff; font-size:9px; font-weight:700;
border-radius:10px; min-width:17px; height:17px; padding:0 4px;
display:none; align-items:center; justify-content:center;
}
#sfm-panel{
position:fixed; bottom:80px; right:22px; z-index:2147483647;
width:390px; background:#0D1117; border:1px solid #21262D;
border-radius:14px; font-family:system-ui,-apple-system,sans-serif;
color:#C9D1D9; box-shadow:0 24px 64px rgba(0,0,0,.75);
display:none; flex-direction:column; overflow:hidden;
}
#sfm-panel.open{display:flex;}
#sfm-head{
padding:11px 14px; background:#161B22; border-bottom:1px solid #21262D;
display:flex; align-items:center; gap:8px;
}
#sfm-head h3{margin:0;font-size:13px;font-weight:600;color:#818CF8;flex:1;}
#sfm-head button{background:none;border:none;color:#6E7681;cursor:pointer;font-size:14px;padding:0;border-radius:4px;}
#sfm-head button:hover{color:#C9D1D9;}
#sfm-status{
padding:7px 14px; font-size:11px; color:#6E7681;
background:#0D1117; border-bottom:1px solid #161B22;
display:flex; align-items:center; gap:6px;
}
.sfm-pulse{width:6px;height:6px;border-radius:50%;background:#10B981;flex-shrink:0;animation:sfpulse 1.4s infinite;}
@keyframes sfpulse{0%,100%{opacity:1;}50%{opacity:.2;}}
#sfm-list{max-height:300px;overflow-y:auto;padding:6px 0;}
#sfm-list::-webkit-scrollbar{width:3px;}
#sfm-list::-webkit-scrollbar-thumb{background:#21262D;border-radius:2px;}
.sfm-empty{padding:28px 16px;text-align:center;color:#444C56;font-size:12px;line-height:1.6;}
.sfm-empty strong{display:block;font-size:13px;color:#6E7681;margin-bottom:5px;}
.sfm-row{
display:flex;align-items:center;gap:10px;padding:9px 14px;
border-bottom:1px solid #161B22;transition:background .13s;cursor:default;
}
.sfm-row:last-child{border-bottom:none;}
.sfm-row:hover{background:#161B22;}
.sfm-ext{
font-size:9px;font-weight:700;letter-spacing:.07em;padding:3px 7px;
border-radius:5px;text-align:center;min-width:34px;flex-shrink:0;
}
.ext-glb{background:#0D2B1F;color:#10B981;}
.ext-gltf{background:#0D2B1F;color:#34D399;}
.ext-obj{background:#0F1F0F;color:#4ADE80;}
.ext-fbx{background:#0F1F0F;color:#86EFAC;}
.ext-zip{background:#1E1A0D;color:#FBBF24;}
.ext-gz{background:#1E1A0D;color:#FCD34D;}
.ext-bin{background:#1A1118;color:#A78BFA;}
.ext-other{background:#21262D;color:#8B949E;}
.sfm-info{flex:1;overflow:hidden;}
.sfm-lbl{font-size:12px;font-weight:500;color:#C9D1D9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sfm-sub{font-size:10.5px;color:#6E7681;margin-top:1px;}
.sfm-dl{
width:30px;height:30px;border-radius:7px;border:1px solid #30363D;
background:transparent;color:#6E7681;cursor:pointer;
display:flex;align-items:center;justify-content:center;
transition:all .14s;flex-shrink:0;
}
.sfm-dl:hover{background:#6366F1;border-color:#6366F1;color:#fff;}
.sfm-dl.done{background:#10B981;border-color:#10B981;color:#fff;}
#sfm-foot{
padding:9px 14px;border-top:1px solid #21262D;
display:flex;gap:8px;
}
#sfm-foot button{
flex:1;padding:8px;border-radius:8px;border:1px solid #30363D;
background:transparent;color:#8B949E;font-size:11.5px;font-weight:500;
cursor:pointer;transition:all .14s;
}
#sfm-foot button:hover{background:#161B22;color:#C9D1D9;}
#sfm-scan-btn{}
#sfm-hint{
padding:9px 14px;background:#13100A;border-top:1px solid #2D2200;
font-size:10.5px;color:#7C6A3A;line-height:1.55;
}
#sfm-hint b{color:#FBBF24;display:block;margin-bottom:2px;}
`);
function extClass(ext) {
return ['glb','gltf','obj','fbx','zip','gz','bin'].includes(ext) ? 'ext-'+ext : 'ext-other';
}
function buildUI() {
const fab = document.createElement('div');
fab.id = 'sfm-fab';
fab.innerHTML = `<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#818CF8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span id="sfm-badge"></span>`;
fab.addEventListener('click', () => {
const p = document.getElementById('sfm-panel');
p.classList.toggle('open');
});
document.body.appendChild(fab);
const panel = document.createElement('div');
panel.id = 'sfm-panel';
panel.innerHTML = `
<div id="sfm-head">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#818CF8" stroke-width="2.5" stroke-linecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<h3>Sketchfab Model Capture</h3>
<button id="sfm-x">✕</button>
</div>
<div id="sfm-status"><span class="sfm-pulse"></span><span id="sfm-status-txt">${statusMsg}</span></div>
<div id="sfm-list"><div class="sfm-empty"><strong>No files detected yet</strong>Let the 3D viewer fully load,<br>then rotate the model around.</div></div>
<div id="sfm-foot"><button id="sfm-scan-btn">🔍 Scan Page</button></div>
<div id="sfm-hint"><b>💡 Tips for best results:</b>
Let the viewer finish loading. Rotate the model fully so all geometry streams in.
<br>• <b>.glb</b> / <b>.obj</b> / <b>.fbx</b> → open directly in Blender, Maya, etc.
<br>• <b>.zip</b> / <b>.gz</b> → extract, open the .osgjs in Blender with the <a href="https://github.com/cedricpinson/osgjs-blender" target="_blank" style="color:#6366F1">OSGJS importer</a></div>
`;
document.body.appendChild(panel);
document.getElementById('sfm-x').addEventListener('click', () => panel.classList.remove('open'));
document.getElementById('sfm-scan-btn').addEventListener('click', () => { scanDOM(); setStatus('Page scanned — check for new files above'); });
}
function refreshPanel() {
const list = document.getElementById('sfm-list');
const badge = document.getElementById('sfm-badge');
if (!list) return;
if (badge) {
badge.style.display = files.size > 0 ? 'flex' : 'none';
badge.textContent = files.size;
}
if (files.size === 0) {
list.innerHTML = `<div class="sfm-empty"><strong>No files detected yet</strong>Let the 3D viewer fully load,<br>then rotate the model around.</div>`;
return;
}
list.innerHTML = '';
// Sort: GLB/OBJ/FBX first (most useful), then zip, then others
const priority = { glb:0, gltf:1, obj:2, fbx:3, zip:4, gz:5, osgjs:6, bin:7 };
const sorted = [...files.values()].sort((a,b) => (priority[a.ext]||9) - (priority[b.ext]||9));
for (const entry of sorted) {
const row = document.createElement('div');
row.className = 'sfm-row';
row.innerHTML = `
<span class="sfm-ext ${extClass(entry.ext)}">${entry.ext.toUpperCase()}</span>
<div class="sfm-info">
<div class="sfm-lbl" title="${entry.label}">${entry.label}</div>
<div class="sfm-sub">${fmtSize(entry.size)}</div>
</div>
<button class="sfm-dl" title="Download ${entry.name}">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>`;
const btn = row.querySelector('.sfm-dl');
btn.addEventListener('click', () => {
downloadFile(entry);
btn.classList.add('done');
btn.innerHTML = '✓';
});
list.appendChild(row);
}
}
/* ══════════════════════════════════════════════════════
BOOT
══════════════════════════════════════════════════════ */
function init() {
buildUI();
// Scan DOM after page settles
setTimeout(scanDOM, 3000);
setTimeout(scanDOM, 7000);
// Watch for dynamically injected scripts
new MutationObserver(scanDOM).observe(document.documentElement, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();