Added UI interface for DupFileManager

This commit is contained in:
David Maisonave
2024-10-28 06:15:03 -04:00
parent 174b489bc8
commit be8e632699
12 changed files with 377 additions and 95 deletions

View File

@@ -0,0 +1,36 @@
.scene-card__date {
color: #bfccd6;
font-size: 0.85em;
}
.scene-card__performer {
display: inline-block;
font-weight: 500;
margin-right: 0.5em;
}
.scene-card__performer a {
color: #137cbd;
}
.scene-card__performers,
.scene-card__tags {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 1;
overflow: hidden;
}
.scene-card__performers:hover,
.scene-card__tags:hover {
-webkit-line-clamp: unset;
overflow: visible;
}
.scene-card__tags .tag-item {
margin-left: 0;
}
.scene-performer-popover .image-thumbnail {
margin: 1em;
}
/*# sourceMappingURL=DupFileManager.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../src/DupFileManager.scss"],"names":[],"mappings":"AAAA;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;;AAIJ;AAAA;EAEE;EACA;EACA;EACA;;AAEA;AAAA;EACE;EACA;;;AAIJ;EACE;;;AAGF;EACE","file":"DupFileManager.css"}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"DupFileManager.js","sourceRoot":"","sources":["../src/DupFileManager.tsx"],"names":[],"mappings":"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"}

View File

@@ -4,7 +4,7 @@
# Note: To call this script outside of Stash, pass argument --url # Note: To call this script outside of Stash, pass argument --url
# Example: python DupFileManager.py --url http://localhost:9999 -a # Example: python DupFileManager.py --url http://localhost:9999 -a
import ModulesValidate import ModulesValidate
ModulesValidate.modulesInstalled(["send2trash", "requests"]) ModulesValidate.modulesInstalled(["send2trash", "requests"], silent=True)
import os, sys, time, pathlib, argparse, platform, shutil, traceback, logging, requests import os, sys, time, pathlib, argparse, platform, shutil, traceback, logging, requests
from datetime import datetime from datetime import datetime
from StashPluginHelper import StashPluginHelper from StashPluginHelper import StashPluginHelper
@@ -46,11 +46,17 @@ stash = StashPluginHelper(
DebugFieldName="zzDebug", DebugFieldName="zzDebug",
) )
stash.convertToAscii = True stash.convertToAscii = True
doJsonReturnModeTypes = ["tag_duplicates_task", "removeDupTag", "addExcludeTag", "removeExcludeTag", "mergeTags", "getLocalDupReportPath", "createDuplicateReportWithoutTagging", "deleteLocalDupReportHtmlFiles"]
doJsonReturn = False
if stash.PLUGIN_TASK_NAME in doJsonReturnModeTypes:
doJsonReturn = True
stash.log_to_norm = stash.LogTo.FILE
stash.Log("******************* Starting *******************") stash.Log("******************* Starting *******************")
if len(sys.argv) > 1: if len(sys.argv) > 1:
stash.Log(f"argv = {sys.argv}") stash.Log(f"argv = {sys.argv}")
else: else:
stash.Trace(f"No command line arguments. JSON_INPUT['args'] = {stash.JSON_INPUT['args']}") stash.Trace(f"No command line arguments. JSON_INPUT['args'] = {stash.JSON_INPUT['args']}; PLUGIN_TASK_NAME = {stash.PLUGIN_TASK_NAME}")
stash.status(logLevel=logging.DEBUG) stash.status(logLevel=logging.DEBUG)
@@ -482,6 +488,8 @@ def logReason(DupFileToKeep, Scene, reason):
reasonDict[DupFileToKeep['id']] = reason reasonDict[DupFileToKeep['id']] = reason
stash.Debug(f"Replacing {DupFileToKeep['files'][0]['path']} with {Scene['files'][0]['path']} for candidate to keep. Reason={reason}") stash.Debug(f"Replacing {DupFileToKeep['files'][0]['path']} with {Scene['files'][0]['path']} for candidate to keep. Reason={reason}")
htmlReportName = f"{stash.PLUGINS_PATH}{os.sep}{stash.Setting('htmlReportName')}"
def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False): def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
global reasonDict global reasonDict
duplicateMarkForDeletion_descp = 'Tag added to duplicate scenes so-as to tag them for deletion.' duplicateMarkForDeletion_descp = 'Tag added to duplicate scenes so-as to tag them for deletion.'
@@ -490,7 +498,6 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
stash.Trace(f"dupTagId={dupTagId} name={duplicateMarkForDeletion}") stash.Trace(f"dupTagId={dupTagId} name={duplicateMarkForDeletion}")
createHtmlReport = stash.Setting('createHtmlReport') createHtmlReport = stash.Setting('createHtmlReport')
previewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview" previewOrStream = "stream" if stash.Setting('streamOverPreview') else "preview"
htmlReportName = f"{stash.PLUGINS_PATH}{os.sep}{stash.Setting('htmlReportName')}"
htmlReportNameHomePage = htmlReportName htmlReportNameHomePage = htmlReportName
htmlReportTableRow = stash.Setting('htmlReportTableRow') htmlReportTableRow = stash.Setting('htmlReportTableRow')
htmlReportTableData = stash.Setting('htmlReportTableData') htmlReportTableData = stash.Setting('htmlReportTableData')
@@ -661,9 +668,11 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
elif moveToTrashCan: elif moveToTrashCan:
sendToTrash(DupFileName) sendToTrash(DupFileName)
stash.destroyScene(DupFile['id'], delete_file=True) stash.destroyScene(DupFile['id'], delete_file=True)
elif tagDuplicates: elif tagDuplicates or fileHtmlReport != None:
QtyTagForDel+=1 QtyTagForDel+=1
QtyTagForDelPaginate+=1 QtyTagForDelPaginate+=1
didAddTag = False
if tagDuplicates:
didAddTag = setTagId_withRetry(duplicateMarkForDeletion, DupFile, DupFileToKeep, ignoreAutoTag=True) didAddTag = setTagId_withRetry(duplicateMarkForDeletion, DupFile, DupFileToKeep, ignoreAutoTag=True)
if fileHtmlReport != None: if fileHtmlReport != None:
stash.Debug(f"Adding scene {DupFile['id']} to HTML report.") stash.Debug(f"Adding scene {DupFile['id']} to HTML report.")
@@ -698,9 +707,9 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
fileHtmlReport.write("</table>") fileHtmlReport.write("</table>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Delete file and remove scene from stash\" value=\"Duplicate\" id=\"{DupFile['id']}\">[Delete]</button>") fileHtmlReport.write(f"<button class=\"link-button\" title=\"Delete file and remove scene from stash\" value=\"Duplicate\" id=\"{DupFile['id']}\">[Delete]</button>")
if dupFileExist: if dupFileExist and tagDuplicates:
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove duplicate tag from scene\" value=\"RemoveDupTag\" id=\"{DupFile['id']}\">[Remove Tag]</button>") fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove duplicate tag from scene\" value=\"removeDupTag\" id=\"{DupFile['id']}\">[Remove Tag]</button>")
fileHtmlReport.write(f"<button class=\"link-button\" title=\"Add exclude scene from deletion tag\" value=\"AddExcludeTag\" id=\"{DupFile['id']}\">[Add Exclude Tag]</button>") fileHtmlReport.write(f"<button class=\"link-button\" title=\"Add exclude scene from deletion tag\" value=\"addExcludeTag\" id=\"{DupFile['id']}\">[Add Exclude Tag]</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"<button class=\"link-button\" title=\"Merge duplicate scene tags with ToKeep scene tags\" value=\"mergeTags\" id=\"{DupFile['id']}:{DupFileToKeep['id']}\">[Merge Tags]</button>")
if dupFileExist: 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=\"Open folder\" href=\"file://{getPath(DupFile, True)}\">[Folder]</a>")
@@ -718,7 +727,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
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=\"ToKeep\" id=\"{DupFileToKeep['id']}\">[Delete]</button>") fileHtmlReport.write(f"<button class=\"link-button\" title=\"Delete [DupFileToKeep] and remove scene from stash\" value=\"ToKeep\" id=\"{DupFileToKeep['id']}\">[Delete]</button>")
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']}\">[Remove Exclude Tag]</button>") fileHtmlReport.write(f"<button class=\"link-button\" title=\"Remove exclude scene from deletion tag\" value=\"removeExcludeTag\" id=\"{DupFileToKeep['id']}\">[Remove Exclude Tag]</button>")
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Open folder\" href=\"file://{getPath(DupFileToKeep, True)}\">[Folder]</a>") fileHtmlReport.write(f"<a class=\"link-items\" title=\"Open folder\" href=\"file://{getPath(DupFileToKeep, True)}\">[Folder]</a>")
if toKeepFileExist: if toKeepFileExist:
fileHtmlReport.write(f"<a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFileToKeep)}\">[Play]</a>") fileHtmlReport.write(f"<a class=\"link-items\" title=\"Play file locally\" href=\"file://{getPath(DupFileToKeep)}\">[Play]</a>")
@@ -762,7 +771,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n") fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n")
fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Scene</th>{htmlReportTableHeader}Duplicate to Delete</th>{htmlReportTableHeader}Scene-ToKeep</th>{htmlReportTableHeader}Duplicate to Keep</th></tr>\n") fileHtmlReport.write(f"{htmlReportTableRow}{htmlReportTableHeader}Scene</th>{htmlReportTableHeader}Duplicate to Delete</th>{htmlReportTableHeader}Scene-ToKeep</th>{htmlReportTableHeader}Duplicate to Keep</th></tr>\n")
if graylistTagging and stash.startsWithInList(graylist, DupFile['files'][0]['path']): if tagDuplicates and graylistTagging and stash.startsWithInList(graylist, DupFile['files'][0]['path']):
stash.addTag(DupFile, graylistMarkForDeletion, ignoreAutoTag=True) stash.addTag(DupFile, graylistMarkForDeletion, ignoreAutoTag=True)
if didAddTag: if didAddTag:
QtyNewlyTag+=1 QtyNewlyTag+=1
@@ -831,6 +840,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False):
stash.optimise_database() stash.optimise_database()
if doGeneratePhash: if doGeneratePhash:
stash.metadata_generate({"phashes": True}) stash.metadata_generate({"phashes": True})
sys.stdout.write("Report complete")
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]]
@@ -932,34 +942,42 @@ def manageTagggedDuplicates(deleteScenes=False, clearTag=False, setGrayListTag=F
stash.metadata_generate({"phashes": True}) stash.metadata_generate({"phashes": True})
def removeDupTag(): def removeDupTag():
if 'removeDupTag' not in stash.JSON_INPUT['args']: if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find removeDupTag in JSON_INPUT ({stash.JSON_INPUT['args']})") stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return return
sceneToRemoveTag = stash.JSON_INPUT['args']['removeDupTag'] scene = stash.JSON_INPUT['args']['Target']
stash.removeTag(sceneToRemoveTag, duplicateMarkForDeletion) stash.Log(f"Processing scene ID# {scene}")
stash.Log(f"Done removing tag from scene {sceneToRemoveTag}.") stash.removeTag(scene, duplicateMarkForDeletion)
stash.Log(f"Done removing tag from scene {scene}.")
jsonReturn = "{'removeDupTag' : 'complete', 'id': '" + f"{scene}" + "'}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
def addExcludeForDelTag(): def addExcludeTag():
if 'addExcludeForDelTag' not in stash.JSON_INPUT['args']: if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find addExcludeForDelTag in JSON_INPUT ({stash.JSON_INPUT['args']})") stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return return
scene = stash.JSON_INPUT['args']['addExcludeForDelTag'] scene = stash.JSON_INPUT['args']['Target']
stash.Log(f"Processing scene ID# {scene}")
stash.addTag(scene, excludeDupFileDeleteTag) stash.addTag(scene, excludeDupFileDeleteTag)
stash.Log(f"Done adding exclude tag to scene {scene}.") stash.Log(f"Done adding exclude tag to scene {scene}.")
sys.stdout.write("{" + f"addExcludeTag : 'complete', id: '{scene}'" + "}")
def removeExcludeForDelTag(): def removeExcludeTag():
if 'removeExcludeForDelTag' not in stash.JSON_INPUT['args']: if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find removeExcludeForDelTag in JSON_INPUT ({stash.JSON_INPUT['args']})") stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return return
scene = stash.JSON_INPUT['args']['removeExcludeForDelTag'] scene = stash.JSON_INPUT['args']['Target']
stash.Log(f"Processing scene ID# {scene}")
stash.removeTag(scene, excludeDupFileDeleteTag) stash.removeTag(scene, excludeDupFileDeleteTag)
stash.Log(f"Done removing exclude tag from scene {scene}.") stash.Log(f"Done removing exclude tag from scene {scene}.")
sys.stdout.write("{" + f"removeExcludeTag : 'complete', id: '{scene}'" + "}")
def mergeTags(): def mergeTags():
if 'mergeScenes' not in stash.JSON_INPUT['args']: if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find mergeScenes in JSON_INPUT ({stash.JSON_INPUT['args']})") stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return return
mergeScenes = stash.JSON_INPUT['args']['mergeScenes'] mergeScenes = stash.JSON_INPUT['args']['Target']
scenes = mergeScenes.split(":") scenes = mergeScenes.split(":")
if len(scenes) < 2: if len(scenes) < 2:
stash.Error(f"Could not get both scenes from string {mergeScenes}") stash.Error(f"Could not get both scenes from string {mergeScenes}")
@@ -969,6 +987,55 @@ def mergeTags():
scene2 = stash.find_scene(int(scenes[1])) scene2 = stash.find_scene(int(scenes[1]))
stash.mergeMetadata(scene1, scene2) stash.mergeMetadata(scene1, scene2)
stash.Log(f"Done merging scenes for scene {scenes[0]} and scene {scenes[1]}") stash.Log(f"Done merging scenes for scene {scenes[0]} and scene {scenes[1]}")
sys.stdout.write("{" + f"mergeTags : 'complete', id1: '{scene1}', id2: '{scene2}'" + "}")
def openWebpage():
htmlReportName = f"{stash.PLUGINS_PATH}{os.sep}{stash.Setting('htmlReportName')}"
stash.Log(f"Opening web page {htmlReportName}")
# chromePath = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
chromePath = r"C:\PROGRA~2\Google\Chrome\Application\chrome.exe"
firefox = r"C:\PROGRA~1\MOZILL~1\firefox.exe"
browserPath = chromePath
if os.path.isfile(browserPath):
stash.Log(f"Opening web page {htmlReportName} with user {os.getlogin()} using {browserPath}")
# pw_record = pwd.getpwnam(os.getlogin())
# uid = pw_record.pw_uid
# gid = pw_record.pw_gid
# os.setgid(gid)
# os.setuid(uid)
# stash.Log(f"Using uid={uid} and gid={gid}")
# stash.executeProcess(f"\"{browserPath}\" -incognito --new-window \"file://{htmlReportName}\"", ExecDetach=True)
os.system(f"{browserPath} -incognito --new-window \"file://{htmlReportName}\"")
# else:
import webbrowser
webbrowser.open(htmlReportName, new=2, autoraise=True)
os.system(f"start file:///{htmlReportName}")
def getLocalDupReportPath():
htmlReportExist = "true" if os.path.isfile(htmlReportName) else "false"
# htmlReportExist = "false"
localPath = htmlReportName.replace("\\", "\\\\")
jsonReturn = "{'LocalDupReportExist' : " + f"{htmlReportExist}" + ", 'Path': '" + f"{localPath}" + "'}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
def deleteLocalDupReportHtmlFiles():
htmlReportExist = "true" if os.path.isfile(htmlReportName) else "false"
if os.path.isfile(htmlReportName):
stash.Log(f"Deleting file {htmlReportName}")
os.remove(htmlReportName)
for x in range(2, 9999):
fileName = htmlReportName.replace(".html", f"_{x-1}.html")
stash.Debug(f"Checking if file '{fileName}' exist.")
if not os.path.isfile(fileName):
break
stash.Log(f"Deleting file {fileName}")
os.remove(fileName)
else:
stash.Log(f"Report file does not exist: {htmlReportName}")
jsonReturn = "{'LocalDupReportExist' : " + f"{htmlReportExist}" + ", 'Path': '" + f"{htmlReportName}" + "', 'qty': '" + f"{x}" + "'}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
# ToDo: Add additional menu items option only for bottom of report: # ToDo: Add additional menu items option only for bottom of report:
# Remove from stash all files no longer part of stash library # Remove from stash all files no longer part of stash library
@@ -993,18 +1060,27 @@ try:
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})
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "remove_a_duplicate_tag": elif stash.PLUGIN_TASK_NAME == "removeDupTag":
removeDupTag() removeDupTag()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "add_an_exclude_tag": elif stash.PLUGIN_TASK_NAME == "addExcludeTag":
addExcludeForDelTag() addExcludeTag()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "remove_an_exclude_tag": elif stash.PLUGIN_TASK_NAME == "removeExcludeTag":
removeExcludeForDelTag() removeExcludeTag()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "merge_tags": elif stash.PLUGIN_TASK_NAME == "mergeTags":
mergeTags() mergeTags()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT") stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "getLocalDupReportPath":
getLocalDupReportPath()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "deleteLocalDupReportHtmlFiles":
deleteLocalDupReportHtmlFiles()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "createDuplicateReportWithoutTagging":
mangeDupFiles(tagDuplicates=False, merge=mergeDupFilename)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif parse_args.dup_tag: elif parse_args.dup_tag:
stash.PLUGIN_TASK_NAME = "dup_tag" stash.PLUGIN_TASK_NAME = "dup_tag"
mangeDupFiles(tagDuplicates=True, merge=mergeDupFilename) mangeDupFiles(tagDuplicates=True, merge=mergeDupFilename)
@@ -1030,5 +1106,7 @@ except Exception as e:
killScanningJobs() killScanningJobs()
stash.convertToAscii = False stash.convertToAscii = False
stash.Error(f"Error: {e}\nTraceBack={tb}") stash.Error(f"Error: {e}\nTraceBack={tb}")
if doJsonReturn:
sys.stdout.write("{" + f"Exception : '{e}; See log file for TraceBack' " + "}")
stash.Log("\n*********************************\nEXITING ***********************\n*********************************") stash.Log("\n*********************************\nEXITING ***********************\n*********************************")

View File

@@ -2,6 +2,13 @@ name: DupFileManager
description: Manages duplicate files. description: Manages duplicate files.
version: 0.1.6 version: 0.1.6
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:
javascript:
- DupFileManager.js
css:
- DupFileManager.css
- DupFileManager.css.map
- DupFileManager.js.map
settings: settings:
mergeDupFilename: mergeDupFilename:
displayName: Merge Duplicate Tags displayName: Merge Duplicate Tags

View File

@@ -118,7 +118,17 @@ table, th, td {border:1px solid black;}
</style> </style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script> <script>
var superQuery = `{ mutation SceneDestroy($input:SceneDestroyInput!) {sceneDestroy(input: $input)} }` function RunPluginDupFileManager(Mode, ActionID, chkBxRemoveValid, button) {
$.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json", dataType: "text",
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){
console.log(result);
button.style.visibility = 'hidden';
if (!chkBxRemoveValid.checked) alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.")
}});
}
$(document).ready(function(){ $(document).ready(function(){
$("button").click(function(){ $("button").click(function(){
chkBxRemoveValid = document.getElementById("RemoveValidatePrompt"); chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
@@ -128,64 +138,19 @@ $(document).ready(function(){
if (!chkBxDisableDeleteConfirm.checked && !confirm("Are you sure you want to delete this file and remove scene from stash?")) { if (!chkBxDisableDeleteConfirm.checked && !confirm("Are you sure you want to delete this file and remove scene from stash?")) {
return return
} }
const SceneId = this.id;
$.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json", $.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json",
data: JSON.stringify({ data: JSON.stringify({
query: `mutation SceneDestroy($input:SceneDestroyInput!) {sceneDestroy(input: $input)}`, query: `mutation SceneDestroy($input:SceneDestroyInput!) {sceneDestroy(input: $input)}`,
variables: {"input":{"delete_file":true,"id":this.id}}, variables: {"input":{"delete_file":true,"id":SceneId}},
}), success: function(result){ }), success: function(result){
$("#div1").html(result); console.log(result);
if (!chkBxRemoveValid.checked) alert("Delete request received for scene ID# " + SceneId)
}}); }});
this.style.visibility = 'hidden'; this.style.visibility = 'hidden';
if (!chkBxRemoveValid.checked) alert("Sent delete request for scene ID# " + this.id)
}
else if (this.value === "RemoveDupTag")
{
$.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json",
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": {"removeDupTag":this.id, "mode":"remove_a_duplicate_tag"}},
}), success: function(result){
$("#div1").html(result);
}});
this.style.visibility = 'hidden';
if (!chkBxRemoveValid.checked) alert("Sent remove duplicate tag request for scene ID# " + this.id)
}
else if (this.value === "AddExcludeTag")
{
$.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json",
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": {"addExcludeForDelTag":this.id, "mode":"add_an_exclude_tag"}},
}), success: function(result){
$("#div1").html(result);
}});
this.style.visibility = 'hidden';
if (!chkBxRemoveValid.checked) alert("Sent add exclude tag request for scene ID# " + this.id)
}
else if (this.value === "RemoveExcludeTag")
{
$.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json",
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": {"removeExcludeForDelTag":this.id, "mode":"remove_an_exclude_tag"}},
}), success: function(result){
$("#div1").html(result);
}});
this.style.visibility = 'hidden';
if (!chkBxRemoveValid.checked) alert("Sent remove exclude tag request for scene ID# " + this.id)
}
else if (this.value === "mergeTags")
{
$.ajax({method: "POST", url: "http://127.0.0.1:9999/graphql", contentType: "application/json",
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": {"mergeScenes":this.id, "mode":"merge_tags"}},
}), success: function(result){
$("#div1").html(result);
}});
this.style.visibility = 'hidden';
if (!chkBxRemoveValid.checked) alert("Sent merge scene request for scenes " + this.id)
} }
else
RunPluginDupFileManager(this.value, this.id, chkBxRemoveValid, this)
}); });
}); });
</script> </script>

View File

@@ -627,7 +627,7 @@ class StashPluginHelper(StashInterface):
def removeTag(self, scene, tagName): # scene can be scene ID or scene metadata def removeTag(self, scene, tagName): # scene can be scene ID or scene metadata
scene_details = scene scene_details = scene
if 'id' not in scene: if isinstance(scene, int) or 'id' not in scene:
scene_details = self.find_scene(scene) scene_details = self.find_scene(scene)
tagIds = [] tagIds = []
doesHaveTagName = False doesHaveTagName = False
@@ -649,7 +649,7 @@ class StashPluginHelper(StashInterface):
if errMsg != None: if errMsg != None:
self.Warn(errMsg) self.Warn(errMsg)
scene_details = scene scene_details = scene
if 'id' not in scene: if isinstance(scene, int) or 'id' not in scene:
scene_details = self.find_scene(scene) scene_details = self.find_scene(scene)
tagIds = [self.createTagId(tagName, tagName_descp=tagName_descp, ignoreAutoTag=ignoreAutoTag)] tagIds = [self.createTagId(tagName, tagName_descp=tagName_descp, ignoreAutoTag=ignoreAutoTag)]
for tag in scene_details['tags']: for tag in scene_details['tags']:
@@ -884,7 +884,8 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags) self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags)
self.mergeItems('performers', 'performer_ids', []) self.mergeItems('performers', 'performer_ids', [])
self.mergeItems('galleries', 'gallery_ids', []) self.mergeItems('galleries', 'gallery_ids', [])
self.mergeItems('movies', 'movies', []) # ToDo: Need to find out why the following line no longer works in new Stash version
# self.mergeItems('movies', 'movies', [])
self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL) self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL)
self.mergeItem('studio', 'studio_id', 'id') self.mergeItem('studio', 'studio_id', 'id')
self.mergeItem('title') self.mergeItem('title')

View File

@@ -627,7 +627,7 @@ class StashPluginHelper(StashInterface):
def removeTag(self, scene, tagName): # scene can be scene ID or scene metadata def removeTag(self, scene, tagName): # scene can be scene ID or scene metadata
scene_details = scene scene_details = scene
if 'id' not in scene: if isinstance(scene, int) or 'id' not in scene:
scene_details = self.find_scene(scene) scene_details = self.find_scene(scene)
tagIds = [] tagIds = []
doesHaveTagName = False doesHaveTagName = False
@@ -649,7 +649,7 @@ class StashPluginHelper(StashInterface):
if errMsg != None: if errMsg != None:
self.Warn(errMsg) self.Warn(errMsg)
scene_details = scene scene_details = scene
if 'id' not in scene: if isinstance(scene, int) or 'id' not in scene:
scene_details = self.find_scene(scene) scene_details = self.find_scene(scene)
tagIds = [self.createTagId(tagName, tagName_descp=tagName_descp, ignoreAutoTag=ignoreAutoTag)] tagIds = [self.createTagId(tagName, tagName_descp=tagName_descp, ignoreAutoTag=ignoreAutoTag)]
for tag in scene_details['tags']: for tag in scene_details['tags']:
@@ -884,7 +884,8 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags) self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags)
self.mergeItems('performers', 'performer_ids', []) self.mergeItems('performers', 'performer_ids', [])
self.mergeItems('galleries', 'gallery_ids', []) self.mergeItems('galleries', 'gallery_ids', [])
self.mergeItems('movies', 'movies', []) # ToDo: Need to find out why the following line no longer works in new Stash version
# self.mergeItems('movies', 'movies', [])
self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL) self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL)
self.mergeItem('studio', 'studio_id', 'id') self.mergeItem('studio', 'studio_id', 'id')
self.mergeItem('title') self.mergeItem('title')

View File

@@ -4,7 +4,7 @@
# Note: To call this script outside of Stash, pass argument --url and the Stash URL. # Note: To call this script outside of Stash, pass argument --url and the Stash URL.
# Example: python filemonitor.py --url http://localhost:9999 # Example: python filemonitor.py --url http://localhost:9999
import ModulesValidate import ModulesValidate
ModulesValidate.modulesInstalled(["watchdog", "schedule", "requests"]) ModulesValidate.modulesInstalled(["stashapp-tools", "watchdog", "schedule", "requests"])
from StashPluginHelper import StashPluginHelper from StashPluginHelper import StashPluginHelper
import os, sys, time, pathlib, argparse, platform, traceback, logging import os, sys, time, pathlib, argparse, platform, traceback, logging
from StashPluginHelper import taskQueue from StashPluginHelper import taskQueue

View File

@@ -627,7 +627,7 @@ class StashPluginHelper(StashInterface):
def removeTag(self, scene, tagName): # scene can be scene ID or scene metadata def removeTag(self, scene, tagName): # scene can be scene ID or scene metadata
scene_details = scene scene_details = scene
if 'id' not in scene: if isinstance(scene, int) or 'id' not in scene:
scene_details = self.find_scene(scene) scene_details = self.find_scene(scene)
tagIds = [] tagIds = []
doesHaveTagName = False doesHaveTagName = False
@@ -649,7 +649,7 @@ class StashPluginHelper(StashInterface):
if errMsg != None: if errMsg != None:
self.Warn(errMsg) self.Warn(errMsg)
scene_details = scene scene_details = scene
if 'id' not in scene: if isinstance(scene, int) or 'id' not in scene:
scene_details = self.find_scene(scene) scene_details = self.find_scene(scene)
tagIds = [self.createTagId(tagName, tagName_descp=tagName_descp, ignoreAutoTag=ignoreAutoTag)] tagIds = [self.createTagId(tagName, tagName_descp=tagName_descp, ignoreAutoTag=ignoreAutoTag)]
for tag in scene_details['tags']: for tag in scene_details['tags']:
@@ -884,7 +884,8 @@ class mergeMetadata: # A class to merge scene metadata from source scene to dest
self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags) self.mergeItems('tags', 'tag_ids', [], excludeName=self.excludeMergeTags)
self.mergeItems('performers', 'performer_ids', []) self.mergeItems('performers', 'performer_ids', [])
self.mergeItems('galleries', 'gallery_ids', []) self.mergeItems('galleries', 'gallery_ids', [])
self.mergeItems('movies', 'movies', []) # ToDo: Need to find out why the following line no longer works in new Stash version
# self.mergeItems('movies', 'movies', [])
self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL) self.mergeItems('urls', listToAdd=self.destData['urls'], NotStartWith=self.stash.STASH_URL)
self.mergeItem('studio', 'studio_id', 'id') self.mergeItem('studio', 'studio_id', 'id')
self.mergeItem('title') self.mergeItem('title')

View File

@@ -3,7 +3,7 @@
# Get the latest developers version from following link: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile # Get the latest developers version from following link: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/RenameFile
# Based on source code from https://github.com/Serechops/Serechops-Stash/tree/main/plugins/Renamer # Based on source code from https://github.com/Serechops/Serechops-Stash/tree/main/plugins/Renamer
import ModulesValidate import ModulesValidate
ModulesValidate.modulesInstalled(["requests"]) ModulesValidate.modulesInstalled(["stashapp-tools", "requests"])
import os, sys, shutil, json, hashlib, pathlib, logging, time, traceback import os, sys, shutil, json, hashlib, pathlib, logging, time, traceback
from pathlib import Path from pathlib import Path
import stashapi.log as log # Importing stashapi.log as log for critical events ONLY import stashapi.log as log # Importing stashapi.log as log for critical events ONLY