first upload

This commit is contained in:
David Maisonave
2024-07-20 01:46:06 -04:00
parent a3baba96e4
commit 91eb3aa81c
5 changed files with 457 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
# RenameFileName:
### Requirements
`pip install stashapp-tools`
`pip install pyYAML`
### Using RenameFileName
`*Note: Changes are made when a scene edit is saved.`
Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab.
The file is renamed after user clicks save button.
Tags are appended to the file name if the tag does not already exist in the original file name.When you have installed the `RenameFileName` plugin, hop into your plugins directory, RenameFileName folder > open renamefilename_settings.py with your favorite code/text editor and you'll see this:
Features are configurable using the renamefilename_settings.py.
Note: On Windows OS, the file can not be renamed while it's playing. Refresh the URL to allow file release and rename.

View File

@@ -0,0 +1,362 @@
import requests
import os
import logging
import shutil
from pathlib import Path
import hashlib
# Importing stashapi.log as log for critical events
import stashapi.log as log
# Import settings from renamefilename_settings.py
from renamefilename_settings import config
# Get the directory of the script
script_dir = Path(__file__).resolve().parent
# Configure logging for your script
log_file_path = script_dir / 'renamefilename.log'
logging.basicConfig(filename=log_file_path, level=logging.INFO, format='%(asctime)s - %(message)s')
logger = logging.getLogger('renamefilename')
endpoint = config.get("graphql_endpoint") # GraphQL endpoint; Update via renamefilename_settings.py
# GraphQL query to fetch all scenes
query_all_scenes = """
query AllScenes {
allScenes {
id
updated_at
}
}
"""
# Function to make GraphQL requests
def graphql_request(query, variables=None):
data = {'query': query}
if variables:
data['variables'] = variables
response = requests.post(endpoint, json=data)
return response.json()
# Function to replace illegal characters in filenames
def replace_illegal_characters(filename):
illegal_characters = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
for char in illegal_characters:
filename = filename.replace(char, '-')
return filename
def should_exclude_path(scene_details, exclude_paths):
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):
return True
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):
filename_parts = []
tag_keys_added = 0
default_title = ''
if_notitle_use_org_filename = config["if_notitle_use_org_filename"]
add_tag_if_not_in_name = config["add_tag_if_not_in_name"]
if if_notitle_use_org_filename:
default_title = original_file_stem
# 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):
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]}")
else:
filename_parts.append(tag_name)
tag_keys_added += 1
else:
log.info(f"Skipping tag not in whitelist: {tag_name}")
logger.info(f"Skipping tag not in whitelist: {tag_name}")
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':
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':
tags = [tag.get('name', '') for tag in scene_details.get('tags', [])]
for tag_name in tags:
if not add_tag_if_not_in_name or tag_name.lower() not in original_file_stem.lower():
add_tag(tag_name)
new_filename = separator.join(filename_parts).replace('--', '-')
# 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.")
logger.info(f"Scene belongs to an excluded path. Skipping filename modification.")
return Path(scene_details['files'][0]['path']).name # Return the original filename
return replace_illegal_characters(new_filename)
def find_scene_by_id(scene_id):
query_find_scene = """
query FindScene($scene_id: ID!) {
findScene(id: $scene_id) {
id
title
date
files {
path
height
video_codec
frame_rate
}
studio {
name
}
performers {
name
}
tags {
name
}
}
}
"""
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):
studio_directory = None
for file_info in scene_details['files']:
path = file_info['path']
original_path = Path(path)
# 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
new_path = original_parent_directory if not move_files else original_parent_directory / scene_details['studio']['name']
if rename_files:
new_path = new_path / (new_filename + original_path.suffix)
try:
if move_files:
if studio_directory is None:
studio_directory = original_parent_directory / scene_details['studio']['name']
studio_directory.mkdir(parents=True, exist_ok=True)
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...")
logger.error(f"File not found: {path}. Skipping...")
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}")
continue
return new_path # Return the new_path variable after the loop
def perform_metadata_scan(metadata_scan_path):
metadata_scan_path_windows = metadata_scan_path.resolve().as_posix()
mutation_metadata_scan = """
mutation {
metadataScan(input: { paths: "%s" })
}
""" % metadata_scan_path_windows
logger.info(f"Attempting metadata scan mutation with path: {metadata_scan_path_windows}")
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):
scene_details = find_scene_by_id(scene_id)
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"]
original_file_path = scene_details['files'][0]['path']
original_parent_directory = Path(original_file_path).parent
# 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
original_path_info = {'original_file_path': original_file_path,
'original_parent_directory': original_parent_directory}
new_path_info = None
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 "
if rename_files:
new_path = original_parent_directory / (new_filename + Path(original_file_path).suffix)
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}'")
logger.info(f"{dry_run_prefix}Moved to directory: '{new_path}'")
# If rename_files is True, attempt renaming even if move_files is False
if rename_files:
new_file_path = original_parent_directory / (new_filename + Path(original_file_name).suffix)
if original_file_name != new_filename:
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}")
logger.error(f"Failed to rename file: {original_file_path}. Error: {e}")
metadata_scan_path = original_parent_directory
perform_metadata_scan(metadata_scan_path)
# Current DB schema allows file folder max length to be 255, and max base filename to be 255
max_filename_length = int(config["max_filename_length"])
if len(new_filename) > max_filename_length:
extension_length = len(Path(original_file_path).suffix)
max_base_filename_length = max_filename_length - extension_length
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
return new_filename, original_path_info, new_path_info
# Execute the GraphQL query to fetch all scenes
scene_result = graphql_request(query_all_scenes)
all_scenes = scene_result.get('data', {}).get('allScenes', [])
if not all_scenes:
log.error("No scenes found.")
logger.error("No scenes found.")
exit()
# Find the scene with the latest updated_at timestamp
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
wrapper_styles = config["wrapper_styles"]
separator = config["separator"]
key_order = config["key_order"]
# Read stash directory from renamefilename_settings.py
stash_directory = config.get('stash_directory', '')
# Extract rename_files and move_files settings from renamefilename_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 = ""
# 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"))
# 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.")

