User Tools

Site Tools


config:stash

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
config:stash [2024/07/28 12:49] – [Custom CSS] Wulf Rajekconfig:stash [2024/07/28 13:08] (current) Wulf Rajek
Line 163: Line 163:
   }   }
 })(); })();
 +</code>
 +
 +===== 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:
 +<code>
 +pip install stashapp-tools
 +</code>
 +
 +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.
 +
 +<code>
 +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           The filename to process
 +
 +options:
 +  -h, --help         show this help message and exit
 +  -a, --all          Process all scenes with duplicate filenames
 +  -d, --delete       Delete non mp4 files after merging scenes
 +  -n, --non-dry-run  Actually perform actions, runs in dry-run mode by default
 +</code>
 +
 +Adjust the scheme/IP/port at the end of the script to your requirements if not http, localhost and port 9999.
 +<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, f:dict={}, filter:dict={"per_page": -1}, q:str="", fragment=None, get_count=False, callback=None):
 +
 +    query = """
 +        query FindScenes($filter: FindFilterType) {
 +            findScenesByPathRegex(filter: $filter) {
 +                count
 +                scenes {
 +                    ...Scene
 +                }
 +            }
 +        }
 +        """
 +    if fragment:
 +        query = re.sub(r'\.\.\.Scene', fragment, query)
 +
 +    filter["q"] = q
 +    variables = {
 +        "filter": filter,
 +        "scene_filter": f
 +    }
 +
 +    result = self.call_GQL(query, variables, callback=callback)
 +    if get_count:
 +        return result['findScenesByPathRegex']['count'], result['findScenes']['scenes']
 +    else:
 +        return result['findScenesByPathRegex']['scenes']
 +
 +def merge_scenes_with_same_filename(stash_scheme, stash_host, stash_port, delete_non_mp4=False, dryrun=True, file=None):
 +    stash = StashInterface({
 +        "scheme": stash_scheme,
 +        "host": stash_host,
 +        "port": stash_port,
 +        "logger": log
 +    })
 +
 +    if dryrun:
 +        print("Dry-run:\n")
 +    else:
 +        print("Live-run:\n")
 +
 +        print("Backing up database ...")
 +        query = """
 +        mutation backupDatabase{
 +            backupDatabase(input: {download: false})
 +        }
 +        """
 +        variables = {}
 +        stash.call_GQL(query, variables)
 +
 +    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('.', maxsplit=1)[0]
 +        scenes = find_scenes_by_path_regex(stash,q=file)
 +        #print(scenes)
 +
 +    # print(len(scenes))
 +
 +    # Group scenes by path without extension
 +    scenes_by_path_ex_ext = {}
 +    for scene in scenes:
 +        #print(scene)
 +        for files in scene['files']:
 +            # note rsplit fails for files without extension in directories starting with dot, but path in stash db is absolute
 +            path = files['path'].rsplit('.', maxsplit=1)[0]
 +            if path not in scenes_by_path_ex_ext:
 +                scenes_by_path_ex_ext[path] = []
 +            scenes_by_path_ex_ext[path].append(scene)
 +    #print(len(scenes_by_path_ex_ext))
 +
 +    for path, scene_group in scenes_by_path_ex_ext.items():
 +        if len(scene_group) > 1:
 +            if dryrun:
 +                print(f"Scene group:\n{scene_group}")
 +            # Identify the target scene to merge into
 +            #target_scene = max(scene_group, key=lambda s: (bool(s['performers']), bool(s['stash_ids']), s['organized']==True))
 +            #always merge into the lowest scene id
 +            target_scene = sorted(scene_group, key=lambda s: s['id'])[0]
 +            if dryrun:
 +                print(f"Target scene:\n{target_scene}")
 +            target_scene_id = target_scene['id']
 +            mp4_file_id = None
 +            print(f"Target scene id: {target_scene_id} Path: {path}")
 +
 +            for scene in scene_group:
 +                if scene['id'] != target_scene_id:
 +                    # Merge scene into target
 +                    print(f"Merging scene {scene['id']} into {target_scene_id} Source title: {scene['title']}")
 +                    if not dryrun:
 +                        stash.merge_scenes(scene['id'], target_scene_id)
 +
 +                # Identify mp4 file
 +                for files in scene['files']:
 +                    if files['path'].endswith('.mp4'):
 +                        mp4_file_id = files['id']
 +                        print(f"scene with mp4 path: {scene['id']} file id: {mp4_file_id} path: {files['path']}")
 +
 +            # Set the primary file to be the mp4 file
 +            if mp4_file_id:
 +                print(f"setting primary id to {mp4_file_id}")
 +                if not dryrun:
 +                    # first set all files NOT future primary to not be primary
 +                    stash.sql_commit("UPDATE `scenes_files` SET `primary`=? WHERE ((`scenes_files`.`scene_id` = ?) AND (`scenes_files`.`file_id` != ?))", ("0", target_scene_id, mp4_file_id) )
 +                    # now set new primary file id
 +                    stash.sql_commit("UPDATE `scenes_files` SET `primary`=? WHERE ((`scenes_files`.`scene_id` = ?) AND (`scenes_files`.`file_id` = ?))", ("1", target_scene_id, mp4_file_id) )
 +
 +            # 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['files']:
 +                        if files['id'] != mp4_file_id:
 +                            print(f"Deleting file id {files['id']} while keeping {mp4_file_id} Deleted file path: {files['path']}")
 +                            if not dryrun:
 +                                stash.destroy_files(files['id'])
 +            print("")
 +
 +def main():
 +    parser = argparse.ArgumentParser(description="Merge stash scenes with the same filenames in different formats, optionally delete non-mp4 files.")
 +
 +    # Optional argument for filename
 +    parser.add_argument('filename', nargs='?', default=None, help='The filename to process')
 +
 +    # Non-positional flags with short forms
 +    parser.add_argument('-a', '--all', action='store_true', help='Process all scenes with duplicate filenames')
 +    parser.add_argument('-d', '--delete', action='store_true', help='Delete non mp4 files after merging scenes')
 +    parser.add_argument('-n', '--non-dry-run', action='store_true', help='Actually perform actions, runs in dry-run mode by default')
 +
 +    if len(sys.argv)==1:
 +        print("Error: No filename or arguments provided.\n")
 +        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 and delete non mp4 files: {args.filename}\n")
 +        merge_scenes_with_same_filename(STASH_SCHEME, STASH_HOST, STASH_PORT, delete_non_mp4=True, dryrun=not args.non_dry_run, file=args.filename)
 +    elif args.filename:
 +        print(f"Merge scenes and keeping files containing: {args.filename}\n")
 +        merge_scenes_with_same_filename(STASH_SCHEME, STASH_HOST, STASH_PORT, delete_non_mp4=False, dryrun=not args.non_dry_run, file=args.filename)
 +    elif args.all and args.delete:
 +        print("Merge all scenes with duplicate files, deleting duplicate files.\n")
 +        merge_scenes_with_same_filename(STASH_SCHEME, STASH_HOST, STASH_PORT, delete_non_mp4=True, dryrun=not args.non_dry_run, file=None)
 +    elif args.all:
 +        print("Merge all scenes with duplicate files, not deleting duplicate files.\n")
 +        merge_scenes_with_same_filename(STASH_SCHEME, STASH_HOST, STASH_PORT, delete_non_mp4=False, dryrun=not args.non_dry_run, file=None)
 +    else:
 +        print("No filename or all argument provided, not doing anything\n")
 +
 +if __name__ == "__main__":
 +    # Replace with your Stash app URL
 +    STASH_SCHEME = "http"
 +    STASH_HOST = "localhost"
 +    STASH_PORT = "9999"
 +
 +    main()
 +    #merge_scenes_with_same_filename(STASH_SCHEME, STASH_HOST, STASH_PORT, DELETE, DRYRUN, FILE)
 </code> </code>
config/stash.txt · Last modified: 2024/07/28 13:08 by Wulf Rajek