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 import traceback, sys
tb = traceback.format_exc() tb = traceback.format_exc()
print(f"ModulesValidate Exception. Error: {e}\nTraceBack={tb}", file=sys.stderr) 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 datetime import datetime
from StashPluginHelper import StashPluginHelper from StashPluginHelper import StashPluginHelper
from stashapi.stash_types import PhashDistance from stashapi.stash_types import PhashDistance
@@ -36,6 +36,7 @@ settings = {
"zvWhitelist": "", "zvWhitelist": "",
"zwGraylist": "", "zwGraylist": "",
"zxBlacklist": "", "zxBlacklist": "",
"zxPinklist": "",
"zyMaxDupToProcess": 0, "zyMaxDupToProcess": 0,
"zySwapHighRes": False, "zySwapHighRes": False,
"zySwapLongLength": False, "zySwapLongLength": False,
@@ -69,7 +70,7 @@ stash = StashPluginHelper(
) )
stash.convertToAscii = True 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", "commonResToDeleteLess", "commonResToDeleteEq", "commonResToDeleteGreater", "commonResToDeleteBlacklistLess", "commonResToDeleteBlacklistEq", "commonResToDeleteBlacklistGreater", "resolutionToDeleteLess", "resolutionToDeleteEq", "resolutionToDeleteGreater",
"resolutionToDeleteBlacklistLess", "resolutionToDeleteBlacklistEq", "resolutionToDeleteBlacklistGreater", "ratingToDeleteLess", "ratingToDeleteEq", "ratingToDeleteGreater", "ratingToDeleteBlacklistLess", "ratingToDeleteBlacklistEq", "ratingToDeleteBlacklistGreater", "resolutionToDeleteBlacklistLess", "resolutionToDeleteBlacklistEq", "resolutionToDeleteBlacklistGreater", "ratingToDeleteLess", "ratingToDeleteEq", "ratingToDeleteGreater", "ratingToDeleteBlacklistLess", "ratingToDeleteBlacklistEq", "ratingToDeleteBlacklistGreater",
"tagToDelete", "tagToDeleteBlacklist", "titleToDelete", "titleToDeleteBlacklist", "pathStrToDelete", "pathStrToDeleteBlacklist"] "tagToDelete", "tagToDeleteBlacklist", "titleToDelete", "titleToDeleteBlacklist", "pathStrToDelete", "pathStrToDeleteBlacklist"]
@@ -232,7 +233,10 @@ stash.Trace(f"whitelist = {whitelist}")
blacklist = stash.Setting('zxBlacklist').split(listSeparator) blacklist = stash.Setting('zxBlacklist').split(listSeparator)
blacklist = [item.lower() for item in blacklist] blacklist = [item.lower() for item in blacklist]
if blacklist == [""] : 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): def realpath(path):
""" """
@@ -561,6 +565,10 @@ htmlHighlightTimeDiff = stash.Setting('htmlHighlightTimeDiff')
htmlPreviewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview" htmlPreviewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview"
def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel = "?", tagDuplicates = False): 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 dupFileExist = True if os.path.isfile(DupFile['files'][0]['path']) else False
toKeepFileExist = True if os.path.isfile(DupFileToKeep['files'][0]['path']) else False toKeepFileExist = True if os.path.isfile(DupFileToKeep['files'][0]['path']) else False
fileHtmlReport.write(f"{htmlReportTableRow}") 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>") fileHtmlReport.write(f"<a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFile)}\">[Play]</a>")
else: else:
fileHtmlReport.write("<b style='color:red;'>[File NOT Exist]<b>") 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>") fileHtmlReport.write("</p></td>")
videoPreview = f"<video {htmlReportVideoPreview} poster=\"{DupFileToKeep['paths']['screenshot']}\"><source src=\"{DupFileToKeep['paths'][htmlPreviewOrStream]}\" type=\"video/mp4\"></video>" 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: else:
fileHtmlReport.write("<b style='color:red;'>[File NOT Exist]<b>") 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>") 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: # ToDo: Add following buttons:
# rename file # rename file
fileHtmlReport.write(f"</p></td>") fileHtmlReport.write(f"</p></td>")
fileHtmlReport.write("</tr>\n") 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): def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlacklistOnly=False, deleteLowerResAndDuration=False):
global reasonDict global reasonDict
global htmlFileData
duplicateMarkForDeletion_descp = 'Tag added to duplicate scenes so-as to tag them for deletion.' duplicateMarkForDeletion_descp = 'Tag added to duplicate scenes so-as to tag them for deletion.'
stash.Trace(f"duplicateMarkForDeletion = {duplicateMarkForDeletion}") stash.Trace(f"duplicateMarkForDeletion = {duplicateMarkForDeletion}")
dupTagId = stash.createTagId(duplicateMarkForDeletion, duplicateMarkForDeletion_descp, ignoreAutoTag=True) dupTagId = stash.createTagId(duplicateMarkForDeletion, duplicateMarkForDeletion_descp, ignoreAutoTag=True)
@@ -671,27 +729,29 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
stash.Trace("#########################################################################") stash.Trace("#########################################################################")
stash.Log(f"Waiting for find_duplicate_scenes_diff to return results; matchDupDistance={matchPhaseDistanceText}; significantTimeDiff={significantTimeDiff}", printTo=LOG_STASH_N_PLUGIN) stash.Log(f"Waiting for find_duplicate_scenes_diff to return results; matchDupDistance={matchPhaseDistanceText}; significantTimeDiff={significantTimeDiff}", printTo=LOG_STASH_N_PLUGIN)
stash.startSpinningProcessBar() stash.startSpinningProcessBar()
htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} " if createHtmlReport else "" mergeFieldData = " code director title rating100 date studio {id name} movies {movie {id} } urls " if merge else ""
mergeFieldData = " code director title rating100 date studio {id} movies {movie {id} } galleries {id} performers {id} urls " if merge else "" if not createHtmlReport:
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) htmlFileData = ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment= fragmentForSceneDetails + mergeFieldData + htmlFileData)
stash.stopSpinningProcessBar() stash.stopSpinningProcessBar()
qtyResults = len(DupFileSets) qtyResults = len(DupFileSets)
stash.setProgressBarIter(qtyResults) stash.setProgressBarIter(qtyResults)
stash.Trace("#########################################################################") stash.Trace("#########################################################################")
stash.Log(f"Found {qtyResults} duplicate sets...") stash.Log(f"Found {qtyResults} duplicate sets...")
fileHtmlReport = None fileHtmlReport = None
if createHtmlReport: if not os.path.isdir(htmlReportNameFolder):
os.mkdir(htmlReportNameFolder)
if not os.path.isdir(htmlReportNameFolder): if not os.path.isdir(htmlReportNameFolder):
os.mkdir(htmlReportNameFolder) stash.Error(f"Failed to create report directory {htmlReportNameFolder}.")
if not os.path.isdir(htmlReportNameFolder): return
stash.Error(f"Failed to create report directory {htmlReportNameFolder}.") if createHtmlReport:
return
deleteLocalDupReportHtmlFiles(False) deleteLocalDupReportHtmlFiles(False)
fileHtmlReport = open(htmlReportName, "w") fileHtmlReport = open(htmlReportName, "w")
fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n") fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n")
fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n") fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n")
htmlReportTableHeader = stash.Setting('htmlReportTableHeader') 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") 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: for DupFileSet in DupFileSets:
# stash.Trace(f"DupFileSet={DupFileSet}", toAscii=True) # stash.Trace(f"DupFileSet={DupFileSet}", toAscii=True)
@@ -819,6 +879,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
elif moveToTrashCan: elif moveToTrashCan:
sendToTrash(DupFileName) sendToTrash(DupFileName)
stash.destroyScene(DupFile['id'], delete_file=True) stash.destroyScene(DupFile['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(DupFile['id'], removeScene = True)
elif tagDuplicates or fileHtmlReport != None: elif tagDuplicates or fileHtmlReport != None:
if excludeFromReportIfSignificantTimeDiff and significantTimeDiffCheck(DupFile, DupFileToKeep, True): 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']}.") 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 # add delete only from stash db code and button using DB delete icon
stash.Debug(f"Adding scene {DupFile['id']} to HTML report.") stash.Debug(f"Adding scene {DupFile['id']} to HTML report.")
writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel, tagDuplicates) writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel, tagDuplicates)
fileDuplicateCandidateForDeletionList.write(json.dumps(DupFile) + "\n")
if QtyTagForDelPaginate >= htmlReportPaginate: if QtyTagForDelPaginate >= htmlReportPaginate:
QtyTagForDelPaginate = 0 QtyTagForDelPaginate = 0
fileHtmlReport.write("</table>\n") 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)): if maxDupToProcess > 0 and ((QtyTagForDel > maxDupToProcess) or (QtyTagForDel == 0 and QtyDup > maxDupToProcess)):
break break
fileDuplicateCandidateForDeletionList.close()
if fileHtmlReport != None: if fileHtmlReport != None:
fileHtmlReport.write("</table>\n") fileHtmlReport.write("</table>\n")
if PaginateId > 0: if PaginateId > 0:
@@ -921,26 +984,32 @@ def findCurrentTagId(tagNames):
for tagName in tagNames: for tagName in tagNames:
tagId = stash.find_tags(q=tagName) tagId = stash.find_tags(q=tagName)
if len(tagId) > 0 and 'id' in tagId[0]: 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'], tagName
return tagId[0]['id'] return "-1", None
return "-1"
def toJson(data): def toJson(data):
import json
# data = data.replace("'", '"') # data = data.replace("'", '"')
data = data.replace("\\", "\\\\") data = data.replace("\\", "\\\\")
data = data.replace("\\\\\\\\", "\\\\") data = data.replace("\\\\\\\\", "\\\\")
return json.loads(data) 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}") stash.Log(f"Processing taskName = {taskName}, target = {target}")
if "Blacklist" in taskName: if "Blacklist" in taskName:
isBlackList = True isBlackList = True
if "Graylist" in taskName:
isGrayList = True
if "Pinklist" in taskName:
isPinkList = True
if "Less" in taskName: if "Less" in taskName:
compareToLess = True compareToLess = True
if "Greater" in taskName: if "Greater" in taskName:
compareToGreater = True compareToGreater = True
if ":TagOnlyScenes" in target:
isTagOnlyScenes = True
target = target.replace(":TagOnlyScenes","")
if "pathToDelete" in taskName: if "pathToDelete" in taskName:
pathToDelete = target.lower() pathToDelete = target.lower()
elif "sizeToDelete" in taskName: elif "sizeToDelete" in taskName:
@@ -961,10 +1030,15 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isBlackList, pathToDelete,
pathStrToDelete = target.lower() pathStrToDelete = target.lower()
elif "fileNotExistToDelete" in taskName: elif "fileNotExistToDelete" in taskName:
fileNotExistToDelete = True 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): def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
isTagOnlyScenes = False
isBlackList = False isBlackList = False
isGrayList = False
isPinkList = False
pathToDelete = "" pathToDelete = ""
sizeToDelete = -1 sizeToDelete = -1
durationToDelete = -1 durationToDelete = -1
@@ -982,16 +1056,27 @@ def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
if "applyCombo" in stash.PLUGIN_TASK_NAME: if "applyCombo" in stash.PLUGIN_TASK_NAME:
jsonObject = toJson(stash.JSON_INPUT['args']['Target']) jsonObject = toJson(stash.JSON_INPUT['args']['Target'])
for taskName in jsonObject: 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: else:
return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, 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 isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, 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): def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=False, tagId=-1, advanceMenuOptionSelected=False):
tagName = None
if tagId == -1: 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: if int(tagId) < 0:
stash.Warn(f"Could not find tag ID for tag '{duplicateMarkForDeletion}'.") stash.Warn(f"Could not find tag ID for tag '{duplicateMarkForDeletion}'.")
return return
@@ -1000,7 +1085,7 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if clearAllDupfileManagerTags: if clearAllDupfileManagerTags:
excludedTags = [duplicateMarkForDeletion, duplicateWhitelistTag, excludeDupFileDeleteTag, graylistMarkForDeletion, longerDurationLowerResolution] 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: 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.") stash.Error("Running advance menu option with no options enabled.")
return return
@@ -1012,7 +1097,11 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
QtyFailedQuery = 0 QtyFailedQuery = 0
stash.Debug("#########################################################################") stash.Debug("#########################################################################")
stash.startSpinningProcessBar() stash.startSpinningProcessBar()
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') 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() stash.stopSpinningProcessBar()
qtyResults = len(scenes) qtyResults = len(scenes)
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})") stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
@@ -1063,6 +1152,12 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if isBlackList: if isBlackList:
if not stash.startsWithInList(blacklist, scene['files'][0]['path']): if not stash.startsWithInList(blacklist, scene['files'][0]['path']):
continue 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 pathToDelete != "":
if not DupFileName.lower().startswith(pathToDelete): if not DupFileName.lower().startswith(pathToDelete):
stash.Debug(f"Skipping file {DupFileName} because it does not start with {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 fileNotExistToDelete:
if os.path.isfile(scene['files'][0]['path']): if os.path.isfile(scene['files'][0]['path']):
continue 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 != "": if alternateTrashCanPath != "":
destPath = f"{alternateTrashCanPath }{os.sep}{DupFileNameOnly}" destPath = f"{alternateTrashCanPath }{os.sep}{DupFileNameOnly}"
if os.path.isfile(destPath): if os.path.isfile(destPath):
@@ -1142,6 +1237,7 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
elif moveToTrashCan: elif moveToTrashCan:
sendToTrash(DupFileName) sendToTrash(DupFileName)
result = stash.destroyScene(scene['id'], delete_file=True) result = stash.destroyScene(scene['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(scene['id'], removeScene = True)
QtyDeleted += 1 QtyDeleted += 1
stash.Debug(f"destroyScene result={result} for file {DupFileName};QtyDeleted={QtyDeleted};Count={QtyDup} of {qtyResults}", toAscii=True) stash.Debug(f"destroyScene result={result} for file {DupFileName};QtyDeleted={QtyDeleted};Count={QtyDup} of {qtyResults}", toAscii=True)
else: else:
@@ -1217,6 +1313,7 @@ def mergeTags():
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}") sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
return return
stash.mergeMetadata(scene1, scene2) stash.mergeMetadata(scene1, scene2)
updateScenesInReports(scene2['id'])
stash.Log(f"Done merging scenes for scene {scene1['id']} and scene {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']}'" + "}") sys.stdout.write("{" + f"mergeTags : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}'" + "}")
@@ -1277,8 +1374,33 @@ def removeAllDupTagsFromAllScenes(deleteTags=False):
else: else:
stash.Log(f"Clear tags {tagsToClear}") 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): def updateScenesInReport(fileName, scene):
stash.Log(f"Updating table rows with scene {scene} in file {fileName}") stash.Log(f"Updating table rows with scene {scene} in file {fileName}")
results = False
scene1 = -1 scene1 = -1
scene2 = -1 scene2 = -1
strToFind = "class=\"ID_" strToFind = "class=\"ID_"
@@ -1286,10 +1408,12 @@ def updateScenesInReport(fileName, scene):
with open(fileName, 'r') as file: with open(fileName, 'r') as file:
lines = file.readlines() lines = file.readlines()
stash.Log(f"line count = {len(lines)}") stash.Log(f"line count = {len(lines)}")
stash.Log(f"Searching for class=\"ID_{scene}\"")
with open(fileName, 'w') as file: with open(fileName, 'w') as file:
for line in lines: for line in lines:
# stash.Debug(f"line = {line}") # stash.Debug(f"line = {line}")
if f"class=\"ID_{scene}\"" in line: if f"class=\"ID_{scene}\"" in line:
stash.Debug(f"Found class ID_{scene} in line: {line}")
idx = 0 idx = 0
while line.find(strToFind, idx) > -1: while line.find(strToFind, idx) > -1:
idx = line.find(strToFind, idx) + len(strToFind) idx = line.find(strToFind, idx) + len(strToFind)
@@ -1304,30 +1428,41 @@ def updateScenesInReport(fileName, scene):
elif scene1 != -1 and scene2 != -1: elif scene1 != -1 and scene2 != -1:
break break
if scene1 != -1 and scene2 != -1: if scene1 != -1 and scene2 != -1:
sceneDetail1 = stash.find_scene(scene1) sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails + htmlFileData)
sceneDetail2 = stash.find_scene(scene2) sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails + htmlFileData)
if sceneDetail1 == None or sceneDetail2 == None: if sceneDetails1 == None or sceneDetails2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetail1={sceneDetail1}; sceneDetail2={sceneDetail2};") stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetails1={sceneDetails1}; sceneDetails2={sceneDetails2};")
else: 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: else:
stash.Error(f"Could not get both scene ID associated with scene {scene}; scene1 = {scene1}; scene2 = {scene2}") stash.Error(f"Could not get both scene ID associated with scene {scene}; scene1 = {scene1}; scene2 = {scene2}")
file.write(line) file.write(line)
else: else:
file.write(line) 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): def updateScenesInReports(scene, ReportName = htmlReportName):
if os.path.isfile(ReportName): if os.path.isfile(ReportName):
updateScenesInReport(ReportName, scene) if updateScenesInReport(ReportName, scene):
return
for x in range(2, 9999): for x in range(2, 9999):
fileName = ReportName.replace(".html", f"_{x-1}.html") fileName = ReportName.replace(".html", f"_{x-1}.html")
stash.Debug(f"Checking if file '{fileName}' exist.") stash.Debug(f"Checking if file '{fileName}' exist.")
if not os.path.isfile(fileName): if not os.path.isfile(fileName):
break break
updateScenesInReport(fileName, scene) if updateScenesInReport(fileName, scene):
break
stash.Debug("updateScenesInReports complete")
else: else:
stash.Log(f"Report file does not exist: {ReportName}") stash.Log(f"Report file does not exist: {ReportName}")
deleteSceneFlagBgColor = "#646464"
def addPropertyToSceneClass(fileName, scene, property): def addPropertyToSceneClass(fileName, scene, property):
stash.Log(f"Inserting property {property} for scene {scene} in file {fileName}") stash.Log(f"Inserting property {property} for scene {scene} in file {fileName}")
doStyleEndTagCheck = True doStyleEndTagCheck = True
@@ -1339,14 +1474,20 @@ def addPropertyToSceneClass(fileName, scene, property):
for line in lines: for line in lines:
# stash.Debug(f"line = {line}") # stash.Debug(f"line = {line}")
if doStyleEndTagCheck: if doStyleEndTagCheck:
if property == "" and line.startswith(f".ID_{scene}" + "{"): if scene == None:
continue if line.startswith(f".ID_") and deleteSceneFlagBgColor not in line:
if line.startswith("</style>"): continue
if property != "": elif line.startswith("</style>"):
styleSetting = f".ID_{scene}{property}\n" doStyleEndTagCheck = False
stash.Log(f"styleSetting = {styleSetting}") else:
file.write(styleSetting) if property == "" and line.startswith(f".ID_{scene}" + "{"):
doStyleEndTagCheck = False continue
if line.startswith("</style>"):
if property != "":
styleSetting = f".ID_{scene}{property}\n"
stash.Log(f"styleSetting = {styleSetting}")
file.write(styleSetting)
doStyleEndTagCheck = False
file.write(line) file.write(line)
def addPropertyToSceneClassToAllFiles(scene, property, ReportName = htmlReportName): def addPropertyToSceneClassToAllFiles(scene, property, ReportName = htmlReportName):
@@ -1370,10 +1511,16 @@ def deleteScene(disableInReport=True, deleteFile=True):
result = None result = None
result = stash.destroyScene(scene, delete_file=deleteFile) result = stash.destroyScene(scene, delete_file=deleteFile)
if disableInReport: 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}") 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}'" + "}") 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): def copyScene(moveScene=False):
scene1, scene2 = getParseData() scene1, scene2 = getParseData()
if scene1 == None or scene2 == None: 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']) result = shutil.copy(scene1['files'][0]['path'], scene2['files'][0]['path'])
if moveScene: if moveScene:
result = stash.destroyScene(scene1['id'], delete_file=True) 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"destroyScene for scene {scene1['id']} results = {result}")
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene1['id']} and {scene2['id']}") 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}'" + "}") 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": elif stash.PLUGIN_TASK_NAME == "flagScene":
flagScene() flagScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") 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": elif stash.PLUGIN_TASK_NAME == "copyScene":
copyScene() copyScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")

