### 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
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 '
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"
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.Log(f"Waiting for find_duplicate_scenes_diff to return results; matchDupDistance={matchPhaseDistanceText}; significantTimeDiff={significantTimeDiff}", printTo=LOG_STASH_N_PLUGIN)
stash.startSpinningProcessBar()
mergeFieldData = " code director title rating100 date studio {id name} movies {movie {id} } urls " if merge else ""
if not createHtmlReport:
htmlFileData = ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment= fragmentForSceneDetails + mergeFieldData + htmlFileData)
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment=fragmentForSceneDetails)
stash.stopSpinningProcessBar()
qtyResults = len(DupFileSets)
stash.setProgressBarIter(qtyResults)
@@ -1102,14 +1103,26 @@ def toJson(data):
data = data.replace("\\\\\\\\", "\\\\")
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}")
if "Blacklist" in taskName:
isBlackList = True
tagOrFlag = "Blacklist"
if "Graylist" in taskName:
isGrayList = True
tagOrFlag = "Graylist"
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:
compareToLess = True
if "Greater" in taskName:
@@ -1145,13 +1158,11 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, isBlackLis
isTagOnlyScenes = False
elif "TagOnlyScenes" in taskName:
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):
isTagOnlyScenes = False
isBlackList = False
isGrayList = False
isPinkList = False
tagOrFlag = None
pathToDelete = ""
sizeToDelete = -1
durationToDelete = -1
@@ -1169,10 +1180,10 @@ def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
if "applyCombo" in stash.PLUGIN_TASK_NAME:
jsonObject = toJson(stash.JSON_INPUT['args']['Target'])
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:
return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
return isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
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, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getScenesFromReport():
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}")
return None
def getFlaggedScenes(ReportName = htmlReportName):
def getFlaggedScenes(flagType=None, ReportName = htmlReportName):
flaggedScenes = []
flagType = stash.JSON_INPUT['args']['Target']
if flagType == "green":
flagType = "#00FF00"
if flagType == None:
flagType = stash.JSON_INPUT['args']['Target']
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}")
if os.path.isfile(ReportName):
@@ -1246,20 +1272,24 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray
if clearAllDupfileManagerTags:
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:
stash.Error("Running advance menu option with no options enabled.")
return
flaggedScenes = None
flagType = None
if checkFlagOption:
flaggedScenes, flagType = getFlaggedScenes()
if checkFlagOption or "Flag" in tagOrFlag:
if checkFlagOption:
flaggedScenes, flagType = getFlaggedScenes()
else:
checkFlagOption = True
flaggedScenes, flagType = getFlaggedScenes(tagOrFlag)
if flaggedScenes == None or len(flaggedScenes) == 0:
stash.Error(f"Early exit, because found no scenes with flag {flagType}.")
return
stash.Debug(f"Fournd {len(flaggedScenes)} scenes with flag {flagType}")
QtyDup = 0
QtyDeleted = 0
QtyClearedTags = 0
@@ -1267,14 +1297,16 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray
QtyFailedQuery = 0
stash.Debug("#########################################################################")
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}")
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:
scenes = getScenesFromReport()
stash.stopSpinningProcessBar()
qtyResults = len(scenes)
if isTagOnlyScenes or (advanceMenuOptionSelected == False and checkFlagOption == False):
if isTagOnlyScenes:
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
else:
stash.Log(f"Found {qtyResults} scenes in report")
@@ -1321,19 +1353,24 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray
elif deleteScenes:
DupFileName = scene['files'][0]['path']
DupFileNameOnly = pathlib.Path(DupFileName).stem
if checkFlagOption:
if checkFlagOption and "Flag" not in tagOrFlag:
if int(scene['id']) in flaggedScenes:
stash.Log(f"Found {flagType} flagged candidate for deletion; Scene ID = {scene['id']}")
else:
continue
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']):
continue
if isGrayList:
if tagOrFlag == "Graylist":
if not stash.startsWithInList(graylist, scene['files'][0]['path']):
continue
if isPinkList:
if tagOrFlag == "Pinklist":
if not stash.startsWithInList(pinklist, scene['files'][0]['path']):
continue
if pathToDelete != "":
@@ -1463,11 +1500,13 @@ def removeExcludeTag():
stash.Log(f"Done removing exclude tag from scene {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']:
stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return None, None
targetsSrc = stash.JSON_INPUT['args']['Target']
if checkIfNotSplitValue and ":" not in targetsSrc:
return targetsSrc, None
targets = targetsSrc.split(":")
if len(targets) < 2:
stash.Error(f"Could not get both targets from string {targetsSrc}")
@@ -1482,12 +1521,45 @@ def getParseData(getSceneDetails1=True, getSceneDetails2=True):
elif len(targets) > 2:
target2 = target2 + targets[2]
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():
scene1, scene2 = getParseData()
scene1, scene2 = getParseData(checkIfNotSplitValue=True)
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
stash.mergeMetadata(scene1, scene2)
updateScenesInReports(scene2['id'])
@@ -1614,8 +1686,8 @@ def updateScenesInReport(fileName, scene):
elif scene1 != -1 and scene2 != -1:
break
if scene1 != -1 and scene2 != -1:
sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails + htmlFileData)
sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails + htmlFileData)
sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails)
sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails)
if sceneDetails1 == None or sceneDetails2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetails1={sceneDetails1}; sceneDetails2={sceneDetails2};")
else:

View File

@@ -1,6 +1,6 @@
name: DupFileManager
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
ui:
javascript:

View File

