First alpha release

Added following features to 1.0.0
### 1.0.0
- Consolidated buttons and links on report into dropdown buttons.
- On report, added dropdown menu options for flags.
- Rename Tools-UI advance duplicate tagged menu to [**Advance Duplicate File Deletion Menu**]
- When [**Advance Duplicate File Deletion Menu**] completes report, gives user prompt to open the report in browser.
- Added performance enhancement for removing (clearing) duplicate tags from all scenes by using SQL call.
- Added option to report to delete files that do not exist by duplicate candidates in report, as well as by tagged files.
- Added logic to disable scene in report if deleted by [**Advance Duplicate File Deletion Menu**]. Note: Requires a refresh.
- Added report option to delete by flags set on the report.
This commit is contained in:
David Maisonave
2024-11-26 19:52:21 -05:00
parent e1f3335db8
commit 214ba134c4
10 changed files with 392 additions and 98 deletions

View File

@@ -60,7 +60,7 @@
const DupFileManagerReportMenuButton = React.createElement(Link, { to: "/plugin/DupFileManager", title: ReportMenuButtonToolTip }, React.createElement(Button, null, "DupFileManager Report Menu"));
const ToolsMenuOptionButton = React.createElement(Link, { to: "/plugin/DupFileManager_ToolsAndUtilities", title: ToolsMenuToolTip }, React.createElement(Button, null, "DupFileManager Tools and Utilities"));
function GetShowReportButton(LocalDuplicateReportPath, ButtonText){return React.createElement("a", { href: LocalDuplicateReportPath, title: ShowReportButtonToolTip}, React.createElement(Button, null, ButtonText));}
function GetAdvanceMenuButton(){return React.createElement("a", { href: AdvanceMenuOptionUrl, title: "Open link to the advance duplicate tagged menu."}, React.createElement(Button, null, "Show Advance Duplicate Tagged Menu"));}
function GetAdvanceMenuButton(){return React.createElement("a", { href: AdvanceMenuOptionUrl, title: "Open link to the [Advance Duplicate File Deletion Menu]."}, React.createElement(Button, null, "Show [Advance Duplicate File Deletion Menu]"));}
function GetCreateReportNoTagButton(ButtonText){return React.createElement(Link, { to: "/plugin/DupFileManager_CreateReportWithNoTagging", title: CreateReportNoTagButtonToolTip }, React.createElement(Button, null, ButtonText));}
function GetCreateReportButton(ButtonText){return React.createElement(Link, { to: "/plugin/DupFileManager_CreateReport", title: CreateReportButtonToolTip }, React.createElement(Button, null, ButtonText));}

View File

