### 1.0.0.3
- Added option on report to merge all metadata missing in [**Duplicate to Keep**] files.
- Added cookies to report so as to remember user options for Disable Complete Confirmation **[Disable Complete Confirmation]** and **[Disable Delete Confirmation]**.
  - This change was needed because sometimes the browser refuse to open local URL's with params on the URL.
  - Using cookies also allows check options status to stay the same after refresh.
- Added code to [**Advance Duplicate File Deletion Menu**] to delete based on flags.
This commit is contained in:
David Maisonave
2024-11-27 15:18:41 -05:00
parent fe0c228045
commit a6f3b352a3
8 changed files with 218 additions and 69 deletions

View File

@@ -796,10 +796,12 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel =
# rename file # rename file
fileHtmlReport.write(f"</p></td>") fileHtmlReport.write(f"</p></td>")
fileHtmlReport.write("</tr>\n") fileHtmlReport.write(f"</tr><!-- ::DuplicateToDelete_SceneID={DupFile['id']}::DuplicateToKeep_SceneID={DupFileToKeep['id']}:: -->\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 ' 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 + "} " htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} "
mergeFieldData = " code director title rating100 date studio {id name} urls "
fragmentForSceneDetails += mergeFieldData + htmlFileData
DuplicateCandidateForDeletionList = f"{htmlReportNameFolder}{os.sep}DuplicateCandidateForDeletionList.txt" 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):
@@ -835,10 +837,9 @@ 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()
mergeFieldData = " code director title rating100 date studio {id name} movies {movie {id} } urls " if merge else ""
if not createHtmlReport: if not createHtmlReport:
htmlFileData = "" htmlFileData = ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment= fragmentForSceneDetails + mergeFieldData + htmlFileData) DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment=fragmentForSceneDetails)
stash.stopSpinningProcessBar() stash.stopSpinningProcessBar()
qtyResults = len(DupFileSets) qtyResults = len(DupFileSets)
stash.setProgressBarIter(qtyResults) stash.setProgressBarIter(qtyResults)
@@ -1102,14 +1103,26 @@ def toJson(data):
data = data.replace("\\\\\\\\", "\\\\") data = data.replace("\\\\\\\\", "\\\\")
return json.loads(data) return json.loads(data)
def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater): def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, tagOrFlag, 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 tagOrFlag = "Blacklist"
if "Graylist" in taskName: if "Graylist" in taskName:
isGrayList = True tagOrFlag = "Graylist"
if "Pinklist" in taskName: if "Pinklist" in taskName:
isPinkList = True tagOrFlag = "Pinklist"
if "YellowFlag" in taskName:
tagOrFlag = "YellowFlag"
if "GreenFlag" in taskName:
tagOrFlag = "GreenFlag"
if "OrangeFlag" in taskName:
tagOrFlag = "OrangeFlag"
if "CyanFlag" in taskName:
tagOrFlag = "CyanFlag"
if "PinkFlag" in taskName:
tagOrFlag = "PinkFlag"
if "RedFlag" in taskName:
tagOrFlag = "RedFlag"
if "Less" in taskName: if "Less" in taskName:
compareToLess = True compareToLess = True
if "Greater" in taskName: if "Greater" in taskName:
@@ -1145,13 +1158,11 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, isBlackLis
isTagOnlyScenes = False isTagOnlyScenes = False
elif "TagOnlyScenes" in taskName: elif "TagOnlyScenes" in taskName:
isTagOnlyScenes = True isTagOnlyScenes = True
return isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater return isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getAdvanceMenuOptionSelected(advanceMenuOptionSelected): def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
isTagOnlyScenes = False isTagOnlyScenes = False
isBlackList = False tagOrFlag = None
isGrayList = False
isPinkList = False
pathToDelete = "" pathToDelete = ""
sizeToDelete = -1 sizeToDelete = -1
durationToDelete = -1 durationToDelete = -1
@@ -1169,10 +1180,10 @@ 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:
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) isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAnAdvanceMenuOptionSelected(taskName, jsonObject[taskName], isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater)
else: else:
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 getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
return isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater return isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getScenesFromReport(): def getScenesFromReport():
stash.Log(f"Getting candidates for deletion from file {DuplicateCandidateForDeletionList}.") stash.Log(f"Getting candidates for deletion from file {DuplicateCandidateForDeletionList}.")
@@ -1206,11 +1217,26 @@ def getFlaggedScenesFromReport(fileName, flagType):
stash.Trace(f"Did not find flag {flagType}") stash.Trace(f"Did not find flag {flagType}")
return None return None
def getFlaggedScenes(ReportName = htmlReportName): def getFlaggedScenes(flagType=None, ReportName = htmlReportName):
flaggedScenes = [] flaggedScenes = []
flagType = stash.JSON_INPUT['args']['Target'] if flagType == None:
if flagType == "green": flagType = stash.JSON_INPUT['args']['Target']
flagType = "#00FF00" if flagType == "green":
flagType = "#00FF00"
else:
if flagType == "YellowFlag":
flagType = "yellow"
if flagType == "GreenFlag":
flagType = "#00FF00"
if flagType == "OrangeFlag":
flagType = "orange"
if flagType == "CyanFlag":
flagType = "cyan"
if flagType == "PinkFlag":
flagType = "pink"
if flagType == "RedFlag":
flagType = "red"
stash.Debug(f"Searching for scenes with flag type {flagType}") stash.Debug(f"Searching for scenes with flag type {flagType}")
if os.path.isfile(ReportName): if os.path.isfile(ReportName):
@@ -1246,15 +1272,19 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray
if clearAllDupfileManagerTags: if clearAllDupfileManagerTags:
excludedTags = [duplicateMarkForDeletion, duplicateWhitelistTag, excludeDupFileDeleteTag, graylistMarkForDeletion, longerDurationLowerResolution] excludedTags = [duplicateMarkForDeletion, duplicateWhitelistTag, excludeDupFileDeleteTag, graylistMarkForDeletion, longerDurationLowerResolution]
isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAdvanceMenuOptionSelected(advanceMenuOptionSelected) isTagOnlyScenes, tagOrFlag, 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
flaggedScenes = None flaggedScenes = None
flagType = None flagType = None
if checkFlagOption: if checkFlagOption or "Flag" in tagOrFlag:
flaggedScenes, flagType = getFlaggedScenes() if checkFlagOption:
flaggedScenes, flagType = getFlaggedScenes()
else:
checkFlagOption = True
flaggedScenes, flagType = getFlaggedScenes(tagOrFlag)
if flaggedScenes == None or len(flaggedScenes) == 0: if flaggedScenes == None or len(flaggedScenes) == 0:
stash.Error(f"Early exit, because found no scenes with flag {flagType}.") stash.Error(f"Early exit, because found no scenes with flag {flagType}.")
return return
@@ -1267,14 +1297,16 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray
QtyFailedQuery = 0 QtyFailedQuery = 0
stash.Debug("#########################################################################") stash.Debug("#########################################################################")
stash.startSpinningProcessBar() stash.startSpinningProcessBar()
if isTagOnlyScenes or (advanceMenuOptionSelected == False and checkFlagOption == False): if advanceMenuOptionSelected == False and checkFlagOption == False:
isTagOnlyScenes = True
if isTagOnlyScenes:
stash.Log(f"Getting candidates for deletion by using tag-ID {tagId} and tag-name {tagName}; isTagOnlyScenes={isTagOnlyScenes};advanceMenuOptionSelected={advanceMenuOptionSelected}") 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') scenes = stash.find_scenes(f={"tags": {"value":tagId, "modifier":"INCLUDES"}}, fragment=fragmentForSceneDetails) # Old setting 'id tags {id name} files {path width height duration size video_codec bit_rate frame_rate} details title rating100')
else: else:
scenes = getScenesFromReport() scenes = getScenesFromReport()
stash.stopSpinningProcessBar() stash.stopSpinningProcessBar()
qtyResults = len(scenes) qtyResults = len(scenes)
if isTagOnlyScenes or (advanceMenuOptionSelected == False and checkFlagOption == False): if isTagOnlyScenes:
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})") stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
else: else:
stash.Log(f"Found {qtyResults} scenes in report") stash.Log(f"Found {qtyResults} scenes in report")
@@ -1321,19 +1353,24 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray
elif deleteScenes: elif deleteScenes:
DupFileName = scene['files'][0]['path'] DupFileName = scene['files'][0]['path']
DupFileNameOnly = pathlib.Path(DupFileName).stem DupFileNameOnly = pathlib.Path(DupFileName).stem
if checkFlagOption: if checkFlagOption and "Flag" not in tagOrFlag:
if int(scene['id']) in flaggedScenes: if int(scene['id']) in flaggedScenes:
stash.Log(f"Found {flagType} flagged candidate for deletion; Scene ID = {scene['id']}") stash.Log(f"Found {flagType} flagged candidate for deletion; Scene ID = {scene['id']}")
else: else:
continue continue
elif advanceMenuOptionSelected: elif advanceMenuOptionSelected:
if isBlackList: if checkFlagOption:
if int(scene['id']) in flaggedScenes:
stash.Trace(f"Found {flagType} flag for Scene ID = {scene['id']}")
else:
continue
if tagOrFlag == "Blacklist":
if not stash.startsWithInList(blacklist, scene['files'][0]['path']): if not stash.startsWithInList(blacklist, scene['files'][0]['path']):
continue continue
if isGrayList: if tagOrFlag == "Graylist":
if not stash.startsWithInList(graylist, scene['files'][0]['path']): if not stash.startsWithInList(graylist, scene['files'][0]['path']):
continue continue
if isPinkList: if tagOrFlag == "Pinklist":
if not stash.startsWithInList(pinklist, scene['files'][0]['path']): if not stash.startsWithInList(pinklist, scene['files'][0]['path']):
continue continue
if pathToDelete != "": if pathToDelete != "":
@@ -1463,11 +1500,13 @@ def removeExcludeTag():
stash.Log(f"Done removing exclude tag from scene {scene}.") stash.Log(f"Done removing exclude tag from scene {scene}.")
sys.stdout.write("{" + f"removeExcludeTag : 'complete', id: '{scene}'" + "}") sys.stdout.write("{" + f"removeExcludeTag : 'complete', id: '{scene}'" + "}")
def getParseData(getSceneDetails1=True, getSceneDetails2=True): def getParseData(getSceneDetails1=True, getSceneDetails2=True, checkIfNotSplitValue=False):
if 'Target' not in stash.JSON_INPUT['args']: if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})") stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return None, None return None, None
targetsSrc = stash.JSON_INPUT['args']['Target'] targetsSrc = stash.JSON_INPUT['args']['Target']
if checkIfNotSplitValue and ":" not in targetsSrc:
return targetsSrc, None
targets = targetsSrc.split(":") targets = targetsSrc.split(":")
if len(targets) < 2: if len(targets) < 2:
stash.Error(f"Could not get both targets from string {targetsSrc}") stash.Error(f"Could not get both targets from string {targetsSrc}")
@@ -1483,11 +1522,44 @@ def getParseData(getSceneDetails1=True, getSceneDetails2=True):
target2 = target2 + targets[2] target2 = target2 + targets[2]
return target1, target2 return target1, target2
def mergeMetadataInThisFile(fileName):
stash.Debug(f"Checking report file '{fileName}' for yellow icons indicating missing metadata in DuplicateToKeep.")
lines = None
with open(fileName, 'r') as file:
lines = file.readlines()
for line in lines:
if "https://www.axter.com/images/stash/Yellow" in line: # FYI: This catches YellowGroup.png as well, even though group is not currently supported for merging
searchStrScene1 = "<!-- ::DuplicateToDelete_SceneID="
idx = line.index(searchStrScene1) + len(searchStrScene1)
scene_id1 = line[idx:]
scene_id1 = scene_id1[:scene_id1.index('::')]
searchStrScene2 = "::DuplicateToKeep_SceneID="
idx = line.index(searchStrScene2, idx) + len(searchStrScene2)
scene_id2 = line[idx:]
scene_id2 = scene_id2[:scene_id2.index('::')]
stash.Log(f"From file {fileName}, merging metadata from scene {scene_id1} to scene {scene_id2}")
stash.mergeMetadata(int(scene_id1), int(scene_id2))
updateScenesInReports(scene_id2)
def mergeMetadataForAll(ReportName = htmlReportName):
if os.path.isfile(ReportName):
mergeMetadataInThisFile(ReportName)
for x in range(2, 9999):
fileName = ReportName.replace(".html", f"_{x-1}.html")
stash.Debug(f"Checking if file '{fileName}' exist.")
if not os.path.isfile(fileName):
break
mergeMetadataInThisFile(fileName)
stash.Log(f"Done merging metadata for all scenes")
sys.stdout.write("{mergeTags : 'complete'}")
def mergeTags(): def mergeTags():
scene1, scene2 = getParseData() scene1, scene2 = getParseData(checkIfNotSplitValue=True)
if scene1 == None or scene2 == None: if scene1 == None or scene2 == None:
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}") if scene1 == "mergeMetadataForAll":
mergeMetadataForAll()
else:
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
return return
stash.mergeMetadata(scene1, scene2) stash.mergeMetadata(scene1, scene2)
updateScenesInReports(scene2['id']) updateScenesInReports(scene2['id'])
@@ -1614,8 +1686,8 @@ 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:
sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails + htmlFileData) sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails)
sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails + htmlFileData) sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails)
if sceneDetails1 == None or sceneDetails2 == None: if sceneDetails1 == None or sceneDetails2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetails1={sceneDetails1}; sceneDetails2={sceneDetails2};") stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetails1={sceneDetails1}; sceneDetails2={sceneDetails2};")
else: else:

