ncikkis sketchfab downloader

Attempts to download the actual model (glTF/glb preferred) and textures (PNG) from Sketchfab viewer. Downloads geometry only if actual data is located.

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

// ==UserScript==
// @name         ncikkis sketchfab downloader
// @namespace    http://tampermonkey.net/
// @version      1.2 // Version incremented for revised geometry logic
// @description  Attempts to download the actual model (glTF/glb preferred) and textures (PNG) from Sketchfab viewer. Downloads geometry only if actual data is located.
// @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 // Kept for potential future network inspection needs
// @grant        unsafeWindow
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const LOG_PREFIX = '[ncikkis_SFD]:';
    let apiInstance = null;
    let downloadButton = null;

    console.log(`${LOG_PREFIX} Initializing v1.2...`);

    function findApiInstance() {
        // Attempt to find the Sketchfab viewer API instance (speculative)
        if (unsafeWindow.viewerApp) {
             if (typeof unsafeWindow.viewerApp.getViewer === 'function') {
                 let viewer = unsafeWindow.viewerApp.getViewer();
                 if (viewer && typeof viewer.getApi === 'function') return viewer.getApi();
             }
             if(unsafeWindow.viewerApp && unsafeWindow.viewerApp.viewers) {
                const viewerKeys = Object.keys(unsafeWindow.viewerApp.viewers);
                if (viewerKeys.length > 0) {
                    const viewer = unsafeWindow.viewerApp.viewers[viewerKeys[0]];
                    if (viewer && viewer.api) return viewer.api;
                }
             }
        }
         if (unsafeWindow.api) return unsafeWindow.api;

        console.log(`${LOG_PREFIX} Viewer API instance not found via common patterns.`);
        return null;
    }

    function createDownloadButton() {
        if (document.getElementById('ncikkis-sf-download-button')) return;

        const viewerElement = document.querySelector('.viewer');
        if (!viewerElement) {
            console.log(`${LOG_PREFIX} Viewer element not found.`);
            return;
        }

        downloadButton = document.createElement('button');
        downloadButton.id = 'ncikkis-sf-download-button';
        downloadButton.textContent = 'Download Model';
        // Apply styles (same as before)
        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);
        viewerElement.style.position = 'relative';
        viewerElement.appendChild(downloadButton);
        console.log(`${LOG_PREFIX} Download button injected.`);
    }

    function getModelName() {
        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() {
        if (!apiInstance) {
            console.error(`${LOG_PREFIX} API instance not available.`);
            alert('ncikkis Downloader: Could not access Sketchfab API instance.');
            return;
        }

        console.log(`${LOG_PREFIX} Download initiated. Attempting to locate actual model data...`);
        downloadButton.textContent = 'Processing...';
        downloadButton.disabled = true;

        const modelName = getModelName();
        let texturesDownloaded = false;
        let geometryDownloaded = false;

        // Callback to run after both attempts (textures & geometry) are complete
        const finalizeCheck = () => {
            if (!geometryDownloaded) {
                 console.warn(`${LOG_PREFIX} Failed to locate or extract actual geometry data.`);
                 alert(`ncikkis Downloader: Failed to locate downloadable geometry data for this model. Texture download may have been attempted.`);
            } else {
                 console.log(`${LOG_PREFIX} Geometry download initiated.`);
                 if (!texturesDownloaded) {
                    console.warn(`${LOG_PREFIX} Texture download attempt complete (may have failed or found no textures).`);
                    alert(`ncikkis Downloader: Geometry download initiated. Texture download may have failed or no textures were found.`);
                 } else {
                    console.log(`${LOG_PREFIX} Texture download attempt complete.`);
                    alert(`ncikkis Downloader: Geometry and texture download initiated. Check your downloads.`);
                 }
            }
             // Reset button regardless of success
             downloadButton.textContent = 'Download Model';
             downloadButton.disabled = false;
        };

        let textureAttemptComplete = false;
        let geometryAttemptComplete = false;

        // --- Attempt Texture Download ---
        try {
            apiInstance.getTextureList((err, textures) => {
                if (err || !textures || textures.length === 0) {
                    console.warn(`${LOG_PREFIX} Could not retrieve texture list via API or no textures found.`);
                } else {
                    console.log(`${LOG_PREFIX} Found ${textures.length} textures. Downloading...`);
                    texturesDownloaded = true; // Mark attempt even if individual downloads fail
                    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} from ${texture.url}`);
                            GM_download({ url: texture.url, name: filename, onerror: (err) => console.error(`${LOG_PREFIX} Error downloading texture ${filename}:`, err), onload: () => console.log(`${LOG_PREFIX} Texture ${filename} download initiated.`) });
                        } else {
                             console.warn(`${LOG_PREFIX} Invalid texture object or URL for texture index ${index}:`, texture);
                        }
                    });
                }
                textureAttemptComplete = true;
                if (geometryAttemptComplete) finalizeCheck();
            });
        } catch (e) {
            console.error(`${LOG_PREFIX} Error during API interaction (getTextureList):`, e);
            textureAttemptComplete = true;
            if (geometryAttemptComplete) finalizeCheck();
        }

        // --- Attempt Geometry Download (Focus on glTF/glb) ---
        console.log(`${LOG_PREFIX} Searching for geometry data (glTF/glb preferred)...`);
        try {
            // Speculative ways to find the model URL or data:
            // 1. Check API response (Hypothetical: maybe getNodeMap or another function returns model URL)
            // 2. Inspect `unsafeWindow` for viewer configuration objects
            // 3. Check performance entries for loaded resources matching common patterns

            let modelUrl = null;
            let modelFormat = null; // 'gltf' or 'glb'

            // Example: Hypothetical check within unsafeWindow structure (adjust based on actual inspection)
            if (unsafeWindow.viewerApp && unsafeWindow.viewerApp.config && 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'; }
                 // Add more checks based on observed structure
            }

            // Example: Check performance entries (less reliable, might miss dynamically loaded URLs)
             if (!modelUrl) {
                try {
                    const resources = performance.getEntriesByType('resource');
                    const sketchfabResources = resources.filter(r => r.name.includes('sketchfab.com') || r.name.includes('skfb.ly'));
                    // Look for likely candidates based on URL patterns (e.g., containing 'models', 'gltf', 'glb')
                    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} Could not access or parse performance entries:`, perfError);
                }
             }

            // Example: Hypothetical API call (if Sketchfab API offered such a thing)
            // if (!modelUrl && typeof apiInstance.getModelUrl === 'function') { ... }

            // --- Conditional Download ---
            if (modelUrl && modelFormat) {
                console.log(`${LOG_PREFIX} Found potential model URL (${modelFormat}): ${modelUrl}`);
                const filename = `${modelName}.${modelFormat}`;
                try {
                    GM_download({
                        url: modelUrl,
                        name: filename,
                        onerror: (err) => {
                             console.error(`${LOG_PREFIX} Error downloading geometry file ${filename}:`, err);
                             geometryDownloaded = false; // Mark as failed on error
                             geometryAttemptComplete = true;
                             if (textureAttemptComplete) finalizeCheck();
                        },
                        onload: () => {
                             console.log(`${LOG_PREFIX} Geometry file ${filename} download initiated.`);
                             geometryDownloaded = true; // Mark success
                             geometryAttemptComplete = true;
                             if (textureAttemptComplete) finalizeCheck();
                        },
                        ontimeout: () => {
                            console.error(`${LOG_PREFIX} Timeout downloading geometry file ${filename}.`);
                             geometryDownloaded = false;
                             geometryAttemptComplete = true;
                             if (textureAttemptComplete) finalizeCheck();
                        }
                    });
                } catch (downloadError) {
                     console.error(`${LOG_PREFIX} Error setting up geometry download:`, downloadError);
                     geometryDownloaded = false;
                     geometryAttemptComplete = true;
                     if (textureAttemptComplete) finalizeCheck();
                }
            } else {
                // If no URL or data found after all checks
                console.warn(`${LOG_PREFIX} Could not locate a downloadable geometry URL (glTF/glb) via known methods.`);
                geometryDownloaded = false;
                geometryAttemptComplete = true;
                if (textureAttemptComplete) finalizeCheck();
            }

        } catch (e) {
            console.error(`${LOG_PREFIX} Error during geometry search/download attempt:`, e);
            geometryDownloaded = false;
            geometryAttemptComplete = true;
            if (textureAttemptComplete) finalizeCheck();
        }
    }

    // --- Initialization Logic ---
    const initCheckInterval = setInterval(() => {
        apiInstance = findApiInstance();
        const viewerElement = document.querySelector('.viewer');
        if (apiInstance && viewerElement) {
            clearInterval(initCheckInterval);
            console.log(`${LOG_PREFIX} Sketchfab API instance potentially found.`, apiInstance);
            createDownloadButton();
        } else if (document.readyState === "complete") {
             // Allow interval to continue checking
        }
    }, 2000);

    setTimeout(() => {
        if (!apiInstance || !document.getElementById('ncikkis-sf-download-button')) {
             clearInterval(initCheckInterval);
             console.warn(`${LOG_PREFIX} Initialization timed out. API instance or viewer element not found.`);
        }
    }, 30000);

})();