View File

@@ -0,0 +1,22 @@
name: RenameFileName
description: "Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab.
The file is renamed after user clicks save button.
Tags are appended to the file name if the tag does not already exist in the original file name.
Features are configurable using the renamefilename_settings.py.
Note: On Windows OS, the file can not be renamed while it's playing. Refresh the URL to allow file release and rename."
version: 0.1
url: https://github.com/David-Maisonave/Axter-Stash
exec:
- python
- "{pluginDir}/renamefilename.py"
interface: raw
hooks:
- name: RenameFiles
description: Renames scene files.
triggeredBy:
- Scene.Update.Post
tasks:
- name: Rename Files Task
description: Renames scene files.
defaultArgs:
mode: rename_files_task

View File

@@ -0,0 +1,58 @@
# Importing config dictionary
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.
"performers": '[]', # Modify these values to change how each part of the filename is wrapped.
"date": '[]', # Use '[]' for square brackets, '{}' for curly brackets, '()' for parentheses, or an empty string for None.
"height": '[]', # Modify these values to change how each part of the filename is wrapped.
"video_codec": '[]', # Use '[]' for square brackets, '{}' for curly brackets, '()' for parentheses, or an empty string for None.
"frame_rate": '[]', # Modify these values to change how each part of the filename is wrapped.
"tag": '[]' # Modify these values to change how each tag part of the filename is wrapped.
},
# Define the separator to use between different parts of the filename.
# Use '-' for hyphen, '_' for underscore, or ' ' for space.
"separator": '-',
# Define the order of keys in the filename.
# Use a list to specify the order of keys.
# Valid keys are 'studio', 'title', 'performers', 'date', 'height', 'video_codec', 'frame_rate', and 'tags'.
"key_order": [
"studio",
"title",
"performers",
"date",
"height",
"video_codec",
"frame_rate",
"tags"
],
# Define keys to exclude from the formed filename
# Specify keys to exclude from the filename formation process. (ie. "exclude_keys": ["studio", "date"],)
"exclude_keys": ["studio", "performers", "date", "height", "video_codec", "frame_rate"],
# Define whether files should be moved when renaming
"move_files": False,
# Define whether files should be renamed when moved
"rename_files": True,
# Define whether the script should run in dry run mode
"dry_run": False,
# Define whether the original file name should be used if title is empty
"if_notitle_use_org_filename": True,
# Define whether to add tag only if tag is not in file name
"add_tag_if_not_in_name": True,
# Define whether to rename all files missing tag names, or only the latest scene having an update
# "rename_all_files": True,
# Define the maximum number of tag keys to include in the filename (None for no limit)
"max_tag_keys": 12,
# Current Stash DB schema only allows maximum base file name length to be 255
"max_filename_length": 255,
"max_filefolder_length": 255, # For future useage
"max_filebase_length": 255, # For future useage
# GraphQL endpoint
"graphql_endpoint": "http://localhost:9999/graphql", # Update with your endpoint
# Define a whitelist of allowed tags or (None to allow all tags)
"tag_whitelist": [], #Example: "tag_whitelist": ["tag1", "tag2", "tag3"]
# Define paths to exclude from modifications
"exclude_paths": [] #Example: "exclude_paths": [r"/path/to/exclude1"]
}

View File

@@ -0,0 +1 @@
stashapp-tools