[StashRandomButton] Add support for scenes, images, performers, studios, tags, groups and galleries (#566)

This commit is contained in:
MrDocSabio
2025-05-29 20:19:17 +02:00
committed by GitHub
parent e0cea86689
commit 703c96a097
3 changed files with 129 additions and 164 deletions

View File

@@ -1,13 +1,23 @@
# Stash Random Button Plugin
https://discourse.stashapp.cc/t/randombutton/1809
[Plugin thread on Discourse](https://discourse.stashapp.cc/t/randombutton/1809)
Adds a "Random" button to the image & scenes page to quickly navigate to a random scene.
Adds a "Random" button to the Stash UI, letting you instantly jump to a random scene, image, performer, studio, group, tag, or gallery—including random "internal" navigation (e.g. a random scene inside a studio).
## Features
- Adds a "Random" button to the Stash UI.
- Selects a random scene via GraphQL query.
- Adds a "Random" button to the Stash UI navigation bar.
- Supports random navigation for:
- **Scenes** (global and within performer, studio, tag, group)
- **Images** (global and within a gallery)
- **Performers** (global)
- **Studios** (global)
- **Groups** (global)
- **Tags** (global)
- **Galleries** (global)
- Lightweight, no external dependencies.
- Uses Stash's GraphQL API.
- Simple, robust, and easy to maintain.
## Installation
@@ -30,7 +40,8 @@ Adds a "Random" button to the image & scenes page to quickly navigate to a rando
- The button should appear on those pages.
## Usage
Click the "Random" button in the navigation bar to jump to a random image or scene depending on the tab.
Click the "Random" button in the navigation bar to jump to a random entity (scene, image, performer, studio, group, tag, or gallery) depending on your current page.
- On internal entity pages (e.g., performer, studio, group, tag, gallery), the button picks a random scene or image from inside that entity.
## Requirements
- Stash version v0.27.2 or higher.
@@ -38,3 +49,6 @@ Click the "Random" button in the navigation bar to jump to a random image or sce
## Development
- Written in JavaScript using the Stash Plugin API.
- Edit `random-button.js` to customize and reload plugins in Stash.
## Changelog
- 2.0.0: Major upgrade! Now supports random navigation for performers, studios, groups, tags, galleries, and images (global and internal).

View File

@@ -1,182 +1,133 @@
(function () {
(function() {
'use strict';
function addRandomButton() {
const existingButton = document.querySelector('.random-btn');
if (existingButton) {
const styles = window.getComputedStyle(existingButton);
return true;
function getIdFromPath(regex) {
let m = window.location.pathname.match(regex);
return m ? m[1] : null;
}
function getPlural(entity) {
return (entity === "Gallery") ? "Galleries"
: (entity === "Tag") ? "Tags"
: (entity === "Image") ? "Images"
: (entity === "Scene") ? "Scenes"
: (entity === "Performer") ? "Performers"
: (entity === "Studio") ? "Studios"
: (entity === "Group") ? "Groups"
: entity + "s";
}
async function randomGlobal(entity, idField, redirectPrefix, internalFilter) {
const realEntityPlural = getPlural(entity);
let filter = { per_page: 1 };
let variables = { filter };
let filterArg = "";
let filterVar = "";
if (internalFilter) {
filterArg = `, $internal_filter: ${entity}FilterType`;
filterVar = `, ${entity.toLowerCase()}_filter: $internal_filter`;
variables.internal_filter = internalFilter;
}
const countQuery = `
query Find${realEntityPlural}($filter: FindFilterType${filterArg}) {
find${realEntityPlural}(filter: $filter${filterVar}) { count }
}
`;
let countResp = await fetch('/graphql', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: countQuery, variables })
});
let countData = await countResp.json();
if (countData.errors) { alert("Error: " + JSON.stringify(countData.errors)); return; }
const totalCount = countData.data[`find${realEntityPlural}`].count;
if (!totalCount) { alert("No results found."); return; }
const randomIndex = Math.floor(Math.random() * totalCount);
let itemVars = { filter: { per_page: 1, page: randomIndex + 1 } };
if (internalFilter) itemVars.internal_filter = internalFilter;
const itemQuery = `
query Find${realEntityPlural}($filter: FindFilterType${filterArg}) {
find${realEntityPlural}(filter: $filter${filterVar}) { ${idField} { id } }
}
`;
let itemResp = await fetch('/graphql', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: itemQuery, variables: itemVars })
});
let itemData = await itemResp.json();
if (itemData.errors) { alert("Error: " + JSON.stringify(itemData.errors)); return; }
const arr = itemData.data[`find${realEntityPlural}`][idField];
if (!arr || arr.length === 0) { alert("No results found."); return; }
window.location.href = `${redirectPrefix}${arr[0].id}`;
}
async function randomButtonHandler() {
const pathname = window.location.pathname.replace(/\/$/, '');
if (pathname === '/scenes') return randomGlobal('Scene', 'scenes', '/scenes/');
if (pathname === '/performers') return randomGlobal('Performer', 'performers', '/performers/');
if (pathname === '/groups') return randomGlobal('Group', 'groups', '/groups/');
if (pathname === '/studios') return randomGlobal('Studio', 'studios', '/studios/');
if (pathname === '/tags') return randomGlobal('Tag', 'tags', '/tags/');
if (pathname === '/galleries') return randomGlobal('Gallery', 'galleries', '/galleries/');
if (pathname === '/images') return randomGlobal('Image', 'images', '/images/');
// --- INTERN ---
let studioId = getIdFromPath(/^\/studios\/(\d+)\/scenes/);
if (studioId) return randomGlobal('Scene', 'scenes', '/scenes/', { studios: { value: [studioId], modifier: "INCLUDES_ALL" } });
let groupId = getIdFromPath(/^\/groups\/(\d+)\/scenes/);
if (groupId) return randomGlobal('Scene', 'scenes', '/scenes/', { groups: { value: [groupId], modifier: "INCLUDES_ALL" } });
let performerId = getIdFromPath(/^\/performers\/(\d+)\/scenes/);
if (performerId) return randomGlobal('Scene', 'scenes', '/scenes/', { performers: { value: [performerId], modifier: "INCLUDES_ALL" } });
let tagId = getIdFromPath(/^\/tags\/(\d+)\/scenes/);
if (tagId) return randomGlobal('Scene', 'scenes', '/scenes/', { tags: { value: [tagId], modifier: "INCLUDES_ALL" } });
let galleryId = getIdFromPath(/^\/galleries\/(\d+)/);
if (galleryId) return randomGlobal('Image', 'images', '/images/', { galleries: { value: [galleryId], modifier: "INCLUDES_ALL" } });
alert('Not supported');
}
function addRandomButton() {
if (document.querySelector('.random-btn')) return;
const navContainer = document.querySelector('.navbar-buttons.flex-row.ml-auto.order-xl-2.navbar-nav');
if (!navContainer) {
return false;
}
if (!navContainer) return;
const randomButtonContainer = document.createElement('div');
randomButtonContainer.className = 'mr-2';
randomButtonContainer.innerHTML = `
<a href="javascript:void(0)">
<button type="button" class="btn btn-primary random-btn" style="display: inline-block !important; visibility: visible !important;">Random</button>
</a>
`;
randomButtonContainer.querySelector('button').addEventListener('click', loadRandomContent);
<a href="javascript:void(0)">
<button type="button" class="btn btn-primary random-btn" style="display: inline-block !important; visibility: visible !important;">Random</button>
</a>
`;
randomButtonContainer.querySelector('button').addEventListener('click', randomButtonHandler);
if (window.location.pathname.match(/^\/(scenes|images)(?:$|\?)/)) {
let refButton = document.querySelector('a[href="/scenes/new"]');
if (window.location.pathname.includes('/images')) {
refButton = document.querySelector('a[href="/stats"]');
}
if (!refButton) {
refButton = navContainer.querySelector('a[href="https://opencollective.com/stashapp"]');
}
if (refButton) {
refButton.parentElement.insertAdjacentElement('afterend', randomButtonContainer);
} else {
navContainer.appendChild(randomButtonContainer);
}
return true;
}
if (window.location.pathname.match(/\/(scenes|images)\/\d+/)) {
const refButton = navContainer.querySelector('a[href="https://opencollective.com/stashapp"]');
if (refButton) {
refButton.insertAdjacentElement('afterend', randomButtonContainer);
} else {
const firstLink = navContainer.querySelector('a');
if (firstLink) {
firstLink.parentElement.insertAdjacentElement('afterend', randomButtonContainer);
} else {
navContainer.appendChild(randomButtonContainer);
}
}
return true;
}
return false;
navContainer.appendChild(randomButtonContainer);
}
function getParentHierarchy(element) {
const hierarchy = [];
let current = element;
while (current && current !== document.body) {
hierarchy.push(current.tagName + (current.className ? '.' + current.className.split(' ').join('.') : ''));
current = current.parentElement;
}
return hierarchy.join(' > ');
}
async function loadRandomContent() {
try {
const isScenes = window.location.pathname.includes('/scenes');
const isImages = window.location.pathname.includes('/images');
const type = isScenes ? 'scenes' : isImages ? 'images' : 'scenes';
const countQuery = `
query Find${type.charAt(0).toUpperCase() + type.slice(1)}($filter: FindFilterType) {
find${type.charAt(0).toUpperCase() + type.slice(1)}(filter: $filter) {
count
}
}
`;
const countVariables = { filter: { per_page: 1 } };
const countResponse = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: countQuery, variables: countVariables })
});
const countResult = await countResponse.json();
if (countResult.errors) {
return;
}
const totalCount = countResult.data[`find${type.charAt(0).toUpperCase() + type.slice(1)}`].count;
if (totalCount === 0) {
return;
}
const randomIndex = Math.floor(Math.random() * totalCount);
const itemQuery = `
query Find${type.charAt(0).toUpperCase() + type.slice(1)}($filter: FindFilterType) {
find${type.charAt(0).toUpperCase() + type.slice(1)}(filter: $filter) {
${type} {
id
}
}
}
`;
const itemVariables = {
filter: { per_page: 1, page: Math.floor(randomIndex / 1) + 1 }
};
const itemResponse = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: itemQuery, variables: itemVariables })
});
const itemResult = await itemResponse.json();
if (itemResult.errors) {
return;
}
const items = itemResult.data[`find${type.charAt(0).toUpperCase() + type.slice(1)}`][type];
if (items.length === 0) {
return;
}
const itemId = items[0].id;
window.location.href = `/${type}/${itemId}`;
} catch (error) {
console.error(error);
}
}
window.addEventListener('load', () => {
addRandomButton();
});
window.addEventListener('load', () => addRandomButton());
document.addEventListener('click', (event) => {
const target = event.target.closest('a');
if (target && target.href) {
setTimeout(() => {
addRandomButton();
}, 1500);
}
if (target && target.href) setTimeout(() => addRandomButton(), 1500);
});
window.addEventListener('popstate', () => {
setTimeout(() => {
addRandomButton();
}, 1500);
});
window.addEventListener('hashchange', () => {
setTimeout(() => {
addRandomButton();
}, 1500);
});
window.addEventListener('popstate', () => setTimeout(() => addRandomButton(), 1500));
window.addEventListener('hashchange', () => setTimeout(() => addRandomButton(), 1500));
const navContainer = document.querySelector('.navbar-buttons.flex-row.ml-auto.order-xl-2.navbar-nav');
if (navContainer) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => {
});
if (!document.querySelector('.random-btn')) {
addRandomButton();
}
const observer = new MutationObserver(() => {
if (!document.querySelector('.random-btn')) addRandomButton();
});
observer.observe(navContainer, { childList: true, subtree: true });
} else {
}
let intervalAttempts = 0;
setInterval(() => {
intervalAttempts++;
addRandomButton();
}, intervalAttempts < 60 ? 500 : 2000);
})();

View File

@@ -1,10 +1,10 @@
name: RandomButton
description: Adds a button to quickly switch to a random scene or image on both overview and detail pages
version: 1.1.0
description: Adds a button to quickly jump to a random scene, image, performer, studio, group, tag, or gallery, both on overview and internal entity pages.
version: 2.0.0
url: https://example.com
ui:
requires: []
javascript:
- random_button.js
css:
- random_button.css
- random_button.css