diff --git a/plugins/RenameFile/.gitignore b/plugins/RenameFile/.gitignore index 7511ba8..dd93ef7 100644 --- a/plugins/RenameFile/.gitignore +++ b/plugins/RenameFile/.gitignore @@ -9,6 +9,8 @@ desktop.ini *.ipch *.lib *.log +*.log.1 +*.log.2 *.manifest *.obj *.pch diff --git a/plugins/RenameFile/README.md b/plugins/RenameFile/README.md index e373aea..b5db4fe 100644 --- a/plugins/RenameFile/README.md +++ b/plugins/RenameFile/README.md @@ -1,4 +1,4 @@ -# RenameFile: Ver 0.2.5 +# RenameFile: Ver 0.3.1 RenameFile is a [Stash](https://github.com/stashapp/stash) plugin which performs the following two main task. - **Rename Scene File Name** (On-The-Fly) - **Append tag names** to file name diff --git a/plugins/RenameFile/manifest b/plugins/RenameFile/manifest new file mode 100644 index 0000000..c376197 --- /dev/null +++ b/plugins/RenameFile/manifest @@ -0,0 +1,14 @@ +id: renamefile +name: RenameFile +metadata: + description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab. +version: 0.4.1 +date: "2024-07-26 08:00:00" +requires: [pip install stashapp-tools, pip install pyYAML] +source_repository: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile +files: +- README.md +- renamefile.yml +- renamefile.py +- renamefile_settings.py +- requirements.txt diff --git a/plugins/RenameFile/renamefile.py b/plugins/RenameFile/renamefile.py index 59e04b1..b09e38e 100644 --- a/plugins/RenameFile/renamefile.py +++ b/plugins/RenameFile/renamefile.py @@ -1,18 +1,19 @@ +# This is a Stash plugin which allows users to rename the video (scene) file name by editing the [Title] field located in the scene [Edit] tab. +# By David Maisonave (aka Axter) 2024 +# Get the latest developers version from following link: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile +# Based on source code from https://github.com/Serechops/Serechops-Stash/tree/main/plugins/Renamer + import requests import os import logging +import stashapi.log as log # Importing stashapi.log as log for critical events ONLY import shutil from pathlib import Path import hashlib import json import sys from stashapi.stashapp import StashInterface - -# This is a Stash plugin which allows users to rename the video (scene) file name by editing the [Title] field located in the scene [Edit] tab. - -# Importing stashapi.log as log for critical events -import stashapi.log as log - +from logging.handlers import RotatingFileHandler # Import settings from renamefile_settings.py from renamefile_settings import config @@ -21,14 +22,24 @@ script_dir = Path(__file__).resolve().parent # Configure logging for your script log_file_path = script_dir / 'renamefile.log' +rfh = RotatingFileHandler( + filename=log_file_path, + mode='a', + maxBytes=2*1024*1024, + backupCount=2, + encoding=None, + delay=0 +) FORMAT = "[%(asctime)s - LN:%(lineno)s] %(message)s" -logging.basicConfig(filename=log_file_path, level=logging.INFO, format=FORMAT) +logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt="%y%m%d %H:%M:%S", handlers=[rfh]) logger = logging.getLogger('renamefile') DEFAULT_ENDPOINT = "http://localhost:9999/graphql" # Default GraphQL endpoint DEFAULT_FIELD_KEY_LIST = "title,performers,studio,tags" # Default Field Key List with the desired order DEFAULT_SEPERATOR = "-" PLUGIN_ARGS = False - +errOccurred = False +inputToUpdateScenePost = False +exitMsg = "Change success!!" # ------------------------------------------ @@ -65,7 +76,11 @@ try: PLUGIN_ARGS = json_input['args']["mode"] except: pass -logger.info(f"\nStarting (debugTracing={debugTracing}) (dry_run={dry_run}) (PLUGIN_ARGS={PLUGIN_ARGS})************************************************") +try: + if json_input['args']['hookContext']['input']: inputToUpdateScenePost = True # This avoid calling rename logic twice +except: + pass +logger.info(f"\nStarting (debugTracing={debugTracing}) (dry_run={dry_run}) (PLUGIN_ARGS={PLUGIN_ARGS}) (inputToUpdateScenePost={inputToUpdateScenePost})************************************************") if debugTracing: logger.info("settings: %s " % (settings,)) if dry_run: logger.info("Dry run mode is enabled.") @@ -102,7 +117,14 @@ separator = settings["zseparators"] # ------------------------------------------ double_separator = separator + separator +# Extract styles from config +wrapper_styles = config["wrapper_styles"] +postfix_styles = config["postfix_styles"] +try: + if debugTracing: logger.info(f"Debug Tracing (json_input['args']={json_input['args']})................") +except: + pass # GraphQL query to fetch all scenes query_all_scenes = """ @@ -142,7 +164,7 @@ def should_exclude_path(scene_details): return False # Function to form the new filename based on scene details and user settings -def form_filename(original_file_stem, scene_details, wrapper_styles): +def form_filename(original_file_stem, scene_details): if debugTracing: logger.info("Debug Tracing................") filename_parts = [] tag_keys_added = 0 @@ -165,7 +187,6 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): def add_tag(tag_name): nonlocal tag_keys_added nonlocal filename_parts - nonlocal wrapper_styles if debugTracing: logger.info(f"Debug Tracing (tag_name={tag_name})................") if max_tag_keys == -1 or (max_tag_keys is not None and tag_keys_added >= int(max_tag_keys)): return # Skip adding more tags if the maximum limit is reached @@ -194,6 +215,7 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): studio_name = scene_details.get('studio', {}).get('name', '') if debugTracing: logger.info(f"Debug Tracing (studio_name={studio_name})................") if studio_name: + studio_name += postfix_styles.get('studio') if debugTracing: logger.info("Debug Tracing................") if include_keyField_if_in_name or studio_name.lower() not in title.lower(): if wrapper_styles.get('studio'): @@ -202,6 +224,7 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): filename_parts.append(studio_name) elif key == 'title': if title: # This value has already been fetch in start of function because it needs to be defined before tags and performers + title += postfix_styles.get('title') if wrapper_styles.get('title'): filename_parts.append(f"{wrapper_styles['title'][0]}{title}{wrapper_styles['title'][1]}") else: @@ -210,6 +233,7 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): if settings["performerAppend"]: performers = '-'.join([performer.get('name', '') for performer in scene_details.get('performers', [])]) if performers: + performers += postfix_styles.get('performers') if debugTracing: logger.info(f"Debug Tracing (include_keyField_if_in_name={include_keyField_if_in_name})................") if include_keyField_if_in_name or performers.lower() not in title.lower(): if debugTracing: logger.info(f"Debug Tracing (performers={performers})................") @@ -221,15 +245,33 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): scene_date = scene_details.get('date', '') if debugTracing: logger.info("Debug Tracing................") if scene_date: + scene_date += postfix_styles.get('date') if debugTracing: logger.info("Debug Tracing................") if wrapper_styles.get('date'): filename_parts.append(f"{wrapper_styles['date'][0]}{scene_date}{wrapper_styles['date'][1]}") else: filename_parts.append(scene_date) + elif key == 'resolution': + width = str(scene_details.get('files', [{}])[0].get('width', '')) # Convert width to string + height = str(scene_details.get('files', [{}])[0].get('height', '')) # Convert height to string + if width and height: + resolution = width + postfix_styles.get('width_height_seperator') + height + postfix_styles.get('resolution') + if wrapper_styles.get('resolution'): + filename_parts.append(f"{wrapper_styles['resolution'][0]}{resolution}{wrapper_styles['width'][1]}") + else: + filename_parts.append(resolution) + elif key == 'width': + width = str(scene_details.get('files', [{}])[0].get('width', '')) # Convert width to string + if width: + width += postfix_styles.get('width') + if wrapper_styles.get('width'): + filename_parts.append(f"{wrapper_styles['width'][0]}{width}{wrapper_styles['width'][1]}") + else: + filename_parts.append(width) elif key == 'height': height = str(scene_details.get('files', [{}])[0].get('height', '')) # Convert height to string if height: - height += 'p' + height += postfix_styles.get('height') if wrapper_styles.get('height'): filename_parts.append(f"{wrapper_styles['height'][0]}{height}{wrapper_styles['height'][1]}") else: @@ -237,6 +279,7 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): elif key == 'video_codec': video_codec = scene_details.get('files', [{}])[0].get('video_codec', '').upper() # Convert to uppercase if video_codec: + video_codec += postfix_styles.get('video_codec') if wrapper_styles.get('video_codec'): filename_parts.append(f"{wrapper_styles['video_codec'][0]}{video_codec}{wrapper_styles['video_codec'][1]}") else: @@ -244,6 +287,7 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): elif key == 'frame_rate': frame_rate = str(scene_details.get('files', [{}])[0].get('frame_rate', '')) + 'FPS' # Convert to string and append ' FPS' if frame_rate: + frame_rate += postfix_styles.get('frame_rate') if wrapper_styles.get('frame_rate'): filename_parts.append(f"{wrapper_styles['frame_rate'][0]}{frame_rate}{wrapper_styles['frame_rate'][1]}") else: @@ -254,6 +298,7 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): for gallery_name in galleries: if debugTracing: logger.info(f"Debug Tracing (include_keyField_if_in_name={include_keyField_if_in_name}) (gallery_name={gallery_name})................") if include_keyField_if_in_name or gallery_name.lower() not in title.lower(): + gallery_name += postfix_styles.get('galleries') if wrapper_styles.get('galleries'): filename_parts.append(f"{wrapper_styles['galleries'][0]}{gallery_name}{wrapper_styles['galleries'][1]}") if debugTracing: logger.info("Debug Tracing................") @@ -269,11 +314,11 @@ def form_filename(original_file_stem, scene_details, wrapper_styles): for tag_name in tags: if debugTracing: logger.info(f"Debug Tracing (include_keyField_if_in_name={include_keyField_if_in_name}) (tag_name={tag_name})................") if include_keyField_if_in_name or tag_name.lower() not in title.lower(): - add_tag(tag_name) + add_tag(tag_name + postfix_styles.get('tag')) if debugTracing: logger.info(f"Debug Tracing (tag_name={tag_name})................") if debugTracing: logger.info("Debug Tracing................") - if debugTracing: logger.info("Debug Tracing................") + if debugTracing: logger.info(f"Debug Tracing (filename_parts={filename_parts})................") new_filename = separator.join(filename_parts).replace(double_separator, separator) if debugTracing: logger.info(f"Debug Tracing (new_filename={new_filename})................") @@ -293,6 +338,7 @@ def find_scene_by_id(scene_id): date files { path + width height video_codec frame_rate @@ -316,6 +362,7 @@ def find_scene_by_id(scene_id): return scene_result.get('data', {}).get('findScene') def move_or_rename_files(scene_details, new_filename, original_parent_directory): + global exitMsg studio_directory = None for file_info in scene_details['files']: path = file_info['path'] @@ -354,10 +401,12 @@ def move_or_rename_files(scene_details, new_filename, original_parent_directory) except FileNotFoundError: log.error(f"File not found: {path}. Skipping...") logger.error(f"File not found: {path}. Skipping...") + exitMsg = "File not found" continue except OSError as e: log.error(f"Failed to move or rename file: {path}. Error: {e}") logger.error(f"Failed to move or rename file: {path}. Error: {e}") + exitMsg = "Failed to move or rename file" continue return new_path # Return the new_path variable after the loop @@ -374,7 +423,8 @@ def perform_metadata_scan(metadata_scan_path): logger.info(f"Mutation string: {mutation_metadata_scan}") graphql_request(mutation_metadata_scan) -def rename_scene(scene_id, wrapper_styles, stash_directory): +def rename_scene(scene_id, stash_directory): + global exitMsg scene_details = find_scene_by_id(scene_id) if debugTracing: logger.info(f"Debug Tracing (scene_details={scene_details})................") if not scene_details: @@ -401,7 +451,7 @@ def rename_scene(scene_id, wrapper_styles, stash_directory): original_file_stem = Path(original_file_path).stem original_file_name = Path(original_file_path).name - new_filename = form_filename(original_file_stem, scene_details, wrapper_styles) + new_filename = form_filename(original_file_stem, scene_details) newFilenameWithExt = new_filename + Path(original_file_path).suffix if debugTracing: logger.info(f"Debug Tracing (original_file_name={original_file_name})(newFilenameWithExt={newFilenameWithExt})................") if original_file_name == newFilenameWithExt: @@ -429,6 +479,7 @@ def rename_scene(scene_id, wrapper_styles, stash_directory): os.rename(original_file_path, new_file_path) logger.info(f"{dry_run_prefix}Renamed file: {original_file_path} -> {new_file_path}") except Exception as e: + exitMsg = "Failed to rename file" log.error(f"Failed to rename file: {original_file_path}. Error: {e}") logger.error(f"Failed to rename file: {original_file_path}. Error: {e}") @@ -442,11 +493,13 @@ def rename_scene(scene_id, wrapper_styles, stash_directory): truncated_filename = new_filename[:max_base_filename_length] hash_suffix = hashlib.md5(new_filename.encode()).hexdigest() new_filename = truncated_filename + '_' + hash_suffix + Path(original_file_path).suffix - + + if debugTracing: logger.info(f"Debug Tracing (exitMsg={exitMsg})................") return new_filename, original_path_info, new_path_info # Main default function for rename scene def rename_files_task(): + global exitMsg if debugTracing: logger.info("Debug Tracing................") # Execute the GraphQL query to fetch all scenes scene_result = graphql_request(query_all_scenes) @@ -466,10 +519,6 @@ def rename_files_task(): # Extract the ID of the latest scene latest_scene_id = latest_scene.get('id') - - # Extract wrapper styles - wrapper_styles = config["wrapper_styles"] - # Read stash directory from renamefile_settings.py stash_directory = config.get('stash_directory', '') if debugTracing: logger.info("Debug Tracing................") @@ -477,8 +526,8 @@ def rename_files_task(): if debugTracing: logger.info("Debug Tracing................") # Rename the latest scene and trigger metadata scan - new_filename = rename_scene(latest_scene_id, wrapper_styles, stash_directory) - if debugTracing: logger.info("Debug Tracing................") + new_filename = rename_scene(latest_scene_id, stash_directory) + if debugTracing: logger.info(f"Debug Tracing (exitMsg={exitMsg})................") # Log dry run state and indicate if no changes were made if dry_run: @@ -487,7 +536,7 @@ def rename_files_task(): elif not new_filename: logger.info("No changes were made.") else: - logger.info("Change success!") + logger.info(f"{exitMsg}") return def fetch_dup_filename_tags(): # Place holder for new implementation @@ -497,7 +546,7 @@ if PLUGIN_ARGS == "fetch_dup_filename_tags": fetch_dup_filename_tags() elif PLUGIN_ARGS == "rename_files_task": rename_files_task() -else: +elif inputToUpdateScenePost: rename_files_task() if debugTracing: logger.info("\n*********************************\nEXITING ***********************\n*********************************") diff --git a/plugins/RenameFile/renamefile.yml b/plugins/RenameFile/renamefile.yml index 0c24906..14006c3 100644 --- a/plugins/RenameFile/renamefile.yml +++ b/plugins/RenameFile/renamefile.yml @@ -1,6 +1,7 @@ name: RenameFile description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab. -version: 0.3.0 +# By David Maisonave (aka Axter) 2024 +version: 0.4.0 url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile settings: performerAppend: @@ -25,7 +26,7 @@ settings: type: BOOLEAN zfieldKeyList: displayName: Key Fields - description: '(Default=title,performers,studio,tags) Define key fields to use to format the file name. This is a comma seperated list, and the list should be in the desired format order. For example, if the user wants the performers name before the title, set the performers name first. Example:"performers,title,tags". This is an example of user adding height:"title,performers,tags,height" Here''s an example using all of the supported fields: "title,performers,tags,studio,galleries,date,height,video_codec,frame_rate".' + description: '(Default=title,performers,studio,tags) Define key fields to use to format the file name. This is a comma seperated list, and the list should be in the desired format order. For example, if the user wants the performers name before the title, set the performers name first. Example:"performers,title,tags". This is an example of user adding height:"title,performers,tags,height" Here''s an example using all of the supported fields: "title,performers,tags,studio,galleries,resolution,width,height,video_codec,frame_rate,date".' type: STRING zgraphqlEndpoint: displayName: GraphQL Endpoint diff --git a/plugins/RenameFile/renamefile_settings.py b/plugins/RenameFile/renamefile_settings.py index 8895a55..c4eeab9 100644 --- a/plugins/RenameFile/renamefile_settings.py +++ b/plugins/RenameFile/renamefile_settings.py @@ -1,4 +1,4 @@ -# Importing config dictionary +# By David Maisonave (aka Axter) 2024 # RenameFile plugin main configuration options are available on the Stash GUI under Settings->Plugins->Plugins->[RenameFile]. # Most users should only use the GUI options. # The configuration options in this file are for advanced users ONLY!!! @@ -9,16 +9,33 @@ config = { # Define wrapper styles for different parts of the filename. # Use '[]' for square brackets, '{}' for curly brackets, '()' for parentheses, or an empty string for None. - "wrapper_styles": { - "studio": '{}', # Modify these values to change how each part of the filename is wrapped. - "title": '', # Use '[]' for square brackets, '{}' for curly brackets, '()' for parentheses, or an empty string for None. + "wrapper_styles": { # Modify these values to change how each part of the filename is wrapped. + "title": '', "performers": '()', + "tag": '[]', + "studio": '{}', "galleries": '()', - "date": '()', + "resolution": '', # Contains both WITH and HEIGHT + "width": '', "height": '', "video_codec": '', "frame_rate": '', - "tag": '[]' + "date": '()', # This field is not populated in the DB by default. It's usually empty. + }, + # Define the field postfix + "postfix_styles": { + "title": '', + "performers": '', + "tag": '', + "studio": '', + "galleries": '', + "resolution": 'P', # Contains both WITH and HEIGHT + "width": 'W', + "height": 'P', + "width_height_seperator": 'x', # Used in RESOLUTION field as the string seperating WITH and HEIGHT. Example: 720x480 or 1280X720 + "video_codec": '', + "frame_rate": 'FR', + "date": '', }, # Define whether files should be renamed when moved "rename_files": True,