ncikkis sketchfab downloader

Attempts to download model (glTF/glb preferred) and textures (PNG) from Sketchfab viewer using revised API detection.

当前为 2025-05-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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

})();