1.1.6 beta

### 1.1.6 beta
Note: This is a beta version, because not all of the javascript ajax functions have been tested yet.
- Added the following to [**Advance Duplicate File Menu**]
  - Scene cover preview image option
  - Webp preview video option
- Fix json string return for all calls made from javascript.
- Added DupFileManagerPyVer field to json when called from javascript.
- When deleting scene using Report, replaced completion prompt with scene background set to gray.
- In Report, when rename occurs, the scene gets renamed inline, without having to reload report page.
- Added GetRunPluginOperationJson to DupFileManager_report.js which allows result to safely be converted to json. If fails, it gracefully returns null.
This commit is contained in:
David Maisonave
2025-01-14 14:07:26 -05:00
parent 5ad7dbcd19
commit e768959934
7 changed files with 164 additions and 77 deletions

View File

@@ -69,6 +69,14 @@ stash = StashPluginHelper(
)
stash.convertToAscii = True
dry_run = stash.Setting("zzdryRun")
DupFileManagerPyVer = "1.1"
taskName_deleteScene = "deleteScene"
taskName_copyScene = "copyScene"
taskName_moveScene = "moveScene"
taskName_mergeScene = "mergeScene"
taskName_addExcludeTag = "addExcludeTag"
taskName_clearFlag = "clearFlag"
advanceMenuOptions = [ "applyCombo", "applyComboPinklist", "applyComboGraylist", "applyComboBlacklist", "pathToDelete", "pathToDeleteBlacklist", "sizeToDeleteLess", "sizeToDeleteGreater", "sizeToDeleteBlacklistLess", "sizeToDeleteBlacklistGreater", "durationToDeleteLess", "durationToDeleteGreater", "durationToDeleteBlacklistLess", "durationToDeleteBlacklistGreater",
"commonResToDeleteLess", "commonResToDeleteEq", "commonResToDeleteGreater", "commonResToDeleteBlacklistLess", "commonResToDeleteBlacklistEq", "commonResToDeleteBlacklistGreater", "resolutionToDeleteLess", "resolutionToDeleteEq", "resolutionToDeleteGreater",
@@ -78,8 +86,10 @@ advanceMenuOptions = [ "applyCombo", "applyComboPinklist", "applyComboGraylist"
doJsonReturnModeTypes = ["tag_duplicates_task", "removeDupTag", "addExcludeTag", "removeExcludeTag", "mergeTags", "getLocalDupReportPath",
"createDuplicateReportWithoutTagging", "deleteLocalDupReportHtmlFiles", "clear_duplicate_tags_task",
"deleteAllDupFileManagerTags", "deleteBlackListTaggedDuplicatesTask", "deleteTaggedDuplicatesLwrResOrLwrDuration",
"deleteBlackListTaggedDuplicatesLwrResOrLwrDuration", "create_duplicate_report_task", "copyScene"]
"deleteBlackListTaggedDuplicatesLwrResOrLwrDuration", "create_duplicate_report_task", "copyScene", "renameFile", "deleteScene",
"removeScene", "flagScene", "flagScene", "moveScene"]
javascriptModeTypes = ["getReport", "getAdvanceMenu"]
startsWithCommands = [taskName_deleteScene, taskName_copyScene, taskName_moveScene, taskName_mergeScene, taskName_addExcludeTag, taskName_clearFlag]
javascriptModeTypes += advanceMenuOptions
javascriptModeTypes += doJsonReturnModeTypes
doJsonReturn = False
@@ -89,6 +99,9 @@ def isReportOrAdvMenu():
return True
if stash.PLUGIN_TASK_NAME.endswith("Flag"):
return True
for startsWithCommand in startsWithCommands:
if stash.PLUGIN_TASK_NAME.startswith(startsWithCommand):
return True
return False
if isReportOrAdvMenu():
@@ -160,6 +173,8 @@ reportHeader = f"{DupFileManagerFolder}{os.sep}DupFileManager_rep
excludeFromReportIfSignificantTimeDiff = False
htmlReportPaginate = stash.Setting('htmlReportPaginate')
htmlIncludeCoverImage = stash.Setting('htmlIncludeCoverImage')
htmlIncludeWebpPreview = stash.Setting('htmlIncludeWebpPreview')
htmlIncludeImagePreview = stash.Setting('htmlIncludeImagePreview')
htmlIncludeVideoPreview = stash.Setting('htmlIncludeVideoPreview')
htmlHighlightTimeDiff = stash.Setting('htmlHighlightTimeDiff')
@@ -220,6 +235,15 @@ if (stash.PLUGIN_TASK_NAME == "tag_duplicates_task" or stash.PLUGIN_TASK_NAME ==
htmlIncludeVideoPreview = True
else:
htmlIncludeVideoPreview = False
if len(targets) > 18:
if targets[17] == "true":
htmlIncludeWebpPreview = True
else:
htmlIncludeWebpPreview = False
if targets[18] == "true":
htmlIncludeCoverImage = True
else:
htmlIncludeCoverImage = False
logTraceForAdvanceMenuOpt = True
if htmlIncludeImagePreview and (htmlImagePreviewSize == htmlImagePreviewPopupSize):
@@ -242,7 +266,7 @@ if significantTimeDiff < .25:
significantTimeDiff = float(0.25)
if logTraceForAdvanceMenuOpt:
stash.Trace(f"PLUGIN_TASK_NAME={stash.PLUGIN_TASK_NAME}; matchDupDistance={matchDupDistance}; matchPhaseDistanceText={matchPhaseDistanceText}; matchPhaseDistance={matchPhaseDistance}; significantTimeDiff={significantTimeDiff}; htmlReportPaginate={htmlReportPaginate}; htmlIncludeImagePreview={htmlIncludeImagePreview}; htmlIncludeVideoPreview={htmlIncludeVideoPreview}; maxDupToProcess={maxDupToProcess}; htmlHighlightTimeDiff={htmlHighlightTimeDiff}; htmlImagePreviewSize={htmlImagePreviewSize}; htmlImagePreviewPopupSize={htmlImagePreviewPopupSize}; htmlImagePreviewPopupEnable={htmlImagePreviewPopupEnable}; htmlPreviewOrStream={htmlPreviewOrStream};")
stash.Trace(f"PLUGIN_TASK_NAME={stash.PLUGIN_TASK_NAME}; matchDupDistance={matchDupDistance}; matchPhaseDistanceText={matchPhaseDistanceText}; matchPhaseDistance={matchPhaseDistance}; significantTimeDiff={significantTimeDiff}; htmlReportPaginate={htmlReportPaginate}; htmlIncludeCoverImage={htmlIncludeCoverImage}; htmlIncludeImagePreview={htmlIncludeImagePreview}; htmlIncludeVideoPreview={htmlIncludeVideoPreview}; maxDupToProcess={maxDupToProcess}; htmlHighlightTimeDiff={htmlHighlightTimeDiff}; htmlImagePreviewSize={htmlImagePreviewSize}; htmlImagePreviewPopupSize={htmlImagePreviewPopupSize}; htmlImagePreviewPopupEnable={htmlImagePreviewPopupEnable}; htmlPreviewOrStream={htmlPreviewOrStream};")
duplicateMarkForDeletion = stash.Setting('DupFileTag')
if duplicateMarkForDeletion == "":
@@ -576,7 +600,7 @@ def getPath(Scene, getParent = False):
path = path.replace("'", "")
path = path.replace("\\\\", "\\")
if getParent:
return pathlib.Path(path).resolve().parent
return pathlib.Path(path).parent
return path
htmlReportPrefix = None
@@ -678,6 +702,26 @@ itemIndexSrchStr = "::itemIndex="
ToDeleteSceneIDSrchStr = "<!-- ::DuplicateToDelete_SceneID="
ToKeepSceneIDSrchStr = "::DuplicateToKeep_SceneID="
def writePreview(fileHtmlReport, scene):
videoPreview = ""
imagePreview = ""
SceneCoverImg = ""
webpPreview = ""
if htmlIncludeVideoPreview:
videoPreview = f"<td><video class=\"ID_{scene['id']}_preview\" {htmlReportVideoPreview} poster=\"{scene['paths']['screenshot']}\"><source src=\"{scene['paths'][htmlPreviewOrStream]}\" type=\"video/mp4\"></video></td>"
if htmlIncludeWebpPreview:
webpPreview = f"<td><img class=\"ID_{scene['id']}_preview\" {htmlReportVideoPreview} src=\"{scene['paths']['webp']}\" alt=\"\"></td>"
if htmlIncludeCoverImage:
SceneCoverImg = f"<td><img class=\"ID_{scene['id']}_preview\" width=\"{htmlImagePreviewSize}\" src=\"{scene['paths']['screenshot']}\" alt=\"\"></td>"
if htmlIncludeImagePreview:
spanPreviewImage = ""
if htmlImagePreviewPopupEnable:
spanPreviewImage = f"<span class=\"large\"><img class=\"ID_{scene['id']}_preview\" src=\"{scene['paths']['sprite']}\" class=\"large-image\" alt=\"\" width=\"{htmlImagePreviewPopupSize}\"></span>"
imagePreview = f"<td><ul><li><img class=\"ID_{scene['id']}_preview\" src=\"{scene['paths']['sprite']}\" alt=\"\" width=\"{htmlImagePreviewSize}\">{spanPreviewImage}</li></ul></td>"
if htmlIncludeVideoPreview or htmlIncludeCoverImage or htmlIncludeImagePreview or htmlIncludeWebpPreview:
fileHtmlReport.write(f"{getSceneID(scene)}<table><tr>{videoPreview}{webpPreview}{imagePreview}{SceneCoverImg}</tr></table></td>")
# //////////////////////////////////////////////////////////////////////////////
# //////////////////////////////////////////////////////////////////////////////
def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, itemIndex, tagDuplicates = False, doTraceDetails = False):
@@ -693,18 +737,9 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, itemIndex, tagD
dupFileExist = True if os.path.isfile(DupFile['files'][0]['path']) else False
toKeepFileExist = True if os.path.isfile(DupFileToKeep['files'][0]['path']) else False
fileHtmlReport.write(f"{htmlReportTableRow}")
videoPreview = f"<video class=\"ID_{DupFile['id']}_preview\" {htmlReportVideoPreview} poster=\"{DupFile['paths']['screenshot']}\"><source src=\"{DupFile['paths'][htmlPreviewOrStream]}\" type=\"video/mp4\"></video>"
if htmlIncludeImagePreview:
spanPreviewImage = ""
if htmlImagePreviewPopupEnable:
spanPreviewImage = f"<span class=\"large\"><img class=\"ID_{DupFile['id']}_preview\" src=\"{DupFile['paths']['sprite']}\" class=\"large-image\" alt=\"\" width=\"{htmlImagePreviewPopupSize}\"></span>"
imagePreview = f"<ul><li><img class=\"ID_{DupFile['id']}_preview\" src=\"{DupFile['paths']['sprite']}\" alt=\"\" width=\"{htmlImagePreviewSize}\">{spanPreviewImage}</li></ul>"
if htmlIncludeVideoPreview:
fileHtmlReport.write(f"{getSceneID(DupFile)}<table><tr><td>{videoPreview}</td><td>{imagePreview}</td></tr></table></td>")
else:
fileHtmlReport.write(f"{getSceneID(DupFile)}{imagePreview}</td>")
elif htmlIncludeVideoPreview:
fileHtmlReport.write(f"{getSceneID(DupFile)}{videoPreview}</td>")
writePreview(fileHtmlReport, DupFile)
fileHtmlReport.write(f"{getSceneID(DupFile)}<a href=\"{stash.STASH_URL}/scenes/{DupFile['id']}\" target=\"_blank\" rel=\"noopener noreferrer\" {fileNameClassID(DupFile)}>{getPath(DupFile)}</a>")
fileHtmlReport.write(f"<p><table><tr class=\"scene-details\"><th>{getMarker(getRes(DupFile), getRes(DupFileToKeep))}Res</th><th>{getMarker(DupFile['files'][0]['duration'], DupFileToKeep['files'][0]['duration'], True)}Durration</th><th>BitRate</th><th>Codec</th><th>FrameRate</th><th>size</th><th>ID</th><th>index</th></tr>")
fileHtmlReport.write(f"<tr class=\"scene-details\"><td {getColor(getRes(DupFile), getRes(DupFileToKeep), True)}>{DupFile['files'][0]['width']}x{DupFile['files'][0]['height']}</td><td {getColor(DupFile['files'][0]['duration'], DupFileToKeep['files'][0]['duration'], True, True, htmlHighlightTimeDiff)}>{DupFile['files'][0]['duration']}</td><td {getColor(DupFile['files'][0]['bit_rate'], DupFileToKeep['files'][0]['bit_rate'])}>{DupFile['files'][0]['bit_rate']}</td><td {getColor(DupFile['files'][0]['video_codec'], DupFileToKeep['files'][0]['video_codec'])}>{DupFile['files'][0]['video_codec']}</td><td {getColor(DupFile['files'][0]['frame_rate'], DupFileToKeep['files'][0]['frame_rate'])}>{DupFile['files'][0]['frame_rate']}</td><td {getColor(DupFile['files'][0]['size'], DupFileToKeep['files'][0]['size'])}>{DupFile['files'][0]['size']}</td><td>{DupFile['id']}</td><td>{itemIndex}</td></tr>")
@@ -846,18 +881,8 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, itemIndex, tagD
fileHtmlReport.write("</p></td>")
# ///////////////////////////////
videoPreview = f"<video class=\"ID_{DupFileToKeep['id']}_preview\" {htmlReportVideoPreview} poster=\"{DupFileToKeep['paths']['screenshot']}\"><source src=\"{DupFileToKeep['paths'][htmlPreviewOrStream]}\" type=\"video/mp4\"></video>"
if htmlIncludeImagePreview:
spanPreviewImage = ""
if htmlImagePreviewPopupEnable:
spanPreviewImage = f"<span class=\"large\"><img class=\"ID_{DupFileToKeep['id']}_preview\" src=\"{DupFileToKeep['paths']['sprite']}\" class=\"large-image\" alt=\"\" width=\"{htmlImagePreviewPopupSize}\"></span>"
imagePreview = f"<ul><li><img class=\"ID_{DupFileToKeep['id']}_preview\" src=\"{DupFileToKeep['paths']['sprite']}\" alt=\"\" width=\"{htmlImagePreviewSize}\">{spanPreviewImage}</li></ul>"
if htmlIncludeVideoPreview:
fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}<table><tr><td>{videoPreview}</td><td>{imagePreview}</td></tr></table></td>")
else:
fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}{imagePreview}</td>")
elif htmlIncludeVideoPreview:
fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}{videoPreview}</td>")
writePreview(fileHtmlReport, DupFileToKeep)
fileHtmlReport.write(f"{getSceneID(DupFileToKeep)}<a href=\"{stash.STASH_URL}/scenes/{DupFileToKeep['id']}\" target=\"_blank\" rel=\"noopener noreferrer\" {fileNameClassID(DupFileToKeep)}>{getPath(DupFileToKeep)}</a>")
fileHtmlReport.write(f"<p><table><tr class=\"scene-details\"><th>Res</th><th>Durration</th><th>BitRate</th><th>Codec</th><th>FrameRate</th><th>size</th><th>ID</th></tr>")
fileHtmlReport.write(f"<tr class=\"scene-details\"><td>{DupFileToKeep['files'][0]['width']}x{DupFileToKeep['files'][0]['height']}</td><td>{DupFileToKeep['files'][0]['duration']}</td><td>{DupFileToKeep['files'][0]['bit_rate']}</td><td>{DupFileToKeep['files'][0]['video_codec']}</td><td>{DupFileToKeep['files'][0]['frame_rate']}</td><td>{DupFileToKeep['files'][0]['size']}</td><td>{DupFileToKeep['id']}</td></tr></table>")
@@ -949,7 +974,7 @@ def writeRowToHtmlReport(fileHtmlReport, DupFile, DupFileToKeep, itemIndex, tagD
fileHtmlReport.write(f"</tr>{ToDeleteSceneIDSrchStr}{DupFile['id']}{ToKeepSceneIDSrchStr}{DupFileToKeep['id']}{itemIndexSrchStr}{itemIndex}:: -->\n")
fragmentForSceneDetails = 'id tags {id name ignore_auto_tag} groups {group {name} } performers {name} galleries {id} files {path width height duration size video_codec bit_rate frame_rate} details '
htmlFileData = " paths {screenshot sprite " + htmlPreviewOrStream + "} "
htmlFileData = " paths {screenshot sprite webp " + htmlPreviewOrStream + "} "
mergeFieldData = " code director title rating100 date studio {id name} urls "
fragmentForSceneDetails += mergeFieldData + htmlFileData
DuplicateCandidateForDeletionList = f"{htmlReportNameFolder}{os.sep}DuplicateCandidateForDeletionList.txt"
@@ -1266,7 +1291,7 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
stash.optimise_database()
if doGeneratePhash:
stash.metadata_generate({"phashes": True})
sys.stdout.write("Report complete")
sys.stdout.write(f"{{'Report-Status' : 'Report complete', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def findCurrentTagId(tagNames):
# tagNames = [i for n, i in enumerate(tagNames) if i not in tagNames[:n]]
@@ -1681,7 +1706,7 @@ def removeDupTag():
stash.Log(f"Processing scene ID# {scene}")
stash.removeTag(scene, duplicateMarkForDeletion)
stash.Log(f"Done removing tag from scene {scene}.")
jsonReturn = "{'removeDupTag' : 'complete', 'id': '" + f"{scene}" + "'}"
jsonReturn = f"{{'removeDupTag' : 'complete', 'id': '{scene}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
@@ -1693,7 +1718,7 @@ def addExcludeTag():
stash.Log(f"Processing scene ID# {scene}")
stash.addTag(scene, excludeDupFileDeleteTag)
stash.Log(f"Done adding exclude tag to scene {scene}.")
sys.stdout.write("{" + f"addExcludeTag : 'complete', id: '{scene}'" + "}")
sys.stdout.write(f"{{'addExcludeTag' : 'complete', 'id': '{scene}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def removeExcludeTag():
if 'Target' not in stash.JSON_INPUT['args']:
@@ -1703,7 +1728,7 @@ def removeExcludeTag():
stash.Log(f"Processing scene ID# {scene}")
stash.removeTag(scene, excludeDupFileDeleteTag)
stash.Log(f"Done removing exclude tag from scene {scene}.")
sys.stdout.write("{" + f"removeExcludeTag : 'complete', id: '{scene}'" + "}")
sys.stdout.write(f"{{'removeExcludeTag' : 'complete', 'id': '{scene}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def getParseData(getSceneDetails1=True, getSceneDetails2=True, checkIfNotSplitValue=False):
if 'Target' not in stash.JSON_INPUT['args']:
@@ -1754,7 +1779,7 @@ def mergeMetadataForAll(ReportName = htmlReportName):
break
mergeMetadataInThisFile(fileName)
stash.Log(f"Done merging metadata for all scenes")
sys.stdout.write("{mergeTags : 'complete'}")
sys.stdout.write(f"{{'mergeTags' : 'complete', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
# ToDo: Rename mergeTags to mergeMetadata
def mergeTags(inputScene1=None):
@@ -1764,7 +1789,7 @@ def mergeTags(inputScene1=None):
if scene1 == "mergeMetadataForAll":
mergeMetadataForAll()
else:
sys.stdout.write("{" + f"mergeTags : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
sys.stdout.write(f"{{'mergeTags' : 'failed', 'id1': '{scene1}', 'id2': '{scene2}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
return
else:
scene1 = inputScene1
@@ -1773,7 +1798,7 @@ def mergeTags(inputScene1=None):
updateScenesInReports(scene2['id'])
stash.Log(f"Done merging scenes for scene {scene1['id']} and scene {scene2['id']}")
if inputScene1 == None:
sys.stdout.write("{" + f"mergeTags : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}'" + "}")
sys.stdout.write(f"{{'mergeTags' : 'complete', 'id1': '{scene1['id']}', 'id2': '{scene2['id']}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def getLocalDupReportPath():
htmlReportExist = "true" if os.path.isfile(htmlReportName) else "false"
@@ -1785,7 +1810,7 @@ def getLocalDupReportPath():
apikey_json = ", 'apiKey':''"
if 'apiKey' in stash.STASH_CONFIGURATION:
apikey_json = f", 'apiKey':'{stash.STASH_CONFIGURATION['apiKey']}'"
jsonReturn = "{" + f"'LocalDupReportExist' : {htmlReportExist}, 'Path': '{localPath}', 'LocalDir': '{LocalDir}', 'ReportUrlDir': '{ReportUrlDir}', 'ReportUrl': '{ReportUrl}', 'AdvMenuUrl': '{AdvMenuUrl}', 'IS_DOCKER': '{stash.IS_DOCKER}', 'remoteReportDirURL': '{stash.Setting('remoteReportDirURL')}' {apikey_json}" + "}"
jsonReturn = f"{{'LocalDupReportExist' : {htmlReportExist}, 'Path': '{localPath}', 'LocalDir': '{LocalDir}', 'ReportUrlDir': '{ReportUrlDir}', 'ReportUrl': '{ReportUrl}', 'AdvMenuUrl': '{AdvMenuUrl}', 'IS_DOCKER': '{stash.IS_DOCKER}', 'remoteReportDirURL': '{stash.Setting('remoteReportDirURL')}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}' {apikey_json}}}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
@@ -1804,7 +1829,7 @@ def deleteLocalDupReportHtmlFiles(doJsonOutput = True):
else:
stash.Log(f"Report file does not exist: {htmlReportName}")
if doJsonOutput:
jsonReturn = "{'LocalDupReportExist' : " + f"{htmlReportExist}" + ", 'Path': '" + f"{htmlReportName}" + "', 'qty': '" + f"{x}" + "'}"
jsonReturn = f"{{'LocalDupReportExist' : {htmlReportExist}, 'Path': '{htmlReportName}', 'qty': '{x}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
@@ -1836,7 +1861,7 @@ def removeAllDupTagsFromAllScenes(deleteTags=False):
if removeTagFromAllScenes(tagToClear, deleteTags):
validTags +=[tagToClear]
if doJsonReturn:
jsonReturn = "{'removeAllDupTagFromAllScenes' : " + f"{duplicateMarkForDeletion}" + ", 'OtherTags': '" + f"{validTags}" + "'}"
jsonReturn = f"{{'removeAllDupTagFromAllScenes' : '{duplicateMarkForDeletion}', 'OtherTags': '{validTags}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
else:
@@ -2024,19 +2049,19 @@ def deleteScene(disableInReport=True, deleteFile=True, scene=None, writeToStdOut
modifyPropertyToSceneClassToAllFiles(f"{scene}_preview", "{display:none;}")
if writeToStdOut:
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene} with results = {result}")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', id: '{scene}', result: '{result}'" + "}")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'complete', 'id': '{scene}', 'result': '{result}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
return result
def clearAllSceneFlags(flagColor=None):
modifyPropertyToSceneClassToAllFiles(None, flagColor)
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for all scenes")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete'" + "}")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'complete', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def copyScene(moveScene=False, inputScene1=None):
if inputScene1 == None:
scene1, scene2 = getParseData()
if scene1 == None or scene2 == None:
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'failed', id1: '{scene1}', id2: '{scene2}'" + "}")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'failed', 'id1': '{scene1}', 'id2': '{scene2}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
return
else:
scene1 = inputScene1
@@ -2061,16 +2086,16 @@ def copyScene(moveScene=False, inputScene1=None):
updateScenesInReports(scene2['id'])
stash.Log(f"{actionStr} complete for scene {scene1['id']} and {scene2['id']}")
if inputScene1 == None:
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', id1: '{scene1['id']}', id2: '{scene2['id']}', result: '{result}'" + "}")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'complete', 'id1': '{scene1['id']}', 'id2': '{scene2['id']}', 'result': '{result}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def renameFile():
scene, newName = getParseData(getSceneDetails2=False)
if scene == None or newName == None:
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'failed', scene: '{scene}', newName: '{newName}'" + "}")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'failed', 'scene': '{scene}', 'newName': '{newName}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
return
newName = newName.strip("'")
ext = pathlib.Path(scene['files'][0]['path']).suffix
newNameFull = f"{pathlib.Path(scene['files'][0]['path']).resolve().parent}{os.sep}{newName}{ext}"
newNameFull = f"{pathlib.Path(scene['files'][0]['path']).parent}{os.sep}{newName}{ext}"
newNameFull = newNameFull.strip("'")
newNameFull = newNameFull.replace("\\\\", "\\")
oldNameFull = scene['files'][0]['path']
@@ -2080,13 +2105,14 @@ def renameFile():
result = os.rename(oldNameFull, newNameFull)
stash.renameFileNameInDB(scene['files'][0]['id'], pathlib.Path(oldNameFull).stem, f"{newName}{ext}", UpdateUsingIdOnly = True)
updateScenesInReports(scene['id'])
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene['id']} ;renamed to {newName}; result={result}")
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', scene: '{scene['id']}', newName: '{newName}', result: '{result}'" + "}")
stash.Log(f"{stash.PLUGIN_TASK_NAME} complete for scene {scene['id']} ;renamed to {newNameFull}; result={result}")
# newNameFull = newNameFull.replace("\\", "\\\\")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'complete', 'scene': '{scene['id']}', 'newName': '{newNameFull}', 'result': '{result}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def flagScene():
scene, flagType = getParseData(False, False)
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}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
return
if " highlight" in flagType:
@@ -2112,9 +2138,9 @@ def flagScene():
modifyPropertyToSceneClassToAllFiles(scene, "")
else:
stash.Log(f"Invalid flagType ({flagType})")
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}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
return
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', scene: '{scene}', flagType: '{flagType}'" + "}")
sys.stdout.write(f"{{'{stash.PLUGIN_TASK_NAME}' : 'complete', 'scene': '{scene}', 'flagType': '{flagType}', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
def getReport():
if 'Target' not in stash.JSON_INPUT['args']:
@@ -2157,12 +2183,6 @@ def getAdvanceMenu():
# Remove only graylist dup
# Exclude graylist from delete
# Include graylist in delete
taskName_deleteScene = "deleteScene"
taskName_copyScene = "copyScene"
taskName_moveScene = "moveScene"
taskName_mergeScene = "mergeScene"
taskName_addExcludeTag = "addExcludeTag"
taskName_clearFlag = "clearFlag"
try:
if stash.PLUGIN_TASK_NAME == "tag_duplicates_task":
@@ -2307,6 +2327,7 @@ except Exception as e:
stash.convertToAscii = False
stash.Error(f"Error: {e}\nTraceBack={tb}")
if doJsonReturn:
sys.stdout.write("{" + f"Exception : '{e}; See log file for TraceBack' " + "}")
errStr = e.replace("'", "`")
sys.stdout.write(f"{{'Exception' : '{errStr}; See log file for TraceBack', 'DupFileManagerPyVer': '{DupFileManagerPyVer}'}}")
stash.Log("\n*********************************\nEXITING ***********************\n*********************************")

View File

@@ -1,6 +1,6 @@
name: DupFileManager
description: Manages duplicate files.
version: 1.1.5
version: 1.1.6 beta
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui:
# css:

View File

@@ -10,6 +10,7 @@ console.log("Cookies = " + document.cookie);
const StrRemoveToKeepConfirm = "RemoveToKeepConfirm=";
const StrRemoveValidatePrompt = "RemoveValidatePrompt=";
const StrDisableReloadPage = "DisableReloadPage=";
var DupFileManagerPyVer = null;
function SetPaginateButtonChange(){
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
@@ -18,9 +19,9 @@ function SetPaginateButtonChange(){
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
DisableReloadPageValue = StrDisableReloadPage + "false";
if (chkBxRemoveValid.checked)
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableDeleteConfirm.checked)
RemoveValidatePromptValue = StrRemoveValidatePrompt + "true";
if (chkBxDisableDeleteConfirm.checked)
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableReloadPage != null && chkBxDisableReloadPage.checked)
DisableReloadPageValue = StrDisableReloadPage + "true";
document.cookie = RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue + "&" + DisableReloadPageValue + "; SameSite=None; Secure";
@@ -32,6 +33,17 @@ function trim(str, ch) {
while(end > start && str[end - 1] === ch) --end;
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
function GetRunPluginOperationJson(result){
try{
jsonResults = JSON.parse(result);
const jsonSubResults = JSON.parse(jsonResults.data.runPluginOperation.replaceAll("'", "\"").replaceAll("\\", "\\\\"));
DupFileManagerPyVer = jsonSubResults.DupFileManagerPyVer
return jsonSubResults;
}catch(error){
console.error(error);
}
return null;
}
function RunPluginOperation(Mode, ActionID, button, asyncAjax){ // Mode=Value and ActionID=id
if (Mode == null || Mode === ""){
console.log("Error: Mode is empty or null; ActionID = " + ActionID);
@@ -49,16 +61,26 @@ function RunPluginOperation(Mode, ActionID, button, asyncAjax){ // Mode=Value an
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);
console.log("For ActionID '" + ActionID + "' result=" + result);
const jsonSubResults = GetRunPluginOperationJson(result);
if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
if (Mode.startsWith("copyScene") || Mode.startsWith("renameFile") || Mode === "clearAllSceneFlags" || Mode.startsWith("clearFlag") || Mode.startsWith("mergeScene") || Mode.startsWith("mergeTags") || (Mode !== "deleteScene" && Mode.startsWith("deleteScene"))){
if (Mode === "deleteScene" || Mode === "removeScene"){
console.log("Delete complete. Setting background color for .ID_" + ActionID);
$('.ID_' + ActionID).css('background-color','gray');
}
else if (Mode === "renameFile" && jsonSubResults != null){
const FnId = ".FN_ID_" + jsonSubResults.scene;
console.log("Changing existing file name ID (" + FnId + ") '" + $(FnId).text() + "' to '" + jsonSubResults.newName + "'");
$(FnId).text(jsonSubResults.newName);
}else if (Mode.startsWith("copyScene") || Mode.startsWith("renameFile") || Mode === "clearAllSceneFlags" || Mode.startsWith("clearFlag") || Mode.startsWith("mergeScene") || Mode.startsWith("mergeTags") || (Mode !== "deleteScene" && Mode.startsWith("deleteScene"))){
const chkBxDisableReloadPage = document.getElementById("DisableReloadPage");
if (chkBxDisableReloadPage == null || !chkBxDisableReloadPage.checked)
window.location.reload();
} else if (!chkBxRemoveValid.checked && Mode !== "flagScene") alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.\\n\\nResults=" + result);
} else if (!chkBxRemoveValid.checked && Mode !== "flagScene")
alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.\\n\\nResults=" + result);
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
if (asyncAjax){
@@ -218,7 +240,7 @@ $(document).ready(function(){
if (!chkBxDisableDeleteConfirm.checked && !confirm(question))
return;
if (Mode === "deleteScene" || Mode === "removeScene"){
$('.ID_' + ActionID).css('background-color','gray');
// $('.ID_' + ActionID).css('background-color','gray');
$('.ID_' + ActionID).css('pointer-events','none');
}
}

View File

@@ -7,6 +7,10 @@
report_config = {
# Paginate HTML report. Maximum number of results to display on one page, before adding (paginating) an additional page.
"htmlReportPaginate" : 100,
# If enabled, report displays the scene cover as a preview image
"htmlIncludeCoverImage" : False,
# If enabled, report displays Webp as a preview image
"htmlIncludeWebpPreview" : False,
# If enabled, report displays an image preview similar to sceneDuplicateChecker
"htmlIncludeImagePreview" : False,
"htmlImagePreviewSize" : 140,

View File

@@ -1,4 +1,4 @@
# DupFileManager: Ver 1.1.5 (By David Maisonave)
# DupFileManager: Ver 1.1.6 beta (By David Maisonave)
DupFileManager is a [Stash](https://github.com/stashapp/stash) plugin which manages duplicate files in the Stash system.
It has both **task** and **tools-UI** components.
@@ -135,7 +135,6 @@ Users can setup a private or alternate remote site by changing variables **remot
- Scheduled Changes
- Remove [Max Dup Process] from the Stash->Plugins GUI. This option already exist in advance menu. Planned for 1.2.0 Version.
- Add chat icon to report which on hover, displays a popup window showing scene details content. Planned for 1.2.0 Version.
- Add image icon to report; on hover show scene cover image. Planned for 1.2.0 Version.
- Add studio icon to report; on hover show studio name. Planned for 1.2.0 Version.
- Add option on report to view rating and change it. Use an icon with a number on it to show rating. Planned for 1.2.0 Version.
- On report make flag toggle logic. In other words, when flag button is selected, and scene is already that color, remove the colored flag. Planned for 1.2.0 Version.

View File

@@ -69,7 +69,7 @@ function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes =
}
console.log("Ajax success with result = " + result);
if (Mode === "tag_duplicates_task" || Mode === "create_duplicate_report_task"){
if (result.indexOf("\"Report complete\"") == -1)
if (result.indexOf("Report complete") == -1)
alert("Stash RunPluginOperation returned unexpected results.\nNot sure if report completed successfully.\n\nResults = " + result);
else{
var Notice = "";
@@ -128,7 +128,7 @@ function GetStashTabUrl(Tab){
function GetReportCreateOptions(Value){
if (Value === "-1")
return "";
const param = Value + ":" + $("#significantTimeDiff").val() + ":" + $("#IncludePreviewImage").prop('checked') + ":" + $("#scenesPerPage").val() + ":" + $("#ImagePreviewSize").val() + ":" + $("#ImagePreviewPopupSize").val() + ":" + $("#TimeDiffHighlight").val() + ":" + $("#maxDupToProcess").val() + ":" + $("#streamOverPreview").prop('checked') + ":" + $("#SupperHighlight").val() + ":" + $("#DetailDiffTextColor").val() + ":" + $("#LowerHighlight").val() + ":" + $("#ReportBackgroundColor").val() + ":" + $("#ReportTextColor").val() + ":" + $("#VideoPreviewWidth").val() + ":" + $("#VideoPreviewHeight").val() + ":" + $("#IncludePreviewVideo").prop('checked');
const param = Value + ":" + $("#significantTimeDiff").val() + ":" + $("#IncludePreviewImage").prop('checked') + ":" + $("#scenesPerPage").val() + ":" + $("#ImagePreviewSize").val() + ":" + $("#ImagePreviewPopupSize").val() + ":" + $("#TimeDiffHighlight").val() + ":" + $("#maxDupToProcess").val() + ":" + $("#streamOverPreview").prop('checked') + ":" + $("#SupperHighlight").val() + ":" + $("#DetailDiffTextColor").val() + ":" + $("#LowerHighlight").val() + ":" + $("#ReportBackgroundColor").val() + ":" + $("#ReportTextColor").val() + ":" + $("#VideoPreviewWidth").val() + ":" + $("#VideoPreviewHeight").val() + ":" + $("#IncludePreviewVideo").prop('checked') + ":" + $("#IncludeWebpPreviewImage").prop('checked') + ":" + $("#IncludeSceneCoverPreviewImage").prop('checked');
console.log("param = " + param);
return param;
}
@@ -294,19 +294,24 @@ $(document).ready(function(){
$("#ImagePreviewPopupSize").prop('disabled', false);
}
else{
$("#ImagePreviewSize").prop('disabled', true);
if ($("#IncludeSceneCoverPreviewImage").prop('checked'))
$("#ImagePreviewSize").prop('disabled', false);
else
$("#ImagePreviewSize").prop('disabled', true);
$("#ImagePreviewPopupSize").prop('disabled', true);
}
if ($("#IncludePreviewVideo").prop('checked')){
$("#streamOverPreview").prop('disabled', false);
if ($("#IncludePreviewVideo").prop('checked') || $("#IncludeWebpPreviewImage").prop('checked')){
$("#VideoPreviewWidth").prop('disabled', false);
$("#VideoPreviewHeight").prop('disabled', false);
}
else{
$("#streamOverPreview").prop('disabled', true);
$("#VideoPreviewWidth").prop('disabled', true);
$("#VideoPreviewHeight").prop('disabled', true);
}
if ($("#IncludePreviewVideo").prop('checked'))
$("#streamOverPreview").prop('disabled', false);
else
$("#streamOverPreview").prop('disabled', true);
$("button").click(function(){
ProcessClick(this);
});
@@ -324,10 +329,19 @@ $(document).ready(function(){
$("#ImagePreviewPopupSize").prop('disabled', false);
}
else{
$("#ImagePreviewSize").prop('disabled', true);
if ($("#IncludeSceneCoverPreviewImage").prop('checked'))
$("#ImagePreviewSize").prop('disabled', false);
else
$("#ImagePreviewSize").prop('disabled', true);
$("#ImagePreviewPopupSize").prop('disabled', true);
}
});
$("#IncludeSceneCoverPreviewImage").change(function() {
if(this.checked || $("#IncludePreviewImage").prop('checked'))
$("#ImagePreviewSize").prop('disabled', false);
else
$("#ImagePreviewSize").prop('disabled', true);
});
$("#IncludePreviewVideo").change(function() {
if(this.checked){
$("#streamOverPreview").prop('disabled', false);
@@ -336,6 +350,22 @@ $(document).ready(function(){
}
else{
$("#streamOverPreview").prop('disabled', true);
if ($("#IncludeWebpPreviewImage").prop('checked')){
$("#VideoPreviewWidth").prop('disabled', false);
$("#VideoPreviewHeight").prop('disabled', false);
}
else {
$("#VideoPreviewWidth").prop('disabled', true);
$("#VideoPreviewHeight").prop('disabled', true);
}
}
});
$("#IncludeWebpPreviewImage").change(function() {
if(this.checked || $("#IncludePreviewVideo").prop('checked')){
$("#VideoPreviewWidth").prop('disabled', false);
$("#VideoPreviewHeight").prop('disabled', false);
}
else{
$("#VideoPreviewWidth").prop('disabled', true);
$("#VideoPreviewHeight").prop('disabled', true);
}
@@ -2023,13 +2053,14 @@ function DeleteDupInPath(){
</tr>
<tr>
<td>
<div class="easyui-tooltip" title="Create report with preview image."><label for="IncludePreviewImage">Include Preview Image:</label><input type="checkbox" id="IncludePreviewImage" name="IncludePreviewImage" value="true"></div>
<div class="easyui-tooltip" title="Create report with a collage preview image."><label for="IncludePreviewImage">Collage preview image:</label><input type="checkbox" id="IncludePreviewImage" name="IncludePreviewImage" value="true"></div>
<div class="easyui-tooltip" title="Create report with scene cover preview image. Requires DupFileManger version 1.1.6 or higher."><label for="IncludeSceneCoverPreviewImage">Scene cover preview image:</label><input type="checkbox" id="IncludeSceneCoverPreviewImage" name="IncludeSceneCoverPreviewImage" value="true"></div>
</td>
<td>
<div class="easyui-tooltip" title="This value is the size of preview image. Default value is 140."><label for="ImagePreviewSize">Preview Image Size:</label><input type="number" min="50" max="600" step="10" id="ImagePreviewSize" name="ImagePreviewSize" value="140"></div>
<div class="easyui-tooltip" title="This value is the width size of the preview image. Default value is 160."><label for="ImagePreviewSize">Preview image width:</label><input type="number" min="50" max="600" step="10" id="ImagePreviewSize" name="ImagePreviewSize" value="160"></div>
</td>
<td>
<div class="easyui-tooltip" title="This value is the size of the popup window when mouse hovers over the image. Default value is 600."><label for="ImagePreviewPopupSize">Image Popup Size:</label><input type="number" min="200" max="3000" step="100" id="ImagePreviewPopupSize" name="ImagePreviewPopupSize" value="600"></div>
<div class="easyui-tooltip" title="This value is the size of the collage popup window when mouse hovers over the image. Default value is 600."><label for="ImagePreviewPopupSize">Collage Image Popup Size:</label><input type="number" min="200" max="3000" step="100" id="ImagePreviewPopupSize" name="ImagePreviewPopupSize" value="600"></div>
</td>
</tr>
<tr><td colspan="3">
@@ -2061,7 +2092,8 @@ function DeleteDupInPath(){
</tr>
<tr>
<td rowspan="2">
<div class="easyui-tooltip" title="Create report with preview video."><label for="IncludePreviewVideo">Include Preview Video:</label><input type="checkbox" id="IncludePreviewVideo" name="IncludePreviewVideo" value="true" checked></div>
<div class="easyui-tooltip" title="Create report with preview video."><label for="IncludePreviewVideo">Preview video:</label><input type="checkbox" id="IncludePreviewVideo" name="IncludePreviewVideo" value="true" checked></div>
<div class="easyui-tooltip" title="Create report with Webp preview video. Requires DupFileManger version 1.1.6 or higher."><label for="IncludeWebpPreviewImage">Webp preview video:</label><input type="checkbox" id="IncludeWebpPreviewImage" name="IncludeWebpPreviewImage" value="true"></div>
</td>
<td colspan="2">
<div class="easyui-tooltip" title="Create report with full video available in the preview section instead of a partial video. This option works in Chrome, but does not work so well in firefox."><label for="streamOverPreview">For preview, display full stream video instead of partial preview video:</label><input type="checkbox" id="streamOverPreview" name="streamOverPreview" value="true"></div>
@@ -2204,4 +2236,3 @@ function DeleteDupInPath(){
<div id="div1"></div>
</body></html>

View File

@@ -108,5 +108,15 @@
### 1.1.5
- After deleting scene from report, disable preview for the deleted scene on the report.
- Add option to report to avoid reloading page after updating report.
### 1.1.6 beta
Note: This is a beta version, because not all of the javascript ajax functions have been tested yet.
- Added the following to [**Advance Duplicate File Menu**]
- Scene cover preview image option
- Webp preview video option
- Fix json string return for all calls made from javascript.
- Added DupFileManagerPyVer field to json when called from javascript.
- When deleting scene using Report, replaced completion prompt with scene background set to gray.
- In Report, when rename occurs, the scene gets renamed inline, without having to reload report page.
- Added GetRunPluginOperationJson to DupFileManager_report.js which allows result to safely be converted to json. If fails, it gracefully returns null.