Fixed bug with adding tags and performers

This commit is contained in:
David Maisonave
2024-07-22 04:10:28 -04:00
parent 92bdb17c60
commit e5a488fbd6
5 changed files with 786 additions and 143 deletions

View File

@@ -4,6 +4,9 @@ import logging
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.
@@ -18,10 +21,81 @@ script_dir = Path(__file__).resolve().parent
# Configure logging for your script
log_file_path = script_dir / 'renamefile.log'
logging.basicConfig(filename=log_file_path, level=logging.INFO, format='%(asctime)s - %(message)s')
FORMAT = "[%(asctime)s - LN:%(lineno)s - %(funcName)20s()] %(message)s"
logging.basicConfig(filename=log_file_path, level=logging.INFO, format=FORMAT)
logger = logging.getLogger('renamefile')
DEFAULT_ENDPOINT = "http://localhost:9999/graphql" # Default GraphQL endpoint
DEFAULT_FIELD_KEY_LIST = "title, performers, tags" # Default Field Key List with the desired order
DEFAULT_SEPERATOR = "-"
# ------------------------------------------
# ------------------------------------------
# Code to fetch variables from Plugin UI
json_input = json.loads(sys.stdin.read())
FRAGMENT_SERVER = json_input["server_connection"]
stash = StashInterface(FRAGMENT_SERVER)
pluginConfiguration = stash.get_configuration()["plugins"]
settings = {
"dryRun": False,
"fileRenameViaMove": False,
"performerAppend": False,
"performerIncludeInFileName": False,
"tagAppend": False,
"tagIncludeInFileName": False,
"zFieldKeyList": DEFAULT_FIELD_KEY_LIST,
"zgraphqlEndpoint": DEFAULT_ENDPOINT,
"zmaximumTagKeys": 12,
"zpathToExclude": "",
"zseparators": DEFAULT_SEPERATOR,
"ztagWhitelist": "",
"zzdebugTracing": False,
}
if "renamefile" in pluginConfiguration:
settings.update(pluginConfiguration["renamefile"])
logger.info("settings: %s " % (settings,))
# ------------------------------------------
debugTracing = settings["zzdebugTracing"]
logger.info(f"\n\nStarting (debugTracing={debugTracing})************************************************\n")
# Extract dry_run setting from settings
dry_run = settings["dryRun"]
dry_run_prefix = ''
if debugTracing: logger.info("Debug Tracing................")
if dry_run:
logger.info("Dry run mode is enabled.")
dry_run_prefix = "Would've "
if debugTracing: logger.info("Debug Tracing................")
max_tag_keys = settings["zmaximumTagKeys"] if settings["zmaximumTagKeys"] != 0 else 12 # Need this incase use explicitly sets value to zero in UI
if debugTracing: logger.info("Debug Tracing................")
# ToDo: Add split logic here to slpit possible string array into an array
exclude_paths = settings["zpathToExclude"]
exclude_paths = exclude_paths.split()
if debugTracing: logger.info(f"Debug Tracing (exclude_paths={exclude_paths})................")
# Extract tag whitelist from settings
tag_whitelist = settings["ztagWhitelist"]
if debugTracing: logger.info("Debug Tracing................")
if not tag_whitelist:
tag_whitelist = ""
endpoint = settings["zgraphqlEndpoint"] # GraphQL endpoint
if debugTracing: logger.info("Debug Tracing................")
if not endpoint or endpoint == "":
endpoint = DEFAULT_ENDPOINT
# Extract rename_files and move_files settings from renamefile_settings.py
rename_files = config["rename_files"]
move_files = settings["fileRenameViaMove"]
if debugTracing: logger.info("Debug Tracing................")
fieldKeyList = settings["zFieldKeyList"] # Default Field Key List with the desired order
if not fieldKeyList or fieldKeyList == "":
fieldKeyList = DEFAULT_FIELD_KEY_LIST
fieldKeyList = fieldKeyList.replace(" ", "")
fieldKeyList = fieldKeyList.replace(";", ",")
fieldKeyList = fieldKeyList.split(",")
if debugTracing: logger.info(f"Debug Tracing (fieldKeyList={fieldKeyList})................")
separator = settings["zseparators"]
# ------------------------------------------
# ------------------------------------------
double_separator = separator + separator
endpoint = config.get("graphql_endpoint") # GraphQL endpoint; Update via renamefile_settings.py
# GraphQL query to fetch all scenes
query_all_scenes = """
@@ -32,13 +106,18 @@ query_all_scenes = """
}
}
"""
if debugTracing: logger.info("Debug Tracing................")
# Function to make GraphQL requests
def graphql_request(query, variables=None):
if debugTracing: logger.info("Debug Tracing................%s", query)
data = {'query': query}
if variables:
data['variables'] = variables
if debugTracing: logger.info("Debug Tracing................")
if debugTracing: logger.info("Debug Tracing................")
response = requests.post(endpoint, json=data)
if debugTracing: logger.info("Debug Tracing................")
return response.json()
# Function to replace illegal characters in filenames
@@ -48,7 +127,7 @@ def replace_illegal_characters(filename):
filename = filename.replace(char, '-')
return filename
def should_exclude_path(scene_details, exclude_paths):
def should_exclude_path(scene_details):
scene_path = scene_details['files'][0]['path'] # Assuming the first file path is representative
for exclude_path in exclude_paths:
if scene_path.startswith(exclude_path):
@@ -56,98 +135,115 @@ def should_exclude_path(scene_details, exclude_paths):
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, separator, key_order, exclude_keys, max_tag_keys=None, tag_whitelist=None, dry_run=None, exclude_paths=None):
def form_filename(original_file_stem, scene_details, wrapper_styles):
if debugTracing: logger.info("Debug Tracing................")
filename_parts = []
tag_keys_added = 0
default_title = ''
if_notitle_use_org_filename = config["if_notitle_use_org_filename"]
exclude_tag_if_in_name = config["exclude_tag_if_in_name"]
include_tag_if_in_name = settings["tagIncludeInFileName"]
include_performer_if_in_name = settings["performerIncludeInFileName"]
if if_notitle_use_org_filename:
default_title = original_file_stem
# ...................
# Title needs to be set here incase user changes the fieldKeyList where tags or performers come before title.
title = scene_details.get('title', default_title)
if not title:
if if_notitle_use_org_filename:
title = default_title
# ...................
if debugTracing: logger.info("Debug Tracing................")
# Function to add tag to filename
def add_tag(tag_name):
nonlocal tag_keys_added
if max_tag_keys is not None and tag_keys_added >= int(max_tag_keys):
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
# Check if the tag name is in the whitelist
if tag_whitelist == "" or tag_whitelist == None or (tag_whitelist and tag_name in tag_whitelist):
if wrapper_styles.get('tag'):
filename_parts.append(f"{wrapper_styles['tag'][0]}{tag_name}{wrapper_styles['tag'][1]}")
if debugTracing: logger.info("Debug Tracing................")
else:
filename_parts.append(tag_name)
if debugTracing: logger.info("Debug Tracing................")
tag_keys_added += 1
if debugTracing: logger.info("Debug Tracing................")
else:
log.info(f"Skipping tag not in whitelist: {tag_name}")
logger.info(f"Skipping tag not in whitelist: {tag_name}")
if debugTracing: logger.info(f"Debug Tracing (tag_keys_added={tag_keys_added})................")
for key in key_order:
if not exclude_keys or key not in exclude_keys:
if key == 'studio':
studio_name = scene_details.get('studio', {}).get('name', '')
if studio_name:
if wrapper_styles.get('studio'):
filename_parts.append(f"{wrapper_styles['studio'][0]}{studio_name}{wrapper_styles['studio'][1]}")
else:
filename_parts.append(studio_name)
elif key == 'title':
title = scene_details.get('title', default_title)
if not title:
if if_notitle_use_org_filename:
title = default_title
if title:
if wrapper_styles.get('title'):
filename_parts.append(f"{wrapper_styles['title'][0]}{title}{wrapper_styles['title'][1]}")
else:
filename_parts.append(title)
elif key == 'performers':
for key in fieldKeyList:
if key == 'studio':
studio_name = scene_details.get('studio', {}).get('name', '')
if studio_name:
if wrapper_styles.get('studio'):
filename_parts.append(f"{wrapper_styles['studio'][0]}{studio_name}{wrapper_styles['studio'][1]}")
else:
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
if wrapper_styles.get('title'):
filename_parts.append(f"{wrapper_styles['title'][0]}{title}{wrapper_styles['title'][1]}")
else:
filename_parts.append(title)
elif key == 'performers':
if settings["performerAppend"]:
performers = '-'.join([performer.get('name', '') for performer in scene_details.get('performers', [])])
if performers:
if wrapper_styles.get('performers'):
filename_parts.append(f"{wrapper_styles['performers'][0]}{performers}{wrapper_styles['performers'][1]}")
else:
filename_parts.append(performers)
elif key == 'date':
scene_date = scene_details.get('date', '')
if scene_date:
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 == 'height':
height = str(scene_details.get('files', [{}])[0].get('height', '')) # Convert height to string
if height:
height += 'p'
if wrapper_styles.get('height'):
filename_parts.append(f"{wrapper_styles['height'][0]}{height}{wrapper_styles['height'][1]}")
else:
filename_parts.append(height)
elif key == 'video_codec':
video_codec = scene_details.get('files', [{}])[0].get('video_codec', '').upper() # Convert to uppercase
if 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:
filename_parts.append(video_codec)
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:
if wrapper_styles.get('frame_rate'):
filename_parts.append(f"{wrapper_styles['frame_rate'][0]}{frame_rate}{wrapper_styles['frame_rate'][1]}")
else:
filename_parts.append(frame_rate)
elif key == 'tags':
if not include_performer_if_in_name or performers.lower() not in title.lower():
if wrapper_styles.get('performers'):
filename_parts.append(f"{wrapper_styles['performers'][0]}{performers}{wrapper_styles['performers'][1]}")
else:
filename_parts.append(performers)
elif key == 'date':
scene_date = scene_details.get('date', '')
if scene_date:
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 == 'height':
height = str(scene_details.get('files', [{}])[0].get('height', '')) # Convert height to string
if height:
height += 'p'
if wrapper_styles.get('height'):
filename_parts.append(f"{wrapper_styles['height'][0]}{height}{wrapper_styles['height'][1]}")
else:
filename_parts.append(height)
elif key == 'video_codec':
video_codec = scene_details.get('files', [{}])[0].get('video_codec', '').upper() # Convert to uppercase
if 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:
filename_parts.append(video_codec)
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:
if wrapper_styles.get('frame_rate'):
filename_parts.append(f"{wrapper_styles['frame_rate'][0]}{frame_rate}{wrapper_styles['frame_rate'][1]}")
else:
filename_parts.append(frame_rate)
elif key == 'tags':
if settings["tagAppend"]:
tags = [tag.get('name', '') for tag in scene_details.get('tags', [])]
if debugTracing: logger.info("Debug Tracing................")
for tag_name in tags:
if not exclude_tag_if_in_name or tag_name.lower() not in original_file_stem.lower():
if debugTracing: logger.info(f"Debug Tracing (include_tag_if_in_name={include_tag_if_in_name})................")
if include_tag_if_in_name or tag_name.lower() not in title.lower():
add_tag(tag_name)
if debugTracing: logger.info("Debug Tracing................")
new_filename = separator.join(filename_parts).replace('--', '-')
new_filename = separator.join(filename_parts).replace(double_separator, separator)
# Check if the scene's path matches any of the excluded paths
if exclude_paths and should_exclude_path(scene_details, exclude_paths):
log.info(f"Scene belongs to an excluded path. Skipping filename modification.")
if exclude_paths and should_exclude_path(scene_details):
logger.info(f"Scene belongs to an excluded path. Skipping filename modification.")
return Path(scene_details['files'][0]['path']).name # Return the original filename
@@ -181,7 +277,7 @@ def find_scene_by_id(scene_id):
scene_result = graphql_request(query_find_scene, variables={"scene_id": scene_id})
return scene_result.get('data', {}).get('findScene')
def move_or_rename_files(scene_details, new_filename, original_parent_directory, move_files, rename_files, dry_run, dry_run_prefix, exclude_paths=None):
def move_or_rename_files(scene_details, new_filename, original_parent_directory):
studio_directory = None
for file_info in scene_details['files']:
path = file_info['path']
@@ -189,7 +285,6 @@ def move_or_rename_files(scene_details, new_filename, original_parent_directory,
# Check if the file's path matches any of the excluded paths
if exclude_paths and any(original_path.match(exclude_path) for exclude_path in exclude_paths):
log.info(f"File {path} belongs to an excluded path. Skipping modification.")
logger.info(f"File {path} belongs to an excluded path. Skipping modification.")
continue
@@ -204,23 +299,19 @@ def move_or_rename_files(scene_details, new_filename, original_parent_directory,
if rename_files: # Check if rename_files is True
if not dry_run:
shutil.move(original_path, new_path)
log.info(f"{dry_run_prefix}Moved and renamed file: {path} -> {new_path}")
logger.info(f"{dry_run_prefix}Moved and renamed file: {path} -> {new_path}")
else:
if not dry_run:
shutil.move(original_path, new_path)
log.info(f"{dry_run_prefix}Moved file: {path} -> {new_path}")
logger.info(f"{dry_run_prefix}Moved file: {path} -> {new_path}")
else:
if rename_files: # Check if rename_files is True
if not dry_run:
original_path.rename(new_path)
log.info(f"{dry_run_prefix}Renamed file: {path} -> {new_path}")
logger.info(f"{dry_run_prefix}Renamed file: {path} -> {new_path}")
else:
if not dry_run:
shutil.move(original_path, new_path)
log.info(f"{dry_run_prefix}Moved file: {path} -> {new_path}")
logger.info(f"{dry_run_prefix}Moved file: {path} -> {new_path}")
except FileNotFoundError:
log.error(f"File not found: {path}. Skipping...")
@@ -244,24 +335,26 @@ 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, separator, key_order, stash_directory, rename_files, move_files, dry_run, max_tag_keys=None, tag_whitelist=None, exclude_paths=None):
def rename_scene(scene_id, wrapper_styles, stash_directory):
scene_details = find_scene_by_id(scene_id)
if debugTracing: logger.info(f"Debug Tracing (scene_details={scene_details})................")
if not scene_details:
log.error(f"Scene with ID {scene_id} not found.")
logger.error(f"Scene with ID {scene_id} not found.")
return
exclude_keys = config["exclude_keys"]
if debugTracing: logger.info(f"Debug Tracing................")
original_file_path = scene_details['files'][0]['path']
original_parent_directory = Path(original_file_path).parent
if debugTracing: logger.info(f"Debug Tracing (original_file_path={original_file_path})................")
# Check if the scene's path matches any of the excluded paths
if exclude_paths and any(Path(original_file_path).match(exclude_path) for exclude_path in exclude_paths):
log.info(f"Scene with ID {scene_id} belongs to an excluded path. Skipping modifications.")
logger.info(f"Scene with ID {scene_id} belongs to an excluded path. Skipping modifications.")
return
if debugTracing: logger.info(f"Debug Tracing................")
original_path_info = {'original_file_path': original_file_path,
'original_parent_directory': original_parent_directory}
@@ -269,25 +362,23 @@ def rename_scene(scene_id, wrapper_styles, separator, key_order, 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, separator, key_order, exclude_keys, max_tag_keys=max_tag_keys, tag_whitelist=tag_whitelist, dry_run=dry_run, exclude_paths=exclude_paths)
dry_run_prefix = ''
if dry_run:
log.info("Dry run mode is enabled.")
logger.info("Dry run mode is enabled.")
dry_run_prefix = "Would've "
new_filename = form_filename(original_file_stem, scene_details, wrapper_styles)
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:
logger.info(f"Nothing to do, because new file name matches original file name: (newFilenameWithExt={newFilenameWithExt})")
return
if debugTracing: logger.info(f"Debug Tracing................")
if rename_files:
new_path = original_parent_directory / (new_filename + Path(original_file_path).suffix)
new_path = original_parent_directory / (newFilenameWithExt)
new_path_info = {'new_file_path': new_path}
log.info(f"{dry_run_prefix}New filename: {new_path}")
logger.info(f"{dry_run_prefix}New filename: {new_path}")
if move_files and original_parent_directory.name != scene_details['studio']['name']:
new_path = original_parent_directory / scene_details['studio']['name'] / (new_filename + Path(original_file_path).suffix)
new_path_info = {'new_file_path': new_path}
move_or_rename_files(scene_details, new_filename, original_parent_directory, move_files, rename_files, dry_run, dry_run_prefix)
log.info(f"{dry_run_prefix}Moved to directory: '{new_path}'")
move_or_rename_files(scene_details, new_filename, original_parent_directory)
logger.info(f"{dry_run_prefix}Moved to directory: '{new_path}'")
# If rename_files is True, attempt renaming even if move_files is False
@@ -297,7 +388,6 @@ def rename_scene(scene_id, wrapper_styles, separator, key_order, stash_directory
try:
if not dry_run:
os.rename(original_file_path, new_file_path)
log.info(f"{dry_run_prefix}Renamed file: {original_file_path} -> {new_file_path}")
logger.info(f"{dry_run_prefix}Renamed file: {original_file_path} -> {new_file_path}")
except Exception as e:
log.error(f"Failed to rename file: {original_file_path}. Error: {e}")
@@ -318,14 +408,18 @@ def rename_scene(scene_id, wrapper_styles, separator, key_order, stash_directory
return new_filename, original_path_info, new_path_info
if debugTracing: logger.info("Debug Tracing................")
# Execute the GraphQL query to fetch all scenes
scene_result = graphql_request(query_all_scenes)
if debugTracing: logger.info("Debug Tracing................")
all_scenes = scene_result.get('data', {}).get('allScenes', [])
if debugTracing: logger.info("Debug Tracing................")
if not all_scenes:
if debugTracing: logger.info("Debug Tracing................")
log.error("No scenes found.")
logger.error("No scenes found.")
exit()
if debugTracing: logger.info("Debug Tracing................")
# Find the scene with the latest updated_at timestamp
latest_scene = max(all_scenes, key=lambda scene: scene['updated_at'])
@@ -333,33 +427,30 @@ latest_scene = max(all_scenes, key=lambda scene: scene['updated_at'])
# Extract the ID of the latest scene
latest_scene_id = latest_scene.get('id')
# Extract dry_run setting from settings
dry_run = config["dry_run"]
# Extract wrapper styles, separator, and key order from settings
# Extract wrapper styles
wrapper_styles = config["wrapper_styles"]
separator = config["separator"]
key_order = config["key_order"]
# Read stash directory from renamefile_settings.py
stash_directory = config.get('stash_directory', '')
if debugTracing: logger.info("Debug Tracing................")
# Extract rename_files and move_files settings from renamefile_settings.py
rename_files_setting = config["rename_files"]
move_files_setting = config["move_files"]
# Extract tag whitelist from settings
tag_whitelist = config.get("tag_whitelist")
if not tag_whitelist:
tag_whitelist = ""
if debugTracing: logger.info("Debug Tracing................")
# Rename the latest scene and trigger metadata scan
new_filename = rename_scene(latest_scene_id, wrapper_styles, separator, key_order, stash_directory, rename_files_setting, move_files_setting, dry_run, max_tag_keys=config["max_tag_keys"], tag_whitelist=tag_whitelist, exclude_paths=config.get("exclude_paths"))
new_filename = rename_scene(latest_scene_id, wrapper_styles, stash_directory)
if debugTracing: logger.info("Debug Tracing................")
# Log dry run state and indicate if no changes were made
if dry_run:
log.info("Dry run: Script executed in dry run mode. No changes were made.")
logger.info("Dry run: Script executed in dry run mode. No changes were made.")
elif not new_filename:
log.info("No changes were made.")
logger.info("No changes were made.")
else:
logger.info("Change success!")
logger.info("*********************************\nEXITING*********************************\n*********************************\n")
# ToDo List
# Add logic to max_filename_length code so it checks base file length and checks folder length, instead of lumping them altogether.
# Add logic to update Sqlite DB on file name change, instead of perform_metadata_scan.
# Get variables from the Plugins Settings UI instead of from renamefile_settings.py