stashapp / stash

An organizer for your porn, written in Go. Documentation: https://docs.stashapp.cc
https://stashapp.cc/
GNU Affero General Public License v3.0
8.73k stars 774 forks source link

[Feature] Integration with StashDB Userscript to check scenes in collection #5058

Open SCP15 opened 1 month ago

SCP15 commented 1 month ago

Is your feature request related to a problem? Please describe.

I like being able to click on performers and studio links to StashDB and seeing the gallery of scenes. However, I find it cumbersome to figure out which scenes are in my collection and which scenes are missing.

Describe the solution you'd like

I created a prototype by doing the following:

  1. Extracting the list of scenes from the SQLite DB and saving them in a JSON that I host in a web server
  2. Creating a ViolentMonkey UserScript to dynamically modify the HTML of StashDB to show ✅/❌ icons for scenes that are in the collections vs missing. After a few iterations I got it to work reliably with filtering and pagination.

The issue obviously is that this requires re-exporting the JSON data periodically. What I'd like is to have an API endpoint in Stash to get all available scenes as a JSON payload so that it can be integrated here.

Let me know if this makes sense, or whether there is a simpler way to get this functionality.

Additional Info

The export script

import sqlite3
import json

# Connect to the SQLite database
conn = sqlite3.connect('./stash-go.sqlite')
cursor = conn.cursor()

# Execute a SELECT query to fetch all file_ids
cursor.execute("SELECT stash_id, scene_id FROM scene_stash_ids WHERE endpoint == 'https://stashdb.org/graphql'")

# Fetch all results
mapping =  {row[0]: row[1] for row in cursor.fetchall()}

# Close the database connection
conn.close()

# Write the file_ids to a JSON file
with open('stash_ids.json', 'w') as f:
    json.dump(mapping, f)

print(f"Extracted stash_ids.json")

And the UserScript

// ==UserScript==
// @name         StashDB Scene Checker
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  Check scenes against a dictionary of IDs and show clickable indicators for matches
// @match        https://stashdb.org/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const config = {
        verbose: false,
        jsonUrl: 'https://example.com/stash_ids.json',
        stashBaseUrl: 'https://stash.example.com/scenes/'
    };

    function log(...args) {
        if (config.verbose) {
            console.log(...args);
        }
    }

    let sceneIdMap = {};
    let observer;

    function fetchJSON(url, callback) {
        log('Fetching JSON from:', url);
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function(response) {
                log('JSON fetched, status:', response.status);
                try {
                    const data = JSON.parse(response.responseText);
                    log('Parsed JSON data:', data);
                    callback(data);
                } catch (error) {
                    console.error('Error parsing JSON:', error);
                }
            },
            onerror: function(error) {
                console.error('Error fetching JSON:', error);
            }
        });
    }

    function addIndicator(sceneElement, stashId, sceneId) {
        if (sceneElement.querySelector('.scene-indicator')) return;
        log(`Adding indicator to:`, sceneElement);
        const indicator = document.createElement('div');
        indicator.className = 'scene-indicator';
        indicator.style.position = 'absolute';
        indicator.style.top = '5px';
        indicator.style.right = '5px';
        indicator.style.fontSize = '24px';
        indicator.style.zIndex = '1000';
        indicator.style.cursor = 'pointer';
        sceneElement.style.position = 'relative';

        if (sceneId) {
            indicator.innerHTML = '✅';
            indicator.style.color = 'green';
            indicator.title = 'Click to open in Stash';
            indicator.onclick = function(e) {
                e.preventDefault();
                e.stopPropagation();
                window.open(`${config.stashBaseUrl}${sceneId}`, '_blank');
            };
        } else {
            indicator.innerHTML = '❌';
            indicator.style.color = 'red';
        }

        sceneElement.appendChild(indicator);
    }

    function processScenes() {
        log('Processing scenes');
        const sceneCards = document.querySelectorAll('.SceneCard.card');
        log('Found scene cards:', sceneCards.length);
        sceneCards.forEach(card => {
            const link = card.querySelector('a[href^="/scenes/"]');
            if (link) {
                const href = link.getAttribute('href');
                const stashId = href.split('/').pop();
                log('Checking stash ID:', stashId);
                const sceneId = sceneIdMap[stashId];
                log(`Stash ID ${stashId} ${sceneId ? 'matches' : 'does not match'}`);
                addIndicator(card, stashId, sceneId);
            }
        });
    }

    function observeSceneChanges() {
        if (observer) {
            observer.disconnect();
        }

        observer = new MutationObserver((mutations) => {
            for (let mutation of mutations) {
                if (mutation.type === 'childList') {
                    const addedNodes = mutation.addedNodes;
                    for (let node of addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE && (node.classList.contains('SceneCard') || node.querySelector('.SceneCard'))) {
                            log('New scene card detected, processing...');
                            processScenes();
                            return;
                        }
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        log('Scene change observer set up');
    }

    function handlePaginationClick() {
        log('Pagination click detected');
        setTimeout(processScenes, 500);  // Add a small delay to allow content to update
    }

    function setupPaginationListener() {
        const paginationContainer = document.querySelector('.pagination');
        if (paginationContainer) {
            paginationContainer.addEventListener('click', handlePaginationClick);
            log('Pagination listener set up');
        } else {
            log('Pagination container not found');
        }
    }

    function initializeScript(idMap) {
        sceneIdMap = idMap;
        processScenes();
        observeSceneChanges();
        setupPaginationListener();
    }

    // Fetch JSON and initialize script
    fetchJSON(config.jsonUrl, initializeScript);

    // Handle navigation events
    window.addEventListener('popstate', processScenes);

    // Handle potential React router navigation
    const pushState = history.pushState;
    history.pushState = function() {
        pushState.apply(history, arguments);
        processScenes();
    };

    log('Script finished initial setup');
})();
feederbox826 commented 1 month ago

https://github.com/millatime1010/stashdb-extension https://github.com/7dJx1qP/stashdb-extension

https://github.com/feederbox826/stashlist ^^ my own code but syncing stashapp to browser sql cache

DogmaDragon commented 1 month ago

https://github.com/timo95/stash-checker supports StashDB and many more sites.

SCP15 commented 1 month ago

Thanks for the pointers.

@DogmaDragon I'm really impressed about all the features of statsh-checker. However, it looks like stash-checker does not show the information on the scene view, but only when you click on each scene? This sort of defeats the usability gain I was looking for.

@feederbox826 interesting stuff, the extension seems to be Chrome only and I use Firefox. Stashlist looks promising but I couldn't figure out how to deploy it myself.

DogmaDragon commented 1 month ago

Stash Checker definitely shows the checkmark with information on scene list page. It should show it on most pages as it targets StashID, not specific URL.

Screenshot ![image](https://github.com/stashapp/stash/assets/103123951/f64cda1f-d42a-47e6-b3a6-a62b57b397e8)

Make sure to configure it after you install it.