Updates to plugins RenameFile and DupFileManager

RenameFile Plugin Changes
### 0.5.6
- Fixed bug with studio getting the studio ID instead of the name of the studio in rename process.
- Improved performance by having code get all required scene details in one call to stash.
- To remove UI clutter, move rarely used options (performerAppendEnable, studioAppendEnable, tagAppendEnable, & fileRenameViaMove) to renamefile_settings.py
- Change options (performerAppendEnable, studioAppendEnable, tagAppendEnable) to default to True (enabled)

DupFileManager Plugin Changes
### 0.2.2
- Added dropdown menu logic to Advance Menu and reports.
- Added Graylist deletion option to Advance Menu.
- Report option to clear all flags from report.
- Report option to clear all (_DuplicateMarkForDeletion_?) tag from all scenes.
- Report option to delete from Stash DB all scenes with missing files in file system.
- Added popup tag list to report which list all tags associated with scene.
- Added popup performer list to report which list all performers associated with scene.
- Added popup gallery list to report which list all galleries associated with scene.
- Added popup group list to report which list all groups associated with scene.
- After merging tags in report, the report gets updated with the merged scene metadata.
- Added graylist deletion option to [**Advance Duplicate File Deletion Menu**].
- Added pinklist option to Settings->Plugins->Plugins and to [**Advance Duplicate File Deletion Menu**]
  - The pinklist is only used with the [**Advance Duplicate File Deletion Menu**], and it's **NOT** used in the primary process to selected candidates for deletion.
- Advance Menu now works with non-tagged scenes that are in the current report.
This commit is contained in:
David Maisonave
2024-11-26 09:29:26 -05:00
parent 1c8c0c6db6
commit e1f3335db8
11 changed files with 464 additions and 160 deletions

View File

