[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

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})")