Attempts to download model (glTF/glb preferred) and textures (PNG) from Sketchfab viewer using revised API detection.
当前为
// ==UserScript==
// @name ncikkis sketchfab downloader
// @namespace http://tampermonkey.net/
// @version 1.5 // Version incremented for revised API finding logic based on provided code
// @description Attempts to download model (glTF/glb preferred) and textures (PNG) from Sketchfab viewer using revised API detection.
// @author ncikkis
// @match *://sketchfab.com/3d-models/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=sketchfab.com
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const LOG_PREFIX = '[ncikkis_SFD]:';
let apiInstance = null; // This will hold the Sketchfab API client object
let downloadButton = null;
console.log(`${LOG_PREFIX} Initializing v1.5...`);
// --- Revised findApiInstance function ---
function findApiInstance() {
console.log(`${LOG_PREFIX} Attempting to find API instance...`);
// Method 1: Check the global array revealed by the provided code
if (unsafeWindow.sketchfabAPIinstances && Array.isArray(unsafeWindow.sketchfabAPIinstances) && unsafeWindow.sketchfabAPIinstances.length > 0) {
console.log(`${LOG_PREFIX} Found sketchfabAPIinstances array with ${unsafeWindow.sketchfabAPIinstances.length} instance(s).`);
for (let i = 0; i < unsafeWindow.sketchfabAPIinstances.length; i++) {
const instance = unsafeWindow.sketchfabAPIinstances[i];
// The actual API methods might be on the _client property based on the analyzed code
if (instance && instance._client) {
// Check if the client seems initialized (e.g., has expected methods)
// We assume getTextureList should exist based on previous attempts.
// This check makes the assumption that the _client object is the final usable API object.
if (typeof instance._client.getTextureList === 'function') {
console.log(`${LOG_PREFIX} Found initialized API client object via sketchfabAPIinstances[${i}]._client.`);
return instance._client;
} else {
console.log(`${LOG_PREFIX} Found instance._client in sketchfabAPIinstances[${i}], but it lacks expected methods.`);
}
} else {
console.log(`${LOG_PREFIX} Instance ${i} in sketchfabAPIinstances is invalid or missing ._client.`);
}
}
console.log(`${LOG_PREFIX} Iterated through sketchfabAPIinstances, no suitable ._client found.`);
} else {
console.log(`${LOG_PREFIX} sketchfabAPIinstances array not found or empty.`);
}
// Method 2: Fallback to previous speculative checks (less likely now)
console.log(`${LOG_PREFIX} Falling back to previous speculative checks...`);
if (unsafeWindow.viewerApp) {
if (typeof unsafeWindow.viewerApp.getViewer === 'function') {
let viewer = unsafeWindow.viewerApp.getViewer();
// Check if getApi exists AND if the returned object has expected methods
if (viewer && typeof viewer.getApi === 'function') {
let potentialApi = viewer.getApi();
if(potentialApi && typeof potentialApi.getTextureList === 'function') {
console.log(`${LOG_PREFIX} Found API instance via unsafeWindow.viewerApp.getViewer().getApi().`);
return potentialApi;
}
}
}
if(unsafeWindow.viewerApp && unsafeWindow.viewerApp.viewers) {
const viewerKeys = Object.keys(unsafeWindow.viewerApp.viewers);
if (viewerKeys.length > 0) {
const viewer = unsafeWindow.viewerApp.viewers[viewerKeys[0]];
// Check if .api exists AND has expected methods
if (viewer && viewer.api && typeof viewer.api.getTextureList === 'function') {
console.log(`${LOG_PREFIX} Found API instance via unsafeWindow.viewerApp.viewers[...].api.`);
return viewer.api;
}
}
}
}
if (unsafeWindow.api && typeof unsafeWindow.api.getTextureList === 'function') {
console.log(`${LOG_PREFIX} Found API instance via unsafeWindow.api.`);
return unsafeWindow.api;
}
console.log(`${LOG_PREFIX} Viewer API instance could not be located via any known method.`);
return null;
}
// --- End of revised function ---
function createDownloadButton() {
// Same implementation as v1.4 (includes delayed append)
if (document.getElementById('ncikkis-sf-download-button')) { console.log(`${LOG_PREFIX} Download button already exists.`); return; }
const viewerElement = document.querySelector('.viewer');
if (!viewerElement) { console.log(`${LOG_PREFIX} Viewer element (.viewer) not found.`); return; }
downloadButton = document.createElement('button');
downloadButton.id = 'ncikkis-sf-download-button';
downloadButton.textContent = 'Download Model';
// Styles
downloadButton.style.position = 'absolute'; downloadButton.style.top = '10px'; downloadButton.style.right = '10px';
downloadButton.style.zIndex = '9999'; downloadButton.style.padding = '8px 12px'; downloadButton.style.backgroundColor = '#FF0000';
downloadButton.style.color = 'white'; downloadButton.style.border = 'none'; downloadButton.style.borderRadius = '4px';
downloadButton.style.cursor = 'pointer'; downloadButton.style.fontSize = '12px'; downloadButton.style.fontFamily = 'sans-serif';
downloadButton.style.fontWeight = 'bold'; downloadButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
downloadButton.addEventListener('click', handleDownload);
const currentPosition = window.getComputedStyle(viewerElement).position;
if (currentPosition === 'static') { viewerElement.style.position = 'relative'; console.log(`${LOG_PREFIX} Set viewer element position to relative.`); }
console.log(`${LOG_PREFIX} Delaying button append slightly...`);
setTimeout(() => { // Delayed append workaround
try {
console.log(`${LOG_PREFIX} Attempting delayed appendChild...`);
viewerElement.appendChild(downloadButton);
console.log(`${LOG_PREFIX} Download button injected successfully (delayed).`);
} catch (e) { console.error(`${LOG_PREFIX} Error during delayed appendChild execution:`, e); alert(`ncikkis Downloader: Critical error appending download button. Error: ${e.message}`); }
}, 100);
}
function getModelName() {
// Same implementation as v1.4
const titleElement = document.querySelector('.model-name__label'); if (titleElement) return titleElement.textContent.trim().replace(/[^a-zA-Z0-9_-]/g, '_');
const urlParts = window.location.pathname.split('/'); const modelSlug = urlParts[urlParts.length - 2]; if (modelSlug && modelSlug !== '3d-models') return modelSlug.replace(/[^a-zA-Z0-9_-]/g, '_');
const modelId = urlParts[urlParts.length - 1]; if (modelId) return modelId.replace(/[^a-zA-Z0-9_-]/g, '_');
return 'sketchfab_model';
}
function handleDownload() {
// Same implementation as v1.4 - requires apiInstance to be correctly found
if (!apiInstance) { console.error(`${LOG_PREFIX} API instance not available for download.`); alert('ncikkis Downloader: API instance not found. Cannot initiate download.'); return; }
console.log(`${LOG_PREFIX} Download initiated. Using located API instance.`);
downloadButton.textContent = 'Processing...'; downloadButton.disabled = true;
const modelName = getModelName(); let texturesDownloaded = false; let geometryDownloaded = false;
const finalizeCheck = () => { /* ... same feedback logic as v1.4 ... */
downloadButton.textContent = 'Download Model'; downloadButton.disabled = false;
if (!geometryDownloaded && !texturesDownloaded) { console.warn(`${LOG_PREFIX} Failed to locate any downloadable data.`); alert(`ncikkis Downloader: Failed to locate any downloadable data.`); }
else if (!geometryDownloaded) { console.warn(`${LOG_PREFIX} Failed to locate geometry data. Texture download attempted.`); alert(`ncikkis Downloader: Failed to locate downloadable geometry. Texture download attempted.`); }
else if (!texturesDownloaded) { console.log(`${LOG_PREFIX} Geometry download initiated. Texture download failed/not found.`); alert(`ncikkis Downloader: Geometry download initiated. Textures not found/failed.`); }
else { console.log(`${LOG_PREFIX} Geometry and texture download initiated.`); alert(`ncikkis Downloader: Geometry and texture download initiated.`); }
};
let textureAttemptComplete = false; let geometryAttemptComplete = false;
try { // Texture attempt
// Use the found apiInstance
apiInstance.getTextureList((err, textures) => { /* ... same texture processing as v1.4 ... */
if (!err && textures && textures.length > 0) {
console.log(`${LOG_PREFIX} Found ${textures.length} textures. Downloading...`); texturesDownloaded = true;
textures.forEach((texture, index) => {
if (texture && texture.url) {
const textureName = texture.name || `texture_${index}`; const sanitizedTextureName = textureName.replace(/[^a-zA-Z0-9_-]/g, '_');
const filename = `${modelName}_${sanitizedTextureName}.png`; console.log(`${LOG_PREFIX} Downloading texture: ${filename}`);
GM_download({ url: texture.url, name: filename, onerror: (err) => console.error(`${LOG_PREFIX} Tex DL Error ${filename}:`, err), onload: () => console.log(`${LOG_PREFIX} Tex DL ${filename} OK.`) });
} else { console.warn(`${LOG_PREFIX} Invalid texture index ${index}:`, texture); } });
} else { console.warn(`${LOG_PREFIX} Texture list empty or error:`, err); }
textureAttemptComplete = true; if (geometryAttemptComplete) finalizeCheck(); });
} catch (e) { console.error(`${LOG_PREFIX} Tex API Error:`, e); textureAttemptComplete = true; if (geometryAttemptComplete) finalizeCheck(); }
try { // Geometry attempt
console.log(`${LOG_PREFIX} Searching for geometry data (glTF/glb)...`); let modelUrl = null, modelFormat = null;
// Speculative checks remain the same logic as v1.4, independent of how apiInstance was found
if (unsafeWindow.viewerApp?.config?.model) { let modelConf = unsafeWindow.viewerApp.config.model; if (modelConf.gltfUrl) { modelUrl = modelConf.gltfUrl; modelFormat = 'gltf'; } else if (modelConf.glbUrl) { modelUrl = modelConf.glbUrl; modelFormat = 'glb'; } }
if (!modelUrl) { try { const resources = performance.getEntriesByType('resource'); const sketchfabResources = resources.filter(r => r.name.includes('sketchfab.com') || r.name.includes('skfb.ly')); const gltfResource = sketchfabResources.find(r => r.name.endsWith('.gltf') || r.name.includes('format=gltf')); const glbResource = sketchfabResources.find(r => r.name.endsWith('.glb') || r.name.includes('format=glb')); if (glbResource) { modelUrl = glbResource.name; modelFormat = 'glb'; } else if (gltfResource) { modelUrl = gltfResource.name; modelFormat = 'gltf'; } } catch (perfError) { console.warn(`${LOG_PREFIX} Perf entries error:`, perfError); } }
if (modelUrl && modelFormat) { // Conditional Download
console.log(`${LOG_PREFIX} Found potential model URL (${modelFormat}): ${modelUrl}`); const filename = `${modelName}.${modelFormat}`;
try { GM_download({ /* ... options ... */ url: modelUrl, name: filename, onerror: (err) => { console.error(`${LOG_PREFIX} Geo DL Error ${filename}:`, err); geometryDownloaded = false; geometryAttemptComplete = true; if (textureAttemptComplete) finalizeCheck(); }, onload: () => { console.log(`${LOG_PREFIX} Geo DL ${filename} OK.`); geometryDownloaded = true; geometryAttemptComplete = true; if (textureAttemptComplete) finalizeCheck(); }, ontimeout: () => { console.error(`${LOG_PREFIX} Geo DL Timeout ${filename}.`); geometryDownloaded = false; geometryAttemptComplete = true; if (textureAttemptComplete) finalizeCheck(); } });
} catch (dlError) { console.error(`${LOG_PREFIX} Geo DL setup error:`, dlError); geometryDownloaded = false; geometryAttemptComplete = true; if (textureAttemptComplete) finalizeCheck(); }
} else { console.warn(`${LOG_PREFIX} Could not locate geometry URL.`); geometryDownloaded = false; geometryAttemptComplete = true; if (textureAttemptComplete) finalizeCheck(); }
} catch (e) { console.error(`${LOG_PREFIX} Geo Search Error:`, e); geometryDownloaded = false; geometryAttemptComplete = true; if (textureAttemptComplete) finalizeCheck(); }
}
// --- Initialization Logic --- (Same logic, calls revised findApiInstance)
const initCheckInterval = setInterval(() => {
// Try to find the API instance using the revised logic
if (!apiInstance) { // Only try finding if not already found
apiInstance = findApiInstance();
}
// Check if viewer element exists separately
const viewerElement = document.querySelector('.viewer');
// Proceed if BOTH are found
if (apiInstance && viewerElement) {
try {
clearInterval(initCheckInterval); // Stop interval once requirements met
console.log(`${LOG_PREFIX} API instance and viewer element located.`);
createDownloadButton(); // Create button now
} catch (e) { console.error (`${LOG_PREFIX} Error during post-initialization step:`, e); }
} else if (document.readyState === "complete" && !apiInstance) {
// Keep logging state if page seems loaded but API is missing
console.log(`${LOG_PREFIX} Page loaded, still searching for API instance...`);
} else if (!viewerElement){
console.log(`${LOG_PREFIX} API instance might be found, but waiting for viewer element...`);
}
}, 2000); // Check interval remains 2 seconds
setTimeout(() => {
// Timeout check remains the same
if (!document.getElementById('ncikkis-sf-download-button')) {
clearInterval(initCheckInterval);
console.warn(`${LOG_PREFIX} Initialization timed out or button creation failed.`);
}
}, 30000); // Timeout remains 30 seconds
})();