ncikkis sketchfab downloader

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

目前為 2025-05-03 提交的版本,檢視 最新版本

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

})();