View File

@@ -1,6 +1,6 @@
name: DupFileManager name: DupFileManager
description: Manages duplicate files. description: Manages duplicate files.
version: 1.0.0.2 version: 1.0.0.3
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:

View File

@@ -195,24 +195,19 @@ var OrgNextPage = null;
var OrgHomePage = null; var OrgHomePage = null;
var RemoveToKeepConfirmValue = null; var RemoveToKeepConfirmValue = null;
var RemoveValidatePromptValue = null; var RemoveValidatePromptValue = null;
function SetPaginateButton(){ const StrRemoveToKeepConfirm = "RemoveToKeepConfirm=";
$("#NextPage").attr("href", OrgNextPage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue); const StrRemoveValidatePrompt = "RemoveValidatePrompt=";
$("#PrevPage").attr("href", OrgPrevPage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue);
$("#HomePage").attr("href", OrgHomePage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue);
$("#NextPage_Top").attr("href", OrgNextPage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue);
$("#PrevPage_Top").attr("href", OrgPrevPage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue);
$("#HomePage_Top").attr("href", OrgHomePage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue);
}
function SetPaginateButtonChange(){ function SetPaginateButtonChange(){
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt"); var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm"); var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=false"; RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = "RemoveValidatePrompt=false"; RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
if (chkBxRemoveValid.checked) if (chkBxRemoveValid.checked)
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=true"; RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableDeleteConfirm.checked) if (chkBxDisableDeleteConfirm.checked)
RemoveValidatePromptValue = "RemoveValidatePrompt=true"; RemoveValidatePromptValue = StrRemoveValidatePrompt + "true";
SetPaginateButton(); document.cookie = RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue + ";";
console.log("Cookies = " + document.cookie);
} }
function trim(str, ch) { function trim(str, ch) {
var start = 0, end = str.length; var start = 0, end = str.length;
@@ -300,24 +295,49 @@ $(document).ready(function(){
const queryString = window.location.search; const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString); const urlParams = new URLSearchParams(queryString);
console.log("urlParams = " + urlParams); console.log("urlParams = " + urlParams);
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=false"; RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = "RemoveValidatePrompt=false"; RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
var FetchCookies = true;
if (urlParams.get('RemoveToKeepConfirm') != null && urlParams.get('RemoveToKeepConfirm') !== ""){ if (urlParams.get('RemoveToKeepConfirm') != null && urlParams.get('RemoveToKeepConfirm') !== ""){
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=" + urlParams.get('RemoveToKeepConfirm'); FetchCookies = false;
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + urlParams.get('RemoveToKeepConfirm');
if (urlParams.get('RemoveToKeepConfirm') === "true") if (urlParams.get('RemoveToKeepConfirm') === "true")
$( "#RemoveToKeepConfirm" ).prop("checked", true); $( "#RemoveToKeepConfirm" ).prop("checked", true);
else else
$( "#RemoveToKeepConfirm" ).prop("checked", false); $( "#RemoveToKeepConfirm" ).prop("checked", false);
} }
if (urlParams.get('RemoveValidatePrompt') != null && urlParams.get('RemoveValidatePrompt') !== ""){ if (urlParams.get('RemoveValidatePrompt') != null && urlParams.get('RemoveValidatePrompt') !== ""){
RemoveValidatePromptValue = "RemoveValidatePrompt=" + urlParams.get('RemoveValidatePrompt'); FetchCookies = false;
RemoveValidatePromptValue = StrRemoveValidatePrompt + urlParams.get('RemoveValidatePrompt');
console.log("RemoveValidatePromptValue = " + RemoveValidatePromptValue); console.log("RemoveValidatePromptValue = " + RemoveValidatePromptValue);
if (urlParams.get('RemoveValidatePrompt') === "true") if (urlParams.get('RemoveValidatePrompt') === "true")
$( "#RemoveValidatePrompt" ).prop("checked", true); $( "#RemoveValidatePrompt" ).prop("checked", true);
else else
$( "#RemoveValidatePrompt" ).prop("checked", false); $( "#RemoveValidatePrompt" ).prop("checked", false);
} }
SetPaginateButton(); if (FetchCookies){
console.log("Cookies = " + document.cookie);
var cookies = document.cookie;
if (cookies.indexOf(StrRemoveToKeepConfirm) > -1){
var idx = cookies.indexOf(StrRemoveToKeepConfirm) + StrRemoveToKeepConfirm.length;
var s = cookies.substring(idx);
console.log("StrRemoveToKeepConfirm Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (cookies.indexOf(StrRemoveValidatePrompt) > -1){
var idx = cookies.indexOf(StrRemoveValidatePrompt) + StrRemoveValidatePrompt.length;
var s = cookies.substring(idx);
console.log("StrRemoveValidatePrompt Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
}
SetPaginateButtonChange();
$("button").click(function(){ $("button").click(function(){
var Mode = this.value; var Mode = this.value;
var ActionID = this.id; var ActionID = this.id;
@@ -386,15 +406,18 @@ $(document).ready(function(){
<td>Date Created: (DateCreatedPlaceHolder)</td> <td>Date Created: (DateCreatedPlaceHolder)</td>
</tr></table></td> </tr></table></td>
<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="RemoveToKeepConfirm" name="RemoveToKeepConfirm"><label for="RemoveToKeepConfirm" title="Disable confirmation prompts for delete scenes">Disable Delete Confirmation</label><br></td>
<td> <td>
<div class="dropdown"> <div class="dropdown">
<button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance Menu <i class="fa fa-caret-down"></i></button> <button id="AdvanceMenu" name="AdvanceMenu">Menu <i class="fa fa-caret-down"></i></button>
<div class="dropdown-content"> <div class="dropdown-content">
<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 id="AdvanceMenu" title="Open [Advance Duplicate File Deletion Menu] on a new tab in the browser." name="AdvanceMenu">Advance Duplicate File Deletion Menu</i></button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<div><button type="button" id="mergeMetadataForAll" value="mergeTags" title="Merge scene metadata from [Duplicate to Delete] to [Duplicate to Keep]. This action make take a few minutes to complete.">Merge Tags, Performers, and Galleries</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 Scenes Dup Tags</button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<div><button type="button" id="fileNotExistToDelete" value="Tagged" title="Delete tagged duplicates for which file does NOT exist.">Delete Tagged Files That do Not Exist</button></div> <div><button type="button" id="fileNotExistToDelete" value="Tagged" title="Delete tagged duplicates for which file does NOT exist.">Delete Tagged Files That do Not Exist</button></div>
<div><button type="button" id="fileNotExistToDelete" value="Report" title="Delete duplicate candidate files in report for which file does NOT exist.">Delete Files That do Not Exist in Report</button></div> <div><button type="button" id="fileNotExistToDelete" value="Report" title="Delete duplicate candidate files in report for which file does NOT exist.">Delete Files That do Not Exist in Report</button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<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="clearAllSceneFlags" value="clearAllSceneFlags" title="Remove flags from report for all scenes, except for deletion flag.">Clear All Scene Flags</button></div>
<div><button title="Delete all yellow flagged scenes in report." value="deleteSceneYellowFlag" id="yellow" style="background-color:yellow" >Delete All Yellow Flagged Scenes</button></div> <div><button title="Delete all yellow flagged scenes in report." value="deleteSceneYellowFlag" id="yellow" style="background-color:yellow" >Delete All Yellow Flagged Scenes</button></div>
<div><button title="Delete all green flagged scenes in report." value="deleteSceneGreenFlag" id="green" style="background-color:#00FF00" >Delete All Green Flagged Scenes</button></div> <div><button title="Delete all green flagged scenes in report." value="deleteSceneGreenFlag" id="green" style="background-color:#00FF00" >Delete All Green Flagged Scenes</button></div>
@@ -406,6 +429,9 @@ $(document).ready(function(){
</div> </div>
</div> </div>
</td> </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>
</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 1.0.0.2 (By David Maisonave) # DupFileManager: Ver 1.0.0.3 (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.
@@ -103,6 +103,7 @@ That's it!!!
### Future Planned Features ### Future Planned Features
- Add logic to merge performers and galaries seperatly from tag merging on report. Planned for 1.5.0 Version. - Add logic to merge performers and galaries seperatly from tag merging on report. Planned for 1.5.0 Version.
- Add code to report to make it when the report updates the screen (due to tag merging), it stays in the same row position. Planned for 1.5.0 Version.
- Add logic to merge group metadata when selecting merge option on report. Planned for 2.0.0 Version. - Add logic to merge group metadata when selecting merge option on report. Planned for 2.0.0 Version.
- Add advanced menu directly to the Settings->Tools menu. Planned for 2.0.0 Version. - Add advanced menu directly to the Settings->Tools menu. Planned for 2.0.0 Version.
- Add report directly to the Settings->Tools menu. Planned for 2.0.0 Version. - Add report directly to the Settings->Tools menu. Planned for 2.0.0 Version.

View File

@@ -256,8 +256,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')) if ($("#tagOrFlagCombobox").val() !== "")
Param += "\"" + "TagOnlyScenes" + Blacklist + "\":\"true\", "; Param += "\"" + $("#tagOrFlagCombobox").val() + Blacklist + "\":\"true\", ";
Param += '}'; Param += '}';
Param = Param.replace(', }', '}'); Param = Param.replace(', }', '}');
if (Param === "{}") if (Param === "{}")
@@ -321,7 +321,21 @@ function DeleteDupInPath(){
</tr> </tr>
<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, 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> <td>
<label for="tagOrFlagCombobox">TagOrFlag:</label>
<select id="tagOrFlagCombobox" name="tagOrFlagCombobox">
<option value="" selected="selected"></option>
<option value="TagOnlyScenes" title="When selected, Multi-Options operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?">Dup Tag</option>
<option value="YellowFlag" style="background-color:Yellow;" title="When selected, Multi-Options operations only apply to scenes with [Yellow] fag.">Yellow Flag</option>
<option value="GreenFlag" style="background-color:#00FF00;" title="When selected, Multi-Options operations only apply to scenes with [Green] fag.">Green Flag</option>
<option value="OrangeFlag" style="background-color:Orange;" title="When selected, Multi-Options operations only apply to scenes with [Orange] fag.">Orange Flag</option>
<option value="CyanFlag" style="background-color:Cyan;" title="When selected, Multi-Options operations only apply to scenes with [Cyan] fag.">Cyan Flag</option>
<option value="PinkFlag" style="background-color:Pink;" title="When selected, Multi-Options operations only apply to scenes with [Pink] fag.">Pink Flag</option>
<option value="RedFlag" style="background-color:Red;" title="When selected, Multi-Options operations only apply to scenes with [Red] fag.">Red Flag</option>
</select>
</td>
</tr> </tr>
<tr> <tr>
<td><form id="pathToDeleteForm" action="javascript:DeleteDupInPath();" target="_self"> <td><form id="pathToDeleteForm" action="javascript:DeleteDupInPath();" target="_self">

View File

@@ -36,3 +36,9 @@
- If data for associated icon are the same, then both icons are black or blue (the default color). - If data for associated icon are the same, then both icons are black or blue (the default color).
- If [**duplicate to keep**] is missing data that is in [**candidate to delete**], than [**candidate to delete**] gets a yellow icon. - If [**duplicate to keep**] is missing data that is in [**candidate to delete**], than [**candidate to delete**] gets a yellow icon.
- If [**candidate to delete**] is missing data that is in [**duplicate to keep**], than [**duplicate to keep**] gets a pink icon. - If [**candidate to delete**] is missing data that is in [**duplicate to keep**], than [**duplicate to keep**] gets a pink icon.
### 1.0.0.3
- Added option on report to merge all metadata missing in [**Duplicate to Keep**] files.
- Added cookies to report so as to remember user options for Disable Complete Confirmation **[Disable Complete Confirmation]** and **[Disable Delete Confirmation]**.
- This change was needed because sometimes the browser refuse to open local URL's with params on the URL.
- Using cookies also allows check options status to stay the same after refresh.
- Added code to [**Advance Duplicate File Deletion Menu**] to delete based on flags.

View File

@@ -92,6 +92,7 @@ class StashPluginHelper(StashInterface):
stopProcessBarSpin = True stopProcessBarSpin = True
updateProgressbarOnIter = 0 updateProgressbarOnIter = 0
currentProgressbarIteration = 0 currentProgressbarIteration = 0
galleryNamesCache = {}
class OS_Type(IntEnum): class OS_Type(IntEnum):
WINDOWS = 1 WINDOWS = 1
@@ -773,6 +774,14 @@ class StashPluginHelper(StashInterface):
errMsg = f"Exception calling [updateScene]. Will retry; count({i}); Error: {e}\nTraceBack={tb}" errMsg = f"Exception calling [updateScene]. Will retry; count({i}); Error: {e}\nTraceBack={tb}"
time.sleep(sleepSecondsBetweenRetry) time.sleep(sleepSecondsBetweenRetry)
# getGalleryName uses a cache so it doesn't have to hit the server for the same ID.
def getGalleryName(self, gallery_id, refreshCache=False):
if refreshCache:
self.galleryNamesCache = {}
if gallery_id not in self.galleryNamesCache:
self.galleryNamesCache[gallery_id] = self.find_gallery(gallery_id)
return self.galleryNamesCache[gallery_id]
def runPlugin(self, plugin_id, task_mode=None, args:dict={}, asyn=False): def runPlugin(self, plugin_id, task_mode=None, args:dict={}, asyn=False):
"""Runs a plugin operation. """Runs a plugin operation.
The operation is run immediately and does not use the job queue. The operation is run immediately and does not use the job queue.
@@ -986,6 +995,8 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags) self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags)
self.mergeItems('performers', 'performer_ids', []) self.mergeItems('performers', 'performer_ids', [])
self.mergeItems('galleries', 'gallery_ids', []) self.mergeItems('galleries', 'gallery_ids', [])
# ToDo: Firgure out how to merge groups
# self.mergeItems('groups', 'group_ids')
# Looks like movies has been removed from new Stash version # Looks like movies has been removed from new Stash version
# self.mergeItems('movies', 'movies', []) # self.mergeItems('movies', 'movies', [])
self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL) self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL)
@@ -1020,9 +1031,13 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
if item not in self.destData[fieldName]: if item not in self.destData[fieldName]:
if NotStartWith == None or not item.startswith(NotStartWith): if NotStartWith == None or not item.startswith(NotStartWith):
if excludeName == None or item['name'] not in excludeName: if excludeName == None or item['name'] not in excludeName:
if fieldName == 'movies': if fieldName == 'groups':
listToAdd += [{"movie_id" : item['movie']['id'], "scene_index" : item['scene_index']}] # listToAdd += [{"group_id" : item['group']['id'], "group_name" : item['group']['name']}]
dataAdded += f"{item['movie']['id']} " listToAdd += [item['group']['id']]
dataAdded += f"{item['group']['id']} "
# elif fieldName == 'movies':
# listToAdd += [{"movie_id" : item['movie']['id'], "scene_index" : item['scene_index']}]
# dataAdded += f"{item['movie']['id']} "
elif updateFieldName == None: elif updateFieldName == None:
listToAdd += [item] listToAdd += [item]
dataAdded += f"{item} " dataAdded += f"{item} "

View File

@@ -92,6 +92,7 @@ class StashPluginHelper(StashInterface):
stopProcessBarSpin = True stopProcessBarSpin = True
updateProgressbarOnIter = 0 updateProgressbarOnIter = 0
currentProgressbarIteration = 0 currentProgressbarIteration = 0
galleryNamesCache = {}
class OS_Type(IntEnum): class OS_Type(IntEnum):
WINDOWS = 1 WINDOWS = 1
@@ -773,6 +774,14 @@ class StashPluginHelper(StashInterface):
errMsg = f"Exception calling [updateScene]. Will retry; count({i}); Error: {e}\nTraceBack={tb}" errMsg = f"Exception calling [updateScene]. Will retry; count({i}); Error: {e}\nTraceBack={tb}"
time.sleep(sleepSecondsBetweenRetry) time.sleep(sleepSecondsBetweenRetry)
# getGalleryName uses a cache so it doesn't have to hit the server for the same ID.
def getGalleryName(self, gallery_id, refreshCache=False):
if refreshCache:
self.galleryNamesCache = {}
if gallery_id not in self.galleryNamesCache:
self.galleryNamesCache[gallery_id] = self.find_gallery(gallery_id)
return self.galleryNamesCache[gallery_id]
def runPlugin(self, plugin_id, task_mode=None, args:dict={}, asyn=False): def runPlugin(self, plugin_id, task_mode=None, args:dict={}, asyn=False):
"""Runs a plugin operation. """Runs a plugin operation.
The operation is run immediately and does not use the job queue. The operation is run immediately and does not use the job queue.
@@ -986,6 +995,8 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags) self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags)
self.mergeItems('performers', 'performer_ids', []) self.mergeItems('performers', 'performer_ids', [])
self.mergeItems('galleries', 'gallery_ids', []) self.mergeItems('galleries', 'gallery_ids', [])
# ToDo: Firgure out how to merge groups
# self.mergeItems('groups', 'group_ids')
# Looks like movies has been removed from new Stash version # Looks like movies has been removed from new Stash version
# self.mergeItems('movies', 'movies', []) # self.mergeItems('movies', 'movies', [])
self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL) self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL)
@@ -1020,9 +1031,13 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
if item not in self.destData[fieldName]: if item not in self.destData[fieldName]:
if NotStartWith == None or not item.startswith(NotStartWith): if NotStartWith == None or not item.startswith(NotStartWith):
if excludeName == None or item['name'] not in excludeName: if excludeName == None or item['name'] not in excludeName:
if fieldName == 'movies': if fieldName == 'groups':
listToAdd += [{"movie_id" : item['movie']['id'], "scene_index" : item['scene_index']}] # listToAdd += [{"group_id" : item['group']['id'], "group_name" : item['group']['name']}]
dataAdded += f"{item['movie']['id']} " listToAdd += [item['group']['id']]
dataAdded += f"{item['group']['id']} "
# elif fieldName == 'movies':
# listToAdd += [{"movie_id" : item['movie']['id'], "scene_index" : item['scene_index']}]
# dataAdded += f"{item['movie']['id']} "
elif updateFieldName == None: elif updateFieldName == None:
listToAdd += [item] listToAdd += [item]
dataAdded += f"{item} " dataAdded += f"{item} "