@@ -195,24 +195,19 @@ var OrgNextPage = null;
var OrgHomePage = null;
var RemoveToKeepConfirmValue = null;
var RemoveValidatePromptValue = null;
function SetPaginateButton(){
$("#NextPage").attr("href", OrgNextPage + "?" + RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue);
$("#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);
}
const StrRemoveToKeepConfirm = "RemoveToKeepConfirm=";
const StrRemoveValidatePrompt = "RemoveValidatePrompt=";
function SetPaginateButtonChange(){
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=false";
RemoveValidatePromptValue = "RemoveValidatePrompt=false";
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
if (chkBxRemoveValid.checked)
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=true";
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableDeleteConfirm.checked)
RemoveValidatePromptValue = "RemoveValidatePrompt=true";
SetPaginateButton();
RemoveValidatePromptValue = StrRemoveValidatePrompt + "true";
document.cookie = RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue + ";";
console.log("Cookies = " + document.cookie);
}
function trim(str, ch) {
var start = 0, end = str.length;
@@ -239,7 +234,7 @@ function RunPluginOperation(Mode, ActionID, button, asyncAjax){
$("body").css("cursor", "default");
}
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags" || (Mode !== "deleteScene" && Mode.startsWith("deleteScene")))
location.href = location.href; // location.replace(location.href);
location.href = location.href; // location.replace(location.href);
if (!chkBxRemoveValid.checked && Mode !== "flagScene") alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.\\n\\nResults=" + result);
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
@@ -300,24 +295,49 @@ $(document).ready(function(){
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
console.log("urlParams = " + urlParams);
RemoveToKeepConfirmValue = "RemoveToKeepConfirm=false";
RemoveValidatePromptValue = "RemoveValidatePrompt=false";
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
var FetchCookies = true;
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")
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (urlParams.get('RemoveValidatePrompt') != null && urlParams.get('RemoveValidatePrompt') !== ""){
RemoveValidatePromptValue = "RemoveValidatePrompt=" + urlParams.get('RemoveValidatePrompt');
FetchCookies = false;
RemoveValidatePromptValue = StrRemoveValidatePrompt + urlParams.get('RemoveValidatePrompt');
console.log("RemoveValidatePromptValue = " + RemoveValidatePromptValue);
if (urlParams.get('RemoveValidatePrompt') === "true")
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#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(){
var Mode = this.value;
var ActionID = this.id;
@@ -386,15 +406,18 @@ $(document).ready(function(){
<td>Date Created: (DateCreatedPlaceHolder)</td>
</tr></table></td>
<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>
<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><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="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 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>
@@ -406,6 +429,9 @@ $(document).ready(function(){
</div>
</div>
</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></center>
<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.
It has both **task** and **tools-UI** components.
@@ -103,6 +103,7 @@ That's it!!!
### Future Planned Features
- 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 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.

View File

@@ -256,8 +256,8 @@ $(document).ready(function(){
Param += "\"" + "pathStrToDelete" + Blacklist + "\":\"" + $("#pathStrToDeleteText").val().replace("\\", "\\\\") + "\", ";
if ($("#fileNotExistCheck").prop('checked'))
Param += "\"" + "fileNotExistToDelete" + Blacklist + "\":\"true\", ";
if ($("#DupTagOnlyCheck_MultiOption").prop('checked'))
Param += "\"" + "TagOnlyScenes" + Blacklist + "\":\"true\", ";
if ($("#tagOrFlagCombobox").val() !== "")
Param += "\"" + $("#tagOrFlagCombobox").val() + Blacklist + "\":\"true\", ";
Param += '}';
Param = Param.replace(', }', '}');
if (Param === "{}")
@@ -321,7 +321,21 @@ function DeleteDupInPath(){
</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>
<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>
<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 [**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.
### 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
updateProgressbarOnIter = 0
currentProgressbarIteration = 0
galleryNamesCache = {}
class OS_Type(IntEnum):
WINDOWS = 1
@@ -773,6 +774,14 @@ class StashPluginHelper(StashInterface):
errMsg = f"Exception calling [updateScene]. Will retry; count({i}); Error: {e}\nTraceBack={tb}"
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):
"""Runs a plugin operation.
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('performers', 'performer_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
# self.mergeItems('movies', 'movies', [])
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 NotStartWith == None or not item.startswith(NotStartWith):
if excludeName == None or item['name'] not in excludeName:
if fieldName == 'movies':
listToAdd += [{"movie_id" : item['movie']['id'], "scene_index" : item['scene_index']}]
dataAdded += f"{item['movie']['id']} "
if fieldName == 'groups':
# listToAdd += [{"group_id" : item['group']['id'], "group_name" : item['group']['name']}]
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:
listToAdd += [item]
dataAdded += f"{item} "

View File

@@ -92,6 +92,7 @@ class StashPluginHelper(StashInterface):
stopProcessBarSpin = True
updateProgressbarOnIter = 0
currentProgressbarIteration = 0
galleryNamesCache = {}
class OS_Type(IntEnum):
WINDOWS = 1
@@ -773,6 +774,14 @@ class StashPluginHelper(StashInterface):
errMsg = f"Exception calling [updateScene]. Will retry; count({i}); Error: {e}\nTraceBack={tb}"
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):
"""Runs a plugin operation.
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('performers', 'performer_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
# self.mergeItems('movies', 'movies', [])
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 NotStartWith == None or not item.startswith(NotStartWith):
if excludeName == None or item['name'] not in excludeName:
if fieldName == 'movies':
listToAdd += [{"movie_id" : item['movie']['id'], "scene_index" : item['scene_index']}]
dataAdded += f"{item['movie']['id']} "
if fieldName == 'groups':
# listToAdd += [{"group_id" : item['group']['id'], "group_name" : item['group']['name']}]
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:
listToAdd += [item]
dataAdded += f"{item} "