forked from Github/Axter-Stash
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:
2
plugins/RenameFile/.gitignore
vendored
2
plugins/RenameFile/.gitignore
vendored
@@ -9,6 +9,8 @@ desktop.ini
|
||||
*.ipch
|
||||
*.lib
|
||||
*.log
|
||||
*.log.1
|
||||
*.log.2
|
||||
*.manifest
|
||||
*.obj
|
||||
*.pch
|
||||
|
||||
@@ -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
|
||||
|
||||
14
plugins/RenameFile/manifest
Normal file
14
plugins/RenameFile/manifest
Normal 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
|
||||
@@ -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*********************************")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user