First alpha release

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
name: DupFileManager name: DupFileManager
description: Manages duplicate files. description: Manages duplicate files.
version: 0.2.2 version: 1.0.0
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui: ui:
javascript: javascript:
@@ -70,6 +70,10 @@ settings:
displayName: Tracing displayName: Tracing
description: Enable tracing and debug so-as to add additional tracing and debug logging in Stash\plugins\DupFileManager\DupFileManager.log description: Enable tracing and debug so-as to add additional tracing and debug logging in Stash\plugins\DupFileManager\DupFileManager.log
type: BOOLEAN 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: exec:
- python - python
- "{pluginDir}/DupFileManager.py" - "{pluginDir}/DupFileManager.py"

View File

@@ -65,7 +65,7 @@ li:hover .large {
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);; box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);;
} }
/******** Dropdown buttons *********/ /******** Dropdown buttons *********/
.dropdown .dropdown_tag .dropdown_performer .dropdown_gallery .dropbtn { .dropdown {
font-size: 14px; font-size: 14px;
border: none; border: none;
outline: none; outline: none;
@@ -94,6 +94,40 @@ li:hover .large {
.dropdown:hover .dropdown-content { .dropdown:hover .dropdown-content {
display: block; 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 Icons --*************/
.dropdown_icon { .dropdown_icon {
height:22px; height:22px;
@@ -187,6 +221,10 @@ function trim(str, ch) {
return (start > 0 || end < str.length) ? str.substring(start, end) : str; return (start > 0 || end < str.length) ? str.substring(start, end) : str;
} }
function RunPluginOperation(Mode, ActionID, button, asyncAjax){ function RunPluginOperation(Mode, ActionID, button, asyncAjax){
if (asyncAjax){
$('html').addClass('wait');
$("body").css("cursor", "progress");
}
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt"); var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
if (apiKey !== "") if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}}); $.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
@@ -196,23 +234,23 @@ function RunPluginOperation(Mode, ActionID, button, asyncAjax){
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}}, variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){ }), success: function(result){
console.log(result); console.log(result);
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags") if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
if (Mode === "renameFile" || Mode === "clearAllSceneFlags" || Mode === "mergeTags" || (Mode !== "deleteScene" && Mode.startsWith("deleteScene")))
location.replace(location.href); location.replace(location.href);
if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete."); if (!chkBxRemoveValid.checked && Mode !== "flagScene") alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.\\n\\nResults=" + result);
}, error: function(XMLHttpRequest, textStatus, errorThrown) { }, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown); console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
} }
}); });
} }
function selectMarker(Mode, ActionID, button){ function SetFlagOnScene(flagType, ActionID){
$('<p>Select desire marker type <select><option>yellow highlight</option><option>green highlight</option><option>orange highlight</option><option>cyan highlight</option><option>pink highlight</option><option>red highlight</option><option>strike-through</option><option>disable-scene</option><option>remove all flags</option></select></p>').confirm(function(answer){
if(answer.response){
console.log("Selected " + $('select',this).val());
var flagType = $('select',this).val();
if (flagType == null){
console.log("Invalid flagType");
return;
}
if (flagType === "yellow highlight") if (flagType === "yellow highlight")
$('.ID_' + ActionID).css('background','yellow'); $('.ID_' + ActionID).css('background','yellow');
else if (flagType === "green highlight") else if (flagType === "green highlight")
@@ -231,11 +269,21 @@ function selectMarker(Mode, ActionID, button){
$('.ID_' + ActionID).css({ 'background' : 'gray', 'pointer-events' : 'none' }); $('.ID_' + ActionID).css({ 'background' : 'gray', 'pointer-events' : 'none' });
else if (flagType === "remove all flags") else if (flagType === "remove all flags")
$('.ID_' + ActionID).removeAttr('style'); //.css({ 'background' : '', 'text-decoration' : '', 'pointer-events' : '' }); $('.ID_' + ActionID).removeAttr('style'); //.css({ 'background' : '', 'text-decoration' : '', 'pointer-events' : '' });
else { else
flagType = "none"; return false;
$('.ID_' + ActionID).css("target-property", ""); return true;
}
function selectMarker(Mode, ActionID, button){
$('<p>Select desire marker type <select><option>yellow highlight</option><option>green highlight</option><option>orange highlight</option><option>cyan highlight</option><option>pink highlight</option><option>red highlight</option><option>strike-through</option><option>disable-scene</option><option>remove all flags</option></select></p>').confirm(function(answer){
if(answer.response){
console.log("Selected " + $('select',this).val());
var flagType = $('select',this).val();
if (flagType == null){
console.log("Invalid flagType");
return; return;
} }
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType; ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID); console.log("ActionID = " + ActionID);
RunPluginOperation(Mode, ActionID, button, false); RunPluginOperation(Mode, ActionID, button, false);
@@ -273,6 +321,8 @@ $(document).ready(function(){
$("button").click(function(){ $("button").click(function(){
var Mode = this.value; var Mode = this.value;
var ActionID = this.id; var ActionID = this.id;
if (Mode === "DoNothing")
return;
if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_") if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{ {
var newUrl = window.location.href; var newUrl = window.location.href;
@@ -280,15 +330,18 @@ $(document).ready(function(){
window.open(newUrl, "_blank"); window.open(newUrl, "_blank");
return; return;
} }
if (Mode === "deleteScene" || Mode === "removeScene"){ if (Mode.startsWith("deleteScene") || Mode === "removeScene"){
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm"); var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
question = "Are you sure you want to delete this file and remove scene from stash?"; 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 (Mode === "removeScene") question = "Are you sure you want to remove scene from stash?";
if (!chkBxDisableDeleteConfirm.checked && !confirm(question)) if (!chkBxDisableDeleteConfirm.checked && !confirm(question))
return; return;
if (Mode === "deleteScene" || Mode === "removeScene"){
$('.ID_' + ActionID).css('background-color','gray'); $('.ID_' + ActionID).css('background-color','gray');
$('.ID_' + ActionID).css('pointer-events','none'); $('.ID_' + ActionID).css('pointer-events','none');
} }
}
else if (Mode === "newName" || Mode === "renameFile"){ else if (Mode === "newName" || Mode === "renameFile"){
var myArray = ActionID.split(":"); var myArray = ActionID.split(":");
var promptStr = "Enter new name for scene ID " + myArray[0] + ", or press escape to cancel."; var promptStr = "Enter new name for scene ID " + myArray[0] + ", or press escape to cancel.";
@@ -303,6 +356,14 @@ $(document).ready(function(){
else if (Mode === "flagScene"){ else if (Mode === "flagScene"){
selectMarker(Mode, ActionID, this); selectMarker(Mode, ActionID, this);
return; 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); RunPluginOperation(Mode, ActionID, this, true);
}); });
@@ -329,11 +390,19 @@ $(document).ready(function(){
<td><input type="checkbox" id="RemoveToKeepConfirm" name="RemoveToKeepConfirm"><label for="RemoveToKeepConfirm" title="Disable confirmation prompts for delete scenes">Disable Delete Confirmation</label><br></td> <td><input type="checkbox" id="RemoveToKeepConfirm" name="RemoveToKeepConfirm"><label for="RemoveToKeepConfirm" title="Disable confirmation prompts for delete scenes">Disable Delete Confirmation</label><br></td>
<td> <td>
<div class="dropdown"> <div class="dropdown">
<button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance Tag Menu <i class="fa fa-caret-down"></i></button> <button id="AdvanceMenu" title="View advance menu for tagged duplicates." name="AdvanceMenu">Advance Menu <i class="fa fa-caret-down"></i></button>
<div class="dropdown-content"> <div class="dropdown-content">
<div><button type="button" id="clearAllSceneFlags" value="clearAllSceneFlags" title="Remove flags from report for all scenes, except for deletion flag.">Clear All Scene Flags</button></div>
<div><button type="button" id="clear_duplicate_tags_task" value="clear_duplicate_tags_task" title="Remove duplicate (_DuplicateMarkForDeletion_?) tag from all scenes. This action make take a few minutes to complete.">Remove All Scenes Tags</button></div> <div><button type="button" id="clear_duplicate_tags_task" value="clear_duplicate_tags_task" title="Remove duplicate (_DuplicateMarkForDeletion_?) tag from all scenes. This action make take a few minutes to complete.">Remove All Scenes Tags</button></div>
<div><button type="button" id="fileNotExistToDelete" value="fileNotExistToDelete" title="Delete tagged duplicates for which file does NOT exist.">Delete Files That do Not Exist</button></div> <div><button type="button" id="fileNotExistToDelete" value="Tagged" title="Delete tagged duplicates for which file does NOT exist.">Delete Tagged Files That do Not Exist</button></div>
<div><button type="button" id="fileNotExistToDelete" value="Report" title="Delete duplicate candidate files in report for which file does NOT exist.">Delete Files That do Not Exist in Report</button></div>
<div><button type="button" id="clearAllSceneFlags" value="clearAllSceneFlags" title="Remove flags from report for all scenes, except for deletion flag.">Clear All Scene Flags</button></div>
<div><button title="Delete all yellow flagged scenes in report." value="deleteSceneYellowFlag" id="yellow" style="background-color:yellow" >Delete All Yellow Flagged Scenes</button></div>
<div><button title="Delete all green flagged scenes in report." value="deleteSceneGreenFlag" id="green" style="background-color:#00FF00" >Delete All Green Flagged Scenes</button></div>
<div><button title="Delete all orange flagged scenes in report." value="deleteSceneOrangeFlag" id="orange" style="background-color:orange" >Delete All Orange Flagged Scenes</button></div>
<div><button title="Delete all cyan flagged scenes in report." value="deleteSceneCyanFlag" id="cyan" style="background-color:cyan" >Delete All Cyan Flagged Scenes</button></div>
<div><button title="Delete all pink flagged scenes in report." value="deleteScenePinkFlag" id="pink" style="background-color:pink" >Delete All Pink Flagged Scenes</button></div>
<div><button title="Delete all red flagged scenes in report." value="deleteSceneRedFlag" id="red" style="background-color:red" >Delete All Red Flagged Scenes</button></div>
<div><button title="Delete all strike-through scenes in report." value="StrikeThrough" id="line-through" >Delete All Strike-through Scenes</button></div>
</div> </div>
</div> </div>
</td> </td>

View File

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

View File

@@ -846,6 +846,22 @@ class StashPluginHelper(StashInterface):
return None return None
return results['rows'][0][0] 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. # Functions which are candidates to be added to parent class use snake_case naming convention.
# ############################################################################################################ # ############################################################################################################

View File

@@ -70,6 +70,8 @@ html.wait, html.wait * { cursor: wait !important; }
</style> </style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <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-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> <script>
var apiKey = ""; // For Stash installations with a password setup, populate this variable with the apiKey found in Stash->Settings->Security->[API Key]; ----- Or pass in the apiKey at the URL command line. Example: advance_options.html?apiKey=12345G4igiJdgssdgiwqInh5cCI6IkprewJ9hgdsfhgfdhd&GQL=http://localhost:9999/graphql var 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 var GraphQl_URL = "http://localhost:9999/graphql";// For Stash installations with non-standard ports or URL's, populate this variable with actual URL; ----- Or pass in the URL at the command line using GQL param. Example: advance_options.html?GQL=http://localhost:9900/graphql
@@ -80,7 +82,7 @@ if (urlParams.get('GQL') != null && urlParams.get('GQL') !== "")
GraphQl_URL = urlParams.get('GQL'); GraphQl_URL = urlParams.get('GQL');
console.log(urlParams); console.log(urlParams);
console.log("GQL = " + GraphQl_URL); console.log("GQL = " + GraphQl_URL);
console.log("apiKey = " + apiKey); console.log("Key = " + apiKey);
function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes = false) { function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes = false) {
$('html').addClass('wait'); $('html').addClass('wait');
@@ -88,25 +90,48 @@ function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes =
if (TagOnlyScenes) if (TagOnlyScenes)
Param += ":TagOnlyScenes"; Param += ":TagOnlyScenes";
console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param); console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param);
//if ($("#InPathCheck").prop('checked')) if (apiKey !== ""){
if (apiKey !== "") console.log("Using apiKey = " + apiKey);
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}}); $.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
}
$.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: Async, async: Async, $.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: Async, async: Async,
data: JSON.stringify({ data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`, query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : Param, "mode":Mode}}, variables: {"plugin_id": "DupFileManager", "args": { "Target" : Param, "mode":Mode}},
}), success: function(result){ }), success: function(result){
console.log("Ajax success with result = " + result);
$('html').removeClass('wait'); $('html').removeClass('wait');
$("body").css("cursor", "default"); $("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) { }, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown); console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
$('html').removeClass('wait');
$("body").css("cursor", "default");
} }
}); });
console.log("Setting default cursor"); console.log("Setting default cursor");
} }
$(document).ready(function(){ $(document).ready(function(){
$("button").click(function(){ $("button").click(function(){
const AddedWarn = "? This will delete the files, and remove them from stash."; const AddedWarn = "? This will delete the files, and remove them from stash.";
console.log(this.id); console.log(this.id);

View File

@@ -20,3 +20,12 @@
- Added pinklist option to Settings->Plugins->Plugins and to [**Advance Duplicate File Deletion Menu**] - Added pinklist option to Settings->Plugins->Plugins and to [**Advance Duplicate File Deletion Menu**]
- The pinklist is only used with the [**Advance Duplicate File Deletion Menu**], and it's **NOT** used in the primary process to selected candidates for deletion. - The pinklist is only used with the [**Advance Duplicate File Deletion Menu**], and it's **NOT** used in the primary process to selected candidates for deletion.
- Advance Menu now works with non-tagged scenes that are in the current report. - Advance Menu now works with non-tagged scenes that are in the current report.
### 1.0.0
- Consolidated buttons and links on report into dropdown buttons.
- On report, added dropdown menu options for flags.
- Rename Tools-UI advance duplicate tagged menu to [**Advance Duplicate File Deletion Menu**]
- When [**Advance Duplicate File Deletion Menu**] completes report, gives user prompt to open the report in browser.
- Added performance enhancement for removing (clearing) duplicate tags from all scenes by using SQL call.
- Added option to report to delete files that do not exist by duplicate candidates in report, as well as by tagged files.
- Added logic to disable scene in report if deleted by [**Advance Duplicate File Deletion Menu**]. Note: Requires a refresh.
- Added report option to delete by flags set on the report.

View File

@@ -846,6 +846,22 @@ class StashPluginHelper(StashInterface):
return None return None
return results['rows'][0][0] 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. # Functions which are candidates to be added to parent class use snake_case naming convention.
# ############################################################################################################ # ############################################################################################################

View File

@@ -846,6 +846,22 @@ class StashPluginHelper(StashInterface):
return None return None
return results['rows'][0][0] 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. # Functions which are candidates to be added to parent class use snake_case naming convention.
# ############################################################################################################ # ############################################################################################################