View File

@@ -1,6 +1,6 @@
name: DupFileManager name: DupFileManager
description: Manages duplicate files. description: Manages duplicate files.
version: 0.2.1 version: 0.2.2
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui: ui:
javascript: javascript:
@@ -34,6 +34,10 @@ settings:
displayName: Black List displayName: Black List
description: Least preferential paths; Determine primary deletion candidates. E.g. C:\Downloads,C:\DelMe-3rd,C:\DelMe-2nd,C:\DeleteMeFirst description: Least preferential paths; Determine primary deletion candidates. E.g. C:\Downloads,C:\DelMe-3rd,C:\DelMe-2nd,C:\DeleteMeFirst
type: STRING 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: zyMaxDupToProcess:
displayName: Max Dup Process displayName: Max Dup Process
description: (Default=0) Maximum number of duplicates to process. If 0, infinity. description: (Default=0) Maximum number of duplicates to process. If 0, infinity.

View File

@@ -64,8 +64,93 @@ li:hover .large {
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);; 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> </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> <script src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css"/> <link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css"/>
<script> <script>
@@ -111,11 +196,8 @@ function RunPluginOperation(Mode, ActionID, button, asyncAjax){
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}}, variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){ }), success: function(result){
console.log(result); console.log(result);
// if (Mode !== "flagScene") button.style.visibility = 'hidden'; if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags")
if (Mode === "renameFile"){ location.replace(location.href);
var myArray = ActionID.split(":");
$('.FN_ID_' + myArray[0]).text(trim(myArray[1],"'"));
}
if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete."); if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.");
}, error: function(XMLHttpRequest, textStatus, errorThrown) { }, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown); console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
@@ -191,7 +273,7 @@ $(document).ready(function(){
$("button").click(function(){ $("button").click(function(){
var Mode = this.value; var Mode = this.value;
var ActionID = this.id; var ActionID = this.id;
if (ActionID === "AdvanceMenu") if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{ {
var newUrl = window.location.href; var newUrl = window.location.href;
newUrl = newUrl.replace(/report\/DuplicateTagScenes[_0-9]*.html/g, "advance_options.html?GQL=" + GraphQl_URL + "&apiKey=" + apiKey); 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><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="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><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></td>
</tr></table></center> </tr></table></center>
<h2>Stash Duplicate Scenes Report (MatchTypePlaceHolder)</h2>\n""", <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. 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. 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) - ![Screenshot 2024-11-22 232208](https://github.com/user-attachments/assets/bf1f3021-3a8c-4875-9737-60ee3d7fe675)
### Future Planned Features ### 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. - Consolidate buttons in report into dropdown buttons. Planned for 1.0.1 Version.
- Greylist deletion option will be added to the advanced menu. Planned for 1.0.5 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 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. - 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; color:red;
} }
html.wait, html.wait * { cursor: wait !important; } 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> </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> <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 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 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'); GraphQl_URL = urlParams.get('GQL');
console.log(urlParams); console.log(urlParams);
console.log("GQL = " + GraphQl_URL); 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'); $('html').addClass('wait');
$("body").css("cursor", "progress"); $("body").css("cursor", "progress");
if (TagOnlyScenes)
Param += ":TagOnlyScenes";
console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param); console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param);
//if ($("#InPathCheck").prop('checked'))
if (apiKey !== "") if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}}); $.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
$.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: Async, async: Async, $.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"); console.log("Setting default cursor");
} }
$(document).ready(function(){ $(document).ready(function(){
$("button").click(function(){ $("button").click(function(){
const AddedWarn = "? This will delete the files, and remove them from stash."; const AddedWarn = "? This will delete the files, and remove them from stash.";
console.log(this.id); console.log(this.id);
var blackliststr = ""; var blackliststr = "";
var comparestr = "less than "; var comparestr = "less than ";
if (this.id.includes("Blacklist")) blackliststr = "in blacklist "; 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 "; if (this.id.includes("Greater")) comparestr = "greater than ";
else if (this.id.includes("Eq")) comparestr = "equal to "; else if (this.id.includes("Eq")) comparestr = "equal to ";
@@ -94,27 +140,27 @@ $(document).ready(function(){
else if (this.id === "pathToDelete" || this.id === "pathToDeleteBlacklist") 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)) 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") 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)) 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") 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)) 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") 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)) 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") 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)) 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") 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 else
result = confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having rating " + comparestr + $("#ratingToDelete").val() + AddedWarn); result = confirm("Are you sure you want to delete duplicate tag scenes " + blackliststr + "having rating " + comparestr + $("#ratingToDelete").val() + AddedWarn);
if (result) if (result)
RunPluginDupFileManager(this.id, $("#ratingToDelete").val()); RunPluginDupFileManager(this.id, $("#ratingToDelete").val(), true, $("#DupTagOnlyCheck").prop('checked'));
} }
else if (this.id === "tagToDelete" || this.id === "tagToDeleteBlacklist") 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)) 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") 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)) 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") 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)) 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") 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?")) 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 = ""; var Blacklist = "";
if (this.id === "applyComboBlacklist") if (this.id === "applyComboBlacklist")
Blacklist = "Blacklist"; Blacklist = "Blacklist";
else if (this.id === "applyComboGraylist")
Blacklist = "Graylist";
else if (this.id === "applyComboPinklist")
Blacklist = "Pinklist";
var Param = "{"; var Param = "{";
if ($("#InPathCheck").prop('checked')) if ($("#InPathCheck").prop('checked'))
Param += "\"" + "pathToDelete" + Blacklist + "\":\"" + $("#pathToDeleteText").val().replace("\\", "\\\\") + "\", "; Param += "\"" + "pathToDelete" + Blacklist + "\":\"" + $("#pathToDeleteText").val().replace("\\", "\\\\") + "\", ";
@@ -181,6 +231,8 @@ $(document).ready(function(){
Param += "\"" + "pathStrToDelete" + Blacklist + "\":\"" + $("#pathStrToDeleteText").val().replace("\\", "\\\\") + "\", "; Param += "\"" + "pathStrToDelete" + Blacklist + "\":\"" + $("#pathStrToDeleteText").val().replace("\\", "\\\\") + "\", ";
if ($("#fileNotExistCheck").prop('checked')) if ($("#fileNotExistCheck").prop('checked'))
Param += "\"" + "fileNotExistToDelete" + Blacklist + "\":\"true\", "; Param += "\"" + "fileNotExistToDelete" + Blacklist + "\":\"true\", ";
if ($("#DupTagOnlyCheck_MultiOption").prop('checked'))
Param += "\"" + "TagOnlyScenes" + Blacklist + "\":\"true\", ";
Param += '}'; Param += '}';
Param = Param.replace(', }', '}'); Param = Param.replace(', }', '}');
if (Param === "{}") if (Param === "{}")
@@ -190,7 +242,7 @@ $(document).ready(function(){
} }
console.log(Param); 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)) 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> </head>
<body> <body>
<center><table style="color:darkgreen;background-color:powderblue;"> <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> <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> <td>
<button type="button" id="applyCombo" title="Apply selected multiple options to delete scenes.">Delete</button> <table style="border-collapse: collapse; border: none;"><tr style="border: none;">
<button type="button" id="applyComboBlacklist" title="Apply selected multiple options to delete scenes in blacklist.">Delete-Blacklist</button> <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>
<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>
<tr> <tr>
<td><form id="pathToDeleteForm" action="javascript:DeleteDupInPath();" target="_self"> <td><form id="pathToDeleteForm" action="javascript:DeleteDupInPath();" target="_self">
@@ -1797,50 +1883,6 @@ function DeleteDupInPath(){
<option value="Greater">Greater</option> <option value="Greater">Greater</option>
</select> </select>
</td> </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>
<tr><td style="font-size: 12px;" colspan="2"> <tr><td style="font-size: 12px;" colspan="2">
<b>Details:</b> <b>Details:</b>
@@ -1901,6 +1943,8 @@ function DeleteDupInPath(){
</ol> </ol>
</td></tr> </td></tr>
</table></center> </table></center>
<div id="div1"></div>
</body></html> </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 ### 0.2.0
- For report, added logic to transfer option settings **[Disable Complete Confirmation]** and **[Disable Delete Confirmation]** when paginating. - 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. - Fixed minor bug in advance_options.html for GQL params.
### 0.2.1 ### 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. - 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. - 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. 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]**. - 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 = { settings = {
"performerAppend": False,
"studioAppend": False,
"tagAppend": False,
"yRenameEvenIfTitleEmpty": False, "yRenameEvenIfTitleEmpty": False,
"z_keyFIeldsIncludeInFileName": False, "z_keyFIeldsIncludeInFileName": False,
"zafileRenameViaMove": False,
"zfieldKeyList": DEFAULT_FIELD_KEY_LIST, "zfieldKeyList": DEFAULT_FIELD_KEY_LIST,
"zmaximumTagKeys": 12, "zmaximumTagKeys": 12,
"zseparators": DEFAULT_SEPERATOR, "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" endpoint = f"{stash.JSON_INPUT['server_connection']['Scheme']}://{endpointHost}:{stash.JSON_INPUT['server_connection']['Port']}/graphql"
# stash.Trace(f"(endpoint={endpoint})") # 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 fieldKeyList = stash.pluginSettings["zfieldKeyList"] # Default Field Key List with the desired order
if not fieldKeyList or fieldKeyList == "": if not fieldKeyList or fieldKeyList == "":
fieldKeyList = DEFAULT_FIELD_KEY_LIST fieldKeyList = DEFAULT_FIELD_KEY_LIST
@@ -150,7 +146,7 @@ def getPerformers(scene, title):
title = title.lower() title = title.lower()
results = "" results = ""
for performer in scene['performers']: for performer in scene['performers']:
name = stash.find_performer(performer['id'])['name'] name = performer['name']
stash.Trace(f"performer = {name}") stash.Trace(f"performer = {name}")
if not include_keyField_if_in_name: if not include_keyField_if_in_name:
if name.lower() in title: if name.lower() in title:
@@ -162,7 +158,7 @@ def getPerformers(scene, title):
def getGalleries(scene, title): def getGalleries(scene, title):
results = "" results = ""
for gallery in scene['galleries']: 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}") stash.Trace(f"gallery = {name}")
if not include_keyField_if_in_name: if not include_keyField_if_in_name:
if name.lower() in title: if name.lower() in title:
@@ -175,10 +171,9 @@ def getTags(scene, title):
title = title.lower() title = title.lower()
results = "" results = ""
for tag in scene['tags']: for tag in scene['tags']:
tag_details = stash.find_tag(int(tag['id'])) name = tag['name']
name = tag_details['name']
stash.Trace(f"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.") stash.Trace(f"Skipping tag name '{name}' because ignore_auto_tag is True.")
continue continue
if not include_keyField_if_in_name: if not include_keyField_if_in_name:
@@ -231,8 +226,12 @@ def form_filename(original_file_stem, scene_details):
for key in fieldKeyList: for key in fieldKeyList:
if key == 'studio': if key == 'studio':
if stash.pluginSettings["studioAppend"]: if stash.Setting("studioAppendEnable"):
studio_name = scene_details.get('code', {}) studio = scene_details.get('studio')
if studio != None:
studio_name = studio.get('name')
else:
studio_name = None
stash.Trace(f"(studio_name={studio_name})") stash.Trace(f"(studio_name={studio_name})")
if studio_name: if studio_name:
studio_name += POSTFIX_STYLES.get('studio') studio_name += POSTFIX_STYLES.get('studio')
@@ -251,7 +250,7 @@ def form_filename(original_file_stem, scene_details):
else: else:
filename_parts.append(title) filename_parts.append(title)
elif key == 'performers': elif key == 'performers':
if stash.pluginSettings["performerAppend"]: if stash.Setting("performerAppendEnable"):
performers = getPerformers(scene_details, title) performers = getPerformers(scene_details, title)
if performers != "": if performers != "":
performers += POSTFIX_STYLES.get('performers') performers += POSTFIX_STYLES.get('performers')
@@ -261,7 +260,7 @@ def form_filename(original_file_stem, scene_details):
else: else:
filename_parts.append(performers) filename_parts.append(performers)
elif key == 'date': elif key == 'date':
scene_date = scene_details.get('date', '') scene_date = scene_details.get('date')
if scene_date: if scene_date:
scene_date += POSTFIX_STYLES.get('date') scene_date += POSTFIX_STYLES.get('date')
if WRAPPER_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: if scene_date not in title:
filename_parts.append(scene_date) filename_parts.append(scene_date)
elif key == 'resolution': elif key == 'resolution':
width = str(scene_details.get('files', [{}])[0].get('width', '')) # Convert width 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 # 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: if width and height:
resolution = width + POSTFIX_STYLES.get('width_height_seperator') + height + POSTFIX_STYLES.get('resolution') resolution = width + POSTFIX_STYLES.get('width_height_seperator') + height + POSTFIX_STYLES.get('resolution')
if WRAPPER_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: if resolution not in title:
filename_parts.append(resolution) filename_parts.append(resolution)
elif key == 'width': 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: if width:
width += POSTFIX_STYLES.get('width') width += POSTFIX_STYLES.get('width')
if WRAPPER_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: if width not in title:
filename_parts.append(width) filename_parts.append(width)
elif key == 'height': 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: if height:
height += POSTFIX_STYLES.get('height') height += POSTFIX_STYLES.get('height')
if WRAPPER_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: if height not in title:
filename_parts.append(height) filename_parts.append(height)
elif key == 'video_codec': 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: if video_codec:
video_codec += POSTFIX_STYLES.get('video_codec') video_codec += POSTFIX_STYLES.get('video_codec')
if WRAPPER_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: if video_codec not in title:
filename_parts.append(video_codec) filename_parts.append(video_codec)
elif key == 'frame_rate': 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: if frame_rate:
frame_rate += POSTFIX_STYLES.get('frame_rate') frame_rate += POSTFIX_STYLES.get('frame_rate')
if WRAPPER_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) filename_parts.append(galleries)
stash.Trace(f"(galleries={galleries})") stash.Trace(f"(galleries={galleries})")
elif key == 'tags': elif key == 'tags':
if stash.pluginSettings["tagAppend"]: if stash.Setting("tagAppendEnable"):
tags = getTags(scene_details, title) tags = getTags(scene_details, title)
if tags != "": if tags != "":
tags += POSTFIX_STYLES.get('tag') tags += POSTFIX_STYLES.get('tag')
@@ -343,7 +344,8 @@ def form_filename(original_file_stem, scene_details):
def rename_scene(scene_id): def rename_scene(scene_id):
global exitMsg global exitMsg
POST_SCAN_DELAY = 3 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})") stash.Trace(f"(scene_details={scene_details})")
if not scene_details: if not scene_details:
stash.Error(f"Scene with ID {scene_id} not found.") stash.Error(f"Scene with ID {scene_id} not found.")

View File

@@ -1,6 +1,6 @@
name: RenameFile name: RenameFile
description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab. 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 url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile
ui: ui:
css: css:
@@ -8,18 +8,6 @@ ui:
javascript: javascript:
- renamefile.js - renamefile.js
settings: 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: yRenameEvenIfTitleEmpty:
displayName: Empty Title Rename displayName: Empty Title Rename
description: If enable, rename files even if TITLE field is empty. description: If enable, rename files even if TITLE field is empty.
@@ -28,10 +16,6 @@ settings:
displayName: Include Existing Key Field displayName: Include Existing Key Field
description: Enable to append performer, tags, studios, & galleries even if name already exists in the original file name. description: Enable to append performer, tags, studios, & galleries even if name already exists in the original file name.
type: BOOLEAN 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: zfieldKeyList:
displayName: Key Fields 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".' 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, "max_filename_length": 255,
# Exclude tags with ignore_auto_tag set to True # Exclude tags with ignore_auto_tag set to True
"excludeIgnoreAutoTags": 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. # handleExe is for Windows only.
# In Windows, a file can't be renamed if the file is opened by another process. # 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)