您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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 })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址