@@ -45,6 +45,7 @@ settings = {
"zySwapBetterFrameRate": False,
"zzDebug": False,
"zzTracing": False,
"zzdryRun": False,
"zzObsoleteSettingsCheckVer2": False, # This is a hidden variable that is NOT displayed in the UI
@@ -69,6 +70,7 @@ stash = StashPluginHelper(
DebugFieldName="zzDebug",
)
stash.convertToAscii = True
dry_run = stash.Setting("zzdryRun")
advanceMenuOptions = [ "applyCombo", "applyComboPinklist", "applyComboGraylist", "applyComboBlacklist", "pathToDelete", "pathToDeleteBlacklist", "sizeToDeleteLess", "sizeToDeleteGreater", "sizeToDeleteBlacklistLess", "sizeToDeleteBlacklistGreater", "durationToDeleteLess", "durationToDeleteGreater", "durationToDeleteBlacklistLess", "durationToDeleteBlacklistGreater",
"commonResToDeleteLess", "commonResToDeleteEq", "commonResToDeleteGreater", "commonResToDeleteBlacklistLess", "commonResToDeleteBlacklistEq", "commonResToDeleteBlacklistGreater", "resolutionToDeleteLess", "resolutionToDeleteEq", "resolutionToDeleteGreater",
@@ -565,6 +567,7 @@ htmlHighlightTimeDiff = stash.Setting('htmlHighlightTimeDiff')
htmlPreviewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview"
def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel = "?", tagDuplicates = False):
fileDoesNotExistStr = "<b style='color:red;background-color:yellow;font-size:10px;'>[File NOT Exist]<b>"
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">'
@@ -602,23 +605,41 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel =
fileHtmlReport.write(f"<tr class=\"reason-details\"><td colspan='8'>Reason: not ExcludeTag vs ExcludeTag</td></tr>")
fileHtmlReport.write("</table>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Delete file and remove scene from stash\" value=\"deleteScene\" id=\"{DupFile['id']}\">[Delete]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove scene from stash only. Do NOT delete file.\" value=\"removeScene\" id=\"{DupFile['id']}\">[Remove]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Copy duplicate to file-to-keep.\" value=\"copyScene\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">[Copy]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Replace file-to-keep with this duplicate, and copy metadata from this duplicate to file-to-keep.\" value=\"moveScene\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">[Move]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Replace file-to-keep file name with this duplicate file name.\" value=\"renameFile\" id=\"{DupFileToKeep['id']}:{stash.asc2(pathlib.Path(DupFile['files'][0]['path']).stem)}\">[CpyName]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScene\" id=\"{DupFile['id']}\">[Flag]</button>")
fileHtmlReport.write('<div class="dropbtn_table"><button value="DoNothing">File Options <i class="fa fa-caret-down"></i></button><div class="dropbtn_table-content">')
fileHtmlReport.write(f"<div><button title=\"Delete file and remove scene from stash\" value=\"deleteScene\" id=\"{DupFile['id']}\">Delete</button></div>")
fileHtmlReport.write(f"<div><button title=\"Remove scene from stash only. Do NOT delete file.\" value=\"removeScene\" id=\"{DupFile['id']}\">Remove Scene</button></div>")
fileHtmlReport.write(f"<div><button title=\"Copy duplicate to file-to-keep.\" value=\"copyScene\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">Copy to [Duplicate to Keep]</button></div>")
fileHtmlReport.write(f"<div><button title=\"Replace file-to-keep with this duplicate, and copy metadata from this duplicate to file-to-keep.\" value=\"moveScene\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">Move to [Duplicate to Keep] and Metadata</button></div>")
fileHtmlReport.write(f"<div><button title=\"Replace file-to-keep file name with this duplicate file name.\" value=\"renameFile\" id=\"{DupFileToKeep['id']}:{stash.asc2(pathlib.Path(DupFile['files'][0]['path']).stem)}\">Copy this Name to [Duplicate to Keep]</button></div>")
fileHtmlReport.write("</div></div>")
fileHtmlReport.write(f"<div class=\"dropbtn_table\"><button value=\"flagScene\" id=\"{DupFile['id']}\">Flag or Tag <i class=\"fa fa-caret-down\"></i></button><div class=\"dropbtn_table-content\">")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScene\" id=\"{DupFile['id']}\">Flag this scene</button></div>")
# ToDo: Add following buttons:
# rename file
if dupFileExist and tagDuplicates:
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove duplicate tag from scene.\" value=\"removeDupTag\" id=\"{DupFile['id']}\">[-Tag]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Add exclude tag to scene. This will exclude scene from deletion via deletion tag\" value=\"addExcludeTag\" id=\"{DupFile['id']}\">[+Exclude]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Merge duplicate scene tags with ToKeep scene tags\" value=\"mergeTags\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">[Merge Tags]</button>")
fileHtmlReport.write(f"<div><button title=\"Remove duplicate tag from scene.\" value=\"removeDupTag\" id=\"{DupFile['id']}\">Remove Duplicate Tag</button></div>")
fileHtmlReport.write(f"<div><button title=\"Add exclude tag to scene. This will exclude scene from deletion via deletion tag\" value=\"addExcludeTag\" id=\"{DupFile['id']}\">Add Exclude Tag</button></div>")
fileHtmlReport.write(f"<div><button title=\"Merge duplicate scene tags with ToKeep scene tags\" value=\"mergeTags\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">Merge Tags, Performers, & Galleries</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagSceneyellow highlight\" id=\"{DupFile['id']}\" style=\"background-color:yellow\">Flag Yellow</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenegreen highlight\" id=\"{DupFile['id']}\" style=\"background-color:#00FF00\">Flag Green</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagSceneorange highlight\" id=\"{DupFile['id']}\" style=\"background-color:orange\">Flag Orange</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenecyan highlight\" id=\"{DupFile['id']}\" style=\"background-color:cyan\">Flag Cyan</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenepink highlight\" id=\"{DupFile['id']}\" style=\"background-color:pink\">Flag Pink</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenered highlight\" id=\"{DupFile['id']}\" style=\"background-color:red\">Flag Red</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenestrike-through\" id=\"{DupFile['id']}\">Flag Strike-through</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenedisable-scene\" id=\"{DupFile['id']}\">Flag Disable-scene</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagSceneremove all flags\" id=\"{DupFile['id']}\">Remove All Flags</button></div>")
fileHtmlReport.write("</div></div>")
if dupFileExist:
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Open folder\" href=\"file://{getPath(DupFile, True)}\">[Folder]</a>")
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFile)}\">[Play]</a>")
fileHtmlReport.write('<div class="dropbtn_table"><button value="DoNothing">Local File <i class="fa fa-caret-down"></i></button><div class="links_table-content">')
fileHtmlReport.write(f"<div><a class=\"link-items\" title=\"Open folder\" href=\"file://{getPath(DupFile, True)}\">[Folder]</a></div>")
fileHtmlReport.write(f"<div><a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFile)}\">[Play]</a></div>")
fileHtmlReport.write("</div></div>")
else:
fileHtmlReport.write("<b style='color:red;'>[File NOT Exist]<b>")
fileHtmlReport.write(fileDoesNotExistStr)
if len(DupFile['tags']) > 0:
fileHtmlReport.write(htmlTagPrefix)
for tag in DupFile['tags']:
@@ -653,17 +674,37 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel =
fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}<a href=\"{stash.STASH_URL}/scenes/{DupFileToKeep['id']}\" target=\"_blank\" rel=\"noopener noreferrer\" {fileNameClassID(DupFileToKeep)}>{getPath(DupFileToKeep)}</a>")
fileHtmlReport.write(f"<p><table><tr class=\"scene-details\"><th>Res</th><th>Durration</th><th>BitRate</th><th>Codec</th><th>FrameRate</th><th>size</th><th>ID</th></tr>")
fileHtmlReport.write(f"<tr class=\"scene-details\"><td>{DupFileToKeep['files'][0]['width']}x{DupFileToKeep['files'][0]['height']}</td><td>{DupFileToKeep['files'][0]['duration']}</td><td>{DupFileToKeep['files'][0]['bit_rate']}</td><td>{DupFileToKeep['files'][0]['video_codec']}</td><td>{DupFileToKeep['files'][0]['frame_rate']}</td><td>{DupFileToKeep['files'][0]['size']}</td><td>{DupFileToKeep['id']}</td></tr></table>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Delete [DupFileToKeep] and remove scene from stash\" value=\"deleteScene\" id=\"{DupFileToKeep['id']}\">[Delete]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove scene from stash only. Do NOT delete file.\" value=\"removeScene\" id=\"{DupFileToKeep['id']}\">[Remove]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Rename file-to-keep.\" value=\"newName\" id=\"{DupFileToKeep['id']}:{stash.asc2(pathlib.Path(DupFileToKeep['files'][0]['path']).stem)}\">[Rename]</button>")
fileHtmlReport.write('<div class="dropbtn_table"><button value="DoNothing">File Options <i class="fa fa-caret-down"></i></button><div class="dropbtn_table-content">')
fileHtmlReport.write(f"<div><button title=\"Delete [DupFileToKeep] and remove scene from stash\" value=\"deleteScene\" id=\"{DupFileToKeep['id']}\">Delete</button></div>")
fileHtmlReport.write(f"<div><button title=\"Remove scene from stash only. Do NOT delete file.\" value=\"removeScene\" id=\"{DupFileToKeep['id']}\">Remove</button></div>")
fileHtmlReport.write(f"<div><button title=\"Rename file-to-keep.\" value=\"newName\" id=\"{DupFileToKeep['id']}:{stash.asc2(pathlib.Path(DupFileToKeep['files'][0]['path']).stem)}\">Rename</button></div>")
fileHtmlReport.write("</div></div>")
fileHtmlReport.write(f"<div class=\"dropbtn_table\"><button value=\"flagScene\" id=\"{DupFileToKeep['id']}\">Flag or Tag <i class=\"fa fa-caret-down\"></i></button><div class=\"dropbtn_table-content\">")
if isTaggedExcluded(DupFileToKeep):
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove exclude scene from deletion tag\" value=\"removeExcludeTag\" id=\"{DupFileToKeep['id']}\">[-Exclude]</button>")
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Open folder\" href=\"file://{getPath(DupFileToKeep, True)}\">[Folder]</a>")
fileHtmlReport.write(f"<div><button title=\"Remove exclude scene from deletion tag\" value=\"removeExcludeTag\" id=\"{DupFileToKeep['id']}\">Remove Exclude Tag</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScene\" id=\"{DupFileToKeep['id']}\">Flag this scene</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagSceneyellow highlight\" id=\"{DupFileToKeep['id']}\" style=\"background-color:yellow\">Flag Yellow</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenegreen highlight\" id=\"{DupFileToKeep['id']}\" style=\"background-color:#00FF00\">Flag Green</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagSceneorange highlight\" id=\"{DupFileToKeep['id']}\" style=\"background-color:orange\">Flag Orange</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenecyan highlight\" id=\"{DupFileToKeep['id']}\" style=\"background-color:cyan\">Flag Cyan</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenepink highlight\" id=\"{DupFileToKeep['id']}\" style=\"background-color:pink\">Flag Pink</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenered highlight\" id=\"{DupFileToKeep['id']}\" style=\"background-color:red\">Flag Red</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenestrike-through\" id=\"{DupFileToKeep['id']}\">Flag Strike-through</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScenedisable-scene\" id=\"{DupFileToKeep['id']}\">Flag Disable-scene</button></div>")
fileHtmlReport.write(f"<div><button title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagSceneremove all flags\" id=\"{DupFileToKeep['id']}\">Remove All Flags</button></div>")
fileHtmlReport.write("</div></div>")
fileHtmlReport.write('<div class="dropbtn_table"><button value="DoNothing">Local File <i class="fa fa-caret-down"></i></button><div class="links_table-content">')
fileHtmlReport.write(f"<div><a class=\"link-items\" title=\"Open folder\" href=\"file://{getPath(DupFileToKeep, True)}\">[Folder]</a></div>")
if toKeepFileExist:
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFileToKeep)}\">[Play]</a>")
else:
fileHtmlReport.write("<b style='color:red;'>[File NOT Exist]<b>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Flag scene as reviewed or as awaiting review.\" value=\"flagScene\" id=\"{DupFileToKeep['id']}\">[Flag]</button>")
fileHtmlReport.write(f"<div><a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFileToKeep)}\">[Play]</a></div>")
fileHtmlReport.write("</div></div>")
if not toKeepFileExist:
fileHtmlReport.write(fileDoesNotExistStr)
if len(DupFileToKeep['tags']) > 0:
fileHtmlReport.write(htmlTagPrefix)
for tag in DupFileToKeep['tags']:
@@ -878,7 +919,11 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
shutil.move(DupFileName, destPath)
elif moveToTrashCan:
sendToTrash(DupFileName)
stash.destroyScene(DupFile['id'], delete_file=True)
if dry_run:
result = f"dry_run enabled, but scene {DupFile['files'][0]['path']} would have been removed from stash with delete_file=True."
stash.Log(result)
else:
stash.destroyScene(DupFile['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(DupFile['id'], removeScene = True)
elif tagDuplicates or fileHtmlReport != None:
if excludeFromReportIfSignificantTimeDiff and significantTimeDiffCheck(DupFile, DupFileToKeep, True):
@@ -982,9 +1027,8 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
def findCurrentTagId(tagNames):
# tagNames = [i for n, i in enumerate(tagNames) if i not in tagNames[:n]]
for tagName in tagNames:
tagId = stash.find_tags(q=tagName)
if len(tagId) > 0 and 'id' in tagId[0]:
return tagId[0]['id'], tagName
if tag := stash.find_tag(tagName):
return tag['id'], tagName
return "-1", None
def toJson(data):
@@ -1030,6 +1074,10 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, isBlackLis
pathStrToDelete = target.lower()
elif "fileNotExistToDelete" in taskName:
fileNotExistToDelete = True
if target == "Tagged":
isTagOnlyScenes = True
else:
isTagOnlyScenes = False
elif "TagOnlyScenes" in taskName:
isTagOnlyScenes = True
return isTagOnlyScenes, isBlackList, isGrayList, isPinkList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
@@ -1071,9 +1119,57 @@ def getScenesFromReport():
scenes += [json.loads(line)]
return scenes
deleteSceneFlagBgColor = "#646464"
def getFlaggedScenesFromReport(fileName, flagType):
stash.Debug(f"Searching for flag type {flagType} in file {fileName}")
flaggedScenes = []
lines = None
with open(fileName, 'r') as file:
lines = file.readlines()
stash.Trace(f"line count = {len(lines)}")
for line in lines:
if line.startswith(f".ID_") and flagType in line and deleteSceneFlagBgColor not in line:
id = int(line[4:line.index("{")])
stash.Debug(f"Found scene id = {id} with flag {flagType}")
flaggedScenes +=[id]
stash.Trace(f"flaggedScenes count = {len(flaggedScenes)}")
elif line.startswith("</style>"):
if len(flaggedScenes) > 0:
return flaggedScenes
break
stash.Trace(f"Did not find flag {flagType}")
return None
def getFlaggedScenes(ReportName = htmlReportName):
flaggedScenes = []
flagType = stash.JSON_INPUT['args']['Target']
if flagType == "green":
flagType = "#00FF00"
stash.Debug(f"Searching for scenes with flag type {flagType}")
if os.path.isfile(ReportName):
results = getFlaggedScenesFromReport(ReportName,flagType)
if results != None:
flaggedScenes += results
stash.Trace(f"flaggedScenes count = {len(flaggedScenes)}")
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
results = getFlaggedScenesFromReport(fileName,flagType)
if results != None:
flaggedScenes += results
stash.Trace(f"flaggedScenes count = {len(flaggedScenes)}")
else:
stash.Log(f"Report file does not exist: {ReportName}")
return flaggedScenes, flagType
# //////////////////////////////////////////////////////////////////////////////
# //////////////////////////////////////////////////////////////////////////////
def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=False, tagId=-1, advanceMenuOptionSelected=False):
def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGrayListTag=False, tagId=-1, advanceMenuOptionSelected=False, checkFlagOption=False):
tagName = None
if tagId == -1:
tagId, tagName = findCurrentTagId([duplicateMarkForDeletion, base1_duplicateMarkForDeletion, base2_duplicateMarkForDeletion, 'DuplicateMarkForDeletion', '_DuplicateMarkForDeletion'])
@@ -1090,6 +1186,15 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
stash.Error("Running advance menu option with no options enabled.")
return
flaggedScenes = None
flagType = None
if checkFlagOption:
flaggedScenes, flagType = getFlaggedScenes()
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
@@ -1097,14 +1202,17 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
QtyFailedQuery = 0
stash.Debug("#########################################################################")
stash.startSpinningProcessBar()
if isTagOnlyScenes or advanceMenuOptionSelected == False:
if isTagOnlyScenes or (advanceMenuOptionSelected == False and checkFlagOption == False):
stash.Log(f"Getting candidates for deletion by using tag-ID {tagId} and tag-name {tagName}; isTagOnlyScenes={isTagOnlyScenes};advanceMenuOptionSelected={advanceMenuOptionSelected}")
scenes = stash.find_scenes(f={"tags": {"value":tagId, "modifier":"INCLUDES"}}, fragment='id tags {id name} files {path width height duration size video_codec bit_rate frame_rate} details title rating100')
else:
scenes = getScenesFromReport()
stash.stopSpinningProcessBar()
qtyResults = len(scenes)
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
if isTagOnlyScenes or (advanceMenuOptionSelected == False and checkFlagOption == False):
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
else:
stash.Log(f"Found {qtyResults} scenes in report")
stash.setProgressBarIter(qtyResults)
for scene in scenes:
QtyDup += 1
@@ -1148,7 +1256,12 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
elif deleteScenes:
DupFileName = scene['files'][0]['path']
DupFileNameOnly = pathlib.Path(DupFileName).stem
if advanceMenuOptionSelected:
if checkFlagOption:
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 not stash.startsWithInList(blacklist, scene['files'][0]['path']):
continue
@@ -1236,12 +1349,11 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
shutil.move(DupFileName, destPath)
elif moveToTrashCan:
sendToTrash(DupFileName)
result = stash.destroyScene(scene['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(scene['id'], removeScene = True)
result = deleteScene(scene=scene['id'], deleteFile=True, writeToStdOut=False)
QtyDeleted += 1
stash.Debug(f"destroyScene result={result} for file {DupFileName};QtyDeleted={QtyDeleted};Count={QtyDup} of {qtyResults}", toAscii=True)
else:
stash.Error("manageTagggedDuplicates called with invlaid input arguments. Doing early exit.")
stash.Error("manageDuplicatesTaggedOrInReport called with invlaid input arguments. Doing early exit.")
return
stash.Debug("#####################################################")
stash.Log(f"QtyDup={QtyDup}, QtyClearedTags={QtyClearedTags}, QtySetGraylistTag={QtySetGraylistTag}, QtyDeleted={QtyDeleted}, QtyFailedQuery={QtyFailedQuery}", printTo=LOG_STASH_N_PLUGIN)
@@ -1344,16 +1456,19 @@ def deleteLocalDupReportHtmlFiles(doJsonOutput = True):
sys.stdout.write(jsonReturn)
def removeTagFromAllScenes(tagName, deleteTags):
# ToDo: Replace code with SQL code if DB version 68
tagId = stash.find_tags(q=tagName)
if len(tagId) > 0 and 'id' in tagId[0]:
if tag := stash.find_tag(tagName):
if deleteTags:
stash.Debug(f"Deleting tag name {tagName} with Tag ID {tagId[0]['id']} from stash.")
stash.destroy_tag(int(tagId[0]['id']))
stash.Debug(f"Deleting tag name {tagName} with Tag ID {tag['id']} from stash.")
stash.destroy_tag(int(tag['id']))
else:
stash.Debug(f"Removing tag name {tagName} with Tag ID {tagId[0]['id']} from all scenes.")
manageTagggedDuplicates(clearTag=True, tagId=int(tagId[0]['id']))
stash.Debug(f"Removing tag name {tagName} with Tag ID {tag['id']} from all scenes.")
if stash.isCorrectDbVersion() and stash.removeTagFromAllScenes(tagID=int(tag['id'])):
stash.Log(f"Removed tag name {tagName} using SQL.")
else:
manageDuplicatesTaggedOrInReport(clearTag=True, tagId=int(tag['id']))
return True
stash.Warn(f"Could not find tag name {tagName}")
return False
def removeAllDupTagsFromAllScenes(deleteTags=False):
@@ -1376,6 +1491,7 @@ def removeAllDupTagsFromAllScenes(deleteTags=False):
def updateDuplicateCandidateForDeletionList(scene, removeScene = False):
lines = None
scene_id = None
if not os.path.isfile(DuplicateCandidateForDeletionList):
return
with open(DuplicateCandidateForDeletionList, 'r') as file:
@@ -1384,6 +1500,7 @@ def updateDuplicateCandidateForDeletionList(scene, removeScene = False):
scene_id = scene
else:
scene_id = scene['id']
stash.Trace(f"Trying to update scene ID {scene_id} in file {DuplicateCandidateForDeletionList}.")
foundScene = False
with open(DuplicateCandidateForDeletionList, 'w') as file:
for line in lines:
@@ -1397,6 +1514,10 @@ def updateDuplicateCandidateForDeletionList(scene, removeScene = False):
foundScene = True
else:
file.write(line)
if foundScene:
stash.Debug(f"Found and updated scene ID {scene_id} in file {DuplicateCandidateForDeletionList}.")
else:
stash.Debug(f"Did not find scene ID {scene_id} in file {DuplicateCandidateForDeletionList}.")
def updateScenesInReport(fileName, scene):
stash.Log(f"Updating table rows with scene {scene} in file {fileName}")
@@ -1462,7 +1583,6 @@ def updateScenesInReports(scene, ReportName = htmlReportName):
else:
stash.Log(f"Report file does not exist: {ReportName}")
deleteSceneFlagBgColor = "#646464"
def addPropertyToSceneClass(fileName, scene, property):
stash.Log(f"Inserting property {property} for scene {scene} in file {fileName}")
doStyleEndTagCheck = True
@@ -1480,10 +1600,12 @@ def addPropertyToSceneClass(fileName, scene, property):
elif line.startswith("</style>"):
doStyleEndTagCheck = False
else:
if property == "remove highlight" and line.startswith(f".ID_{scene}" + "{") and deleteSceneFlagBgColor not in line and "background-color" in line:
continue
if property == "" and line.startswith(f".ID_{scene}" + "{"):
continue
if line.startswith("</style>"):
if property != "":
if property != "" and property != "remove highlight":
styleSetting = f".ID_{scene}{property}\n"
stash.Log(f"styleSetting = {styleSetting}")
file.write(styleSetting)
@@ -1502,19 +1624,26 @@ def addPropertyToSceneClassToAllFiles(scene, property, ReportName = htmlReportNa
else:
stash.Log(f"Report file does not exist: {ReportName}")
def deleteScene(disableInReport=True, deleteFile=True):
def deleteScene(disableInReport=True, deleteFile=True, scene=None, writeToStdOut=True): # Scene ID
if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return
scene = stash.JSON_INPUT['args']['Target']
if scene == None:
scene = stash.JSON_INPUT['args']['Target']
stash.Log(f"Processing scene ID# {scene}")
result = None
result = stash.destroyScene(scene, delete_file=deleteFile)
if dry_run:
result = f"dry_run enabled, but scene {scene} would have been removed from stash with delete_file={deleteFile}."
stash.Log(result)
else:
result = stash.destroyScene(scene, delete_file=deleteFile)
if disableInReport:
addPropertyToSceneClassToAllFiles(scene, "remove highlight")
addPropertyToSceneClassToAllFiles(scene, "{background-color:" + deleteSceneFlagBgColor + ";pointer-events:none;}")
updateDuplicateCandidateForDeletionList(scene, removeScene = True)
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene} with results = {result}")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', id: '{scene}', result: '{result}'" + "}")
if writeToStdOut:
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}'" + "}")
return result
def clearAllSceneFlags():
addPropertyToSceneClassToAllFiles(None, None)
@@ -1530,7 +1659,11 @@ def copyScene(moveScene=False):
stash.mergeMetadata(scene1, scene2)
result = shutil.copy(scene1['files'][0]['path'], scene2['files'][0]['path'])
if moveScene:
result = stash.destroyScene(scene1['id'], delete_file=True)
if dry_run:
result = f"dry_run enabled, but scene {scene1['files'][0]['path']} would have been removed from stash with delete_file=True."
stash.Log(result)
else:
result = stash.destroyScene(scene1['id'], delete_file=True)
updateDuplicateCandidateForDeletionList(scene1['id'], removeScene = True)
stash.Log(f"destroyScene for scene {scene1['id']} results = {result}")
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene1['id']} and {scene2['id']}")
@@ -1561,6 +1694,10 @@ def flagScene():
if scene == None or flagType == None:
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'failed', scene: '{scene}', flagType: '{flagType}'" + "}")
return
if " highlight" in flagType:
addPropertyToSceneClassToAllFiles(scene, "remove highlight")
if flagType == "disable-scene":
addPropertyToSceneClassToAllFiles(scene, "{background-color:gray;pointer-events:none;}")
elif flagType == "strike-through":
@@ -1604,7 +1741,10 @@ try:
mangeDupFiles(tagDuplicates=False, merge=mergeDupFilename)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "delete_tagged_duplicates_task":
manageTagggedDuplicates(deleteScenes=True)
manageDuplicatesTaggedOrInReport(deleteScenes=True)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME.startswith("deleteScene"):
manageDuplicatesTaggedOrInReport(deleteScenes=True, checkFlagOption=True)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "delete_duplicates_task":
mangeDupFiles(deleteDup=True, merge=mergeDupFilename)
@@ -1613,7 +1753,7 @@ try:
removeAllDupTagsFromAllScenes()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "graylist_tag_task":
manageTagggedDuplicates(setGrayListTag=True)
manageDuplicatesTaggedOrInReport(setGrayListTag=True)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "generate_phash_task":
stash.metadata_generate({"phashes": True})
@@ -1678,7 +1818,7 @@ try:
stash.Debug(f"Tag duplicate EXIT")
elif parse_args.del_tag:
stash.PLUGIN_TASK_NAME = "del_tag"
manageTagggedDuplicates(deleteScenes=True)
manageDuplicatesTaggedOrInReport(deleteScenes=True)
stash.Debug(f"Delete Tagged duplicates EXIT")
elif parse_args.clear_tag:
stash.PLUGIN_TASK_NAME = "clear_tag"
@@ -1689,7 +1829,7 @@ try:
mangeDupFiles(deleteDup=True, merge=mergeDupFilename)
stash.Debug(f"Delete duplicate EXIT")
elif len(sys.argv) < 2 and stash.PLUGIN_TASK_NAME in advanceMenuOptions:
manageTagggedDuplicates(deleteScenes=True, advanceMenuOptionSelected=True)
manageDuplicatesTaggedOrInReport(deleteScenes=True, advanceMenuOptionSelected=True)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
else:
stash.Log(f"Nothing to do!!! (PLUGIN_ARGS_MODE={stash.PLUGIN_TASK_NAME})")

