From 14bb86a529d5105a292947db6ec38b0f35884333 Mon Sep 17 00:00:00 2001 From: David Maisonave <47364845+David-Maisonave@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:44:52 -0500 Subject: [PATCH] Version 1.1.0 See version history for details on all the changes. --- plugins/DupFileManager/DupFileManager.py | 686 ++++++++++++------ plugins/DupFileManager/DupFileManager.yml | 6 +- .../DupFileManager_report_config.py | 447 ++++++------ plugins/DupFileManager/README.md | 38 +- plugins/DupFileManager/StashPluginHelper.py | 13 +- plugins/DupFileManager/advance_options.html | 489 +++++++++---- plugins/DupFileManager/test.html | 51 ++ .../DupFileManager/version_history/README.md | 59 +- plugins/FileMonitor/StashPluginHelper.py | 13 +- plugins/RenameFile/StashPluginHelper.py | 13 +- 10 files changed, 1187 insertions(+), 628 deletions(-) create mode 100644 plugins/DupFileManager/test.html diff --git a/plugins/DupFileManager/DupFileManager.py b/plugins/DupFileManager/DupFileManager.py index 778ead5..018ec8f 100644 --- a/plugins/DupFileManager/DupFileManager.py +++ b/plugins/DupFileManager/DupFileManager.py @@ -80,10 +80,18 @@ advanceMenuOptions = [ "applyCombo", "applyComboPinklist", "applyComboGraylist" doJsonReturnModeTypes = ["tag_duplicates_task", "removeDupTag", "addExcludeTag", "removeExcludeTag", "mergeTags", "getLocalDupReportPath", "createDuplicateReportWithoutTagging", "deleteLocalDupReportHtmlFiles", "clear_duplicate_tags_task", "deleteAllDupFileManagerTags", "deleteBlackListTaggedDuplicatesTask", "deleteTaggedDuplicatesLwrResOrLwrDuration", - "deleteBlackListTaggedDuplicatesLwrResOrLwrDuration", "create_duplicate_report_task"] + "deleteBlackListTaggedDuplicatesLwrResOrLwrDuration", "create_duplicate_report_task", "copyScene"] doJsonReturnModeTypes += [advanceMenuOptions] doJsonReturn = False -if len(sys.argv) < 2 and stash.PLUGIN_TASK_NAME in doJsonReturnModeTypes: +def isReportOrAdvMenu(): + if len(sys.argv) < 2: + if stash.PLUGIN_TASK_NAME in doJsonReturnModeTypes: + return True + if stash.PLUGIN_TASK_NAME.endswith("Flag"): + return True + return False + +if isReportOrAdvMenu(): doJsonReturn = True stash.log_to_norm = stash.LogTo.FILE elif stash.PLUGIN_TASK_NAME == "doEarlyExit": @@ -146,10 +154,26 @@ bitRateIsImporantComp = stash.Setting('bitRateIsImporantComp') codecIsImporantComp = stash.Setting('codecIsImporantComp') excludeFromReportIfSignificantTimeDiff = False +htmlReportPaginate = stash.Setting('htmlReportPaginate') +htmlIncludeImagePreview = stash.Setting('htmlIncludeImagePreview') +htmlIncludeVideoPreview = stash.Setting('htmlIncludeVideoPreview') +htmlHighlightTimeDiff = stash.Setting('htmlHighlightTimeDiff') +htmlImagePreviewSize = stash.Setting('htmlImagePreviewSize') +htmlImagePreviewPopupSize = stash.Setting('htmlImagePreviewPopupSize') +htmlPreviewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview" +htmlSupperHighlight = stash.Setting('htmlSupperHighlight') +htmlDetailDiffTextColor = stash.Setting('htmlDetailDiffTextColor') +htmlLowerHighlight = stash.Setting('htmlLowerHighlight') +htmlReportBackgroundColor = stash.Setting('htmlReportBackgroundColor') +htmlReportTextColor = stash.Setting('htmlReportTextColor') +htmlVideoPreviewWidth = stash.Setting('htmlVideoPreviewWidth') +htmlVideoPreviewHeight = stash.Setting('htmlVideoPreviewHeight') +htmlImagePreviewPopupEnable = True matchDupDistance = int(stash.Setting('matchDupDistance')) matchPhaseDistance = PhashDistance.EXACT matchPhaseDistanceText = "Exact Match" +logTraceForAdvanceMenuOpt = False if (stash.PLUGIN_TASK_NAME == "tag_duplicates_task" or stash.PLUGIN_TASK_NAME == "create_duplicate_report_task") and 'Target' in stash.JSON_INPUT['args']: stash.enableProgressBar(False) if stash.JSON_INPUT['args']['Target'].startswith("0"): @@ -161,9 +185,40 @@ if (stash.PLUGIN_TASK_NAME == "tag_duplicates_task" or stash.PLUGIN_TASK_NAME == elif stash.JSON_INPUT['args']['Target'].startswith("3"): matchDupDistance = 3 - if stash.JSON_INPUT['args']['Target'].find(":") == 1: - significantTimeDiff = float(stash.JSON_INPUT['args']['Target'][2:]) + stash.Trace(f"Target = {stash.JSON_INPUT['args']['Target']}") + targets = stash.JSON_INPUT['args']['Target'].split(":") + if len(targets) > 1: + significantTimeDiff = float(targets[1]) excludeFromReportIfSignificantTimeDiff = True + if len(targets) > 16: + if targets[2] == "true": + htmlIncludeImagePreview = True + else: + htmlIncludeImagePreview = False + htmlReportPaginate = int(targets[3]) + htmlImagePreviewSize = int(targets[4]) + htmlImagePreviewPopupSize = int(targets[5]) + htmlHighlightTimeDiff = int(targets[6]) + maxDupToProcess = int(targets[7]) + if targets[8] == "true": + htmlPreviewOrStream = "stream" + else: + htmlPreviewOrStream = "preview" + htmlSupperHighlight = targets[9] + htmlDetailDiffTextColor = targets[10] + htmlLowerHighlight = targets[11] + htmlReportBackgroundColor = targets[12] + htmlReportTextColor = targets[13] + htmlVideoPreviewWidth = targets[14] + htmlVideoPreviewHeight = targets[15] + if targets[16] == "true": + htmlIncludeVideoPreview = True + else: + htmlIncludeVideoPreview = False + logTraceForAdvanceMenuOpt = True + +if htmlIncludeImagePreview and (htmlImagePreviewSize == htmlImagePreviewPopupSize): + htmlImagePreviewPopupEnable = False if matchDupDistance == 1: matchPhaseDistance = PhashDistance.HIGH @@ -181,6 +236,8 @@ if significantTimeDiff > 1: if significantTimeDiff < .25: significantTimeDiff = float(0.25) +if logTraceForAdvanceMenuOpt: + stash.Trace(f"PLUGIN_TASK_NAME={stash.PLUGIN_TASK_NAME}; matchDupDistance={matchDupDistance}; matchPhaseDistanceText={matchPhaseDistanceText}; matchPhaseDistance={matchPhaseDistance}; significantTimeDiff={significantTimeDiff}; htmlReportPaginate={htmlReportPaginate}; htmlIncludeImagePreview={htmlIncludeImagePreview}; htmlIncludeVideoPreview={htmlIncludeVideoPreview}; maxDupToProcess={maxDupToProcess}; htmlHighlightTimeDiff={htmlHighlightTimeDiff}; htmlImagePreviewSize={htmlImagePreviewSize}; htmlImagePreviewPopupSize={htmlImagePreviewPopupSize}; htmlImagePreviewPopupEnable={htmlImagePreviewPopupEnable}; htmlPreviewOrStream={htmlPreviewOrStream};") duplicateMarkForDeletion = stash.Setting('DupFileTag') if duplicateMarkForDeletion == "": @@ -524,12 +581,12 @@ def getHtmlReportTableRow(qtyResults, tagDuplicates): htmlReportPrefix = htmlReportPrefix.replace('(QtyPlaceHolder)', f'{qtyResults}') htmlReportPrefix = htmlReportPrefix.replace('(MatchTypePlaceHolder)', f'(Match Type = {matchPhaseDistanceText})') htmlReportPrefix = htmlReportPrefix.replace('(DateCreatedPlaceHolder)', datetime.now().strftime("%d-%b-%Y, %H:%M:%S")) + htmlReportPrefix = htmlReportPrefix.replace("Report Info", f"Report Info - [Distance={matchDupDistance}]") + htmlReportPrefix = htmlReportPrefix.replace("BackgroundColorPlaceHolder", htmlReportBackgroundColor) + htmlReportPrefix = htmlReportPrefix.replace("TextColorPlaceHolder", htmlReportTextColor) return htmlReportPrefix htmlReportTableData = stash.Setting('htmlReportTableData') -htmlDetailDiffTextColor = stash.Setting('htmlDetailDiffTextColor') -htmlSupperHighlight = stash.Setting('htmlSupperHighlight') -htmlLowerHighlight = stash.Setting('htmlLowerHighlight') def getColor(Scene1, Scene2, ifScene1HigherChangeColor = False, roundUpNumber = False, qtyDiff=0): if (Scene1 == Scene2) or (roundUpNumber and int(Scene1) == int(Scene2)): return "" @@ -539,6 +596,14 @@ def getColor(Scene1, Scene2, ifScene1HigherChangeColor = False, roundUpNumber = return f' style="color:{htmlDetailDiffTextColor};background-color:{htmlLowerHighlight};"' return f' style="color:{htmlDetailDiffTextColor};"' +def getMarker(Scene1, Scene2, roundUpNumber = False, Marker = "*", LessThanMarker = ""): + if (Scene1 == Scene2) or (roundUpNumber and int(Scene1) == int(Scene2)): + return "" + if int(Scene1) > int(Scene2): + return Marker + return LessThanMarker + + def getRes(Scene): return int(Scene['files'][0]['width']) * int(Scene['files'][0]['height']) @@ -551,8 +616,8 @@ def logReason(DupFileToKeep, Scene, reason): stash.Debug(f"Replacing {DupFileToKeep['files'][0]['path']} with {Scene['files'][0]['path']} for candidate to keep. Reason={reason}") -def getSceneID(scene): - return htmlReportTableData.replace("" if htmlIncludeImagePreview: - imagePreview = f"" - fileHtmlReport.write(f"{getSceneID(DupFile)}
{videoPreview}{imagePreview}
") - else: + spanPreviewImage = "" + if htmlImagePreviewPopupEnable: + spanPreviewImage = f"\"\"" + imagePreview = f"" + if htmlIncludeVideoPreview: + fileHtmlReport.write(f"{getSceneID(DupFile)}
{videoPreview}{imagePreview}
") + else: + fileHtmlReport.write(f"{getSceneID(DupFile)}{imagePreview}") + elif htmlIncludeVideoPreview: fileHtmlReport.write(f"{getSceneID(DupFile)}{videoPreview}") fileHtmlReport.write(f"{getSceneID(DupFile)}{getPath(DupFile)}") - fileHtmlReport.write(f"

") - fileHtmlReport.write(f"") + fileHtmlReport.write(f"

ResDurrationBitRateCodecFrameRatesizeIDindex
{DupFile['files'][0]['width']}x{DupFile['files'][0]['height']}{DupFile['files'][0]['duration']}{DupFile['files'][0]['bit_rate']}{DupFile['files'][0]['video_codec']}{DupFile['files'][0]['frame_rate']}{DupFile['files'][0]['size']}{DupFile['id']}{QtyTagForDel}
") + fileHtmlReport.write(f"") + if doTraceDetails: + stash.Trace(f"Adding html report details for scene {DupFile['id']} / {DupFileToKeep['id']}; Duration {DupFile['files'][0]['duration']} / {DupFileToKeep['files'][0]['duration']}; Size {DupFile['files'][0]['size']} / {DupFileToKeep['files'][0]['size']}; bit_rate {DupFile['files'][0]['bit_rate']} / {DupFileToKeep['files'][0]['bit_rate']}; path {DupFile['files'][0]['path']} / {DupFileToKeep['files'][0]['path']}") if DupFile['id'] in reasonDict: fileHtmlReport.write(f"") @@ -642,161 +716,222 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel = fileHtmlReport.write(f"") fileHtmlReport.write("
{getMarker(getRes(DupFile), getRes(DupFileToKeep))}Res{getMarker(DupFile['files'][0]['duration'], DupFileToKeep['files'][0]['duration'], True)}DurrationBitRateCodecFrameRatesizeIDindex
{DupFile['files'][0]['width']}x{DupFile['files'][0]['height']}{DupFile['files'][0]['duration']}{DupFile['files'][0]['bit_rate']}{DupFile['files'][0]['video_codec']}{DupFile['files'][0]['frame_rate']}{DupFile['files'][0]['size']}{DupFile['id']}{itemIndex}
Reason: {reasonDict[DupFile['id']]}
Reason: not ExcludeTag vs ExcludeTag
") - fileHtmlReport.write('

') - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write("
") - - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - # ToDo: Add following buttons: - # rename file - if dupFileExist and tagDuplicates: - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write("
") - - - if dupFileExist: - fileHtmlReport.write('
") - else: - fileHtmlReport.write(fileDoesNotExistStr) DupToKeepMissingTag, DelCandidateMissingTag = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'tags') + DupToKeepMissingPerformer, DelCandidateMissingPerformer = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'performers') + DupToKeepMissingGallery, DelCandidateMissingGallery = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'galleries', 'title') + DupToKeepMissingGroup, DelCandidateMissingGroup = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'groups') + + QtyIconsOnToolBar = 2 + MaxQtyIconsOnToolBar = 7 + fileHtmlReport.write(f"
") + fileHtmlReport.write(f"File") + fileHtmlReport.write(f"Tag/Flag") if len(DupFile['tags']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" if DupToKeepMissingTag: - fileHtmlReport.write(htmlTagPrefix.replace(defaultColorTag, "YellowTag.png")) - else: - fileHtmlReport.write(htmlTagPrefix) + menuitem = menuitem.replace("icon-blue-tag", "icon-yellow-tag") + fileHtmlReport.write(menuitem) + if len(DupFile['performers']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DupToKeepMissingPerformer: + menuitem = menuitem.replace("icon-headshot", "icon-yellow-headshot") + fileHtmlReport.write(menuitem) + if len(DupFile['galleries']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DupToKeepMissingGallery: + menuitem = menuitem.replace("icon-galleries", "icon-yellow-galleries") + fileHtmlReport.write(menuitem) + if len(DupFile['groups']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DupToKeepMissingGroup: + menuitem = menuitem.replace("icon-group", "icon-yellow-group") + fileHtmlReport.write(menuitem) + fileHtmlReport.write(f"") + fileHtmlReport.write(f"") + if QtyIconsOnToolBar > 5: + MaxQtyIconsOnToolBar = 6 + elif QtyIconsOnToolBar < 4: + MaxQtyIconsOnToolBar = 8 + if QtyIconsOnToolBar < MaxQtyIconsOnToolBar: + QtyIconsOnToolBar += 1 + fileHtmlReport.write(f"") + if QtyIconsOnToolBar < MaxQtyIconsOnToolBar: + QtyIconsOnToolBar += 1 + fileHtmlReport.write(f"") + if QtyIconsOnToolBar < MaxQtyIconsOnToolBar: + QtyIconsOnToolBar += 1 + fileHtmlReport.write(f"") + if QtyIconsOnToolBar < MaxQtyIconsOnToolBar: + QtyIconsOnToolBar += 1 + fileHtmlReport.write(f"") + if QtyIconsOnToolBar < MaxQtyIconsOnToolBar: + QtyIconsOnToolBar += 1 + fileHtmlReport.write(f"") + fileHtmlReport.write("
") + fileHtmlReport.write(f"
") + fileHtmlReport.write(f"
Delete
") + fileHtmlReport.write(f"
Remove Scene
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Copy to [Duplicate to Keep]
") + fileHtmlReport.write(f"
Move to [Duplicate to Keep] and Metadata
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Copy this Name to [Duplicate to Keep]
") + if dupFileExist: + fileHtmlReport.write('') + fileHtmlReport.write(f"
[Folder]
") + fileHtmlReport.write(f"
[Play]
") + fileHtmlReport.write("
") + fileHtmlReport.write(f"
") + if dupFileExist and tagDuplicates: + fileHtmlReport.write(f"
Remove Duplicate Tag
") + fileHtmlReport.write(f"
Add Exclude Tag
") + fileHtmlReport.write(f"
Merge Tags, Performers, & Galleries
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Flag this scene
") + fileHtmlReport.write(f"
Flag Cyan
") + fileHtmlReport.write(f"
Flag Green
") + fileHtmlReport.write(f"
Flag Orange
") + fileHtmlReport.write(f"
Flag Yellow
") + fileHtmlReport.write(f"
Flag Pink
") + fileHtmlReport.write(f"
Flag Red
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Flag Strike-through
") + fileHtmlReport.write(f"
Flag Disable-scene
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Erase All Flags
") + fileHtmlReport.write("
") + if len(DupFile['tags']) > 0: + fileHtmlReport.write(f"
") for tag in DupFile['tags']: # if not tag['ignore_auto_tag']: - fileHtmlReport.write(f"
{tag['name']}
") - fileHtmlReport.write("
") - DupToKeepMissingPerformer, DelCandidateMissingPerformer = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'performers') + fileHtmlReport.write(f"
{tag['name']}
") + fileHtmlReport.write("") if len(DupFile['performers']) > 0: - if DupToKeepMissingPerformer: - fileHtmlReport.write(htmlPerformerPrefix.replace(defaultColorPerformer, "YellowHeadshot.png")) - else: - fileHtmlReport.write(htmlPerformerPrefix) + fileHtmlReport.write(f"
") for performer in DupFile['performers']: - fileHtmlReport.write(f"
{performer['name']}
") - fileHtmlReport.write("
") - DupToKeepMissingGallery, DelCandidateMissingGallery = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'galleries', 'title') + fileHtmlReport.write(f"
{performer['name']}
") + fileHtmlReport.write("") if len(DupFile['galleries']) > 0: - if DupToKeepMissingGallery: - fileHtmlReport.write(htmlGalleryPrefix.replace(defaultColorGalleries, "YellowGalleries.png")) - else: - fileHtmlReport.write(htmlGalleryPrefix) + fileHtmlReport.write(f"
") for gallery in DupFile['galleries']: gallery = stash.getGalleryName(gallery['id']) - fileHtmlReport.write(f"
{gallery['title']}
") - fileHtmlReport.write("
") - DupToKeepMissingGroup, DelCandidateMissingGroup = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'groups') + fileHtmlReport.write(f"
{gallery['title']}
") + fileHtmlReport.write("") if len(DupFile['groups']) > 0: - if DupToKeepMissingGroup: - fileHtmlReport.write(htmlGroupPrefix.replace(defaultColorGroup, "YellowGroup.png")) - else: - fileHtmlReport.write(htmlGroupPrefix) + fileHtmlReport.write(f"
") for group in DupFile['groups']: - fileHtmlReport.write(f"
{group['group']['name']}
") - fileHtmlReport.write("
") - + fileHtmlReport.write(f"
{group['group']['name']}
") + fileHtmlReport.write("") + if not dupFileExist: + fileHtmlReport.write(fileDoesNotExistStr) fileHtmlReport.write("

") + # /////////////////////////////// videoPreview = f"" if htmlIncludeImagePreview: - imagePreview = f"" - fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}
{videoPreview}{imagePreview}
") - else: + spanPreviewImage = "" + if htmlImagePreviewPopupEnable: + spanPreviewImage = f"\"\"" + imagePreview = f"" + if htmlIncludeVideoPreview: + fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}
{videoPreview}{imagePreview}
") + else: + fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}{imagePreview}") + elif htmlIncludeVideoPreview: fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}{videoPreview}") fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}{getPath(DupFileToKeep)}") fileHtmlReport.write(f"

") fileHtmlReport.write(f"
ResDurrationBitRateCodecFrameRatesizeID
{DupFileToKeep['files'][0]['width']}x{DupFileToKeep['files'][0]['height']}{DupFileToKeep['files'][0]['duration']}{DupFileToKeep['files'][0]['bit_rate']}{DupFileToKeep['files'][0]['video_codec']}{DupFileToKeep['files'][0]['frame_rate']}{DupFileToKeep['files'][0]['size']}{DupFileToKeep['id']}
") - fileHtmlReport.write('

') - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write("
") + fileHtmlReport.write(f"
") + fileHtmlReport.write(f"File") + fileHtmlReport.write(f"Tag/Flag") + if len(DupFileToKeep['tags']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DelCandidateMissingTag: + menuitem = menuitem.replace("icon-blue-tag", "icon-pink-tag") + fileHtmlReport.write(menuitem) + if len(DupFileToKeep['performers']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DelCandidateMissingPerformer: + menuitem = menuitem.replace("icon-headshot", "icon-pink-headshot") + fileHtmlReport.write(menuitem) + if len(DupFileToKeep['galleries']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DelCandidateMissingGallery: + menuitem = menuitem.replace("icon-galleries", "icon-pink-galleries") + fileHtmlReport.write(menuitem) + if len(DupFileToKeep['groups']) > 0: + QtyIconsOnToolBar += 1 + menuitem = f"" + if DelCandidateMissingGroup: + menuitem = menuitem.replace("icon-group", "icon-pink-group") + fileHtmlReport.write(menuitem) + fileHtmlReport.write("
") - fileHtmlReport.write(f"
") - if isTaggedExcluded(DupFileToKeep): - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write(f"
") - fileHtmlReport.write("
") - - - fileHtmlReport.write('
") - if not toKeepFileExist: - fileHtmlReport.write(fileDoesNotExistStr) + fileHtmlReport.write(f"
[Play]
") + fileHtmlReport.write("
") + + fileHtmlReport.write(f"
") + if isTaggedExcluded(DupFileToKeep): + fileHtmlReport.write(f"
Remove Exclude Tag
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Flag this scene
") + fileHtmlReport.write(f"
Flag Cyan
") + fileHtmlReport.write(f"
Flag Green
") + fileHtmlReport.write(f"
Flag Orange
") + fileHtmlReport.write(f"
Flag Yellow
") + fileHtmlReport.write(f"
Flag Pink
") + fileHtmlReport.write(f"
Flag Red
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Flag Strike-through
") + fileHtmlReport.write(f"
Flag Disable-scene
") + fileHtmlReport.write('') + fileHtmlReport.write(f"
Remove All Flags
") + fileHtmlReport.write("
") if len(DupFileToKeep['tags']) > 0: - if DelCandidateMissingTag: - fileHtmlReport.write(htmlTagPrefix.replace(defaultColorTag, "RedTag.png")) - else: - fileHtmlReport.write(htmlTagPrefix) + fileHtmlReport.write(f"
") for tag in DupFileToKeep['tags']: # if not tag['ignore_auto_tag']: - fileHtmlReport.write(f"
{tag['name']}
") - fileHtmlReport.write("
") + fileHtmlReport.write(f"
{tag['name']}
") + fileHtmlReport.write("") if len(DupFileToKeep['performers']) > 0: - if DelCandidateMissingPerformer: - fileHtmlReport.write(htmlPerformerPrefix.replace(defaultColorPerformer, "PinkHeadshot.png")) - else: - fileHtmlReport.write(htmlPerformerPrefix) + fileHtmlReport.write(f"
") for performer in DupFileToKeep['performers']: - fileHtmlReport.write(f"
{performer['name']}
") - fileHtmlReport.write("
") + fileHtmlReport.write(f"
{performer['name']}
") + fileHtmlReport.write("") if len(DupFileToKeep['galleries']) > 0: - if DelCandidateMissingGallery: - fileHtmlReport.write(htmlGalleryPrefix.replace(defaultColorGalleries, "PinkGalleries.png")) - else: - fileHtmlReport.write(htmlGalleryPrefix) + fileHtmlReport.write(f"
") for gallery in DupFileToKeep['galleries']: gallery = stash.getGalleryName(gallery['id']) - fileHtmlReport.write(f"
{gallery['title']}
") - fileHtmlReport.write("
") + fileHtmlReport.write(f"
{gallery['title']}
") + fileHtmlReport.write("") if len(DupFileToKeep['groups']) > 0: - if DelCandidateMissingGroup: - fileHtmlReport.write(htmlGroupPrefix.replace(defaultColorGroup, "PinkGroup.png")) - else: - fileHtmlReport.write(htmlGroupPrefix) + fileHtmlReport.write(f"
") for group in DupFileToKeep['groups']: - fileHtmlReport.write(f"
{group['group']['name']}
") - fileHtmlReport.write("
") - # ToDo: Add following buttons: - # rename file + fileHtmlReport.write(f"
{group['group']['name']}
") + fileHtmlReport.write("") + + if not toKeepFileExist: + fileHtmlReport.write(fileDoesNotExistStr) fileHtmlReport.write(f"

") - fileHtmlReport.write(f"\n") + fileHtmlReport.write(f"{ToDeleteSceneIDSrchStr}{DupFile['id']}{ToKeepSceneIDSrchStr}{DupFileToKeep['id']}{itemIndexSrchStr}{itemIndex}:: -->\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 + "} " @@ -804,6 +939,8 @@ 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): global reasonDict global htmlFileData @@ -813,7 +950,6 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack stash.Trace(f"dupTagId={dupTagId} name={duplicateMarkForDeletion}") createHtmlReport = stash.Setting('createHtmlReport') htmlReportNameHomePage = htmlReportName - htmlReportPaginate = stash.Setting('htmlReportPaginate') addDupWhitelistTag() @@ -855,9 +991,14 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack deleteLocalDupReportHtmlFiles(False) fileHtmlReport = open(htmlReportName, "w") fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n") + fileHtmlReport.write("
Next
") fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n") htmlReportTableHeader = stash.Setting('htmlReportTableHeader') - fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Scene{htmlReportTableHeader}Duplicate to Delete{htmlReportTableHeader}Scene-ToKeep{htmlReportTableHeader}Duplicate to Keep\n") + SceneTableHeader = htmlReportTableHeader + if htmlIncludeVideoPreview or htmlIncludeImagePreview: + fileHtmlReport.write(f"{htmlReportTableRow}{SceneTableHeader}Scene{htmlReportTableHeader}Duplicate to Delete{SceneTableHeader}Scene-ToKeep{htmlReportTableHeader}Duplicate to Keep\n") + else: + fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Duplicate to Delete{htmlReportTableHeader}Duplicate to Keep\n") fileDuplicateCandidateForDeletionList = open(DuplicateCandidateForDeletionList, "w") for DupFileSet in DupFileSets: @@ -1009,20 +1150,21 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack # add delete only from stash db code and button using DB delete icon stash.Debug(f"Adding scene {DupFile['id']} to HTML report.") writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, QtyTagForDel, tagDuplicates) + DupFile['DupFileToKeep'] = DupFileToKeep['id'] fileDuplicateCandidateForDeletionList.write(json.dumps(DupFile) + "\n") if QtyTagForDelPaginate >= htmlReportPaginate: QtyTagForDelPaginate = 0 fileHtmlReport.write("\n") - homeHtmReportLink = f"[Home]" + homeHtmReportLink = f"Home" prevHtmReportLink = "" if PaginateId > 0: if PaginateId > 1: prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html") else: prevHtmReport = htmlReportNameHomePage - prevHtmReportLink = f"[Prev]" + prevHtmReportLink = f"Prev" nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html") - nextHtmReportLink = f"[Next]" + nextHtmReportLink = f"Next" fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}{nextHtmReportLink}
") fileHtmlReport.write(f"{stash.Setting('htmlReportPostfix')}") fileHtmlReport.close() @@ -1033,16 +1175,19 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html") else: prevHtmReport = htmlReportNameHomePage - prevHtmReportLink = f"[Prev]" + prevHtmReportLink = f"Prev" if len(DupFileSets) > (QtyTagForDel + htmlReportPaginate): nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html") - nextHtmReportLink = f"[Next]" + nextHtmReportLink = f"Next" fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}{nextHtmReportLink}
") else: stash.Debug(f"DupFileSets Qty = {len(DupFileSets)}; DupFileDetailList Qty = {len(DupFileDetailList)}; QtyTagForDel = {QtyTagForDel}; htmlReportPaginate = {htmlReportPaginate}; QtyTagForDel + htmlReportPaginate = {QtyTagForDel+htmlReportPaginate}") fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}
") fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n") - fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Scene{htmlReportTableHeader}Duplicate to Delete{htmlReportTableHeader}Scene-ToKeep{htmlReportTableHeader}Duplicate to Keep\n") + if htmlIncludeVideoPreview or htmlIncludeImagePreview: + fileHtmlReport.write(f"{htmlReportTableRow}{SceneTableHeader}Scene{htmlReportTableHeader}Duplicate to Delete{SceneTableHeader}Scene-ToKeep{htmlReportTableHeader}Duplicate to Keep\n") + else: + fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Duplicate to Delete{htmlReportTableHeader}Duplicate to Keep\n") if tagDuplicates and graylistTagging and stash.startsWithInList(graylist, DupFile['files'][0]['path']): stash.addTag(DupFile, graylistMarkForDeletion, ignoreAutoTag=True) @@ -1061,16 +1206,18 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack if fileHtmlReport != None: fileHtmlReport.write("\n") if PaginateId > 0: - homeHtmReportLink = f"[Home]" + homeHtmReportLink = f"Home" if PaginateId > 1: prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html") else: prevHtmReport = htmlReportNameHomePage - prevHtmReportLink = f"[Prev]" + prevHtmReportLink = f"Prev" fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}
") fileHtmlReport.write(f"

