[DupFileManager] plugin updated to version 1.0.0 (#470)

This commit is contained in:
David Maisonave
2024-11-28 20:38:12 -06:00
committed by GitHub
parent 512fbb83fa
commit 3ddd7fa068
8 changed files with 19433 additions and 392 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,7 @@ except Exception as e:
import traceback, sys
tb = traceback.format_exc()
print(f"ModulesValidate Exception. Error: {e}\nTraceBack={tb}", file=sys.stderr)
import os, sys, time, pathlib, argparse, platform, shutil, traceback, logging, requests
import os, sys, time, pathlib, argparse, platform, shutil, traceback, logging, requests, json
from datetime import datetime
from StashPluginHelper import StashPluginHelper
from stashapi.stash_types import PhashDistance
@@ -36,6 +36,7 @@ settings = {
"zvWhitelist": "",
"zwGraylist": "",
"zxBlacklist": "",
"zxPinklist": "",
"zyMaxDupToProcess": 0,
"zySwapHighRes": False,
"zySwapLongLength": False,
@@ -44,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
@@ -68,8 +70,9 @@ stash = StashPluginHelper(
DebugFieldName="zzDebug",
)
stash.convertToAscii = True
dry_run = stash.Setting("zzdryRun")
advanceMenuOptions = [ "applyCombo", "applyComboBlacklist", "pathToDelete", "pathToDeleteBlacklist", "sizeToDeleteLess", "sizeToDeleteGreater", "sizeToDeleteBlacklistLess", "sizeToDeleteBlacklistGreater", "durationToDeleteLess", "durationToDeleteGreater", "durationToDeleteBlacklistLess", "durationToDeleteBlacklistGreater",
advanceMenuOptions = [ "applyCombo", "applyComboPinklist", "applyComboGraylist", "applyComboBlacklist", "pathToDelete", "pathToDeleteBlacklist", "sizeToDeleteLess", "sizeToDeleteGreater", "sizeToDeleteBlacklistLess", "sizeToDeleteBlacklistGreater", "durationToDeleteLess", "durationToDeleteGreater", "durationToDeleteBlacklistLess", "durationToDeleteBlacklistGreater",
"commonResToDeleteLess", "commonResToDeleteEq", "commonResToDeleteGreater", "commonResToDeleteBlacklistLess", "commonResToDeleteBlacklistEq", "commonResToDeleteBlacklistGreater", "resolutionToDeleteLess", "resolutionToDeleteEq", "resolutionToDeleteGreater",
"resolutionToDeleteBlacklistLess", "resolutionToDeleteBlacklistEq", "resolutionToDeleteBlacklistGreater", "ratingToDeleteLess", "ratingToDeleteEq", "ratingToDeleteGreater", "ratingToDeleteBlacklistLess", "ratingToDeleteBlacklistEq", "ratingToDeleteBlacklistGreater",
"tagToDelete", "tagToDeleteBlacklist", "titleToDelete", "titleToDeleteBlacklist", "pathStrToDelete", "pathStrToDeleteBlacklist"]
@@ -95,7 +98,7 @@ if len(sys.argv) > 1:
else:
stash.Debug(f"No command line arguments. JSON_INPUT['args'] = {stash.JSON_INPUT['args']}; PLUGIN_TASK_NAME = {stash.PLUGIN_TASK_NAME}; argv = {sys.argv}")
stash.status(logLevel=logging.DEBUG)
obsoleteSettingsToConvert = {"zWhitelist" : "zvWhitelist", "zxGraylist" : "zwGraylist", "zyBlacklist" : "zxBlacklist", "zyMatchDupDistance" : "matchDupDistance", "zSwapHighRes" : "zySwapHighRes", "zSwapLongLength" : "zySwapLongLength", "zSwapBetterBitRate" : "zySwapBetterBitRate", "zSwapCodec" : "zySwapCodec", "zSwapBetterFrameRate" : "zySwapBetterFrameRate"}
stash.replaceObsoleteSettings(obsoleteSettingsToConvert, "zzObsoleteSettingsCheckVer2")
@@ -232,7 +235,10 @@ stash.Trace(f"whitelist = {whitelist}")
blacklist = stash.Setting('zxBlacklist').split(listSeparator)
blacklist = [item.lower() for item in blacklist]
if blacklist == [""] : blacklist = []
stash.Trace(f"blacklist = {blacklist}")
pinklist = stash.Setting('zxPinklist').split(listSeparator)
pinklist = [item.lower() for item in pinklist]
if pinklist == [""] : pinklist = []
stash.Trace(f"pinklist = {pinklist}")
def realpath(path):
"""
@@ -511,6 +517,8 @@ def getHtmlReportTableRow(qtyResults, tagDuplicates):
htmlReportPrefix = stash.Setting('htmlReportPrefix')
htmlReportPrefix = htmlReportPrefix.replace('http://127.0.0.1:9999/graphql', stash.url)
htmlReportPrefix = htmlReportPrefix.replace('http://localhost:9999/graphql', stash.url)
if 'apiKey' in stash.STASH_CONFIGURATION and stash.STASH_CONFIGURATION['apiKey'] != "":
htmlReportPrefix = htmlReportPrefix.replace('var apiKey = "";', f"var apiKey = \"{stash.STASH_CONFIGURATION['apiKey']}\";")
if tagDuplicates == False:
htmlReportPrefix = htmlReportPrefix.replace('<td><button id="AdvanceMenu"', '<td hidden><button id="AdvanceMenu"')
htmlReportPrefix = htmlReportPrefix.replace('(QtyPlaceHolder)', f'{qtyResults}')
@@ -549,6 +557,39 @@ def getSceneID(scene):
def fileNameClassID(scene):
return f" class=\"FN_ID_{scene['id']}\" "
def doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, listName, itemName = 'name'):
DelCandidateItems = []
DupToKeepItems = []
DupToKeepMissingItem = False
DelCandidateMissingItem = False
for item in DupFileToKeep[listName]:
if listName != 'tags' or not item['ignore_auto_tag']:
if listName == "groups":
DupToKeepItems += [item['group']['name']]
elif listName == "galleries":
item = stash.getGalleryName(item['id'])
DupToKeepItems += [item[itemName]]
else:
DupToKeepItems += [item[itemName]]
for item in DupFile[listName]:
if listName != 'tags' or not item['ignore_auto_tag']:
if listName == "groups":
name = item['group'][itemName]
elif listName == "galleries":
item = stash.getGalleryName(item['id'])
name = item[itemName]
else:
name = item[itemName]
DelCandidateItems += [name]
if name not in DupToKeepItems:
DupToKeepMissingItem = True
for name in DupToKeepItems:
if name not in DelCandidateItems:
DelCandidateMissingItem = True
break
return DupToKeepMissingItem, DelCandidateMissingItem
htmlReportNameFolder = f"{stash.PLUGINS_PATH}{os.sep}DupFileManager{os.sep}report"
htmlReportName = f"{htmlReportNameFolder}{os.sep}{stash.Setting('htmlReportName')}"
htmlReportTableRow = stash.Setting('htmlReportTableRow')
@@ -559,6 +600,15 @@ 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>"
defaultColorTag = "BlueTag.png"
defaultColorPerformer = "Headshot.png"
defaultColorGalleries = "Galleries.png"
defaultColorGroup = "Group.png"
htmlTagPrefix = '<div class="dropdown_icon"><img src="https://www.axter.com/images/stash/' + defaultColorTag + '" 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/' + defaultColorPerformer + '" 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/' + defaultColorGalleries + '" alt="Galleries" title="Galleries" style="width:20px;height:20px;"><i class="fa fa-caret-down"></i><div class="dropdown_gallery-content">'
htmlGroupPrefix = '<div class="dropdown_icon"><img src="https://www.axter.com/images/stash/' + defaultColorGroup + '" alt="Groups" title="Groups" style="width:20px;height:20px;"><i class="fa fa-caret-down"></i><div class="dropdown_group-content">'
dupFileExist = True if os.path.isfile(DupFile['files'][0]['path']) else False
toKeepFileExist = True if os.path.isfile(DupFileToKeep['files'][0]['path']) else False
fileHtmlReport.write(f"{htmlReportTableRow}")
@@ -592,23 +642,80 @@ 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)
DupToKeepMissingTag, DelCandidateMissingTag = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'tags')
if len(DupFile['tags']) > 0:
if DupToKeepMissingTag:
fileHtmlReport.write(htmlTagPrefix.replace(defaultColorTag, "YellowTag.png"))
else:
fileHtmlReport.write(htmlTagPrefix)
for tag in DupFile['tags']:
# if not tag['ignore_auto_tag']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{tag['name']}</div>")
fileHtmlReport.write("</div></div>")
DupToKeepMissingPerformer, DelCandidateMissingPerformer = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'performers')
if len(DupFile['performers']) > 0:
if DupToKeepMissingPerformer:
fileHtmlReport.write(htmlPerformerPrefix.replace(defaultColorPerformer, "YellowHeadshot.png"))
else:
fileHtmlReport.write(htmlPerformerPrefix)
for performer in DupFile['performers']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{performer['name']}</div>")
fileHtmlReport.write("</div></div>")
DupToKeepMissingGallery, DelCandidateMissingGallery = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'galleries', 'title')
if len(DupFile['galleries']) > 0:
if DupToKeepMissingGallery:
fileHtmlReport.write(htmlGalleryPrefix.replace(defaultColorGalleries, "YellowGalleries.png"))
else:
fileHtmlReport.write(htmlGalleryPrefix)
for gallery in DupFile['galleries']:
gallery = stash.getGalleryName(gallery['id'])
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{gallery['title']}</div>")
fileHtmlReport.write("</div></div>")
DupToKeepMissingGroup, DelCandidateMissingGroup = doesDelCandidateHaveMetadataNotInDupToKeep(DupFile, DupFileToKeep, 'groups')
if len(DupFile['groups']) > 0:
if DupToKeepMissingGroup:
fileHtmlReport.write(htmlGroupPrefix.replace(defaultColorGroup, "YellowGroup.png"))
else:
fileHtmlReport.write(htmlGroupPrefix)
for group in DupFile['groups']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{group['group']['name']}</div>")
fileHtmlReport.write("</div></div>")
fileHtmlReport.write("</p></td>")
videoPreview = f"<video {htmlReportVideoPreview} poster=\"{DupFileToKeep['paths']['screenshot']}\"><source src=\"{DupFileToKeep['paths'][htmlPreviewOrStream]}\" type=\"video/mp4\"></video>"
@@ -620,25 +727,86 @@ 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:
if DelCandidateMissingTag:
fileHtmlReport.write(htmlTagPrefix.replace(defaultColorTag, "RedTag.png"))
else:
fileHtmlReport.write(htmlTagPrefix)
for tag in DupFileToKeep['tags']:
# if not tag['ignore_auto_tag']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{tag['name']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFileToKeep['performers']) > 0:
if DelCandidateMissingPerformer:
fileHtmlReport.write(htmlPerformerPrefix.replace(defaultColorPerformer, "PinkHeadshot.png"))
else:
fileHtmlReport.write(htmlPerformerPrefix)
for performer in DupFileToKeep['performers']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{performer['name']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFileToKeep['galleries']) > 0:
if DelCandidateMissingGallery:
fileHtmlReport.write(htmlGalleryPrefix.replace(defaultColorGalleries, "PinkGalleries.png"))
else:
fileHtmlReport.write(htmlGalleryPrefix)
for gallery in DupFileToKeep['galleries']:
gallery = stash.getGalleryName(gallery['id'])
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{gallery['title']}</div>")
fileHtmlReport.write("</div></div>")
if len(DupFileToKeep['groups']) > 0:
if DelCandidateMissingGroup:
fileHtmlReport.write(htmlGroupPrefix.replace(defaultColorGroup, "PinkGroup.png"))
else:
fileHtmlReport.write(htmlGroupPrefix)
for group in DupFileToKeep['groups']:
fileHtmlReport.write(f"<div style='color:black;font-size: 12px;'>{group['group']['name']}</div>")
fileHtmlReport.write("</div></div>")
# ToDo: Add following buttons:
# rename file
fileHtmlReport.write(f"</p></td>")
fileHtmlReport.write("</tr>\n")
fileHtmlReport.write(f"</tr><!-- ::DuplicateToDelete_SceneID={DupFile['id']}::DuplicateToKeep_SceneID={DupFileToKeep['id']}:: -->\n")
fragmentForSceneDetails = 'id tags {id name ignore_auto_tag} groups {group {name} } performers {name} galleries {id} files {path width height duration size video_codec bit_rate frame_rate} details '
htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} "
mergeFieldData = " code director title rating100 date studio {id name} urls "
fragmentForSceneDetails += mergeFieldData + htmlFileData
DuplicateCandidateForDeletionList = f"{htmlReportNameFolder}{os.sep}DuplicateCandidateForDeletionList.txt"
def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlacklistOnly=False, deleteLowerResAndDuration=False):
global reasonDict
global htmlFileData
duplicateMarkForDeletion_descp = 'Tag added to duplicate scenes so-as to tag them for deletion.'
stash.Trace(f"duplicateMarkForDeletion = {duplicateMarkForDeletion}")
dupTagId = stash.createTagId(duplicateMarkForDeletion, duplicateMarkForDeletion_descp, ignoreAutoTag=True)
@@ -669,27 +837,28 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
stash.Trace("#########################################################################")
stash.Log(f"Waiting for find_duplicate_scenes_diff to return results; matchDupDistance={matchPhaseDistanceText}; significantTimeDiff={significantTimeDiff}", printTo=LOG_STASH_N_PLUGIN)
stash.startSpinningProcessBar()
htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} " if createHtmlReport else ""
mergeFieldData = " code director title rating100 date studio {id} movies {movie {id} } galleries {id} performers {id} urls " if merge else ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment='id tags {id name} files {path width height duration size video_codec bit_rate frame_rate} details ' + mergeFieldData + htmlFileData)
if not createHtmlReport:
htmlFileData = ""
DupFileSets = stash.find_duplicate_scenes(matchPhaseDistance, fragment=fragmentForSceneDetails)
stash.stopSpinningProcessBar()
qtyResults = len(DupFileSets)
stash.setProgressBarIter(qtyResults)
stash.Trace("#########################################################################")
stash.Log(f"Found {qtyResults} duplicate sets...")
fileHtmlReport = None
if createHtmlReport:
if not os.path.isdir(htmlReportNameFolder):
os.mkdir(htmlReportNameFolder)
if not os.path.isdir(htmlReportNameFolder):
os.mkdir(htmlReportNameFolder)
if not os.path.isdir(htmlReportNameFolder):
stash.Error(f"Failed to create report directory {htmlReportNameFolder}.")
return
stash.Error(f"Failed to create report directory {htmlReportNameFolder}.")
return
if createHtmlReport:
deleteLocalDupReportHtmlFiles(False)
fileHtmlReport = open(htmlReportName, "w")
fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n")
fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n")
htmlReportTableHeader = stash.Setting('htmlReportTableHeader')
fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Scene</th>{htmlReportTableHeader}Duplicate to Delete</th>{htmlReportTableHeader}Scene-ToKeep</th>{htmlReportTableHeader}Duplicate to Keep</th></tr>\n")
fileDuplicateCandidateForDeletionList = open(DuplicateCandidateForDeletionList, "w")
for DupFileSet in DupFileSets:
# stash.Trace(f"DupFileSet={DupFileSet}", toAscii=True)
@@ -816,7 +985,12 @@ 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):
stash.Log(f"Skipping duplicate {DupFile['files'][0]['path']} (ID={DupFile['id']}), because of time difference greater than {significantTimeDiff} for file {DupFileToKeep['files'][0]['path']}.")
@@ -835,19 +1009,20 @@ 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)
fileDuplicateCandidateForDeletionList.write(json.dumps(DupFile) + "\n")
if QtyTagForDelPaginate >= htmlReportPaginate:
QtyTagForDelPaginate = 0
fileHtmlReport.write("</table>\n")
homeHtmReportLink = f"<a class=\"link-items\" title=\"Home Page\" href=\"file://{htmlReportNameHomePage}\">[Home]</a>"
homeHtmReportLink = f"<a id=\"HomePage\" class=\"link-items\" title=\"Home Page\" href=\"file://{htmlReportNameHomePage}\">[Home]</a>"
prevHtmReportLink = ""
if PaginateId > 0:
if PaginateId > 1:
prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html")
else:
prevHtmReport = htmlReportNameHomePage
prevHtmReportLink = f"<a class=\"link-items\" title=\"Previous Page\" href=\"file://{prevHtmReport}\">[Prev]</a>"
prevHtmReportLink = f"<a id=\"PrevPage\" class=\"link-items\" title=\"Previous Page\" href=\"file://{prevHtmReport}\">[Prev]</a>"
nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html")
nextHtmReportLink = f"<a class=\"link-items\" title=\"Next Page\" href=\"file://{nextHtmReport}\">[Next]</a>"
nextHtmReportLink = f"<a id=\"NextPage\" class=\"link-items\" title=\"Next Page\" href=\"file://{nextHtmReport}\">[Next]</a>"
fileHtmlReport.write(f"<center><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td><td>{nextHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"{stash.Setting('htmlReportPostfix')}")
fileHtmlReport.close()
@@ -858,10 +1033,10 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html")
else:
prevHtmReport = htmlReportNameHomePage
prevHtmReportLink = f"<a class=\"link-items\" title=\"Previous Page\" href=\"file://{prevHtmReport}\">[Prev]</a>"
prevHtmReportLink = f"<a id=\"PrevPage_Top\" class=\"link-items\" title=\"Previous Page\" href=\"file://{prevHtmReport}\">[Prev]</a>"
if len(DupFileSets) > (QtyTagForDel + htmlReportPaginate):
nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html")
nextHtmReportLink = f"<a class=\"link-items\" title=\"Next Page\" href=\"file://{nextHtmReport}\">[Next]</a>"
nextHtmReportLink = f"<a id=\"NextPage_Top\" class=\"link-items\" title=\"Next Page\" href=\"file://{nextHtmReport}\">[Next]</a>"
fileHtmlReport.write(f"<center><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td><td>{nextHtmReportLink}</td></tr></table></center>")
else:
stash.Debug(f"DupFileSets Qty = {len(DupFileSets)}; DupFileDetailList Qty = {len(DupFileDetailList)}; QtyTagForDel = {QtyTagForDel}; htmlReportPaginate = {htmlReportPaginate}; QtyTagForDel + htmlReportPaginate = {QtyTagForDel+htmlReportPaginate}")
@@ -882,10 +1057,11 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
if maxDupToProcess > 0 and ((QtyTagForDel > maxDupToProcess) or (QtyTagForDel == 0 and QtyDup > maxDupToProcess)):
break
fileDuplicateCandidateForDeletionList.close()
if fileHtmlReport != None:
fileHtmlReport.write("</table>\n")
if PaginateId > 0:
homeHtmReportLink = f"<a class=\"link-items\" title=\"Home Page\" href=\"file://{htmlReportNameHomePage}\">[Home]</a>"
homeHtmReportLink = f"<a id=\"HomePage_Top\" class=\"link-items\" title=\"Home Page\" href=\"file://{htmlReportNameHomePage}\">[Home]</a>"
if PaginateId > 1:
prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html")
else:
@@ -917,28 +1093,45 @@ 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]:
stash.Debug(f"Using tag name {tagName} with Tag ID {tagId[0]['id']}")
return tagId[0]['id']
return "-1"
if tag := stash.find_tag(tagName):
return tag['id'], tagName
return "-1", None
def toJson(data):
import json
# data = data.replace("'", '"')
data = data.replace("\\", "\\\\")
data = data.replace("\\\\\\\\", "\\\\")
return json.loads(data)
def getAnAdvanceMenuOptionSelected(taskName, target, isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater):
def getAnAdvanceMenuOptionSelected(taskName, target, isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater):
stash.Log(f"Processing taskName = {taskName}, target = {target}")
if "Blacklist" in taskName:
isBlackList = True
tagOrFlag = "Blacklist"
if "Graylist" in taskName:
tagOrFlag = "Graylist"
if "Pinklist" in taskName:
tagOrFlag = "Pinklist"
if "YellowFlag" in taskName:
tagOrFlag = "YellowFlag"
if "GreenFlag" in taskName:
tagOrFlag = "GreenFlag"
if "OrangeFlag" in taskName:
tagOrFlag = "OrangeFlag"
if "CyanFlag" in taskName:
tagOrFlag = "CyanFlag"
if "PinkFlag" in taskName:
tagOrFlag = "PinkFlag"
if "RedFlag" in taskName:
tagOrFlag = "RedFlag"
if "Less" in taskName:
compareToLess = True
if "Greater" in taskName:
compareToGreater = True
if ":TagOnlyScenes" in target:
isTagOnlyScenes = True
target = target.replace(":TagOnlyScenes","")
if "pathToDelete" in taskName:
pathToDelete = target.lower()
elif "sizeToDelete" in taskName:
@@ -959,10 +1152,17 @@ def getAnAdvanceMenuOptionSelected(taskName, target, isBlackList, pathToDelete,
pathStrToDelete = target.lower()
elif "fileNotExistToDelete" in taskName:
fileNotExistToDelete = True
return isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
if target == "Tagged":
isTagOnlyScenes = True
else:
isTagOnlyScenes = False
elif "TagOnlyScenes" in taskName:
isTagOnlyScenes = True
return isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
isBlackList = False
isTagOnlyScenes = False
tagOrFlag = None
pathToDelete = ""
sizeToDelete = -1
durationToDelete = -1
@@ -980,16 +1180,90 @@ def getAdvanceMenuOptionSelected(advanceMenuOptionSelected):
if "applyCombo" in stash.PLUGIN_TASK_NAME:
jsonObject = toJson(stash.JSON_INPUT['args']['Target'])
for taskName in jsonObject:
isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAnAdvanceMenuOptionSelected(taskName, jsonObject[taskName], isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAnAdvanceMenuOptionSelected(taskName, jsonObject[taskName], isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater)
else:
return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
return isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
return getAnAdvanceMenuOptionSelected(stash.PLUGIN_TASK_NAME, stash.JSON_INPUT['args']['Target'], isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, compareToLess, compareToGreater)
return isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater
def getScenesFromReport():
stash.Log(f"Getting candidates for deletion from file {DuplicateCandidateForDeletionList}.")
scenes = []
lines = None
with open(DuplicateCandidateForDeletionList, 'r') as file:
lines = file.readlines()
for line in lines:
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(flagType=None, ReportName = htmlReportName):
flaggedScenes = []
if flagType == None:
flagType = stash.JSON_INPUT['args']['Target']
if flagType == "green":
flagType = "#00FF00"
else:
if flagType == "YellowFlag":
flagType = "yellow"
if flagType == "GreenFlag":
flagType = "#00FF00"
if flagType == "OrangeFlag":
flagType = "orange"
if flagType == "CyanFlag":
flagType = "cyan"
if flagType == "PinkFlag":
flagType = "pink"
if flagType == "RedFlag":
flagType = "red"
stash.Debug(f"Searching for scenes with flag type {flagType}")
if os.path.isfile(ReportName):
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 = findCurrentTagId([duplicateMarkForDeletion, base1_duplicateMarkForDeletion, base2_duplicateMarkForDeletion, 'DuplicateMarkForDeletion', '_DuplicateMarkForDeletion'])
tagId, tagName = findCurrentTagId([duplicateMarkForDeletion, base1_duplicateMarkForDeletion, base2_duplicateMarkForDeletion, 'DuplicateMarkForDeletion', '_DuplicateMarkForDeletion'])
if int(tagId) < 0:
stash.Warn(f"Could not find tag ID for tag '{duplicateMarkForDeletion}'.")
return
@@ -998,11 +1272,24 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if clearAllDupfileManagerTags:
excludedTags = [duplicateMarkForDeletion, duplicateWhitelistTag, excludeDupFileDeleteTag, graylistMarkForDeletion, longerDurationLowerResolution]
isBlackList, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAdvanceMenuOptionSelected(advanceMenuOptionSelected)
isTagOnlyScenes, tagOrFlag, pathToDelete, sizeToDelete, durationToDelete, resolutionToDelete, ratingToDelete, tagToDelete, titleToDelete, pathStrToDelete, fileNotExistToDelete, compareToLess, compareToGreater = getAdvanceMenuOptionSelected(advanceMenuOptionSelected)
if advanceMenuOptionSelected and deleteScenes and pathToDelete == "" and tagToDelete == "" and titleToDelete == "" and pathStrToDelete == "" and sizeToDelete == -1 and durationToDelete == -1 and resolutionToDelete == -1 and ratingToDelete == -1 and fileNotExistToDelete == False:
stash.Error("Running advance menu option with no options enabled.")
return
flaggedScenes = None
flagType = None
if checkFlagOption 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:
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
@@ -1010,10 +1297,19 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
QtyFailedQuery = 0
stash.Debug("#########################################################################")
stash.startSpinningProcessBar()
scenes = stash.find_scenes(f={"tags": {"value":tagId, "modifier":"INCLUDES"}}, fragment='id tags {id name} files {path width height duration size video_codec bit_rate frame_rate} details title rating100')
if advanceMenuOptionSelected == False and checkFlagOption == False:
isTagOnlyScenes = True
if isTagOnlyScenes:
stash.Log(f"Getting candidates for deletion by using tag-ID {tagId} and tag-name {tagName}; isTagOnlyScenes={isTagOnlyScenes};advanceMenuOptionSelected={advanceMenuOptionSelected}")
scenes = stash.find_scenes(f={"tags": {"value":tagId, "modifier":"INCLUDES"}}, fragment=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)
stash.Log(f"Found {qtyResults} scenes with tag ({duplicateMarkForDeletion})")
if isTagOnlyScenes:
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
@@ -1057,10 +1353,26 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
elif deleteScenes:
DupFileName = scene['files'][0]['path']
DupFileNameOnly = pathlib.Path(DupFileName).stem
if advanceMenuOptionSelected:
if isBlackList:
if 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:
continue
elif advanceMenuOptionSelected:
if checkFlagOption:
if int(scene['id']) in flaggedScenes:
stash.Trace(f"Found {flagType} flag for Scene ID = {scene['id']}")
else:
continue
if tagOrFlag == "Blacklist":
if not stash.startsWithInList(blacklist, scene['files'][0]['path']):
continue
if tagOrFlag == "Graylist":
if not stash.startsWithInList(graylist, scene['files'][0]['path']):
continue
if tagOrFlag == "Pinklist":
if not stash.startsWithInList(pinklist, scene['files'][0]['path']):
continue
if pathToDelete != "":
if not DupFileName.lower().startswith(pathToDelete):
stash.Debug(f"Skipping file {DupFileName} because it does not start with {pathToDelete}.")
@@ -1131,7 +1443,7 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
if fileNotExistToDelete:
if os.path.isfile(scene['files'][0]['path']):
continue
stash.Warn(f"Deleting duplicate '{DupFileName}'", toAscii=True, printTo=LOG_STASH_N_PLUGIN)
stash.Log(f"Deleting duplicate '{DupFileName}'", toAscii=True, printTo=LOG_STASH_N_PLUGIN)
if alternateTrashCanPath != "":
destPath = f"{alternateTrashCanPath }{os.sep}{DupFileNameOnly}"
if os.path.isfile(destPath):
@@ -1139,11 +1451,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)
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)
@@ -1188,11 +1500,13 @@ def removeExcludeTag():
stash.Log(f"Done removing exclude tag from scene {scene}.")
sys.stdout.write("{" + f"removeExcludeTag : 'complete', id: '{scene}'" + "}")
def getParseData(getSceneDetails1=True, getSceneDetails2=True):
def getParseData(getSceneDetails1=True, getSceneDetails2=True, checkIfNotSplitValue=False):
if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return None, None
targetsSrc = stash.JSON_INPUT['args']['Target']
if checkIfNotSplitValue and ":" not in targetsSrc:
return targetsSrc, None
targets = targetsSrc.split(":")
if len(targets) < 2:
stash.Error(f"Could not get both targets from string {targetsSrc}")
@@ -1207,14 +1521,48 @@ def getParseData(getSceneDetails1=True, getSceneDetails2=True):
elif len(targets) > 2:
target2 = target2 + targets[2]
return target1, target2
def mergeMetadataInThisFile(fileName):
stash.Debug(f"Checking report file '{fileName}' for yellow icons indicating missing metadata in DuplicateToKeep.")
lines = None
with open(fileName, 'r') as file:
lines = file.readlines()
for line in lines:
if "https://www.axter.com/images/stash/Yellow" in line: # FYI: This catches YellowGroup.png as well, even though group is not currently supported for merging
searchStrScene1 = "<!-- ::DuplicateToDelete_SceneID="
idx = line.index(searchStrScene1) + len(searchStrScene1)
scene_id1 = line[idx:]
scene_id1 = scene_id1[:scene_id1.index('::')]
searchStrScene2 = "::DuplicateToKeep_SceneID="
idx = line.index(searchStrScene2, idx) + len(searchStrScene2)
scene_id2 = line[idx:]
scene_id2 = scene_id2[:scene_id2.index('::')]
stash.Log(f"From file {fileName}, merging metadata from scene {scene_id1} to scene {scene_id2}")
stash.mergeMetadata(int(scene_id1), int(scene_id2))
updateScenesInReports(scene_id2)
def mergeMetadataForAll(ReportName = htmlReportName):
if os.path.isfile(ReportName):
mergeMetadataInThisFile(ReportName)
for x in range(2, 9999):
fileName = ReportName.replace(".html", f"_{x-1}.html")
stash.Debug(f"Checking if file '{fileName}' exist.")
if not os.path.isfile(fileName):
break
mergeMetadataInThisFile(fileName)
stash.Log(f"Done merging metadata for all scenes")
sys.stdout.write("{mergeTags : 'complete'}")
def mergeTags():
scene1, scene2 = getParseData()
scene1, scene2 = getParseData(checkIfNotSplitValue=True)
if scene1 == None or scene2 == None:
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
if scene1 == "mergeMetadataForAll":
mergeMetadataForAll()
else:
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
return
stash.mergeMetadata(scene1, scene2)
updateScenesInReports(scene2['id'])
stash.Log(f"Done merging scenes for scene {scene1['id']} and scene {scene2['id']}")
sys.stdout.write("{" + f"mergeTags : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}'" + "}")
@@ -1245,16 +1593,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):
@@ -1275,8 +1626,39 @@ def removeAllDupTagsFromAllScenes(deleteTags=False):
else:
stash.Log(f"Clear tags {tagsToClear}")
def updateDuplicateCandidateForDeletionList(scene, removeScene = False):
lines = None
scene_id = None
if not os.path.isfile(DuplicateCandidateForDeletionList):
return
with open(DuplicateCandidateForDeletionList, 'r') as file:
lines = file.readlines()
if removeScene:
scene_id = scene
else:
scene_id = scene['id']
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:
if foundScene:
file.write(line)
else:
sceneDetails = json.loads(line)
if sceneDetails['id'] == scene_id:
if not removeScene:
file.write(json.dumps(scene) + "\n")
foundScene = True
else:
file.write(line)
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}")
results = False
scene1 = -1
scene2 = -1
strToFind = "class=\"ID_"
@@ -1284,10 +1666,12 @@ def updateScenesInReport(fileName, scene):
with open(fileName, 'r') as file:
lines = file.readlines()
stash.Log(f"line count = {len(lines)}")
stash.Log(f"Searching for class=\"ID_{scene}\"")
with open(fileName, 'w') as file:
for line in lines:
# stash.Debug(f"line = {line}")
if f"class=\"ID_{scene}\"" in line:
stash.Debug(f"Found class ID_{scene} in line: {line}")
idx = 0
while line.find(strToFind, idx) > -1:
idx = line.find(strToFind, idx) + len(strToFind)
@@ -1302,49 +1686,67 @@ def updateScenesInReport(fileName, scene):
elif scene1 != -1 and scene2 != -1:
break
if scene1 != -1 and scene2 != -1:
sceneDetail1 = stash.find_scene(scene1)
sceneDetail2 = stash.find_scene(scene2)
if sceneDetail1 == None or sceneDetail2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetail1={sceneDetail1}; sceneDetail2={sceneDetail2};")
sceneDetails1 = stash.find_scene(scene1, fragment=fragmentForSceneDetails)
sceneDetails2 = stash.find_scene(scene2, fragment=fragmentForSceneDetails)
if sceneDetails1 == None or sceneDetails2 == None:
stash.Error("Could not get scene details for both scene1 ({scene1}) and scene2 ({scene2}); sceneDetails1={sceneDetails1}; sceneDetails2={sceneDetails2};")
else:
writeRowToHtmlReport(file, sceneDetail1, sceneDetail2)
stash.Log(f"Updating in report {fileName} scene {scene1} and scene {scene2}")
writeRowToHtmlReport(file, sceneDetails1, sceneDetails2)
if scene == sceneDetails1['id']:
results = True
updateDuplicateCandidateForDeletionList(sceneDetails1)
else:
stash.Error(f"Could not get both scene ID associated with scene {scene}; scene1 = {scene1}; scene2 = {scene2}")
file.write(line)
else:
file.write(line)
if scene1 == -1 or scene2 == -1:
stash.Log(f"Did not find both scene ID's associated with scene {scene}; scene1 = {scene1}; scene2 = {scene2}")
return results
def updateScenesInReports(scene, ReportName = htmlReportName):
if os.path.isfile(ReportName):
updateScenesInReport(ReportName, scene)
if updateScenesInReport(ReportName, scene):
return
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
updateScenesInReport(fileName, scene)
if updateScenesInReport(fileName, scene):
break
stash.Debug("updateScenesInReports complete")
else:
stash.Log(f"Report file does not exist: {ReportName}")
def addPropertyToSceneClass(fileName, scene, property):
stash.Log(f"Inserting property {property} for scene {scene} in file {fileName}")
stash.Debug(f"Inserting property {property} for scene {scene} in file {fileName}")
doStyleEndTagCheck = True
lines = None
with open(fileName, 'r') as file:
lines = file.readlines()
stash.Log(f"line count = {len(lines)}")
stash.Debug(f"line count = {len(lines)}")
with open(fileName, 'w') as file:
for line in lines:
# stash.Debug(f"line = {line}")
if doStyleEndTagCheck:
if property == "" and line.startswith(f".ID_{scene}" + "{"):
continue
if line.startswith("</style>"):
if property != "":
styleSetting = f".ID_{scene}{property}\n"
stash.Log(f"styleSetting = {styleSetting}")
file.write(styleSetting)
doStyleEndTagCheck = False
if scene == None:
if line.startswith(f".ID_") and deleteSceneFlagBgColor not in line:
continue
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 != "" and property != "remove highlight":
styleSetting = f".ID_{scene}{property}\n"
stash.Debug(f"styleSetting = {styleSetting}")
file.write(styleSetting)
doStyleEndTagCheck = False
file.write(line)
def addPropertyToSceneClassToAllFiles(scene, property, ReportName = htmlReportName):
@@ -1359,18 +1761,31 @@ 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, "{background-color:gray;pointer-events:none;}")
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}'" + "}")
addPropertyToSceneClassToAllFiles(scene, "remove highlight")
addPropertyToSceneClassToAllFiles(scene, "{background-color:" + deleteSceneFlagBgColor + ";pointer-events:none;}")
updateDuplicateCandidateForDeletionList(scene, removeScene = True)
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)
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for all scenes")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete'" + "}")
def copyScene(moveScene=False):
scene1, scene2 = getParseData()
@@ -1379,9 +1794,15 @@ def copyScene(moveScene=False):
return
if moveScene:
stash.mergeMetadata(scene1, scene2)
stash.Debug(f"Coping file {scene1['files'][0]['path']} to {scene2['files'][0]['path']}")
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']}")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}', result: '{result}'" + "}")
@@ -1411,6 +1832,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":
@@ -1454,7 +1879,7 @@ 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 == "delete_duplicates_task":
mangeDupFiles(deleteDup=True, merge=mergeDupFilename)
@@ -1463,7 +1888,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})
@@ -1471,6 +1896,9 @@ try:
elif stash.PLUGIN_TASK_NAME == "deleteScene":
deleteScene()
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 == "removeScene":
deleteScene(deleteFile=False)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
@@ -1480,6 +1908,9 @@ try:
elif stash.PLUGIN_TASK_NAME == "flagScene":
flagScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "clearAllSceneFlags":
clearAllSceneFlags()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "copyScene":
copyScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
@@ -1525,7 +1956,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"
@@ -1536,7 +1967,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.1.9
version: 1.0.0
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui:
javascript:
@@ -34,6 +34,10 @@ settings:
displayName: Black List
description: Least preferential paths; Determine primary deletion candidates. E.g. C:\Downloads,C:\DelMe-3rd,C:\DelMe-2nd,C:\DeleteMeFirst
type: STRING
zxPinklist:
displayName: Pink List
description: An [Advance Duplicate File Deletion Menu] option for deletion using paths. E.g. C:\SomeRandomDir,E:\DelPath2
type: STRING
zyMaxDupToProcess:
displayName: Max Dup Process
description: (Default=0) Maximum number of duplicates to process. If 0, infinity.
@@ -66,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

@@ -64,11 +64,151 @@ li:hover .large {
border-radius: 4px;
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);;
}
/******** Dropdown buttons *********/
.dropdown {
font-size: 14px;
border: none;
outline: none;
color: white;
padding: 6px 10px;
background-color: transparent;
font-family: inherit; /* Important for vertical align on mobile phones */
margin: 0; /* Important for vertical align on mobile phones */
}
.dropdown-content{
display: none;
position: absolute;
background-color: inherit;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
float: none;
color: black;
padding: 6px 10px;
text-decoration: none;
display: block;
text-align: left;
}
.dropdown:hover .dropdown-content {
display: block;
}
/*** Dropdown 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;
width:30px;
float:left;
}
/*** Dropdown Tag ***/
.dropdown_tag-content{
display: none;
position: absolute;
background-color: LightCoral;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_tag-content {
display: block;
}
/*** Dropdown Performer ***/
.dropdown_performer-content{
display: none;
position: absolute;
background-color: LightBlue;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_performer-content {
display: block;
}
/*** Dropdown Gallery ***/
.dropdown_gallery-content{
display: none;
position: absolute;
background-color: AntiqueWhite;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_gallery-content {
display: block;
}
/*** Dropdown Group ***/
.dropdown_group-content{
display: none;
position: absolute;
background-color: BurlyWood;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown_icon:hover .dropdown_group-content {
display: block;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://www.axter.com/js/jquery-3.7.1.min.js"></script>
<script src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css"/>
<script>
var apiKey = "";
var GraphQl_URL = "http://localhost:9999/graphql";
var OrgPrevPage = null;
var OrgNextPage = null;
var OrgHomePage = null;
var RemoveToKeepConfirmValue = null;
var RemoveValidatePromptValue = null;
const StrRemoveToKeepConfirm = "RemoveToKeepConfirm=";
const StrRemoveValidatePrompt = "RemoveValidatePrompt=";
function SetPaginateButtonChange(){
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
if (chkBxRemoveValid.checked)
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableDeleteConfirm.checked)
RemoveValidatePromptValue = StrRemoveValidatePrompt + "true";
document.cookie = RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue + ";";
console.log("Cookies = " + document.cookie);
}
function trim(str, ch) {
var start = 0, end = str.length;
while(start < end && str[start] === ch) ++start;
@@ -76,20 +216,57 @@ 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");
$.ajax({method: "POST", url: "http://localhost:9999/graphql", contentType: "application/json", dataType: "text", cache: asyncAjax, async: asyncAjax,
if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
$.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: asyncAjax, async: asyncAjax,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){
console.log(result);
// if (Mode !== "flagScene") button.style.visibility = 'hidden';
if (Mode === "renameFile"){
var myArray = ActionID.split(":");
$('.FN_ID_' + myArray[0]).text(trim(myArray[1],"'"));
if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.");
}});
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags" || (Mode !== "deleteScene" && Mode.startsWith("deleteScene")))
location.href = location.href; // location.replace(location.href);
if (!chkBxRemoveValid.checked && Mode !== "flagScene") alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.\\n\\nResults=" + result);
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
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){
@@ -100,29 +277,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);
@@ -131,24 +287,80 @@ function selectMarker(Mode, ActionID, button){
});
}
$(document).ready(function(){
OrgPrevPage = $("#PrevPage").attr('href');
OrgNextPage = $("#NextPage").attr('href');
OrgHomePage = $("#HomePage").attr('href');
console.log("OrgNextPage = " + OrgNextPage);
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
console.log("urlParams = " + urlParams);
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
var FetchCookies = true;
if (urlParams.get('RemoveToKeepConfirm') != null && urlParams.get('RemoveToKeepConfirm') !== ""){
FetchCookies = false;
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + urlParams.get('RemoveToKeepConfirm');
if (urlParams.get('RemoveToKeepConfirm') === "true")
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (urlParams.get('RemoveValidatePrompt') != null && urlParams.get('RemoveValidatePrompt') !== ""){
FetchCookies = false;
RemoveValidatePromptValue = StrRemoveValidatePrompt + urlParams.get('RemoveValidatePrompt');
console.log("RemoveValidatePromptValue = " + RemoveValidatePromptValue);
if (urlParams.get('RemoveValidatePrompt') === "true")
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
if (FetchCookies){
console.log("Cookies = " + document.cookie);
var cookies = document.cookie;
if (cookies.indexOf(StrRemoveToKeepConfirm) > -1){
var idx = cookies.indexOf(StrRemoveToKeepConfirm) + StrRemoveToKeepConfirm.length;
var s = cookies.substring(idx);
console.log("StrRemoveToKeepConfirm Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (cookies.indexOf(StrRemoveValidatePrompt) > -1){
var idx = cookies.indexOf(StrRemoveValidatePrompt) + StrRemoveValidatePrompt.length;
var s = cookies.substring(idx);
console.log("StrRemoveValidatePrompt Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
}
SetPaginateButtonChange();
$("button").click(function(){
var Mode = this.value;
var ActionID = this.id;
if (ActionID === "AdvanceMenu")
if (Mode === "DoNothing")
return;
if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{
var newUrl = window.location.href;
newUrl = newUrl.replace(/report\/DuplicateTagScenes[_0-9]*.html/g, "advance_options.html?GQL=http://localhost:9999/graphql");
newUrl = newUrl.replace(/report\/DuplicateTagScenes[_0-9]*.html/g, "advance_options.html?GQL=" + GraphQl_URL + "&apiKey=" + apiKey);
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(":");
@@ -164,9 +376,24 @@ $(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);
});
$("#RemoveValidatePrompt").change(function() {
console.log("checkbox clicked");
SetPaginateButtonChange();
});
$("#RemoveToKeepConfirm").change(function() {
SetPaginateButtonChange();
});
});
</script>
</head>
@@ -179,9 +406,32 @@ $(document).ready(function(){
<td>Date Created: (DateCreatedPlaceHolder)</td>
</tr></table></td>
<td><table><tr>
<td>
<div class="dropdown">
<button id="AdvanceMenu" name="AdvanceMenu">Menu <i class="fa fa-caret-down"></i></button>
<div class="dropdown-content">
<div><button id="AdvanceMenu" title="Open [Advance Duplicate File Deletion Menu] on a new tab in the browser." name="AdvanceMenu">Advance Duplicate File Deletion Menu</i></button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<div><button type="button" id="mergeMetadataForAll" value="mergeTags" title="Merge scene metadata from [Duplicate to Delete] to [Duplicate to Keep]. This action make take a few minutes to complete.">Merge Tags, Performers, and Galleries</button></div>
<div><button type="button" id="clear_duplicate_tags_task" value="clear_duplicate_tags_task" title="Remove duplicate (_DuplicateMarkForDeletion_?) tag from all scenes. This action make take a few minutes to complete.">Remove Scenes Dup Tags</button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<div><button type="button" id="fileNotExistToDelete" value="Tagged" title="Delete tagged duplicates for which file does NOT exist.">Delete Tagged Files That do Not Exist</button></div>
<div><button type="button" id="fileNotExistToDelete" value="Report" title="Delete duplicate candidate files in report for which file does NOT exist.">Delete Files That do Not Exist in Report</button></div>
<div style="height:2px;width:220px;border-width:0;color:gray;background-color:gray;">_</div>
<div><button type="button" id="clearAllSceneFlags" value="clearAllSceneFlags" title="Remove flags from report for all scenes, except for deletion flag.">Clear All Scene Flags</button></div>
<div><button title="Delete all yellow flagged scenes in report." value="deleteSceneYellowFlag" id="yellow" style="background-color:yellow" >Delete All Yellow Flagged Scenes</button></div>
<div><button title="Delete all green flagged scenes in report." value="deleteSceneGreenFlag" id="green" style="background-color:#00FF00" >Delete All Green Flagged Scenes</button></div>
<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>
<td><input type="checkbox" id="RemoveValidatePrompt" name="RemoveValidatePrompt"><label for="RemoveValidatePrompt" title="Disable notice for task completion (Popup).">Disable Complete Confirmation</label><br></td>
<td><input type="checkbox" id="RemoveToKeepConfirm" name="RemoveToKeepConfirm"><label for="RemoveToKeepConfirm" title="Disable confirmation prompts for delete scenes">Disable Delete Confirmation</label><br></td>
<td><button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance Tag Menu</button></td>
</tr></table></td>
</tr></table></center>
<h2>Stash Duplicate Scenes Report (MatchTypePlaceHolder)</h2>\n""",

View File

@@ -1,4 +1,4 @@
# DupFileManager: Ver 0.1.9 (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.
@@ -24,15 +24,18 @@ It has both **task** and **tools-UI** components.
- Normally when Stash searches the file name for tag names, performers, and studios, it only does so using the primary file.
- Advance menu (for specially tagged duplicates)
![Screenshot 2024-11-22 145139](https://github.com/user-attachments/assets/d76646f0-c5a8-4069-ad0f-a6e5e96e7ed0)
- Delete only specially tagged duplicates in blacklist path.
- Delete duplicates with specified file path.
- Delete duplicates with specific string in File name.
- Delete duplicates with specified file size range.
- Delete with specified duration range.
- Delete with resolution range.
- Delete duplicates having specified tags.
- Delete duplicates with specified rating.
- Delete duplicates with any of the above combinations.
- Advance menu can be access from the Settings->Tools->**[DupFileManager Tools and Utilities]** menu or from the **reports**.
- Only access Advance Menu from the report when using Stash setup requiring a password.
- Here are **some** of the options available in the **Advance Menu**.
- Delete only specially tagged duplicates in blacklist path.
- Delete duplicates with specified file path.
- Delete duplicates with specific string in File name.
- Delete duplicates with specified file size range.
- Delete with specified duration range.
- Delete with resolution range.
- Delete duplicates having specified tags.
- Delete duplicates with specified rating.
- Delete duplicates with any of the above combinations.
- Bottom extended portion of the Advanced Menu screen.
- ![Screenshot 2024-11-22 232005](https://github.com/user-attachments/assets/9a0d2e9d-783b-4ea2-8fa5-3805b40af4eb)
- Delete duplicate file task with the following options:
@@ -101,10 +104,8 @@ That's it!!!
### Future Planned Features
- Currently, the report and advanced menu do not work with Stash settings requiring a password. Additional logic will be added to have them use the API Key. Planned for 1.0.0 Version.
- Add an advanced menu that will work with non-tagged reports. It will iterated through the existing report file(s) to aplly deletions, instead of searching Stash DB for tagged files. Planned for 1.1.0 Version.
- Greylist deletion option will be added to the advanced menu. Planned for 1.0.5 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.
- Remove all flags from all scenes option. Planned for 1.0.5 Version.
- Transfer option settings **[Disable Complete Confirmation]** and **[Disable Delete Confirmation]** when paginating. Planned for 1.0.5 Version.
- Add logic to merge performers and galaries seperatly from tag merging on report. Planned for 1.5.0 Version.
- Add code to report to make it when the report updates the screen (due to tag merging), it stays in the same row position. Planned for 1.5.0 Version.
- Add logic to merge group metadata when selecting merge option on report. Planned for 2.0.0 Version.
- Add 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

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

View File

@@ -47,22 +47,73 @@
html.wait * {
cursor: wait !important;
}
/* The dropdown container */
.dropdown {
float: left;
overflow: hidden;
}
/******** Dropdown button *********/
.dropdown .dropbtn {
font-size: 14px;
border: none;
outline: none;
color: white;
padding: 6px 10px;
background-color: transparent;
font-family: inherit; /* Important for vertical align on mobile phones */
margin: 0; /* Important for vertical align on mobile phones */
}
/* Dropdown content (hidden by default) */
.dropdown-content {
display: none;
position: absolute;
background-color: inherit;
min-width: 80px;
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
}
/* Links inside the dropdown */
.dropdown-content a {
float: none;
color: black;
padding: 6px 10px;
text-decoration: none;
display: block;
text-align: left;
}
/* Show the dropdown menu on hover */
.dropdown:hover .dropdown-content {
display: block;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<script src="https://www.axter.com/js/jquery-3.7.1.min.js"></script>
<script src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css" />
<script>
var GqlFromParam = false;
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
console.log(urlParams);
var GraphQl_URL = "http://localhost:9999/graphql";
if (urlParams.get("GQL") != null && urlParams.get("GQL") === "")
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
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("apiKey") != null && urlParams.get("apiKey") !== "")
apiKey = urlParams.get("apiKey");
if (urlParams.get("GQL") != null && urlParams.get("GQL") !== "")
GraphQl_URL = urlParams.get("GQL");
GqlFromParam = true;
console.log(urlParams);
console.log("GQL = " + GraphQl_URL);
console.log("Key = " + apiKey);
function RunPluginDupFileManager(Mode, Param = 0, Async = false) {
function RunPluginDupFileManager(
Mode,
Param = 0,
Async = false,
TagOnlyScenes = false
) {
$("html").addClass("wait");
$("body").css("cursor", "progress");
if (TagOnlyScenes) Param += ":TagOnlyScenes";
console.log(
"GraphQl_URL = " +
GraphQl_URL +
@@ -71,6 +122,14 @@
"; Param = " +
Param
);
if (apiKey !== "") {
console.log("Using apiKey = " + apiKey);
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader("apiKey", apiKey);
},
});
}
$.ajax({
method: "POST",
url: GraphQl_URL,
@@ -86,7 +145,54 @@
},
}),
success: function (result) {
console.log(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");
},
@@ -101,6 +207,8 @@
var blackliststr = "";
var comparestr = "less than ";
if (this.id.includes("Blacklist")) blackliststr = "in blacklist ";
if (this.id.includes("Graylist")) blackliststr = "in graylist ";
if (this.id.includes("Pinklist")) blackliststr = "in pinklist ";
if (this.id.includes("Greater")) comparestr = "greater than ";
else if (this.id.includes("Eq")) comparestr = "equal to ";
@@ -139,7 +247,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#pathToDeleteText").val());
RunPluginDupFileManager(
this.id,
$("#pathToDeleteText").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "sizeToDeleteLess" ||
this.id === "sizeToDeleteGreater" ||
@@ -156,7 +269,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#sizeToDelete").val());
RunPluginDupFileManager(
this.id,
$("#sizeToDelete").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "durationToDeleteLess" ||
this.id === "durationToDeleteGreater" ||
@@ -173,7 +291,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#durationToDelete").val());
RunPluginDupFileManager(
this.id,
$("#durationToDelete").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "commonResToDeleteLess" ||
this.id === "commonResToDeleteEq" ||
@@ -192,7 +315,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#commonResToDelete").val());
RunPluginDupFileManager(
this.id,
$("#commonResToDelete").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "resolutionToDeleteLess" ||
this.id === "resolutionToDeleteEq" ||
@@ -211,7 +339,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#resolutionToDelete").val());
RunPluginDupFileManager(
this.id,
$("#resolutionToDelete").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "ratingToDeleteLess" ||
this.id === "ratingToDeleteEq" ||
@@ -245,7 +378,12 @@
AddedWarn
);
if (result)
RunPluginDupFileManager(this.id, $("#ratingToDelete").val());
RunPluginDupFileManager(
this.id,
$("#ratingToDelete").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "tagToDelete" ||
this.id === "tagToDeleteBlacklist"
@@ -259,7 +397,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#tagToDeleteText").val());
RunPluginDupFileManager(
this.id,
$("#tagToDeleteText").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "titleToDelete" ||
this.id === "titleToDeleteBlacklist"
@@ -273,7 +416,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#titleToDeleteText").val());
RunPluginDupFileManager(
this.id,
$("#titleToDeleteText").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "pathStrToDelete" ||
this.id === "pathStrToDeleteBlacklist"
@@ -287,7 +435,12 @@
AddedWarn
)
)
RunPluginDupFileManager(this.id, $("#pathStrToDeleteText").val());
RunPluginDupFileManager(
this.id,
$("#pathStrToDeleteText").val(),
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "fileNotExistToDelete" ||
this.id === "fileNotExistToDeleteBlacklist"
@@ -299,13 +452,23 @@
"having _DuplicateMarkForDeletion tags, and that do NOT exist in the file system?"
)
)
RunPluginDupFileManager(this.id, true);
RunPluginDupFileManager(
this.id,
true,
true,
$("#DupTagOnlyCheck").prop("checked")
);
} else if (
this.id === "applyCombo" ||
this.id === "applyComboBlacklist"
this.id === "applyCombo_" ||
this.id === "applyComboBlacklist" ||
this.id === "applyComboGraylist" ||
this.id === "applyComboPinklist"
) {
var Blacklist = "";
if (this.id === "applyComboBlacklist") Blacklist = "Blacklist";
else if (this.id === "applyComboGraylist") Blacklist = "Graylist";
else if (this.id === "applyComboPinklist") Blacklist = "Pinklist";
var Param = "{";
if ($("#InPathCheck").prop("checked"))
Param +=
@@ -393,6 +556,9 @@
'", ';
if ($("#fileNotExistCheck").prop("checked"))
Param += '"' + "fileNotExistToDelete" + Blacklist + '":"true", ';
if ($("#tagOrFlagCombobox").val() !== "")
Param +=
'"' + $("#tagOrFlagCombobox").val() + Blacklist + '":"true", ';
Param += "}";
Param = Param.replace(", }", "}");
if (Param === "{}") {
@@ -410,7 +576,7 @@
Param
)
)
RunPluginDupFileManager(this.id, Param);
RunPluginDupFileManager(this.id, Param, true);
}
});
});
@@ -424,47 +590,288 @@
<table style="color: darkgreen; background-color: powderblue">
<tr>
<th>
DupFileManager Advance
<b style="color: red">_DuplicateMarkForDeletion_?</b> Tagged Files
Menu
<div>
<b style="color: red"><i>DupFileManager</i></b>
</div>
Advance Duplicate File Deletion Menu
</th>
<th>Apply Multiple Options</th>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="tag_duplicates_task"
value="-1"
title="Create new report which tags duplicates with tag name _DuplicateMarkForDeletion using user settings for [Match Duplicate Distance]."
>
Create Duplicate Report with Tagging
</button>
<button
type="button"
id="viewreport"
title="View duplicate file report."
>
View Dup Report
</button>
</center>
<table style="border-collapse: collapse; border: none">
<tr>
<th colspan="3" style="font-size: 12px; border: none">
Create report overriding user [Match Duplicate Distance] and
[significantTimeDiff] settings
</th>
</tr>
<tr>
<td style="border: none">
<div class="dropdown">
<button
type="button"
id="create_duplicate_report_task"
value="-1"
title="Create new report WITHOUT tags using user settings for [Match Duplicate Distance]."
>
Create Duplicate Report <i class="fa fa-caret-down"></i>
</button>
<div class="dropdown-content">
<div>
<button
type="button"
id="create_duplicate_report_task0a"
value="0"
title="Create report using [Match Duplicate Distance]=0 (Exact Match). NO tagging."
>
Create Duplicate Report [Exact Match]
</button>
</div>
<div>
<button
type="button"
id="create_duplicate_report_task1a"
value="1"
title="Create report using [Match Duplicate Distance]=1 (High Match). NO tagging."
>
Create Duplicate Report [High Match]
</button>
</div>
<div>
<button
type="button"
id="create_duplicate_report_task2a"
value="2"
title="Create report using [Match Duplicate Distance]=2 (Medium Match). NO tagging."
>
Create Duplicate Report [Medium Match]
</button>
</div>
<div>
<button
type="button"
id="create_duplicate_report_task3a"
value="3"
title="Create report using [Match Duplicate Distance]=3 (Low Match). NO tagging."
>
Create Duplicate Report [Low Match]
</button>
</div>
<div
style="
height: 2px;
width: 220px;
border-width: 0;
color: gray;
background-color: gray;
"
>
_
</div>
<div>
<button
type="button"
id="tag_duplicates_task"
value="-1"
title="Create new report which tags duplicates with tag name _DuplicateMarkForDeletion using user settings for [Match Duplicate Distance]."
>
Create Duplicate Report with Tagging (With Default
Match Setting)
</button>
</div>
<div>
<button
type="button"
id="tag_duplicates_task0a"
value="0"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_0 and using [Match Duplicate Distance]=0 (Exact Match)."
>
Create Duplicate Tagging Report [Exact Match]
</button>
</div>
<div>
<button
type="button"
id="tag_duplicates_task1a"
value="1"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_1 and using [Match Duplicate Distance]=1 (High Match)."
>
Create Duplicate Tagging Report [High Match]
</button>
</div>
<div>
<button
type="button"
id="tag_duplicates_task2a"
value="2"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_2 and using [Match Duplicate Distance]=2 (Medium Match)."
>
Create Duplicate Tagging Report [Medium Match]
</button>
</div>
<div>
<button
type="button"
id="tag_duplicates_task3a"
value="3"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_3 and using [Match Duplicate Distance]=3 (Low Match)."
>
Create Duplicate Tagging Report [Low Match]
</button>
</div>
</div>
</div>
</td>
<td style="border: none; padding: 0 15px">
<label
for="significantTimeDiff"
title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%."
>Time Difference%:</label
>
<input
type="number"
min="0.25"
max="1.00"
step="0.01"
id="significantTimeDiff"
name="significantTimeDiff"
title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%."
value="0.90"
/>
</td>
<td style="border: none; padding: 0 15px">
<button
type="button"
id="viewreport"
title="View duplicate file report."
>
View Dup Report
</button>
</td>
</tr>
</table>
</td>
<td>
<button
type="button"
id="applyCombo"
title="Apply selected multiple options to delete scenes."
>
Delete
</button>
<button
type="button"
id="applyComboBlacklist"
title="Apply selected multiple options to delete scenes in blacklist."
>
Delete-Blacklist
</button>
<div class="dropdown">
<center>
Multi-Options:
<button
id="applyCombo"
title="Apply selected multiple options to delete scenes."
>
Delete <i class="fa fa-caret-down"></i>
</button>
</center>
<div class="dropdown-content">
<div>
<button
type="button"
id="applyCombo_"
title="Apply selected multiple options to delete scenes."
>
Delete All Candidates
</button>
</div>
<div>
<button
type="button"
id="applyComboBlacklist"
title="Apply selected multiple options to delete scenes in blacklist."
>
Delete-Blacklist
</button>
</div>
<div>
<button
type="button"
id="applyComboGraylist"
title="Apply selected multiple options to delete scenes in graylist."
>
Delete-Graylist
</button>
</div>
<div>
<button
type="button"
id="applyComboPinklist"
title="Apply selected multiple options to delete scenes in pinklist."
>
Delete-Pinklist
</button>
</div>
</div>
</div>
</td>
</tr>
<tr>
<td>
<label
title="When enabled, operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?"
for="DupTagOnlyCheck"
>Apply action only to scenes with
<b>_DuplicateMarkForDeletion_?</b> tag:</label
><input
title="When enabled, operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?"
type="checkbox"
id="DupTagOnlyCheck"
name="DupTagOnlyCheck"
value="true"
/>
</td>
<td>
<label for="tagOrFlagCombobox">TagOrFlag:</label>
<select id="tagOrFlagCombobox" name="tagOrFlagCombobox">
<option value="" selected="selected"></option>
<option
value="TagOnlyScenes"
title="When selected, Multi-Options operations only apply to scenes which have the special tag _DuplicateMarkForDeletion_?"
>
Dup Tag
</option>
<option
value="YellowFlag"
style="background-color: Yellow"
title="When selected, Multi-Options operations only apply to scenes with [Yellow] fag."
>
Yellow Flag
</option>
<option
value="GreenFlag"
style="background-color: #00ff00"
title="When selected, Multi-Options operations only apply to scenes with [Green] fag."
>
Green Flag
</option>
<option
value="OrangeFlag"
style="background-color: Orange"
title="When selected, Multi-Options operations only apply to scenes with [Orange] fag."
>
Orange Flag
</option>
<option
value="CyanFlag"
style="background-color: Cyan"
title="When selected, Multi-Options operations only apply to scenes with [Cyan] fag."
>
Cyan Flag
</option>
<option
value="PinkFlag"
style="background-color: Pink"
title="When selected, Multi-Options operations only apply to scenes with [Pink] fag."
>
Pink Flag
</option>
<option
value="RedFlag"
style="background-color: Red"
title="When selected, Multi-Options operations only apply to scenes with [Red] fag."
>
Red Flag
</option>
</select>
</td>
</tr>
<tr>
@@ -2427,178 +2834,6 @@
</select>
</td>
</tr>
</table>
</center>
<div id="div1"></div>
<br />
<center>
<table style="color: darkgreen; background-color: powderblue">
<tr>
<th colspan="2">
Create report with different
<b style="color: red">[Match Duplicate Distance]</b> options <br />
<div style="font-size: 12px">
Overrides user [Match Duplicate Distance] and
[significantTimeDiff] settings
</div>
<form
id="significantTimeDiffForm"
action="javascript:DeleteDupInPath();"
target="_self"
>
<label
for="significantTimeDiff"
title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%."
>Time Difference%:</label
>
<input
type="number"
min="0.25"
max="1.00"
step="0.01"
id="significantTimeDiff"
name="significantTimeDiff"
title="Significant time difference setting, where 1 equals 100% and (.9) equals 90%."
value="0.90"
/>
</form>
</th>
</tr>
<tr>
<td>
<table style="color: darkgreen; background-color: powderblue">
<tr>
<th
title="Create report with tagging (_DuplicateMarkForDeletion_)"
>
Create Report with Tagging
</th>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="tag_duplicates_task0"
value="0"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_0 and using [Match Duplicate Distance]=0 (Exact Match)."
>
Create Duplicate Tagging Report [Exact Match]
</button>
</center>
</td>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="tag_duplicates_task1"
value="1"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_1 and using [Match Duplicate Distance]=1 (High Match)."
>
Create Duplicate Tagging Report [High Match]
</button>
</center>
</td>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="tag_duplicates_task2"
value="2"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_2 and using [Match Duplicate Distance]=2 (Medium Match)."
>
Create Duplicate Tagging Report [Medium Match]
</button>
</center>
</td>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="tag_duplicates_task3"
value="3"
title="Create report which tags duplicates with tag name _DuplicateMarkForDeletion_3 and using [Match Duplicate Distance]=3 (Low Match)."
>
Create Duplicate Tagging Report [Low Match]
</button>
</center>
</td>
</tr>
</table>
</td>
<td>
<table style="color: darkgreen; background-color: powderblue">
<tr>
<th title="Create report with NO tagging (NO Dup Tag)">
Create Report without Tagging
</th>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="create_duplicate_report_task0"
value="0"
title="Create report using [Match Duplicate Distance]=0 (Exact Match). NO tagging."
>
Create Duplicate Report [Exact Match]
</button>
</center>
</td>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="create_duplicate_report_task1"
value="1"
title="Create report using [Match Duplicate Distance]=1 (High Match). NO tagging."
>
Create Duplicate Report [High Match]
</button>
</center>
</td>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="create_duplicate_report_task2"
value="2"
title="Create report using [Match Duplicate Distance]=2 (Medium Match). NO tagging."
>
Create Duplicate Report [Medium Match]
</button>
</center>
</td>
</tr>
<tr>
<td>
<center>
<button
type="button"
id="create_duplicate_report_task3"
value="3"
title="Create report using [Match Duplicate Distance]=3 (Low Match). NO tagging."
>
Create Duplicate Report [Low Match]
</button>
</center>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="font-size: 12px" colspan="2">
<b>Details:</b>
@@ -2704,5 +2939,6 @@
</tr>
</table>
</center>
<div id="div1"></div>
</body>
</html>