Added additional fields

Added fields resolution, width, and galleries.
Fixed bug with studio.
Modified GUI option to simplify.
Added postfix styles option to advance renamefile_settings.py file.
Added logic to avoid running rename logic twice for the same file.
Implemented code to limit max logging file size.
This commit is contained in:
David Maisonave
2024-07-26 06:28:22 -04:00
parent 0e50b59957
commit 2f20c5507c
6 changed files with 117 additions and 34 deletions

View File

@@ -9,6 +9,8 @@ desktop.ini
*.ipch
*.lib
*.log
*.log.1
*.log.2
*.manifest
*.obj
*.pch

View File

@@ -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

View File

@@ -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

View File

@@ -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*********************************")

View File

@@ -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

View File

@@ -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,