config:stash
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
config:stash [2023/07/23 14:45] – created Wulf Rajek | config:stash [2024/07/28 13:08] (current) – Wulf Rajek | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== Stash ====== | ====== Stash ====== | ||
+ | |||
+ | Sort order/ | ||
+ | The default sort order of items of the tabs is stored with the default filter. Overwriting an existing filter does not overwrite the sort order though. The filter needs to be deleted, then saved and set to default again. | ||
+ | The filters are stored in the database, not the configuration file. | ||
+ | |||
+ | Plugins: | ||
+ | https:// | ||
User script for local performer face detection: | User script for local performer face detection: | ||
https:// | https:// | ||
+ | |||
+ | https:// | ||
+ | |||
+ | |||
+ | https:// | ||
+ | |||
+ | https:// | ||
+ | |||
+ | |||
+ | ===== Custom CSS ===== | ||
+ | |||
+ | Custom CSS needs to be enabled in Settings-> | ||
+ | |||
+ | Note: Firefox before v120/121 needs: about: | ||
+ | |||
+ | <code css> | ||
+ | /* [Performer tab] Show more item per row */ | ||
+ | @media only screen and (min-device-width: | ||
+ | : | ||
+ | width: 15%; | ||
+ | } | ||
+ | : | ||
+ | width: 100%; | ||
+ | } | ||
+ | .performer-card h5 { | ||
+ | text-align: center !important; | ||
+ | display: block; | ||
+ | } | ||
+ | .performer-card-image { | ||
+ | height: 18rem; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /* [Studios tab] Show more item per row */ | ||
+ | : | ||
+ | width: 15% | ||
+ | } | ||
+ | : | ||
+ | width: 100% | ||
+ | } | ||
+ | .studio-card h5 { | ||
+ | text-align: center !important; | ||
+ | display: block; | ||
+ | } | ||
+ | |||
+ | |||
+ | /* [Global changes] Hide the Donate button */ | ||
+ | .btn-primary.btn.donate.minimal { | ||
+ | display: none; | ||
+ | } | ||
+ | button.minimal.donate.btn.btn-primary { | ||
+ | display: none; | ||
+ | } | ||
+ | |||
+ | |||
+ | /* [Global changes] Modify card when checkbox is selected | ||
+ | Note: Firefox before v120/121 needs: about: | ||
+ | */ | ||
+ | .grid-card.card: | ||
+ | | ||
+ | } | ||
+ | |||
+ | /* Fix face AI plugin being popup being behind the content */ | ||
+ | .face-tabs.svelte-p95y28 { | ||
+ | // height: 60%; | ||
+ | z-index: 1; | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | |||
+ | ===== api ===== | ||
+ | |||
+ | https:// | ||
+ | |||
+ | https:// | ||
+ | |||
+ | |||
+ | ===== Video Compare Userscript ===== | ||
+ | |||
+ | This needs violentmonkey or similar browser plugin. | ||
+ | |||
+ | Base script from: https:// | ||
+ | |||
+ | <code javascript video_compare_userscript.user.js> | ||
+ | // ==UserScript== | ||
+ | // @name Stash Video Compare Userscript | ||
+ | // @description Userscript for Stash | ||
+ | // @match | ||
+ | // @match | ||
+ | // @grant | ||
+ | // @require | ||
+ | // @require | ||
+ | // @version | ||
+ | // @author | ||
+ | // ==/ | ||
+ | |||
+ | (function() { | ||
+ | 'use strict'; | ||
+ | console.log(' | ||
+ | waitForKeyElements(" | ||
+ | |||
+ | function addbutton() { | ||
+ | const navBar = document.querySelector(" | ||
+ | const buttonClass = navBar.firstChild.attributes.class.value; | ||
+ | const linkClass = navBar.firstChild.firstChild.attributes.class.value; | ||
+ | const newButton = document.createElement(" | ||
+ | newButton.setAttribute(" | ||
+ | newButton.onclick = compare | ||
+ | const innerLink = document.createElement(" | ||
+ | innerLink.setAttribute(" | ||
+ | const buttonLabel = document.createElement(" | ||
+ | buttonLabel.innerText = "Video Compare"; | ||
+ | innerLink.appendChild(buttonLabel); | ||
+ | newButton.appendChild(innerLink); | ||
+ | navBar.appendChild(newButton); | ||
+ | } | ||
+ | |||
+ | function compare() { | ||
+ | if (window.location.pathname == "/ | ||
+ | var numberOfChecked = document.querySelectorAll(' | ||
+ | if (numberOfChecked == 2) { | ||
+ | var lr = [] | ||
+ | const list = document.querySelectorAll(' | ||
+ | for (let item of list) { | ||
+ | const row = item.closest(' | ||
+ | const nextAnchor = row.querySelector(' | ||
+ | const url = nextAnchor.getAttribute(' | ||
+ | lr.push(window.location.origin.concat(url)) | ||
+ | } | ||
+ | var site = " | ||
+ | var url = site.concat("? | ||
+ | GM_openInTab(url, | ||
+ | } | ||
+ | } else { | ||
+ | var numberOfChecked = document.querySelectorAll(' | ||
+ | if (numberOfChecked == 2) { | ||
+ | const r = / | ||
+ | var lr = [] | ||
+ | const list = document.querySelectorAll(' | ||
+ | for (let item of list) { | ||
+ | var urlstuff = item.nextElementSibling.innerHTML | ||
+ | var m = r.exec(urlstuff) | ||
+ | lr.push(m[0]) | ||
+ | } | ||
+ | var site = " | ||
+ | var url = site.concat("? | ||
+ | GM_openInTab(url, | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | })(); | ||
+ | </ | ||
+ | |||
+ | ===== Scene merge script after video conversions ===== | ||
+ | |||
+ | After converting video files already included in stash and doing a rescan, new scenes will be created for the new files. This leaves duplicate scenes in the database. | ||
+ | |||
+ | This script creates a backup of the stash database first, then finds all scenes in stash that have the same filenames but different extensions, merges the scenes into the older one and optionally delete the non-mp4 file from the merged scene. | ||
+ | |||
+ | This script uses stashapp-tools module to interface with stash. Installation of this: | ||
+ | < | ||
+ | pip install stashapp-tools | ||
+ | </ | ||
+ | |||
+ | This script performs a dry run unless the -n option is provided to avoid data loss. It can take a filename or part of a path as option or the -a option to scan all stash scenes. The -d parameter deletes the non-mp4 files after merging scenes. | ||
+ | |||
+ | < | ||
+ | usage: stash-merge.py [-h] [-a] [-d] [-n] [filename] | ||
+ | |||
+ | Merge stash scenes with the same filenames in different formats, optionally delete non-mp4 files. | ||
+ | |||
+ | positional arguments: | ||
+ | filename | ||
+ | |||
+ | options: | ||
+ | -h, --help | ||
+ | -a, --all Process all scenes with duplicate filenames | ||
+ | -d, --delete | ||
+ | -n, --non-dry-run | ||
+ | </ | ||
+ | |||
+ | Adjust the scheme/ | ||
+ | <code python stash-merge.py> | ||
+ | import stashapi.log as log | ||
+ | from stashapi.stashapp import StashInterface | ||
+ | import argparse | ||
+ | import os, sys | ||
+ | |||
+ | def find_scenes_by_path_regex(self, | ||
+ | |||
+ | query = """ | ||
+ | query FindScenes($filter: | ||
+ | findScenesByPathRegex(filter: | ||
+ | count | ||
+ | scenes { | ||
+ | ...Scene | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | """ | ||
+ | if fragment: | ||
+ | query = re.sub(r' | ||
+ | |||
+ | filter[" | ||
+ | variables = { | ||
+ | " | ||
+ | " | ||
+ | } | ||
+ | |||
+ | result = self.call_GQL(query, | ||
+ | if get_count: | ||
+ | return result[' | ||
+ | else: | ||
+ | return result[' | ||
+ | |||
+ | def merge_scenes_with_same_filename(stash_scheme, | ||
+ | stash = StashInterface({ | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | }) | ||
+ | |||
+ | if dryrun: | ||
+ | print(" | ||
+ | else: | ||
+ | print(" | ||
+ | |||
+ | print(" | ||
+ | query = """ | ||
+ | mutation backupDatabase{ | ||
+ | backupDatabase(input: | ||
+ | } | ||
+ | """ | ||
+ | variables = {} | ||
+ | stash.call_GQL(query, | ||
+ | |||
+ | if file is None: # Fetch all scenes | ||
+ | scenes = stash.find_scenes() | ||
+ | else: | ||
+ | # note rsplit fails for files without extension in directories starting with dot, but path in stash db is absolute | ||
+ | file = file.rsplit(' | ||
+ | scenes = find_scenes_by_path_regex(stash, | ||
+ | # | ||
+ | |||
+ | # print(len(scenes)) | ||
+ | |||
+ | # Group scenes by path without extension | ||
+ | scenes_by_path_ex_ext = {} | ||
+ | for scene in scenes: | ||
+ | # | ||
+ | for files in scene[' | ||
+ | # note rsplit fails for files without extension in directories starting with dot, but path in stash db is absolute | ||
+ | path = files[' | ||
+ | if path not in scenes_by_path_ex_ext: | ||
+ | scenes_by_path_ex_ext[path] = [] | ||
+ | scenes_by_path_ex_ext[path].append(scene) | ||
+ | # | ||
+ | |||
+ | for path, scene_group in scenes_by_path_ex_ext.items(): | ||
+ | if len(scene_group) > 1: | ||
+ | if dryrun: | ||
+ | print(f" | ||
+ | # Identify the target scene to merge into | ||
+ | # | ||
+ | #always merge into the lowest scene id | ||
+ | target_scene = sorted(scene_group, | ||
+ | if dryrun: | ||
+ | print(f" | ||
+ | target_scene_id = target_scene[' | ||
+ | mp4_file_id = None | ||
+ | print(f" | ||
+ | |||
+ | for scene in scene_group: | ||
+ | if scene[' | ||
+ | # Merge scene into target | ||
+ | print(f" | ||
+ | if not dryrun: | ||
+ | stash.merge_scenes(scene[' | ||
+ | |||
+ | # Identify mp4 file | ||
+ | for files in scene[' | ||
+ | if files[' | ||
+ | mp4_file_id = files[' | ||
+ | print(f" | ||
+ | |||
+ | # Set the primary file to be the mp4 file | ||
+ | if mp4_file_id: | ||
+ | print(f" | ||
+ | if not dryrun: | ||
+ | # first set all files NOT future primary to not be primary | ||
+ | stash.sql_commit(" | ||
+ | # now set new primary file id | ||
+ | stash.sql_commit(" | ||
+ | |||
+ | # Optionally delete non-mp4 files if more than 1 file is assigned to the scene | ||
+ | if delete_non_mp4 and len(scene_group) > 1 and mp4_file_id: | ||
+ | for scene in scene_group: | ||
+ | for files in scene[' | ||
+ | if files[' | ||
+ | print(f" | ||
+ | if not dryrun: | ||
+ | stash.destroy_files(files[' | ||
+ | print("" | ||
+ | |||
+ | def main(): | ||
+ | parser = argparse.ArgumentParser(description=" | ||
+ | |||
+ | # Optional argument for filename | ||
+ | parser.add_argument(' | ||
+ | |||
+ | # Non-positional flags with short forms | ||
+ | parser.add_argument(' | ||
+ | parser.add_argument(' | ||
+ | parser.add_argument(' | ||
+ | |||
+ | if len(sys.argv)==1: | ||
+ | print(" | ||
+ | parser.print_help(sys.stderr) | ||
+ | sys.exit(1) | ||
+ | |||
+ | args = parser.parse_args() | ||
+ | |||
+ | if args.filename: | ||
+ | FILE=args.filename | ||
+ | |||
+ | if args.filename and args.delete: | ||
+ | print(f" | ||
+ | merge_scenes_with_same_filename(STASH_SCHEME, | ||
+ | elif args.filename: | ||
+ | print(f" | ||
+ | merge_scenes_with_same_filename(STASH_SCHEME, | ||
+ | elif args.all and args.delete: | ||
+ | print(" | ||
+ | merge_scenes_with_same_filename(STASH_SCHEME, | ||
+ | elif args.all: | ||
+ | print(" | ||
+ | merge_scenes_with_same_filename(STASH_SCHEME, | ||
+ | else: | ||
+ | print(" | ||
+ | |||
+ | if __name__ == " | ||
+ | # Replace with your Stash app URL | ||
+ | STASH_SCHEME = " | ||
+ | STASH_HOST = " | ||
+ | STASH_PORT = " | ||
+ | |||
+ | main() | ||
+ | # | ||
+ | </ |
config/stash.1690119941.txt.gz · Last modified: by Wulf Rajek