View File

@@ -1,6 +1,6 @@
name: DupFileManager
description: Manages duplicate files.
version: 0.2.2
version: 1.0.0
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui:
javascript:
@@ -70,6 +70,10 @@ settings:
displayName: Tracing
description: Enable tracing and debug so-as to add additional tracing and debug logging in Stash\plugins\DupFileManager\DupFileManager.log
type: BOOLEAN
zzdryRun:
displayName: Dry Run
description: Enable to run script in [Dry Run] mode. In dry run mode, files are NOT deleted, and only logging is performed. Use the logging to determine if deletion will occur as expected.
type: BOOLEAN
exec:
- python
- "{pluginDir}/DupFileManager.py"

View File

@@ -65,7 +65,7 @@ li:hover .large {
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);;
}
/******** Dropdown buttons *********/
.dropdown .dropdown_tag .dropdown_performer .dropdown_gallery .dropbtn {
.dropdown {
font-size: 14px;
border: none;
outline: none;
@@ -94,6 +94,40 @@ li:hover .large {
.dropdown:hover .dropdown-content {
display: block;
}
/*** Dropdown Buttons in Table ***/
.dropbtn_table {
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 */
float:left;
}
.dropbtn_table-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;
}
.dropbtn_table:hover .dropbtn_table-content {
display: block;
}
.links_table-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;
}
.dropbtn_table:hover .links_table-content {
display: block;
}
/*************-- Dropdown Icons --*************/
.dropdown_icon {
height:22px;
@@ -187,6 +221,10 @@ function trim(str, ch) {
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
function RunPluginOperation(Mode, ActionID, button, asyncAjax){
if (asyncAjax){
$('html').addClass('wait');
$("body").css("cursor", "progress");
}
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
@@ -196,14 +234,45 @@ function RunPluginOperation(Mode, ActionID, button, asyncAjax){
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){
console.log(result);
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags")
if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags" || (Mode !== "deleteScene" && Mode.startsWith("deleteScene")))
location.replace(location.href);
if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.");
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);
if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
}
});
}
function SetFlagOnScene(flagType, ActionID){
if (flagType === "yellow highlight")
$('.ID_' + ActionID).css('background','yellow');
else if (flagType === "green highlight")
$('.ID_' + ActionID).css('background','#00FF00');
else if (flagType === "orange highlight")
$('.ID_' + ActionID).css('background','orange');
else if (flagType === "cyan highlight")
$('.ID_' + ActionID).css('background','cyan');
else if (flagType === "pink highlight")
$('.ID_' + ActionID).css('background','pink');
else if (flagType === "red highlight")
$('.ID_' + ActionID).css('background','red');
else if (flagType === "strike-through")
$('.ID_' + ActionID).css('text-decoration', 'line-through');
else if (flagType === "disable-scene")
$('.ID_' + ActionID).css({ 'background' : 'gray', 'pointer-events' : 'none' });
else if (flagType === "remove all flags")
$('.ID_' + ActionID).removeAttr('style'); //.css({ 'background' : '', 'text-decoration' : '', 'pointer-events' : '' });
else
return false;
return true;
}
function selectMarker(Mode, ActionID, button){
$('<p>Select desire marker type <select><option>yellow highlight</option><option>green highlight</option><option>orange highlight</option><option>cyan highlight</option><option>pink highlight</option><option>red highlight</option><option>strike-through</option><option>disable-scene</option><option>remove all flags</option></select></p>').confirm(function(answer){
if(answer.response){
@@ -213,29 +282,8 @@ function selectMarker(Mode, ActionID, button){
console.log("Invalid flagType");
return;
}
if (flagType === "yellow highlight")
$('.ID_' + ActionID).css('background','yellow');
else if (flagType === "green highlight")
$('.ID_' + ActionID).css('background','#00FF00');
else if (flagType === "orange highlight")
$('.ID_' + ActionID).css('background','orange');
else if (flagType === "cyan highlight")
$('.ID_' + ActionID).css('background','cyan');
else if (flagType === "pink highlight")
$('.ID_' + ActionID).css('background','pink');
else if (flagType === "red highlight")
$('.ID_' + ActionID).css('background','red');
else if (flagType === "strike-through")
$('.ID_' + ActionID).css('text-decoration', 'line-through');
else if (flagType === "disable-scene")
$('.ID_' + ActionID).css({ 'background' : 'gray', 'pointer-events' : 'none' });
else if (flagType === "remove all flags")
$('.ID_' + ActionID).removeAttr('style'); //.css({ 'background' : '', 'text-decoration' : '', 'pointer-events' : '' });
else {
flagType = "none";
$('.ID_' + ActionID).css("target-property", "");
return;
}
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID);
RunPluginOperation(Mode, ActionID, button, false);
@@ -273,6 +321,8 @@ $(document).ready(function(){
$("button").click(function(){
var Mode = this.value;
var ActionID = this.id;
if (Mode === "DoNothing")
return;
if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{
var newUrl = window.location.href;
@@ -280,14 +330,17 @@ $(document).ready(function(){
window.open(newUrl, "_blank");
return;
}
if (Mode === "deleteScene" || Mode === "removeScene"){
if (Mode.startsWith("deleteScene") || Mode === "removeScene"){
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
question = "Are you sure you want to delete this file and remove scene from stash?";
if (Mode !== "deleteScene" && Mode.startsWith("deleteScene")) question = "Are you sure you want to delete all the flagged files and remove them from stash?";
if (Mode === "removeScene") question = "Are you sure you want to remove scene from stash?";
if (!chkBxDisableDeleteConfirm.checked && !confirm(question))
return;
$('.ID_' + ActionID).css('background-color','gray');
$('.ID_' + ActionID).css('pointer-events','none');
if (Mode === "deleteScene" || Mode === "removeScene"){
$('.ID_' + ActionID).css('background-color','gray');
$('.ID_' + ActionID).css('pointer-events','none');
}
}
else if (Mode === "newName" || Mode === "renameFile"){
var myArray = ActionID.split(":");
@@ -303,6 +356,14 @@ $(document).ready(function(){
else if (Mode === "flagScene"){
selectMarker(Mode, ActionID, this);
return;
}
else if (Mode.startsWith("flagScene")){
var flagType = Mode.substring(9);
Mode = "flagScene";
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID);
}
RunPluginOperation(Mode, ActionID, this, true);
});
@@ -329,11 +390,19 @@ $(document).ready(function(){
<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 Tag Menu <i class="fa fa-caret-down"></i></button>
<button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance 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><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="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="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>
<div><button title="Delete all orange flagged scenes in report." value="deleteSceneOrangeFlag" id="orange" style="background-color:orange" >Delete All Orange Flagged Scenes</button></div>
<div><button title="Delete all cyan flagged scenes in report." value="deleteSceneCyanFlag" id="cyan" style="background-color:cyan" >Delete All Cyan Flagged Scenes</button></div>
<div><button title="Delete all pink flagged scenes in report." value="deleteScenePinkFlag" id="pink" style="background-color:pink" >Delete All Pink Flagged Scenes</button></div>
<div><button title="Delete all red flagged scenes in report." value="deleteSceneRedFlag" id="red" style="background-color:red" >Delete All Red Flagged Scenes</button></div>
<div><button title="Delete all strike-through scenes in report." value="StrikeThrough" id="line-through" >Delete All Strike-through Scenes</button></div>
</div>
</div>
</td>

View File

@@ -1,4 +1,4 @@
# DupFileManager: Ver 0.2.2 (By David Maisonave)
# DupFileManager: Ver 1.0.0 (By David Maisonave)
DupFileManager is a [Stash](https://github.com/stashapp/stash) plugin which manages duplicate files in the Stash system.
It has both **task** and **tools-UI** components.
@@ -102,9 +102,8 @@ That's it!!!
- ![Screenshot 2024-11-22 232208](https://github.com/user-attachments/assets/bf1f3021-3a8c-4875-9737-60ee3d7fe675)
### Future Planned Features
- Consolidate buttons in report into dropdown buttons. Planned for 1.0.1 Version.
- Add logic to merge performers and galaries seperatly from tag merging on report. Planned for 1.0.5 Version.
- Add logic to merge group metadata when selecting merge option on report. Planned for 1.1.0 Version.
- Add advanced menu directly to the Settings->Tools menu. Planned for 1.5.0 Version.
- Add report directly to the Settings->Tools menu. Planned for 1.5.0 Version.
- Add logic to merge performers and galaries seperatly from tag merging on report. Planned for 1.1.0 Version.
- Add logic to merge group metadata when selecting merge option on report. Planned for 1.2.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

@@ -846,6 +846,22 @@ class StashPluginHelper(StashInterface):
return None
return results['rows'][0][0]
def removeTagFromAllScenes(self, tagName=None, tagID=-1): # Requires either tagName or tagID to be populated.
if tagID < 1:
if tagName == None or tagName == "":
self.Error("Called removeTagFromAllScenes without a tagName or a tagID. One of these two fields MUST be populated.")
return False
if tag := self.find_tag(tagName):
tagID = tag['id']
else:
self.Warn(f"Failed to get tag {tagName}.")
return False
self.Debug(f"Removing tag ID {tagID} from all scenes.")
results = self.sql_commit(f"delete from scenes_tags where tag_id = {tagID}")
self.Debug(f"Called sql_commit and received results {results}.")
return True
# ############################################################################################################
# Functions which are candidates to be added to parent class use snake_case naming convention.
# ############################################################################################################

View File

@@ -70,6 +70,8 @@ html.wait, html.wait * { cursor: wait !important; }
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://www.axter.com/js/jquery-3.7.1.min.js"></script>
<script src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css"/>
<script>
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
@@ -80,7 +82,7 @@ if (urlParams.get('GQL') != null && urlParams.get('GQL') !== "")
GraphQl_URL = urlParams.get('GQL');
console.log(urlParams);
console.log("GQL = " + GraphQl_URL);
console.log("apiKey = " + apiKey);
console.log("Key = " + apiKey);
function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes = false) {
$('html').addClass('wait');
@@ -88,25 +90,48 @@ function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes =
if (TagOnlyScenes)
Param += ":TagOnlyScenes";
console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param);
//if ($("#InPathCheck").prop('checked'))
if (apiKey !== "")
if (apiKey !== ""){
console.log("Using 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,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : Param, "mode":Mode}},
}), success: function(result){
console.log("Ajax success with result = " + result);
$('html').removeClass('wait');
$("body").css("cursor", "default");
if (result.startsWith("{\"errors\"")){
console.log("Ajax FAILED with result = " + result);
if (result.indexOf("{\"runPluginOperation\":null}") > 0)
alert("Stash RunPluginOperation failed with possible source code error.\nCheck Stash logging for details.\n\nResults = " + result);
else
alert("Stash RunPluginOperation failed with result = " + result);
return;
}
console.log("Ajax success with result = " + result);
if (Mode === "tag_duplicates_task" || Mode === "create_duplicate_report_task"){
if (result.indexOf("\"Report complete\"") == -1)
alert("Stash RunPluginOperation returned unexpected results.\nNot sure if report completed successfully.\n\nResults = " + result);
else
$('<p>Report complete. Click on OK to open report in browser.</p>').confirm(function(e){
if(e.response){
var reportUrl = window.location.href;
reportUrl = reportUrl.replace("advance_options.html", "report/DuplicateTagScenes.html");
console.log("reportUrl = " + reportUrl);
window.open(reportUrl, "_blank");
}
});
}
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
});
console.log("Setting default cursor");
}
$(document).ready(function(){
$("button").click(function(){
const AddedWarn = "? This will delete the files, and remove them from stash.";
console.log(this.id);

View File

@@ -20,3 +20,12 @@
- 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.
### 1.0.0
- Consolidated buttons and links on report into dropdown buttons.
- On report, added dropdown menu options for flags.
- Rename Tools-UI advance duplicate tagged menu to [**Advance Duplicate File Deletion Menu**]
- When [**Advance Duplicate File Deletion Menu**] completes report, gives user prompt to open the report in browser.
- Added performance enhancement for removing (clearing) duplicate tags from all scenes by using SQL call.
- Added option to report to delete files that do not exist by duplicate candidates in report, as well as by tagged files.
- Added logic to disable scene in report if deleted by [**Advance Duplicate File Deletion Menu**]. Note: Requires a refresh.
- Added report option to delete by flags set on the report.

View File

@@ -846,6 +846,22 @@ class StashPluginHelper(StashInterface):
return None
return results['rows'][0][0]
def removeTagFromAllScenes(self, tagName=None, tagID=-1): # Requires either tagName or tagID to be populated.
if tagID < 1:
if tagName == None or tagName == "":
self.Error("Called removeTagFromAllScenes without a tagName or a tagID. One of these two fields MUST be populated.")
return False
if tag := self.find_tag(tagName):
tagID = tag['id']
else:
self.Warn(f"Failed to get tag {tagName}.")
return False
self.Debug(f"Removing tag ID {tagID} from all scenes.")
results = self.sql_commit(f"delete from scenes_tags where tag_id = {tagID}")
self.Debug(f"Called sql_commit and received results {results}.")
return True
# ############################################################################################################
# Functions which are candidates to be added to parent class use snake_case naming convention.
# ############################################################################################################

View File

@@ -846,6 +846,22 @@ class StashPluginHelper(StashInterface):
return None
return results['rows'][0][0]
def removeTagFromAllScenes(self, tagName=None, tagID=-1): # Requires either tagName or tagID to be populated.
if tagID < 1:
if tagName == None or tagName == "":
self.Error("Called removeTagFromAllScenes without a tagName or a tagID. One of these two fields MUST be populated.")
return False
if tag := self.find_tag(tagName):
tagID = tag['id']
else:
self.Warn(f"Failed to get tag {tagName}.")
return False
self.Debug(f"Removing tag ID {tagID} from all scenes.")
results = self.sql_commit(f"delete from scenes_tags where tag_id = {tagID}")
self.Debug(f"Called sql_commit and received results {results}.")
return True
# ############################################################################################################
# Functions which are candidates to be added to parent class use snake_case naming convention.
# ############################################################################################################