@@ -10,7 +10,7 @@ except Exception as e:
import traceback, sys
tb = traceback.format_exc()
print(f"ModulesValidate Exception. Error: {e}\nTraceBack={tb}", file=sys.stderr)
import os, sys, time, pathlib, argparse, platform, shutil, traceback, logging, requests
import os, sys, time, pathlib, argparse, platform, shutil, traceback, logging, requests, json
from datetime import datetime
from StashPluginHelper import StashPluginHelper
from stashapi.stash_types import PhashDistance
@@ -36,6 +36,7 @@ settings = {
"zvWhitelist": "",
"zwGraylist": "",
"zxBlacklist": "",
"zxPinklist": "",
"zyMaxDupToProcess": 0,
"zySwapHighRes": False,
"zySwapLongLength": False,
@@ -69,7 +70,7 @@ stash = StashPluginHelper(
)
stash.convertToAscii = True
advanceMenuOptions = [ "applyCombo", "applyComboBlacklist", "pathToDelete", "pathToDeleteBlacklist", "sizeToDeleteLess", "sizeToDeleteGreater", "sizeToDeleteBlacklistLess", "sizeToDeleteBlacklistGreater", "durationToDeleteLess", "durationToDeleteGreater", "durationToDeleteBlacklistLess", "durationToDeleteBlacklistGreater",
advanceMenuOptions = [ "applyCombo", "applyComboPinklist", "applyComboGraylist", "applyComboBlacklist", "pathToDelete", "pathToDeleteBlacklist", "sizeToDeleteLess", "sizeToDeleteGreater", "sizeToDeleteBlacklistLess", "sizeToDeleteBlacklistGreater", "durationToDeleteLess", "durationToDeleteGreater", "durationToDeleteBlacklistLess", "durationToDeleteBlacklistGreater",
"commonResToDeleteLess", "commonResToDeleteEq", "commonResToDeleteGreater", "commonResToDeleteBlacklistLess", "commonResToDeleteBlacklistEq", "commonResToDeleteBlacklistGreater", "resolutionToDeleteLess", "resolutionToDeleteEq", "resolutionToDeleteGreater",
"resolutionToDeleteBlacklistLess", "resolutionToDeleteBlacklistEq", "resolutionToDeleteBlacklistGreater", "ratingToDeleteLess", "ratingToDeleteEq", "ratingToDeleteGreater", "ratingToDeleteBlacklistLess", "ratingToDeleteBlacklistEq", "ratingToDeleteBlacklistGreater",
"tagToDelete", "tagToDeleteBlacklist", "titleToDelete", "titleToDeleteBlacklist", "pathStrToDelete", "pathStrToDeleteBlacklist"]
@@ -232,7 +233,10 @@ stash.Trace(f"whitelist = {whitelist}")
blacklist = stash.Setting('zxBlacklist').split(listSeparator)
blacklist = [item.lower() for item in blacklist]
if blacklist == [""] : blacklist = []
stash.Trace(f"blacklist = {blacklist}")
pinklist = stash.Setting('zxPinklist').split(listSeparator)
pinklist = [item.lower() for item in pinklist]
if pinklist == [""] : pinklist = []
stash.Trace(f"pinklist = {pinklist}")
def realpath(path):
"""
@@ -561,6 +565,10 @@ htmlHighlightTimeDiff = stash.Setting('htmlHighlightTimeDiff')
htmlPreviewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview"
def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel = "?", tagDuplicates = False):
htmlTagPrefix = '<div class="dropdown_icon"><img src="https://www.axter.com/images/stash/tag.png" alt="Tags" style="width:20px;height:20px;"><i class="fa fa-caret-down"></i><div class="dropdown_tag-content">'
htmlPerformerPrefix = '<div class="dropdown_icon"><img src="https://www.axter.com/images/stash/performer.png" alt="Performers" title="Performers" style="width:20px;height:20px;"><i class="fa fa-caret-down"></i><div class="dropdown_performer-content">'
htmlGalleryPrefix = '<div class="dropdown_icon"><img src="https://www.axter.com/images/stash/galleries.png" alt="Galleries" title="Galleries" style="width:20px;height:20px;"><i class="fa fa-caret-down"></i><div class="dropdown_gallery-content">'
htmlGroupPrefix = '<div class="dropdown_icon"><img src="https://www.axter.com/images/stash/group.png" alt="Groups" title="Groups" style="width:20px;height:20px;"><i class="fa fa-caret-down"></i><div class="dropdown_group-content">'
dupFileExist = True if os.path.isfile(DupFile['files'][0]['path']) else False
toKeepFileExist = True if os.path.isfile(DupFileToKeep['files'][0]['path']) else False
fileHtmlReport.write(f"{htmlReportTableRow}")
@@ -611,6 +619,29 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel =
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFile)}\">[Play]</a>")
else:
fileHtmlReport.write("<b style='color:red;'>[File NOT Exist]<b>")
if len(DupFile['tags']) > 0:
fileHtmlReport.write(htmlTagPrefix)
for tag in DupFile['tags']:
# if not tag['ignore_auto_tag']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{tag['name']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFile['performers']) > 0:
fileHtmlReport.write(htmlPerformerPrefix)
for performer in DupFile['performers']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{performer['name']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFile['galleries']) > 0:
fileHtmlReport.write(htmlGalleryPrefix)
for gallery in DupFile['galleries']:
gallery = stash.find_gallery(gallery['id'])
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{gallery['title']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFile['groups']) > 0:
fileHtmlReport.write(htmlGroupPrefix)
for group in DupFile['groups']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{group['group']['name']}</div>")
fileHtmlReport.write("</div></div>")
fileHtmlReport.write("</p></td>")
videoPreview = f"<video {htmlReportVideoPreview} poster=\"{DupFileToKeep['paths']['screenshot']}\"><source src=\"{DupFileToKeep['paths'][htmlPreviewOrStream]}\" type=\"video/mp4\"></video>"
@@ -633,14 +664,41 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel =
else:
fileHtmlReport.write("<b style='color:red;'>[File NOT Exist]<b>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScene\" id=\"{DupFileToKeep['id']}\">[Flag]</button>")
if len(DupFileToKeep['tags']) > 0:
fileHtmlReport.write(htmlTagPrefix)
for tag in DupFileToKeep['tags']:
# if not tag['ignore_auto_tag']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{tag['name']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFileToKeep['performers']) > 0:
fileHtmlReport.write(htmlPerformerPrefix)
for performer in DupFileToKeep['performers']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{performer['name']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFileToKeep['galleries']) > 0:
fileHtmlReport.write(htmlGalleryPrefix)
for gallery in DupFileToKeep['galleries']:
gallery = stash.find_gallery(gallery['id'])
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{gallery['title']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFileToKeep['groups']) > 0:
fileHtmlReport.write(htmlGroupPrefix)
for group in DupFileToKeep['groups']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{group['group']['name']}</div>")
fileHtmlReport.write("</div></div>")
# ToDo: Add following buttons:
# rename file
fileHtmlReport.write(f"</p></td>")
fileHtmlReport.write("</tr>\n")
fragmentForSceneDetails = 'id tags {id name ignore_auto_tag} groups {group {name} } performers {name} galleries {id} files {path width height duration size video_codec bit_rate frame_rate} details '
htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} "
DuplicateCandidateForDeletionList = f"{htmlReportNameFolder}{os.sep}DuplicateCandidateForDeletionList.txt"
def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlacklistOnly=False, deleteLowerResAndDuration=False):
global reasonDict
global htmlFileData
duplicateMarkForDeletion_descp = 'Tag added to duplicate scenes so-as to tag them for deletion.'
stash.Trace(f"duplicateMarkForDeletion = {duplicateMarkForDeletion}")
dupTagId = stash.createTagId(duplicateMarkForDeletion, duplicateMarkForDeletion_descp, ignoreAutoTag=True)
@@ -671,27 +729,29 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
stash.Trace("#########################################################################")
stash.Log(f"Waiting for find_duplicate_scenes_diff to return results; matchDupDistance={matchPhaseDistanceText}; significantTimeDiff={significantTimeDiff}", printTo=LOG_STASH_N_PLUGIN)
stash.startSpinningProcessBar()
htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} " if createHtmlReport else ""
mergeFieldData = " code director title rating100 date studio {id} movies {movie {id} } galleries {id} performers {id} urls " if merge else ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment='id tags {id name} files {path width height duration size video_codec bit_rate frame_rate} details ' + mergeFieldData + htmlFileData)
mergeFieldData = " code director title rating100 date studio {id name} movies {movie {id} } urls " if merge else ""
if not createHtmlReport:
htmlFileData = ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment= fragmentForSceneDetails + mergeFieldData + htmlFileData)
stash.stopSpinningProcessBar()
qtyResults = len(DupFileSets)
stash.setProgressBarIter(qtyResults)
stash.Trace("#########################################################################")
stash.Log(f"Found {qtyResults} duplicate sets...")
fileHtmlReport = None
if createHtmlReport:
if not os.path.isdir(htmlReportNameFolder):
os.mkdir(htmlReportNameFolder)
if not os.path.isdir(htmlReportNameFolder):
stash.Error(f"Failed to create report directory {htmlReportNameFolder}.")
return
if createHtmlReport:
deleteLocalDupReportHtmlFiles(False)
fileHtmlReport = open(htmlReportName, "w")
fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n")
fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n")
htmlReportTableHeader = stash.Setting('htmlReportTableHeader')
fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Scene</th>{htmlReportTableHeader}Duplicate to Delete</th>{htmlReportTableHeader}Scene-ToKeep</th>{htmlReportTableHeader}Duplicate to Keep</th></tr>\n")
fileDuplicateCandidateForDeletionList = open(DuplicateCandidateForDeletionList, "w")
for DupFileSet in DupFileSets:
# stash.Trace(f"DupFileSet={DupFileSet}", toAscii=True)
@@ -819,6 +879,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
elif moveToTrashCan:
sendToTrash(DupFileName)
stash.destroyScene(DupFile['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(DupFile['id'], removeScene = True)
elif tagDuplicates or fileHtmlReport != None:
if excludeFromReportIfSignificantTimeDiff and significantTimeDiffCheck(DupFile, DupFileToKeep, True):
stash.Log(f"Skipping duplicate {DupFile['files'][0]['path']} (ID={DupFile['id']}), because of time difference greater than {significantTimeDiff} for file {DupFileToKeep['files'][0]['path']}.")
@@ -837,6 +898,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
# add delete only from stash db code and button using DB delete icon
stash.Debug(f"Adding scene {DupFile['id']} to HTML report.")
writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel, tagDuplicates)
fileDuplicateCandidateForDeletionList.write(json.dumps(DupFile) + "\n")
if QtyTagForDelPaginate >= htmlReportPaginate:
QtyTagForDelPaginate = 0
fileHtmlReport.write("</table>\n")
@@ -884,6 +946,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
if maxDupToProcess > 0 and ((QtyTagForDel > maxDupToProcess) or (QtyTagForDel == 0 and QtyDup > maxDupToProcess)):
break
fileDuplicateCandidateForDeletionList.close()
if fileHtmlReport != None:
fileHtmlReport.write("</table>\n")
if PaginateId > 0:
@@ -921,26 +984,32 @@ def findCurrentTagId(tagNames):
for tagName in tagNames:
tagId = stash.find_tags(q=tagName)
if len(tagId) > 0 and 'id' in tagId[0]:
stash.Debug(f"Using tag name {tagName} with Tag ID {tagId[0]['id']}")
return tagId[0]['id']
return "-1"
return tagId[0]['id'], tagName
return "-1", None
def toJson(data):
import json
# data = data.replace("'", '"')
data = data.replace("\\", "\\\\")
data = data.replace("\\\\\\\\", "\\\\")
return json.loads(data)
def getAnAdvanceMenuOptionSelected(taskName, target, isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater):
def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater):
stash.Log(f"Processing taskName = {taskName}, target = {target}")
if "Blacklist" in taskName:
isBlackList = True
if "Graylist" in taskName:
isGrayList = True
if "Pinklist" in taskName:
isPinkList = True
if "Less" in taskName:
compareToLess = True
if "Greater" in taskName:
compareToGreater = True
if ":TagOnlyScenes" in target:
isTagOnlyScenes = True
target = target.replace(":TagOnlyScenes","")
if "pathToDelete" in taskName:
pathToDelete = target.lower()
elif "sizeToDelete" in taskName:
@@ -961,10 +1030,15 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isBlackList, pathToDelete,
pathStrToDelete = target.lower()
elif "fileNotExistToDelete" in taskName:
fileNotExistToDelete = True
return isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
elif "TagOnlyScenes" in taskName:
isTagOnlyScenes = True
return isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
isTagOnlyScenes = False
isBlackList = False
isGrayList = False
isPinkList = False
pathToDelete = ""
sizeToDelete = -1
durationToDelete = -1
@@ -982,16 +1056,27 @@ def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
if "applyCombo" in stash.PLUGIN_TASK_NAME:
jsonObject = toJson(stash.JSON_INPUT['args']['Target'])
for taskName in jsonObject:
isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAnAdvanceMenuOptionSelected(taskName, jsonObject[taskName], isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAnAdvanceMenuOptionSelected(taskName, jsonObject[taskName], isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater)
else:
return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
return isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
return isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getScenesFromReport():
stash.Log(f"Getting candidates for deletion from file {DuplicateCandidateForDeletionList}.")
scenes = []
lines = None
with open(DuplicateCandidateForDeletionList, 'r') as file:
lines = file.readlines()
for line in lines:
scenes += [json.loads(line)]
return scenes
# //////////////////////////////////////////////////////////////////////////////
# //////////////////////////////////////////////////////////////////////////////
def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=False, tagId=-1, advanceMenuOptionSelected=False):
tagName = None
if tagId == -1:
tagId = findCurrentTagId([duplicateMarkForDeletion, base1_duplicateMarkForDeletion, base2_duplicateMarkForDeletion, 'DuplicateMarkForDeletion', '_DuplicateMarkForDeletion'])
tagId, tagName = findCurrentTagId([duplicateMarkForDeletion, base1_duplicateMarkForDeletion, base2_duplicateMarkForDeletion, 'DuplicateMarkForDeletion', '_DuplicateMarkForDeletion'])
if int(tagId) < 0:
stash.Warn(f"Could not find tag ID for tag '{duplicateMarkForDeletion}'.")
return
@@ -1000,7 +1085,7 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if clearAllDupfileManagerTags:
excludedTags = [duplicateMarkForDeletion, duplicateWhitelistTag, excludeDupFileDeleteTag, graylistMarkForDeletion, longerDurationLowerResolution]
isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAdvanceMenuOptionSelected(advanceMenuOptionSelected)
isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAdvanceMenuOptionSelected(advanceMenuOptionSelected)
if advanceMenuOptionSelected and deleteScenes and pathToDelete == "" and tagToDelete == "" and titleToDelete == "" and pathStrToDelete == "" and sizeToDelete == -1 and durationToDelete == -1 and resolutionToDelete == -1 and ratingToDelete == -1 and fileNotExistToDelete == False:
stash.Error("Running advance menu option with no options enabled.")
return
@@ -1012,7 +1097,11 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
QtyFailedQuery = 0
stash.Debug("#########################################################################")
stash.startSpinningProcessBar()
if isTagOnlyScenes or advanceMenuOptionSelected == False:
stash.Log(f"Getting candidates for deletion by using tag-ID {tagId} and tag-name {tagName}; isTagOnlyScenes={isTagOnlyScenes};advanceMenuOptionSelected={advanceMenuOptionSelected}")
scenes = stash.find_scenes(f={"tags": {"value":tagId, "modifier":"INCLUDES"}}, fragment='id tags {id name} files {path width height duration size video_codec bit_rate frame_rate} details title rating100')
else:
scenes = getScenesFromReport()
stash.stopSpinningProcessBar()
qtyResults = len(scenes)
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
@@ -1063,6 +1152,12 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if isBlackList:
if not stash.startsWithInList(blacklist, scene['files'][0]['path']):
continue
if isGrayList:
if not stash.startsWithInList(graylist, scene['files'][0]['path']):
continue
if isPinkList:
if not stash.startsWithInList(pinklist, scene['files'][0]['path']):
continue
if pathToDelete != "":
if not DupFileName.lower().startswith(pathToDelete):
stash.Debug(f"Skipping file {DupFileName} because it does not start with {pathToDelete}.")
@@ -1133,7 +1228,7 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if fileNotExistToDelete:
if os.path.isfile(scene['files'][0]['path']):
continue
stash.Warn(f"Deleting duplicate '{DupFileName}'", toAscii=True, printTo=LOG_STASH_N_PLUGIN)
stash.Log(f"Deleting duplicate '{DupFileName}'", toAscii=True, printTo=LOG_STASH_N_PLUGIN)
if alternateTrashCanPath != "":
destPath = f"{alternateTrashCanPath }{os.sep}{DupFileNameOnly}"
if os.path.isfile(destPath):
@@ -1142,6 +1237,7 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
elif moveToTrashCan:
sendToTrash(DupFileName)
result = stash.destroyScene(scene['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(scene['id'], removeScene = True)
QtyDeleted += 1
stash.Debug(f"destroyScene result={result} for file {DupFileName};QtyDeleted={QtyDeleted};Count={QtyDup} of {qtyResults}", toAscii=True)
else:
@@ -1217,6 +1313,7 @@ def mergeTags():
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
return
stash.mergeMetadata(scene1, scene2)
updateScenesInReports(scene2['id'])
stash.Log(f"Done merging scenes for scene {scene1['id']} and scene {scene2['id']}")
sys.stdout.write("{" + f"mergeTags : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}'" + "}")
@@ -1277,8 +1374,33 @@ def removeAllDupTagsFromAllScenes(deleteTags=False):
else:
stash.Log(f"Clear tags {tagsToClear}")
def updateDuplicateCandidateForDeletionList(scene, removeScene = False):
lines = None
if not os.path.isfile(DuplicateCandidateForDeletionList):
return
with open(DuplicateCandidateForDeletionList, 'r') as file:
lines = file.readlines()
if removeScene:
scene_id = scene
else:
scene_id = scene['id']
foundScene = False
with open(DuplicateCandidateForDeletionList, 'w') as file:
for line in lines:
if foundScene:
file.write(line)
else:
sceneDetails = json.loads(line)
if sceneDetails['id'] == scene_id:
if not removeScene:
file.write(json.dumps(scene) + "\n")
foundScene = True
else:
file.write(line)
def updateScenesInReport(fileName, scene):
stash.Log(f"Updating table rows with scene {scene} in file {fileName}")
results = False
scene1 = -1
scene2 = -1
strToFind = "class=\"ID_"
@@ -1286,10 +1408,12 @@ def updateScenesInReport(fileName, scene):
with open(fileName, 'r') as file:
lines = file.readlines()
stash.Log(f"line count = {len(lines)}")
stash.Log(f"Searching for class=\"ID_{scene}\"")
with open(fileName, 'w') as file:
for line in lines:
# stash.Debug(f"line = {line}")
if f"class=\"ID_{scene}\"" in line:
stash.Debug(f"Found class ID_{scene} in line: {line}")
idx = 0
while line.find(strToFind, idx) > -1:
idx = line.find(strToFind, idx) + len(strToFind)
@@ -1304,30 +1428,41 @@ def updateScenesInReport(fileName, scene):
elif scene1 != -1 and scene2 != -1:
break
if scene1 != -1 and scene2 != -1:
sceneDetail1 = stash.find_scene(scene1)
sceneDetail2 = stash.find_scene(scene2)
if sceneDetail1 == None or sceneDetail2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetail1={sceneDetail1}; sceneDetail2={sceneDetail2};")
sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails + htmlFileData)
sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails + htmlFileData)
if sceneDetails1 == None or sceneDetails2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetails1={sceneDetails1}; sceneDetails2={sceneDetails2};")
else:
writeRowToHtmlReport(file, sceneDetail1, sceneDetail2)
stash.Log(f"Updating in report {fileName} scene {scene1} and scene {scene2}")
writeRowToHtmlReport(file, sceneDetails1, sceneDetails2)
if scene == sceneDetails1['id']:
results = True
updateDuplicateCandidateForDeletionList(sceneDetails1)
else:
stash.Error(f"Could not get both scene ID associated with scene {scene}; scene1 = {scene1}; scene2 = {scene2}")
file.write(line)
else:
file.write(line)
if scene1 == -1 or scene2 == -1:
stash.Log(f"Did not find both scene ID's associated with scene {scene}; scene1 = {scene1}; scene2 = {scene2}")
return results
def updateScenesInReports(scene, ReportName = htmlReportName):
if os.path.isfile(ReportName):
updateScenesInReport(ReportName, scene)
if updateScenesInReport(ReportName, scene):
return
for x in range(2, 9999):
fileName = ReportName.replace(".html", f"_{x-1}.html")
stash.Debug(f"Checking if file '{fileName}' exist.")
if not os.path.isfile(fileName):
break
updateScenesInReport(fileName, scene)
if updateScenesInReport(fileName, scene):
break
stash.Debug("updateScenesInReports complete")
else:
stash.Log(f"Report file does not exist: {ReportName}")
deleteSceneFlagBgColor = "#646464"
def addPropertyToSceneClass(fileName, scene, property):
stash.Log(f"Inserting property {property} for scene {scene} in file {fileName}")
doStyleEndTagCheck = True
@@ -1339,6 +1474,12 @@ def addPropertyToSceneClass(fileName, scene, property):
for line in lines:
# stash.Debug(f"line = {line}")
if doStyleEndTagCheck:
if scene == None:
if line.startswith(f".ID_") and deleteSceneFlagBgColor not in line:
continue
elif line.startswith("</style>"):
doStyleEndTagCheck = False
else:
if property == "" and line.startswith(f".ID_{scene}" + "{"):
continue
if line.startswith("</style>"):
@@ -1370,10 +1511,16 @@ def deleteScene(disableInReport=True, deleteFile=True):
result = None
result = stash.destroyScene(scene, delete_file=deleteFile)
if disableInReport:
addPropertyToSceneClassToAllFiles(scene, "{background-color:gray;pointer-events:none;}")
addPropertyToSceneClassToAllFiles(scene, "{background-color:" + deleteSceneFlagBgColor + ";pointer-events:none;}")
updateDuplicateCandidateForDeletionList(scene, removeScene = True)
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene} with results = {result}")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', id: '{scene}', result: '{result}'" + "}")
def clearAllSceneFlags():
addPropertyToSceneClassToAllFiles(None, None)
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for all scenes")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete'" + "}")
def copyScene(moveScene=False):
scene1, scene2 = getParseData()
if scene1 == None or scene2 == None:
@@ -1384,6 +1531,7 @@ def copyScene(moveScene=False):
result = shutil.copy(scene1['files'][0]['path'], scene2['files'][0]['path'])
if moveScene:
result = stash.destroyScene(scene1['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(scene1['id'], removeScene = True)
stash.Log(f"destroyScene for scene {scene1['id']} results = {result}")
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene1['id']} and {scene2['id']}")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}', result: '{result}'" + "}")
@@ -1482,6 +1630,9 @@ try:
elif stash.PLUGIN_TASK_NAME == "flagScene":
flagScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "clearAllSceneFlags":
clearAllSceneFlags()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "copyScene":
copyScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")

View File

@@ -1,6 +1,6 @@
name: DupFileManager
description: Manages duplicate files.
version: 0.2.1
version: 0.2.2
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui:
javascript:
@@ -34,6 +34,10 @@ settings:
displayName: Black List
description: Least preferential paths; Determine primary deletion candidates. E.g. C:\Downloads,C:\DelMe-3rd,C:\DelMe-2nd,C:\DeleteMeFirst
type: STRING
zxPinklist:
displayName: Pink List
description: An [Advance Duplicate File Deletion Menu] option for deletion using paths. E.g. C:\SomeRandomDir,E:\DelPath2
type: STRING
zyMaxDupToProcess:
displayName: Max Dup Process
description: (Default=0) Maximum number of duplicates to process. If 0, infinity.

View File

@@ -64,8 +64,93 @@ li:hover .large {
border-radius: 4px;
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);;
}
/******** Dropdown buttons *********/
.dropdown .dropdown_tag .dropdown_performer .dropdown_gallery .dropbtn {
font-size: 14px;
border: none;
outline: none;
color: white;
padding: 6px 10px;
background-color: transparent;
font-family: inherit; /* Important for vertical align on mobile phones */
margin: 0; /* Important for vertical align on mobile phones */
}
.dropdown-content{
display: none;
position: absolute;
background-color: inherit;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
float: none;
color: black;
padding: 6px 10px;
text-decoration: none;
display: block;
text-align: left;
}
.dropdown:hover .dropdown-content {
display: block;
}
/*************-- Dropdown Icons --*************/
.dropdown_icon {
height:22px;
width:30px;
float:left;
}
/*** Dropdown Tag ***/
.dropdown_tag-content{
display: none;
position: absolute;
background-color: LightCoral;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_tag-content {
display: block;
}
/*** Dropdown Performer ***/
.dropdown_performer-content{
display: none;
position: absolute;
background-color: LightBlue;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_performer-content {
display: block;
}
/*** Dropdown Gallery ***/
.dropdown_gallery-content{
display: none;
position: absolute;
background-color: AntiqueWhite;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_gallery-content {
display: block;
}
/*** Dropdown Group ***/
.dropdown_group-content{
display: none;
position: absolute;
background-color: BurlyWood;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_group-content {
display: block;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://www.axter.com/js/jquery-3.7.1.min.js"></script>
<script src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css"/>
<script>
@@ -111,11 +196,8 @@ function RunPluginOperation(Mode, ActionID, button, asyncAjax){
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){
console.log(result);
// if (Mode !== "flagScene") button.style.visibility = 'hidden';
if (Mode === "renameFile"){
var myArray = ActionID.split(":");
$('.FN_ID_' + myArray[0]).text(trim(myArray[1],"'"));
}
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags")
location.replace(location.href);
if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.");
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
@@ -191,7 +273,7 @@ $(document).ready(function(){
$("button").click(function(){
var Mode = this.value;
var ActionID = this.id;
if (ActionID === "AdvanceMenu")
if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{
var newUrl = window.location.href;
newUrl = newUrl.replace(/report\/DuplicateTagScenes[_0-9]*.html/g, "advance_options.html?GQL=" + GraphQl_URL + "&apiKey=" + apiKey);
@@ -245,7 +327,16 @@ $(document).ready(function(){
<td><table><tr>
<td><input type="checkbox" id="RemoveValidatePrompt" name="RemoveValidatePrompt"><label for="RemoveValidatePrompt" title="Disable notice for task completion (Popup).">Disable Complete Confirmation</label><br></td>
<td><input type="checkbox" id="RemoveToKeepConfirm" name="RemoveToKeepConfirm"><label for="RemoveToKeepConfirm" title="Disable confirmation prompts for delete scenes">Disable Delete Confirmation</label><br></td>
<td><button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance Tag Menu</button></td>
<td>
<div class="dropdown">
<button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance Tag Menu <i class="fa fa-caret-down"></i></button>
<div class="dropdown-content">
<div><button type="button" id="clearAllSceneFlags" value="clearAllSceneFlags" title="Remove flags from report for all scenes, except for deletion flag.">Clear All Scene Flags</button></div>
<div><button type="button" id="clear_duplicate_tags_task" value="clear_duplicate_tags_task" title="Remove duplicate (_DuplicateMarkForDeletion_?) tag from all scenes. This action make take a few minutes to complete.">Remove All Scenes Tags</button></div>
<div><button type="button" id="fileNotExistToDelete" value="fileNotExistToDelete" title="Delete tagged duplicates for which file does NOT exist.">Delete Files That do Not Exist</button></div>
</div>
</div>
</td>
</tr></table></td>
</tr></table></center>
<h2>Stash Duplicate Scenes Report (MatchTypePlaceHolder)</h2>\n""",

View File

@@ -1,4 +1,4 @@
# DupFileManager: Ver 0.2.1 (By David Maisonave)
# DupFileManager: Ver 0.2.2 (By David Maisonave)
DupFileManager is a [Stash](https://github.com/stashapp/stash) plugin which manages duplicate files in the Stash system.
It has both **task** and **tools-UI** components.
@@ -102,10 +102,9 @@ That's it!!!
- ![Screenshot 2024-11-22 232208](https://github.com/user-attachments/assets/bf1f3021-3a8c-4875-9737-60ee3d7fe675)
### Future Planned Features
- Add an advanced menu that will work with non-tagged reports. It will iterated through the existing report file(s) to aplly deletions, instead of searching Stash DB for tagged files. Planned for 1.1.0 Version.
- Greylist deletion option will be added to the advanced menu. Planned for 1.0.5 Version.
- Consolidate buttons in report into dropdown buttons. Planned for 1.0.1 Version.
- Add logic to merge performers and galaries seperatly from tag merging on report. Planned for 1.0.5 Version.
- Add logic to merge group metadata when selecting merge option on report. Planned for 1.1.0 Version.
- Add advanced menu directly to the Settings->Tools menu. Planned for 1.5.0 Version.
- Add report directly to the Settings->Tools menu. Planned for 1.5.0 Version.
- Remove all flags from all scenes option. Planned for 1.0.5 Version.

View File

@@ -29,8 +29,47 @@ table, th, td {border:1px solid black;}
color:red;
}
html.wait, html.wait * { cursor: wait !important; }
/* The dropdown container */
.dropdown {
float: left;
overflow: hidden;
}
/******** Dropdown button *********/
.dropdown .dropbtn {
font-size: 14px;
border: none;
outline: none;
color: white;
padding: 6px 10px;
background-color: transparent;
font-family: inherit; /* Important for vertical align on mobile phones */
margin: 0; /* Important for vertical align on mobile phones */
}
/* Dropdown content (hidden by default) */
.dropdown-content {
display: none;
position: absolute;
background-color: inherit;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
/* Links inside the dropdown */
.dropdown-content a {
float: none;
color: black;
padding: 6px 10px;
text-decoration: none;
display: block;
text-align: left;
}
/* Show the dropdown menu on hover */
.dropdown:hover .dropdown-content {
display: block;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://www.axter.com/js/jquery-3.7.1.min.js"></script>
<script>
var apiKey = ""; // For Stash installations with a password setup, populate this variable with the apiKey found in Stash->Settings->Security->[API Key]; ----- Or pass in the apiKey at the URL command line. Example: advance_options.html?apiKey=12345G4igiJdgssdgiwqInh5cCI6IkprewJ9hgdsfhgfdhd&GQL=http://localhost:9999/graphql
var GraphQl_URL = "http://localhost:9999/graphql";// For Stash installations with non-standard ports or URL's, populate this variable with actual URL; ----- Or pass in the URL at the command line using GQL param. Example: advance_options.html?GQL=http://localhost:9900/graphql
@@ -41,11 +80,15 @@ if (urlParams.get('GQL') != null && urlParams.get('GQL') !== "")
GraphQl_URL = urlParams.get('GQL');
console.log(urlParams);
console.log("GQL = " + GraphQl_URL);
console.log("apiKey = " + apiKey);
function RunPluginDupFileManager(Mode, Param = 0, Async = false) {
function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes = false) {
$('html').addClass('wait');
$("body").css("cursor", "progress");
if (TagOnlyScenes)
Param += ":TagOnlyScenes";
console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param);
//if ($("#InPathCheck").prop('checked'))
if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
$.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: Async, async: Async,
@@ -63,12 +106,15 @@ function RunPluginDupFileManager(Mode, Param = 0, Async = false) {
console.log("Setting default cursor");
}
$(document).ready(function(){
$("button").click(function(){
const AddedWarn = "? This will delete the files, and remove them from stash.";
console.log(this.id);
var blackliststr = "";
var comparestr = "less than ";
if (this.id.includes("Blacklist")) blackliststr = "in blacklist ";
if (this.id.includes("Graylist")) blackliststr = "in graylist ";
if (this.id.includes("Pinklist")) blackliststr = "in pinklist ";
if (this.id.includes("Greater")) comparestr = "greater than ";
else if (this.id.includes("Eq")) comparestr = "equal to ";
@@ -94,27 +140,27 @@ $(document).ready(function(){
else if (this.id === "pathToDelete" || this.id === "pathToDeleteBlacklist")
{
if (confirm("Are you sure you want to delete tag scenes " + blackliststr + "having _DuplicateMarkForDeletion tags, and in path " + $("#pathToDeleteText").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#pathToDeleteText").val());
RunPluginDupFileManager(this.id, $("#pathToDeleteText").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "sizeToDeleteLess" || this.id === "sizeToDeleteGreater" || this.id === "sizeToDeleteBlacklistLess" || this.id === "sizeToDeleteBlacklistGreater")
{
if (confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having file size " + comparestr + $("#sizeToDelete").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#sizeToDelete").val());
RunPluginDupFileManager(this.id, $("#sizeToDelete").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "durationToDeleteLess" || this.id === "durationToDeleteGreater" || this.id === "durationToDeleteBlacklistLess" || this.id === "durationToDeleteBlacklistGreater")
{
if (confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having file duration " + comparestr + $("#durationToDelete").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#durationToDelete").val());
RunPluginDupFileManager(this.id, $("#durationToDelete").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "commonResToDeleteLess" || this.id === "commonResToDeleteEq" || this.id === "commonResToDeleteGreater" || this.id === "commonResToDeleteBlacklistLess" || this.id === "commonResToDeleteBlacklistEq" || this.id === "commonResToDeleteBlacklistGreater")
{
if (confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having resolution " + comparestr + $("#commonResToDelete").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#commonResToDelete").val());
RunPluginDupFileManager(this.id, $("#commonResToDelete").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "resolutionToDeleteLess" || this.id === "resolutionToDeleteEq" || this.id === "resolutionToDeleteGreater" || this.id === "resolutionToDeleteBlacklistLess" || this.id === "resolutionToDeleteBlacklistEq" || this.id === "resolutionToDeleteBlacklistGreater")
{
if (confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having resolution " + comparestr + $("#resolutionToDelete").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#resolutionToDelete").val());
RunPluginDupFileManager(this.id, $("#resolutionToDelete").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "ratingToDeleteLess" || this.id === "ratingToDeleteEq" || this.id === "ratingToDeleteGreater" || this.id === "ratingToDeleteBlacklistLess" || this.id === "ratingToDeleteBlacklistEq" || this.id === "ratingToDeleteBlacklistGreater")
{
@@ -126,33 +172,37 @@ $(document).ready(function(){
else
result = confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having rating " + comparestr + $("#ratingToDelete").val() + AddedWarn);
if (result)
RunPluginDupFileManager(this.id, $("#ratingToDelete").val());
RunPluginDupFileManager(this.id, $("#ratingToDelete").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "tagToDelete" || this.id === "tagToDeleteBlacklist")
{
if (confirm("Are you sure you want to delete tag scenes " + blackliststr + "having _DuplicateMarkForDeletion tags, and having tag " + $("#tagToDeleteText").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#tagToDeleteText").val());
RunPluginDupFileManager(this.id, $("#tagToDeleteText").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "titleToDelete" || this.id === "titleToDeleteBlacklist")
{
if (confirm("Are you sure you want to delete tag scenes " + blackliststr + "having _DuplicateMarkForDeletion tags, and having title containing " + $("#titleToDeleteText").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#titleToDeleteText").val());
RunPluginDupFileManager(this.id, $("#titleToDeleteText").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "pathStrToDelete" || this.id === "pathStrToDeleteBlacklist")
{
if (confirm("Are you sure you want to delete tag scenes " + blackliststr + "having _DuplicateMarkForDeletion tags, and having path containing " + $("#pathStrToDeleteText").val() + AddedWarn))
RunPluginDupFileManager(this.id, $("#pathStrToDeleteText").val());
RunPluginDupFileManager(this.id, $("#pathStrToDeleteText").val(), true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "fileNotExistToDelete" || this.id === "fileNotExistToDeleteBlacklist")
{
if (confirm("Are you sure you want to delete tag scenes " + blackliststr + "having _DuplicateMarkForDeletion tags, and that do NOT exist in the file system?"))
RunPluginDupFileManager(this.id, true);
RunPluginDupFileManager(this.id, true, true, $("#DupTagOnlyCheck").prop('checked'));
}
else if (this.id === "applyCombo" || this.id === "applyComboBlacklist")
else if (this.id === "applyCombo" || this.id === "applyCombo_" || this.id === "applyComboBlacklist" || this.id === "applyComboGraylist" || this.id === "applyComboPinklist")
{
var Blacklist = "";
if (this.id === "applyComboBlacklist")
Blacklist = "Blacklist";
else if (this.id === "applyComboGraylist")
Blacklist = "Graylist";
else if (this.id === "applyComboPinklist")
Blacklist = "Pinklist";
var Param = "{";
if ($("#InPathCheck").prop('checked'))
Param += "\"" + "pathToDelete" + Blacklist + "\":\"" + $("#pathToDeleteText").val().replace("\\", "\\\\") + "\", ";
@@ -181,6 +231,8 @@ $(document).ready(function(){
Param += "\"" + "pathStrToDelete" + Blacklist + "\":\"" + $("#pathStrToDeleteText").val().replace("\\", "\\\\") + "\", ";
if ($("#fileNotExistCheck").prop('checked'))
Param += "\"" + "fileNotExistToDelete" + Blacklist + "\":\"true\", ";
if ($("#DupTagOnlyCheck_MultiOption").prop('checked'))
Param += "\"" + "TagOnlyScenes" + Blacklist + "\":\"true\", ";
Param += '}';
Param = Param.replace(', }', '}');
if (Param === "{}")
@@ -190,7 +242,7 @@ $(document).ready(function(){
}
console.log(Param);
if (confirm("Are you sure you want to delete tag scenes " + blackliststr + "having _DuplicateMarkForDeletion tags, and having the selected options" + AddedWarn + "\nSelected options:\n" + Param))
RunPluginDupFileManager(this.id, Param);
RunPluginDupFileManager(this.id, Param, true);
}
});
});
@@ -201,16 +253,50 @@ function DeleteDupInPath(){
</head>
<body>
<center><table style="color:darkgreen;background-color:powderblue;">
<tr><th>DupFileManager Advance <b style="color:red;">_DuplicateMarkForDeletion_?</b> Tagged Files Menu</th><th>Apply Multiple Options</th></tr>
<tr><th><div><b style="color:red;"><i>DupFileManager</i></b></div>Advance Duplicate File Deletion Menu</th><th>Apply Multiple Options</th></tr>
<tr>
<td><center>
<button type="button" id="tag_duplicates_task" value="-1" title="Create new report which tags duplicates with tag name _DuplicateMarkForDeletion using user settings for [Match Duplicate Distance].">Create Duplicate Report with Tagging</button>
<button type="button" id="viewreport" title="View duplicate file report.">View Dup Report</button>
</center></td>
<td>
<button type="button" id="applyCombo" title="Apply selected multiple options to delete scenes.">Delete</button>
<button type="button" id="applyComboBlacklist" title="Apply selected multiple options to delete scenes in blacklist.">Delete-Blacklist</button>
<table style="border-collapse: collapse; border: none;"><tr style="border: none;">
<tr><th colspan="3" style="font-size: 12px;border: none;">Create report overriding user [Match Duplicate Distance] and [significantTimeDiff] settings</th></tr>
<td style="border: none;"><div class="dropdown">
<button type="button" id="create_duplicate_report_task" value="-1" title="Create new report WITHOUT tags using user settings for [Match Duplicate Distance].">Create Duplicate Report <i class="fa fa-caret-down"></i></button>
<div class="dropdown-content">
<div><button type="button" id="create_duplicate_report_task0a" value="0" title="Create report using [Match Duplicate Distance]=0 (Exact Match). NO tagging.">Create Duplicate Report [Exact Match]</button></div>
<div><button type="button" id="create_duplicate_report_task1a" value="1" title="Create report using [Match Duplicate Distance]=1 (High Match). NO tagging.">Create Duplicate Report [High Match]</button></div>
<div><button type="button" id="create_duplicate_report_task2a" value="2" title="Create report using [Match Duplicate Distance]=2 (Medium Match). NO tagging.">Create Duplicate Report [Medium Match]</button></div>
<div><button type="button" id="create_duplicate_report_task3a" value="3" title="Create report using [Match Duplicate Distance]=3 (Low Match). NO tagging.">Create Duplicate Report [Low Match]</button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<div><button type="button" id="tag_duplicates_task" value="-1" title="Create new report which tags duplicates with tag name _DuplicateMarkForDeletion using user settings for [Match Duplicate Distance].">Create Duplicate Report with Tagging (With Default Match Setting)</button></div>
<div><button type="button" id="tag_duplicates_task0a" value="0" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_0 and using [Match Duplicate Distance]=0 (Exact Match).">Create Duplicate Tagging Report [Exact Match]</button></div>
<div><button type="button" id="tag_duplicates_task1a" value="1" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_1 and using [Match Duplicate Distance]=1 (High Match).">Create Duplicate Tagging Report [High Match]</button></div>
<div><button type="button" id="tag_duplicates_task2a" value="2" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_2 and using [Match Duplicate Distance]=2 (Medium Match).">Create Duplicate Tagging Report [Medium Match]</button></div>
<div><button type="button" id="tag_duplicates_task3a" value="3" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_3 and using [Match Duplicate Distance]=3 (Low Match).">Create Duplicate Tagging Report [Low Match]</button></div>
</div>
</div></td>
<td style="border: none;padding: 0 15px;">
<label for="significantTimeDiff" title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%.">Time Difference%:</label>
<input type="number" min="0.25" max="1.00" step="0.01" id="significantTimeDiff" name="significantTimeDiff" title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%." value="0.90">
</td>
<td style="border: none;padding: 0 15px;">
<button type="button" id="viewreport" title="View duplicate file report.">View Dup Report</button>
</td>
</tr></table>
</td>
<td>
<div class="dropdown">
<center>Multi-Options: <button id="applyCombo" title="Apply selected multiple options to delete scenes.">Delete <i class="fa fa-caret-down"></i></button></center>
<div class="dropdown-content">
<div><button type="button" id="applyCombo_" title="Apply selected multiple options to delete scenes.">Delete All Candidates</button></div>
<div><button type="button" id="applyComboBlacklist" title="Apply selected multiple options to delete scenes in blacklist.">Delete-Blacklist</button></div>
<div><button type="button" id="applyComboGraylist" title="Apply selected multiple options to delete scenes in graylist.">Delete-Graylist</button></div>
<div><button type="button" id="applyComboPinklist" title="Apply selected multiple options to delete scenes in pinklist.">Delete-Pinklist</button></div>
</div>
</div>
</td>
</tr>
<tr>
<td><label title="When enabled, operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?" for="DupTagOnlyCheck">Apply action only to scenes with <b>_DuplicateMarkForDeletion_?</b> tag:</label><input title="When enabled, operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?" type="checkbox" id="DupTagOnlyCheck" name="DupTagOnlyCheck" value="true"></td>
<td><label title="When enabled, Multi-Options operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?" for="DupTagOnlyCheck_MultiOption">Dup Tag:</label><input title="When enabled, Multi-Options operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?" type="checkbox" id="DupTagOnlyCheck_MultiOption" name="DupTagOnlyCheck" value="true"></td>
</tr>
<tr>
<td><form id="pathToDeleteForm" action="javascript:DeleteDupInPath();" target="_self">
@@ -1798,50 +1884,6 @@ function DeleteDupInPath(){
</select>
</td>
</tr>
</table></center>
<div id="div1"></div>
<br>
<center><table style="color:darkgreen;background-color:powderblue;">
<tr><th colspan="2">Create report with different <b style="color:red;">[Match Duplicate Distance]</b> options
<br><div style="font-size: 12px;">Overrides user [Match Duplicate Distance] and [significantTimeDiff] settings</div>
<form id="significantTimeDiffForm" action="javascript:DeleteDupInPath();" target="_self">
<label for="significantTimeDiff" title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%.">Time Difference%:</label>
<input type="number" min="0.25" max="1.00" step="0.01" id="significantTimeDiff" name="significantTimeDiff" title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%." value="0.90">
</form>
</th></tr>
<tr>
<td><table style="color:darkgreen;background-color:powderblue;">
<tr><th title="Create report with tagging (_DuplicateMarkForDeletion_)">Create Report with Tagging</th></tr>
<tr><td><center>
<button type="button" id="tag_duplicates_task0" value="0" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_0 and using [Match Duplicate Distance]=0 (Exact Match).">Create Duplicate Tagging Report [Exact Match]</button>
</center></td></tr>
<tr><td><center>
<button type="button" id="tag_duplicates_task1" value="1" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_1 and using [Match Duplicate Distance]=1 (High Match).">Create Duplicate Tagging Report [High Match]</button>
</center></td></tr>
<tr><td><center>
<button type="button" id="tag_duplicates_task2" value="2" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_2 and using [Match Duplicate Distance]=2 (Medium Match).">Create Duplicate Tagging Report [Medium Match]</button>
</center></td></tr>
<tr><td><center>
<button type="button" id="tag_duplicates_task3" value="3" title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_3 and using [Match Duplicate Distance]=3 (Low Match).">Create Duplicate Tagging Report [Low Match]</button>
</center></td></tr>
</table></td>
<td><table style="color:darkgreen;background-color:powderblue;">
<tr><th title="Create report with NO tagging (NO Dup Tag)">Create Report without Tagging</th></tr>
<tr><td><center>
<button type="button" id="create_duplicate_report_task0" value="0" title="Create report using [Match Duplicate Distance]=0 (Exact Match). NO tagging.">Create Duplicate Report [Exact Match]</button>
</center></td></tr>
<tr><td><center>
<button type="button" id="create_duplicate_report_task1" value="1" title="Create report using [Match Duplicate Distance]=1 (High Match). NO tagging.">Create Duplicate Report [High Match]</button>
</center></td></tr>
<tr><td><center>
<button type="button" id="create_duplicate_report_task2" value="2" title="Create report using [Match Duplicate Distance]=2 (Medium Match). NO tagging.">Create Duplicate Report [Medium Match]</button>
</center></td></tr>
<tr><td><center>
<button type="button" id="create_duplicate_report_task3" value="3" title="Create report using [Match Duplicate Distance]=3 (Low Match). NO tagging.">Create Duplicate Report [Low Match]</button>
</center></td></tr>
</table></td>
</tr>
<tr><td style="font-size: 12px;" colspan="2">
<b>Details:</b>
<ol type="I" style="padding-left: 16px;">
@@ -1901,6 +1943,8 @@ function DeleteDupInPath(){
</ol>
</td></tr>
</table></center>
<div id="div1"></div>
</body></html>

View File

@@ -1,7 +1,22 @@
##### This page was added starting on version 0.2.0 to keep track of changes between versions.
##### This page was added starting on version 0.2.0 to keep track of newly added features between versions.
### 0.2.0
- For report, added logic to transfer option settings **[Disable Complete Confirmation]** and **[Disable Delete Confirmation]** when paginating.
- Fixed minor bug in advance_options.html for GQL params.
### 0.2.1
- Added logic to have reports and advanced menu to work with Stash settings requiring a password by adding API-Key as param argument for advance menu, and adding API-Key as variable in reports.
- When **[Advance Tag Menu]** is called from reports, it's given both the GQL URL and the apiKey on the URL param, which allows advance menu to work with non-standard URL's and with API-Key.
### 0.2.2
- Added dropdown menu logic to Advance Menu and reports.
- Added Graylist deletion option to Advance Menu.
- Report option to clear all flags from report.
- Report option to clear all (_DuplicateMarkForDeletion_?) tag from all scenes.
- Report option to delete from Stash DB all scenes with missing files in file system.
- Added popup tag list to report which list all tags associated with scene.
- Added popup performer list to report which list all performers associated with scene.
- Added popup gallery list to report which list all galleries associated with scene.
- Added popup group list to report which list all groups associated with scene.
- After merging tags in report, the report gets updated with the merged scene metadata.
- Added graylist deletion option to [**Advance Duplicate File Deletion Menu**].
- Added pinklist option to Settings->Plugins->Plugins and to [**Advance Duplicate File Deletion Menu**]
- The pinklist is only used with the [**Advance Duplicate File Deletion Menu**], and it's **NOT** used in the primary process to selected candidates for deletion.
- Advance Menu now works with non-tagged scenes that are in the current report.

View File

@@ -1,4 +1,4 @@
# RenameFile: Ver 0.5.5 (By David Maisonave)
# RenameFile: Ver 0.5.6 (By David Maisonave)
RenameFile is a [Stash](https://github.com/stashapp/stash) plugin. Starting version 0.5.5, user can add the current title to the title input field by clicking on the current title. Also, the Stash database gets updated directly instead of running a scan task as long as the database is version 68.
- The plugin allows user to rename one scene at a time by editing the **[Title]** field and then clicking **[Save]**.

View File

@@ -42,12 +42,8 @@ exitMsg = "Change success!!"
# **********************************************************************
# ----------------------------------------------------------------------
settings = {
"performerAppend": False,
"studioAppend": False,
"tagAppend": False,
"yRenameEvenIfTitleEmpty": False,
"z_keyFIeldsIncludeInFileName": False,
"zafileRenameViaMove": False,
"zfieldKeyList": DEFAULT_FIELD_KEY_LIST,
"zmaximumTagKeys": 12,
"zseparators": DEFAULT_SEPERATOR,
@@ -114,7 +110,7 @@ if endpointHost == "0.0.0.0":
endpoint = f"{stash.JSON_INPUT['server_connection']['Scheme']}://{endpointHost}:{stash.JSON_INPUT['server_connection']['Port']}/graphql"
# stash.Trace(f"(endpoint={endpoint})")
move_files = stash.pluginSettings["zafileRenameViaMove"]
move_files = stash.Setting("fileRenameViaMove")
fieldKeyList = stash.pluginSettings["zfieldKeyList"] # Default Field Key List with the desired order
if not fieldKeyList or fieldKeyList == "":
fieldKeyList = DEFAULT_FIELD_KEY_LIST
@@ -150,7 +146,7 @@ def getPerformers(scene, title):
title = title.lower()
results = ""
for performer in scene['performers']:
name = stash.find_performer(performer['id'])['name']
name = performer['name']
stash.Trace(f"performer = {name}")
if not include_keyField_if_in_name:
if name.lower() in title:
@@ -162,7 +158,7 @@ def getPerformers(scene, title):
def getGalleries(scene, title):
results = ""
for gallery in scene['galleries']:
name = stash.find_gallery(gallery['id'])['title']
name = gallery = stash.find_gallery(gallery['id'])['title']
stash.Trace(f"gallery = {name}")
if not include_keyField_if_in_name:
if name.lower() in title:
@@ -175,10 +171,9 @@ def getTags(scene, title):
title = title.lower()
results = ""
for tag in scene['tags']:
tag_details = stash.find_tag(int(tag['id']))
name = tag_details['name']
name = tag['name']
stash.Trace(f"tag = {name}")
if excludeIgnoreAutoTags == True and tag_details['ignore_auto_tag'] == True:
if excludeIgnoreAutoTags == True and tag['ignore_auto_tag'] == True:
stash.Trace(f"Skipping tag name '{name}' because ignore_auto_tag is True.")
continue
if not include_keyField_if_in_name:
@@ -231,8 +226,12 @@ def form_filename(original_file_stem, scene_details):
for key in fieldKeyList:
if key == 'studio':
if stash.pluginSettings["studioAppend"]:
studio_name = scene_details.get('code', {})
if stash.Setting("studioAppendEnable"):
studio = scene_details.get('studio')
if studio != None:
studio_name = studio.get('name')
else:
studio_name = None
stash.Trace(f"(studio_name={studio_name})")
if studio_name:
studio_name += POSTFIX_STYLES.get('studio')
@@ -251,7 +250,7 @@ def form_filename(original_file_stem, scene_details):
else:
filename_parts.append(title)
elif key == 'performers':
if stash.pluginSettings["performerAppend"]:
if stash.Setting("performerAppendEnable"):
performers = getPerformers(scene_details, title)
if performers != "":
performers += POSTFIX_STYLES.get('performers')
@@ -261,7 +260,7 @@ def form_filename(original_file_stem, scene_details):
else:
filename_parts.append(performers)
elif key == 'date':
scene_date = scene_details.get('date', '')
scene_date = scene_details.get('date')
if scene_date:
scene_date += POSTFIX_STYLES.get('date')
if WRAPPER_STYLES.get('date'):
@@ -269,8 +268,10 @@ def form_filename(original_file_stem, scene_details):
if scene_date not in title:
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
# 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
width = str(scene_details['files'][0]['width'])
height = str(scene_details['files'][0]['height'])
if width and height:
resolution = width + POSTFIX_STYLES.get('width_height_seperator') + height + POSTFIX_STYLES.get('resolution')
if WRAPPER_STYLES.get('resolution'):
@@ -278,7 +279,7 @@ def form_filename(original_file_stem, scene_details):
if resolution not in title:
filename_parts.append(resolution)
elif key == 'width':
width = str(scene_details.get('files', [{}])[0].get('width', '')) # Convert width to string
width = str(scene_details['files'][0]['width'])
if width:
width += POSTFIX_STYLES.get('width')
if WRAPPER_STYLES.get('width'):
@@ -286,7 +287,7 @@ def form_filename(original_file_stem, scene_details):
if width not in title:
filename_parts.append(width)
elif key == 'height':
height = str(scene_details.get('files', [{}])[0].get('height', '')) # Convert height to string
height = str(scene_details['files'][0]['height'])
if height:
height += POSTFIX_STYLES.get('height')
if WRAPPER_STYLES.get('height'):
@@ -294,7 +295,7 @@ def form_filename(original_file_stem, scene_details):
if height not in title:
filename_parts.append(height)
elif key == 'video_codec':
video_codec = scene_details.get('files', [{}])[0].get('video_codec', '').upper() # Convert to uppercase
video_codec = scene_details['files'][0]['video_codec'].upper() # Convert to uppercase
if video_codec:
video_codec += POSTFIX_STYLES.get('video_codec')
if WRAPPER_STYLES.get('video_codec'):
@@ -302,7 +303,7 @@ def form_filename(original_file_stem, scene_details):
if video_codec not in title:
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'
frame_rate = str(scene_details['files'][0]['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'):
@@ -319,7 +320,7 @@ def form_filename(original_file_stem, scene_details):
filename_parts.append(galleries)
stash.Trace(f"(galleries={galleries})")
elif key == 'tags':
if stash.pluginSettings["tagAppend"]:
if stash.Setting("tagAppendEnable"):
tags = getTags(scene_details, title)
if tags != "":
tags += POSTFIX_STYLES.get('tag')
@@ -343,7 +344,8 @@ def form_filename(original_file_stem, scene_details):
def rename_scene(scene_id):
global exitMsg
POST_SCAN_DELAY = 3
scene_details = stash.find_scene(scene_id)
fragment = 'id title performers {name} tags {id name ignore_auto_tag} studio {name} galleries {id} files {id path width height video_codec frame_rate} date'
scene_details = stash.find_scene(scene_id, fragment)
stash.Trace(f"(scene_details={scene_details})")
if not scene_details:
stash.Error(f"Scene with ID {scene_id} not found.")

View File

@@ -1,6 +1,6 @@
name: RenameFile
description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab.
version: 0.5.5
version: 0.5.6
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile
ui:
css:
@@ -8,18 +8,6 @@ ui:
javascript:
- renamefile.js
settings:
performerAppend:
displayName: Append Performers
description: Enable to append performers name to file name when renaming a file. Requires performers to be included in [Key Fields] list, which by default it is included.
type: BOOLEAN
studioAppend:
displayName: Append Studio
description: Enable to append studio name to file name when renaming a file. Requires studio to be included in [Key Fields] list, which by default it is included.
type: BOOLEAN
tagAppend:
displayName: Append Tags
description: Enable to append tag names to file name when renaming a file. Requires tags to be included in [Key Fields] list, which by default it is included.
type: BOOLEAN
yRenameEvenIfTitleEmpty:
displayName: Empty Title Rename
description: If enable, rename files even if TITLE field is empty.
@@ -28,10 +16,6 @@ settings:
displayName: Include Existing Key Field
description: Enable to append performer, tags, studios, & galleries even if name already exists in the original file name.
type: BOOLEAN
zafileRenameViaMove:
displayName: Move Instead of Rename
description: Enable to move file instead of rename file. (Not recommended for Windows OS)
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,resolution,width,height,video_codec,frame_rate,date".'

View File

@@ -49,6 +49,14 @@ config = {
"max_filename_length": 255,
# Exclude tags with ignore_auto_tag set to True
"excludeIgnoreAutoTags": True,
# Enable to append performers name to file name when renaming a file. Requires performers to be included in [Key Fields] list, which by default it is included.
"performerAppendEnable": True,
# Enable to append studio name to file name when renaming a file. Requires studio to be included in [Key Fields] list, which by default it is included.
"studioAppendEnable": True,
# Enable to append tag names to file name when renaming a file. Requires tags to be included in [Key Fields] list, which by default it is included.
"tagAppendEnable": True,
# Enable to move file instead of rename file. (Not recommended for Windows OS)
"fileRenameViaMove": False,
# handleExe is for Windows only.
# In Windows, a file can't be renamed if the file is opened by another process.

View File

@@ -0,0 +1,6 @@
##### This page was added starting on version 0.5.6 to keep track of newly added features between versions.
### 0.5.6
- Fixed bug with studio getting the studio ID instead of the name of the studio in rename process.
- Improved performance by having code get all required scene details in one call to stash.
- To remove UI clutter, move rarely used options (performerAppendEnable, studioAppendEnable, tagAppendEnable, & fileRenameViaMove) to renamefile_settings.py
- Change options (performerAppendEnable, studioAppendEnable, tagAppendEnable) to default to True (enabled)