Total Tagged for Deletion {QtyTagForDel}

\n") fileHtmlReport.write(f"{stash.Setting('htmlReportPostfix')}") fileHtmlReport.close() + if PaginateId == 0: + modifyPropertyToSceneClassToAllFiles("NextPage_Top", "{display : none;}") stash.Log(f"************************************************************", printTo = stash.LogTo.STASH) stash.Log(f"************************************************************", printTo = stash.LogTo.STASH) stash.Log(f"View Stash duplicate report using Stash->Settings->Tools->[Duplicate File Report]", printTo = stash.LogTo.STASH) @@ -1103,7 +1250,7 @@ def toJson(data): data = data.replace("\\\\\\\\", "\\\\") return json.loads(data) -def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater): +def getAnAdvanceMenuOptionSelected(taskName, target, getTagScenesFromStash, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater): stash.Log(f"Processing taskName = {taskName}, target = {target}") if "Blacklist" in taskName: tagOrFlag = "Blacklist" @@ -1129,7 +1276,7 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, tagOrFlag, compareToGreater = True if ":TagOnlyScenes" in target: - isTagOnlyScenes = True + getTagScenesFromStash = True target = target.replace(":TagOnlyScenes","") if "pathToDelete" in taskName: @@ -1153,15 +1300,15 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, tagOrFlag, elif "fileNotExistToDelete" in taskName: fileNotExistToDelete = True if target == "Tagged": - isTagOnlyScenes = True + getTagScenesFromStash = True else: - isTagOnlyScenes = False + getTagScenesFromStash = False elif "TagOnlyScenes" in taskName: - isTagOnlyScenes = True - return isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater + getTagScenesFromStash = True + return getTagScenesFromStash, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater def getAdvanceMenuOptionSelected(advanceMenuOptionSelected): - isTagOnlyScenes = False + getTagScenesFromStash = False tagOrFlag = None pathToDelete = "" sizeToDelete = -1 @@ -1180,10 +1327,10 @@ def getAdvanceMenuOptionSelected(advanceMenuOptionSelected): if "applyCombo" in stash.PLUGIN_TASK_NAME: jsonObject = toJson(stash.JSON_INPUT['args']['Target']) for taskName in jsonObject: - 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) + getTagScenesFromStash, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAnAdvanceMenuOptionSelected(taskName, jsonObject[taskName], getTagScenesFromStash, 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, 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 + return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], getTagScenesFromStash, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater) + return getTagScenesFromStash, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater def getScenesFromReport(): stash.Log(f"Getting candidates for deletion from file {DuplicateCandidateForDeletionList}.") @@ -1260,7 +1407,8 @@ def getFlaggedScenes(flagType=None, ReportName = htmlReportName): # ////////////////////////////////////////////////////////////////////////////// # ////////////////////////////////////////////////////////////////////////////// -def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGrayListTag=False, tagId=-1, advanceMenuOptionSelected=False, checkFlagOption=False): +def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGrayListTag=False, tagId=-1, advanceMenuOptionSelected=False, checkFlagOption=False, flagColor=None, flagAction=None): + global cleanAfterDel tagName = None if tagId == -1: tagId, tagName = findCurrentTagId([duplicateMarkForDeletion, base1_duplicateMarkForDeletion, base2_duplicateMarkForDeletion, 'DuplicateMarkForDeletion', '_DuplicateMarkForDeletion']) @@ -1272,23 +1420,35 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray if clearAllDupfileManagerTags: excludedTags = [duplicateMarkForDeletion, duplicateWhitelistTag, excludeDupFileDeleteTag, graylistMarkForDeletion, longerDurationLowerResolution] - isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAdvanceMenuOptionSelected(advanceMenuOptionSelected) + getTagScenesFromStash, 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 or (tagOrFlag != None and "Flag" in tagOrFlag): + button_property = None + flagActionToLog = flagAction + if flagColor != None: + cleanAfterDel = False + if flagAction == None and deleteScenes == True: + flagActionToLog = "deleteScenes" + stash.Log(f"Processing [Duplicate-to-Delete] scenes with flag color {flagColor} and performing action {flagActionToLog}") + flaggedScenes, flagType = getFlaggedScenes(flagColor) + if stash.isEmpty(flaggedScenes): + stash.Error(f"Early exit, because found no scenes with flag {flagColor}.") + return + elif checkFlagOption or (tagOrFlag != None and "Flag" in tagOrFlag): if checkFlagOption: flaggedScenes, flagType = getFlaggedScenes() else: checkFlagOption = True flaggedScenes, flagType = getFlaggedScenes(tagOrFlag) - if flaggedScenes == None or len(flaggedScenes) == 0: + if stash.isEmpty(flaggedScenes): stash.Error(f"Early exit, because found no scenes with flag {flagType}.") return - stash.Debug(f"Fournd {len(flaggedScenes)} scenes with flag {flagType}") + button_property ="{background-color:" + flagType + ";}" + stash.Debug(f"Fournd {len(flaggedScenes)} scenes with flag {flagType}; button_property={button_property}") QtyDup = 0 QtyDeleted = 0 @@ -1297,16 +1457,16 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray QtyFailedQuery = 0 stash.Debug("#########################################################################") stash.startSpinningProcessBar() - 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}") + if advanceMenuOptionSelected == False and checkFlagOption == False and flagColor == None: + getTagScenesFromStash = True + if getTagScenesFromStash: + stash.Log(f"Getting candidates for deletion by using tag-ID {tagId} and tag-name {tagName}; getTagScenesFromStash={getTagScenesFromStash};advanceMenuOptionSelected={advanceMenuOptionSelected}") 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: + if getTagScenesFromStash: stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})") else: stash.Log(f"Found {qtyResults} scenes in report") @@ -1350,10 +1510,24 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray stash.Log(f"Added tag {graylistMarkForDeletion} to scene {scene['files'][0]['path']};QtySetGraylistTag={QtySetGraylistTag};Count={QtyDup} of {qtyResults}") else: stash.Trace(f"Scene already had tag {graylistMarkForDeletion}; {scene['files'][0]['path']}") + elif flagAction != None: + if int(scene['id']) in flaggedScenes: + stash.Log(f"Found {flagType} flagged candidate for {flagAction}; Scene ID = {scene['id']}") + else: + continue + if flagAction == "addExcludeTag": + stash.addTag(scene['id'], excludeDupFileDeleteTag) + stash.Log(f"Done adding exclude tag to scene {scene['id']}.") + elif flagAction == "copyScene" or flagAction == "moveScene": + copyScene(flagAction == "moveScene", scene) + elif flagAction == "mergeScene": + mergeTags(scene) + else: + stash.Error(f"flagAction not recognized. flagAction={flagAction}") elif deleteScenes: DupFileName = scene['files'][0]['path'] DupFileNameOnly = pathlib.Path(DupFileName).stem - if checkFlagOption and (tagOrFlag == None or "Flag" not in tagOrFlag): + if flagColor != None or (checkFlagOption and (tagOrFlag == None or "Flag" not in tagOrFlag)): if int(scene['id']) in flaggedScenes: stash.Log(f"Found {flagType} flagged candidate for deletion; Scene ID = {scene['id']}") else: @@ -1451,7 +1625,7 @@ def manageDuplicatesTaggedOrInReport(deleteScenes=False, clearTag=False, setGray shutil.move(DupFileName, destPath) elif moveToTrashCan: sendToTrash(DupFileName) - result = deleteScene(scene=scene['id'], deleteFile=True, writeToStdOut=False) + result = deleteScene(scene=scene['id'], deleteFile=True, writeToStdOut=False, button_property=button_property) QtyDeleted += 1 stash.Debug(f"destroyScene result={result} for file {DupFileName};QtyDeleted={QtyDeleted};Count={QtyDup} of {qtyResults}", toAscii=True) else: @@ -1529,12 +1703,10 @@ def mergeMetadataInThisFile(fileName): 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 = " + + + + -
- +
+
+
+
+
DupFileManager
Advance Duplicate File Deletion Menu
Apply Multiple Options
+ - + - + - + - + - + +
DupFileManager
Advance Duplicate File Menu
Apply Multiple Options
- + - - -
Create report overriding user [Match Duplicate Distance] and [significantTimeDiff] settings
Create report overriding user [Match Duplicate Distance] and other user settings
+
+ Create Duplicate Report + + +
+
+
Create Duplicate Report [Exact Match]
+
Create Duplicate Report [High Match]
+
Create Duplicate Report [Medium Match]
+
Create Duplicate Report [Low Match]
+ +
Create Duplicate Tagging Report [Exact Match]
+
Create Duplicate Tagging Report [High Match]
+
Create Duplicate Tagging Report [Medium Match]
+
Create Duplicate Tagging Report [Low Match]
+
+
+ + +
Stash Plugins
+
Stash Tools
-
- - - -
-
- - @@ -347,7 +375,7 @@ function DeleteDupInPath(){
@@ -442,32 +470,32 @@ function DeleteDupInPath(){
- - + +
- - + + - + - +
- + @@ -494,7 +522,7 @@ function DeleteDupInPath(){
@@ -1925,33 +1953,109 @@ function DeleteDupInPath(){
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ These options override the UI plugin user settings and the DupFileManager_config.py settings. +
These options apply to [Create Duplicate Report] sub-menu options, that have specific match value.
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Note: Color entries can be a color name, or it can be a hexadecimal value in the form: #rrggbb
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
- Details:
  1. Match Duplicate Distance Number Details
    1. Exact Match
    2. -
        +
        1. Safest and most reliable option
        2. Uses tag name _DuplicateMarkForDeletion_0
        3. Has the fewest results, and it's very rare to have false matches.
      1. High Match
      2. -
          +
          1. Recommended Setting
          2. Safe and usually reliable
          3. Uses tag name _DuplicateMarkForDeletion_1
          4. Scenes tagged by Exact Match will have both tags (_DuplicateMarkForDeletion_0 and _DuplicateMarkForDeletion_1)
        1. Medium Match
        2. -
            +
            1. Not so safe. Some false matches
            2. To reduce false matches use a time difference of .96 or higher.
            3. Uses tag name _DuplicateMarkForDeletion_2
            4. Scenes tagged by 0 and 1 will have three tags.
          1. Low Match
          2. -
              +
              1. Unsafe, and many false matches
              2. To reduce false matches use a time difference of .98 or higher.
              3. Uses tag name _DuplicateMarkForDeletion_3
              4. @@ -1963,7 +2067,7 @@ function DeleteDupInPath(){
                1. Significant time difference setting, where 1 equals 100% and (.9) equals 90%.
                2. This setting overrides the setting in DupFileManager_config.py.
                3. -
                    +
                    1. See setting significantTimeDiff in DupFileManager_config.py
                  1. This setting is generally not useful for [Exact Match] reports.
                  2. @@ -1981,12 +2085,79 @@ function DeleteDupInPath(){
                  3. The report is created much faster. It usually takes a few seconds to complete.
                  4. This is the recommended report type to create if the DupFileManager Advance Menu is not needed or desired.
                  +
                4. Tag V.S. Flag
                5. +
                    +
                  1. A tag is part Stash scene attributes. Tags are stored in the Stash database.
                  2. +
                      +
                    1. When a new DupFileManager report is created, it does NOT delete existing tags.
                    2. +
                    +
                  3. A flag is only used in the reports created by DupFileManager.
                  4. +
                      +
                    1. If a new report is created, ALL the flags are deleted!
                    2. +
                    3. Flag Purpose
                    4. +
                        +
                      1. Allow the user to review the DupFileManager report, and flag files for futher action or flag them as review complete.
                      2. +
                      +
                    5. The DupFileManager report, has options that allows the user to perform the same action on all files with a selected color flag.
                    6. +
                    7. The following flag group options are available.
                    8. +
                        +
                      1. Delete all files with specified flag color.
                      2. +
                      3. Copy all scene files to assosciated Duplicate-to-Keep file where Duplicate-to-Delete is flaged with specified flag color.
                      4. +
                      5. Move all scene files to assosciated Duplicate-to-Keep file where Duplicate-to-Delete is flaged with specified flag color.
                      6. +
                          +
                        1. A move operation also copies the metadata like tags, performers, and galleries.
                        2. +
                        3. Note: Future versions of this program might also delete the Duplicate-to-Delete scene after successfully coping the file.
                        4. +
                        +
                      7. Copy tags, performers, and galleries from Duplicate-to-Delete to associated Duplicate-to-Keep.
                      8. +
                      9. Add exclude tag to all files with specified flag color.
                      10. +
                      +
                    +
                  +
                6. List
                7. +
                    +
                  1. DupFileManager supports 4 types of list that are configured in Stash=>Settings->Plugins->DupFileManager.
                  2. +
                      +
                    1. Whitelist
                    2. +
                        +
                      1. A list of protected paths.
                      2. +
                      3. Videos under these paths are NOT to be deleted.
                      4. +
                      5. E.g. C:\Favorite\,E:\MustKeep\
                      6. +
                      +
                    3. Graylist
                    4. +
                        +
                      1. A list of preferential paths.
                      2. +
                      3. Videos under these paths are only designated for deletion if:
                      4. +
                          +
                        1. Counter duplicate is in the whitelist.
                        2. +
                        3. If the video quality is poor compare to blacklist duplicate.
                        4. +
                        +
                      5. E.g. C:\2nd_Fav\,E:\ShouldKeep\
                      6. +
                      +
                    5. Blacklist
                    6. +
                        +
                      1. A list of least preferential paths.
                      2. +
                      3. Videos under these paths are primary candidates for deletion.
                      4. +
                      5. E.g. C:\Downloads\,E:\DeleteMeFirst\
                      6. +
                      +
                    7. Pinklist
                    8. +
                        +
                      1. The pinklist is NOT used at all when creating the Duplicate Report.
                      2. +
                      3. The pinklist is only used in the Advance Menu.
                      4. +
                          +
                        1. This gives the user additional manual deletion options.
                        2. +
                        3. This option is similar to the Path field option.
                        4. +
                        +
                      +
                    +
                  3. Whitelist is not available in the Advance Menu because the paths are designated as protected from deletion.
                  4. +
+ + +
- - + diff --git a/plugins/DupFileManager/test.html b/plugins/DupFileManager/test.html new file mode 100644 index 0000000..d20c4be --- /dev/null +++ b/plugins/DupFileManager/test.html @@ -0,0 +1,51 @@ + + + + + menu demo + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/DupFileManager/version_history/README.md b/plugins/DupFileManager/version_history/README.md index b24daee..434f933 100644 --- a/plugins/DupFileManager/version_history/README.md +++ b/plugins/DupFileManager/version_history/README.md @@ -1,5 +1,9 @@ ##### This page was added starting on version 0.2.0 to keep track of newly added features between versions. -- Note: Sub versions (x.x.x.**x**) are only listed on this page. +- Note: + - 4th number sub versions (x.x.x.**x**) are only listed on this page. It's associated with very minor changes or very minor bug fixes. + - Changes to the 3rd number (x.x.**x**.x) are related to bug fixes and minor changes. + - The 2nd number (x.**x**.x.x) is incremented when new feature(s) is/are added. + - The 1st number (**x**.x.x.x) is incremented when a change is not backwardly compatible to an older Stash version, or when an extremely major change is made. ### 0.2.0 - For report, added logic to transfer option settings **[Disable Complete Confirmation]** and **[Disable Delete Confirmation]** when paginating. - Fixed minor bug in advance_options.html for GQL params. @@ -17,18 +21,18 @@ - Added popup gallery list to report which list all galleries associated with scene. - Added popup group list to report which list all groups associated with scene. - After merging tags in report, the report gets updated with the merged scene metadata. -- Added graylist deletion option to [**Advance Duplicate File Deletion Menu**]. -- Added pinklist option to Settings->Plugins->Plugins and to [**Advance Duplicate File Deletion Menu**] - - The pinklist is only used with the [**Advance Duplicate File Deletion Menu**], and it's **NOT** used in the primary process to selected candidates for deletion. +- Added graylist deletion option to [**Advance Duplicate File Menu**]. +- Added pinklist option to Settings->Plugins->Plugins and to [**Advance Duplicate File Menu**] + - The pinklist is only used with the [**Advance Duplicate File 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. +- Rename Tools-UI advance duplicate tagged menu to [**Advance Duplicate File Menu**] +- When [**Advance Duplicate File 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 logic to disable scene in report if deleted by [**Advance Duplicate File Menu**]. Note: Requires a refresh. - Added report option to delete by flags set on the report. ### 1.0.0.1 - Fixed bug with report delete scene request. @@ -42,8 +46,43 @@ - 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. +- Added code to [**Advance Duplicate File Menu**] to delete based on flags. ### 1.0.1 -- Change [**Advance Duplicate File Deletion Menu**] default input values to placeholder text. +- Change [**Advance Duplicate File Menu**] default input values to placeholder text. - Change stash toolbar icon for DupFileManager to a file icon with a video camera. -- Removed **plugin/** from the URL for the Tools-UI menu. \ No newline at end of file +- Removed **plugin/** from the URL for the Tools-UI menu. +### 1.1.0 +- On report, when scene gets deleted using flag option, change the color of the buttons to the associated flag color. +- Enhanced [**Advance Duplicate File Menu**] dropdown buttons to splitdown buttons, and added associated icons. +- Added refresh button to report. +- Ehanced the report GUI. + - Replace scene options with a menubar. + - Consolidated the options into two menu items (File and Flag/Tag). + - Moved metadata icons (Tag, Performer, Gallery, and Group) to the new menubar. These icons are only displayed when the scene has associated metadata. + - Added quick access button to the menubar for most commonly used options. + - Implemented the logic so quick access buttons are only included if there's space available. + - Two of the six quick access buttons are always displayed, and the remaining 4 are displayed depending on how many metadata icons are displayed per scene. +- On report, consolidated some menu items into sub menu items. +- Added option to [**Advance Duplicate File Menu**] to limit number of scenes in each page for paginate. +- Added following color options to [**Advance Duplicate File Menu**]. + - Report background color + - Report text color + - Report main highlight color + - Report text color for differential metadata + - Report minor highlight color +- Added option to [**Advance Duplicate File Menu**] to display full stream video as preview in the report. +- Change paginate [Prev] and [Next] links to buttons on report. +- Add logic to update Stash when changing file without doing full path scan. +- Add option to [**Advance Duplicate File Menu**] to open http://localhost:9999/settings?tab=plugins +- Add option to [**Advance Duplicate File Menu**] to open http://localhost:9999/settings?tab=tools +- Add option to report to open http://localhost:9999/settings?tab=plugins +- Add option to report to open http://localhost:9999/settings?tab=tools +- Add link to [Axter-Stash](https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins) to both report and [**Advance Duplicate File Menu**]. +- Added report option to clear all flags of a specific color. +- Added option to [**Advance Duplicate File Menu**] to create report with or without preview video and with or without preview image. +- Always include [Next] paginate on top of first report page, and CSS hide it if only one page. +- Added code to report to make it when the report updates the screen (due to tag merging), it stays in the same row position on the page. +- Added plugin task [Create Duplicate Report] + + + diff --git a/plugins/FileMonitor/StashPluginHelper.py b/plugins/FileMonitor/StashPluginHelper.py index 27bfae8..985d831 100644 --- a/plugins/FileMonitor/StashPluginHelper.py +++ b/plugins/FileMonitor/StashPluginHelper.py @@ -830,11 +830,13 @@ class StashPluginHelper(StashInterface): data = data.replace("\\\\\\\\", "\\\\") return json.loads(data) - def isCorrectDbVersion(self, verNumber = 68): + def isCorrectDbVersion(self, verNumber = 68, isEqualOrGreater = True): results = self.sql_query("select version from schema_migrations") # self.Log(results) if len(results['rows']) == 0 or len(results['rows'][0]) == 0: return False + if isEqualOrGreater: + return int(results['rows'][0][0]) >= verNumber return int(results['rows'][0][0]) == verNumber def renameFileNameInDB(self, fileId, oldName, newName, UpdateUsingIdOnly = False): @@ -848,6 +850,11 @@ class StashPluginHelper(StashInterface): return True return False + # This only works if filename has not changed. If file name has changed, call renameFileNameInDB first. + def updateFileScene(self, fullFilePath): + mylist = [fullFilePath] + return self.metadata_scan(mylist) + def getFileNameFromDB(self, id): results = self.sql_query(f'select basename from files where id = {id};') self.Trace(f"results = ({results})") @@ -870,6 +877,10 @@ class StashPluginHelper(StashInterface): self.Debug(f"Called sql_commit and received results {results}.") return True + def isEmpty(self, data): + if data == None or len(data) == 0: + return True + return False # ############################################################################################################ # Functions which are candidates to be added to parent class use snake_case naming convention. diff --git a/plugins/RenameFile/StashPluginHelper.py b/plugins/RenameFile/StashPluginHelper.py index 27bfae8..985d831 100644 --- a/plugins/RenameFile/StashPluginHelper.py +++ b/plugins/RenameFile/StashPluginHelper.py @@ -830,11 +830,13 @@ class StashPluginHelper(StashInterface): data = data.replace("\\\\\\\\", "\\\\") return json.loads(data) - def isCorrectDbVersion(self, verNumber = 68): + def isCorrectDbVersion(self, verNumber = 68, isEqualOrGreater = True): results = self.sql_query("select version from schema_migrations") # self.Log(results) if len(results['rows']) == 0 or len(results['rows'][0]) == 0: return False + if isEqualOrGreater: + return int(results['rows'][0][0]) >= verNumber return int(results['rows'][0][0]) == verNumber def renameFileNameInDB(self, fileId, oldName, newName, UpdateUsingIdOnly = False): @@ -848,6 +850,11 @@ class StashPluginHelper(StashInterface): return True return False + # This only works if filename has not changed. If file name has changed, call renameFileNameInDB first. + def updateFileScene(self, fullFilePath): + mylist = [fullFilePath] + return self.metadata_scan(mylist) + def getFileNameFromDB(self, id): results = self.sql_query(f'select basename from files where id = {id};') self.Trace(f"results = ({results})") @@ -870,6 +877,10 @@ class StashPluginHelper(StashInterface): self.Debug(f"Called sql_commit and received results {results}.") return True + def isEmpty(self, data): + if data == None or len(data) == 0: + return True + return False # ############################################################################################################ # Functions which are candidates to be added to parent class use snake_case naming convention.