### 1.1.3
- Added access to report from https://stash.axter.com/1.1/file.html
  - This allows access to report from any browser and access to report from a Docker Stash setup.
  - On Stash installation using passwords or non-standard URL, the file.html link should be accessed from the advance menu or from the Stash->Tools->[DupFileManager Report Menu].
- Added fields remoteReportDirURL and js_DirURL to allow users to setup their own private or alternate remote path for javascript files.
- On Stash installations having password, the Advance Menu can now be accessed from the Stash->Tools->[DupFileManager Report Menu].
This commit is contained in:
David Maisonave
2024-12-26 12:39:20 -05:00
parent 98f4a5291d
commit 8e5e451916
12 changed files with 716 additions and 361 deletions

View File

@@ -6,7 +6,83 @@
const GQL = PluginApi.GQL;
const { Button } = PluginApi.libraries.Bootstrap;
const { Link, NavLink } = PluginApi.libraries.ReactRouterDOM;
class StashPlugin {
#urlParams = new URLSearchParams(window.location.search);
#apiKey = "";
#doApiLog = true;
constructor(PluginID, doApiLog = true, DataType = "json", Async = false) {
this.#doApiLog = doApiLog;
this.PluginID = PluginID;
this.DataType = DataType;
this.Async = Async;
this.#apiKey = this.getParam("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
this.GraphQl_URL = this.getParam("GQL", "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
console.log("GQL = " + this.GraphQl_URL + "; apiKey = " + this.#apiKey + "; urlParams = " + this.#urlParams + "; Cookies = " + document.cookie);
}
getParam(ParamName, DefaultValue = ""){
if (this.#urlParams.get(ParamName) != null && this.#urlParams.get(ParamName) !== "")
return this.#urlParams.get(ParamName);
return DefaultValue;
}
CallBackOnSuccess(result, Args, This){ // Only called on asynchronous calls
console.log("Ajax success.");
}
CallBackOnFail(textStatus, errorThrown, This){
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
alert("Error on StashPlugin Ajax call!!!\nReturn-Status: " + textStatus + "\nThrow-Error: " + errorThrown);
}
RunPluginOperation(Args = {}, OnSuccess = this.CallBackOnSuccess, OnFail = this.CallBackOnFail) {
console.log("PluginID = " + this.PluginID + "; Args = " + Args + "; GQL = " + this.GraphQl_URL + "; DataType = " + this.DataType + "; Async = " + this.Async);
if (this.#apiKey !== ""){
if (this.#doApiLog) console.log("Using apiKey = " + this.#apiKey);
const apiKey = this.#apiKey;
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
}
const AjaxData = $.ajax({method: "POST", url: this.GraphQl_URL, contentType: "application/json", dataType: this.DataType, cache: this.Async, async: this.Async,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": this.PluginID, "args": Args},
}), success: function(result){
if (this.Async == true) OnSuccess(result, Args, this);
}, error: function(jqXHR, textStatus, errorThrown) {
OnFail(textStatus, errorThrown, this);
}
});
if (this.Async == true) // Make sure to use callback functions for asynchronous calls.
return;
if (this.DataType == "text")
return AjaxData.responseText;
return AjaxData.responseJSON.data.runPluginOperation;
}
};
class Plugin_DupFileManager extends StashPlugin{
constructor() {
super("DupFileManager", "json", true);
this.IS_DOCKER = this.getParam("IS_DOCKER") === "True";
this.PageNo = parseInt(this.getParam("PageNo", "0"));
}
MyCallBackOnSuccess(result, Args, This){ // Only called on asynchronous calls
console.log("Ajax success.");
$( "#FileDiv" ).append( result );
}
GetFile(Mode = "getReport") {
this.RunPluginOperation({ "Target" : this.PageNo, "mode":Mode}, this.MyCallBackOnSuccess);
return;
//const strResults = JSON.stringify(results);
//if (strResults.indexOf("INF: 'Error: Nothing to do!!!") > -1){
// console.log("Ajax failed for function " + Mode +" and page " + this.PageNo + " with results = " + JSON.stringify(results));
// return "<p>Failed to get report do to ajax error!!!</p>";
//}
//var rootPath = window.location.href;
//if (rootPath.indexOf("://localhost:") > 0){
// results = results.replaceAll("://127.0.0.1:", "://localhost:");
//} else if (rootPath.indexOf("://127.0.0.1:") > 0){
// results = results.replaceAll("://localhost:", "//127.0.0.1:");
//}
//return results;
}
}
var PluginDupFileManager = new Plugin_DupFileManager();
var rootPath = window.location.href;
var myArray = rootPath.split("/");
rootPath = myArray[0] + "//" + myArray[2]
@@ -41,19 +117,20 @@
var apiKey = "";
var UrlParam = "";
var IS_DOCKER = "";
var remoteReportDirURL = "";
function GetLocalDuplicateReportPath(){
var LocalDuplicateReport = RunPluginDupFileManager("getLocalDupReportPath", "json");
var LocalDuplicateReportPath = "file://" + LocalDuplicateReport.Path;
console.log("LocalDuplicateReport=" + JSON.stringify(LocalDuplicateReport));
remoteReportDirURL = LocalDuplicateReport.remoteReportDirURL;
apiKey = LocalDuplicateReport.apiKey;
IS_DOCKER = LocalDuplicateReport.IS_DOCKER;
UrlParam = "?GQL=" + rootPath + "/graphql&IS_DOCKER=" + IS_DOCKER + "&apiKey=" + apiKey;
console.log("LocalDuplicateReportPath=" + JSON.stringify(LocalDuplicateReportPath) + "; document.cookie=" + document.cookie);
var LocalDuplicateReportPath = remoteReportDirURL + "file.html" + UrlParam; //"file://" + LocalDuplicateReport.Path;
AdvanceMenuOptionUrl = LocalDuplicateReport.AdvMenuUrl + UrlParam;
console.log("AdvanceMenuOptionUrl=" + AdvanceMenuOptionUrl);
LocalDupReportExist = LocalDuplicateReport.LocalDupReportExist;
return LocalDuplicateReportPath;
}
// ToolTip text
const CreateReportButtonToolTip = "Tag duplicate files, and create a new duplicate file report listing all duplicate files and using existing DupFileManager plugin options selected.";
const CreateReportNoTagButtonToolTip = "Create a new duplicate file report listing all duplicate files and using existing DupFileManager plugin options selected. Do NOT tag files.";
@@ -76,7 +153,7 @@
}
function GetAdvanceMenuButton()
{
return React.createElement("a", { href: "https://stash.axter.com/1.1.2/advance_options.html" + UrlParam, title: "Open link to the [Advance Duplicate File Menu].", target:"_blank"}, React.createElement(Button, null, "Show [Advance Duplicate File Menu]"));
return React.createElement("a", { href: remoteReportDirURL + "advance_options.html" + UrlParam, title: "Open link to the [Advance Duplicate File Menu].", target:"_blank"}, React.createElement(Button, null, "Advance Duplicate File Menu"));
// The following does not work with Chrome, or with an apiKey, or with a non-standard Stash URL.
//return React.createElement("a", { href: AdvanceMenuOptionUrl, title: "Open link to the [Advance Duplicate File Menu].", target:"_blank"}, React.createElement(Button, null, "Show [Advance Duplicate File Menu]"));
}
@@ -294,11 +371,39 @@
));
}
return ToolsAndUtilities();
};
const HomePageBeta = () => {
var LocalDuplicateReportPath = GetLocalDuplicateReportPath();
console.log(LocalDupReportExist);
var MyHeader = React.createElement("h1", null, "DupFileManager Report Menu");
if (LocalDupReportExist)
return (React.createElement("div", {id:"FileDiv"}, "FileDiv",
React.createElement("center", null,
MyHeader,
GetShowReportButton(LocalDuplicateReportPath, "Show Duplicate-File Report"),
React.createElement("p", null),
React.createElement(Link, { to: "/DupFileManager_AdvanceMenu" }, React.createElement(Button, null, "Advance Menu")),
React.createElement("p", null),
ToolsMenuOptionButton)
));
return (React.createElement("center", null,
MyHeader,
ToolsMenuOptionButton
));
};
const AdvanceMenu = () => {
PluginDupFileManager.GetFile("getAdvanceMenu");
//const html = PluginDupFileManager.GetFile("getAdvanceMenu");
//console.log("Sending file to FileDiv; html=" + html.substring(0, 50));
//$("body").append( html );
return (React.createElement("div", {id:"FileDiv"}));
};
PluginApi.register.route("/DupFileManager", HomePage);
PluginApi.register.route("/DupFileManager_CreateReport", CreateReport);
PluginApi.register.route("/DupFileManager_CreateReportWithNoTagging", CreateReportWithNoTagging);
PluginApi.register.route("/DupFileManager_ToolsAndUtilities", ToolsAndUtilities);
// PluginApi.register.route("/DupFileManager_HomePageBeta", HomePageBeta);
PluginApi.register.route("/DupFileManager_AdvanceMenu", AdvanceMenu);
PluginApi.register.route("/DupFileManager_ClearAllDuplicateTags", ClearAllDuplicateTags);
PluginApi.register.route("/DupFileManager_deleteLocalDupReportHtmlFiles", deleteLocalDupReportHtmlFiles);
PluginApi.register.route("/DupFileManager_deleteAllDupFileManagerTags", deleteAllDupFileManagerTags);
@@ -316,6 +421,7 @@
props.children,
React.createElement(Setting, { heading: React.createElement(Link, { to: "/DupFileManager", title: ReportMenuButtonToolTip }, React.createElement(Button, null, "Duplicate File Report (DupFileManager)"))}),
React.createElement(Setting, { heading: React.createElement(Link, { to: "/DupFileManager_ToolsAndUtilities", title: ToolsMenuToolTip }, React.createElement(Button, null, "DupFileManager Tools and Utilities"))}),
// React.createElement(Setting, { heading: React.createElement(Link, { to: "/DupFileManager_HomePageBeta", title: ReportMenuButtonToolTip }, React.createElement(Button, null, "Duplicate File Report [Beta]"))}),
)),
},
];

View File

@@ -16,8 +16,6 @@ from StashPluginHelper import StashPluginHelper
from stashapi.stash_types import PhashDistance
from DupFileManager_config import config # Import config from DupFileManager_config.py
from DupFileManager_report_config import report_config
# ToDo: make sure the following line of code works
config |= report_config
parser = argparse.ArgumentParser()
@@ -81,11 +79,13 @@ doJsonReturnModeTypes = ["tag_duplicates_task", "removeDupTag", "addExcludeTag",
"createDuplicateReportWithoutTagging", "deleteLocalDupReportHtmlFiles", "clear_duplicate_tags_task",
"deleteAllDupFileManagerTags", "deleteBlackListTaggedDuplicatesTask", "deleteTaggedDuplicatesLwrResOrLwrDuration",
"deleteBlackListTaggedDuplicatesLwrResOrLwrDuration", "create_duplicate_report_task", "copyScene"]
doJsonReturnModeTypes += [advanceMenuOptions]
javascriptModeTypes = ["getReport", "getAdvanceMenu"]
javascriptModeTypes += advanceMenuOptions
javascriptModeTypes += doJsonReturnModeTypes
doJsonReturn = False
def isReportOrAdvMenu():
if len(sys.argv) < 2:
if stash.PLUGIN_TASK_NAME in doJsonReturnModeTypes:
if stash.PLUGIN_TASK_NAME in javascriptModeTypes:
return True
if stash.PLUGIN_TASK_NAME.endswith("Flag"):
return True
@@ -100,7 +100,7 @@ elif stash.PLUGIN_TASK_NAME == "doEarlyExit":
time.sleep(3)
exit(0)
stash.Log("******************* Starting *******************")
stash.Log(f"******************* Starting ******************* json={doJsonReturn}")
if len(sys.argv) > 1:
stash.Log(f"argv = {sys.argv}")
else:
@@ -286,6 +286,10 @@ if stash.Setting('appendMatchDupDistance'):
stash.initMergeMetadata(excludeMergeTags)
apiKey = ""
if 'apiKey' in stash.STASH_CONFIGURATION:
apiKey = stash.STASH_CONFIGURATION['apiKey']
FileLink = f"file.html?GQL={stash.url}&apiKey={apiKey}&IS_DOCKER={stash.IS_DOCKER}&PageNo="
graylist = stash.Setting('zwGraylist').split(listSeparator)
graylist = [item.lower() for item in graylist]
if graylist == [""] : graylist = []
@@ -584,6 +588,8 @@ def getHtmlReportTableRow(qtyResults, tagDuplicates):
htmlReportPrefix = file.read()
htmlReportPrefix = htmlReportPrefix.replace('http://127.0.0.1:9999/graphql', stash.url)
htmlReportPrefix = htmlReportPrefix.replace('http://localhost:9999/graphql', stash.url)
htmlReportPrefix = htmlReportPrefix.replace("[remoteReportDirURL]", stash.Setting('remoteReportDirURL'))
htmlReportPrefix = htmlReportPrefix.replace("[js_DirURL]", stash.Setting('js_DirURL'))
if 'apiKey' in stash.STASH_CONFIGURATION and stash.STASH_CONFIGURATION['apiKey'] != "":
htmlReportPrefix = htmlReportPrefix.replace('var apiKey = "";', f"var apiKey = \"{stash.STASH_CONFIGURATION['apiKey']}\";")
if tagDuplicates == False:
@@ -1000,7 +1006,8 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
deleteLocalDupReportHtmlFiles(False)
fileHtmlReport = open(htmlReportName, "w")
fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n")
fileHtmlReport.write("<center class=\"ID_NextPage_Top\"><a target=\"_self\" id=\"NextPage_Top\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"DuplicateTagScenes_1.html\">Next</a></center>")
fileHtmlReport.write("<center class=\"ID_NextPage_Top\" id=\"ID_NextPage_Top\"><a target=\"_self\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"DuplicateTagScenes_1.html\">Next</a></center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Top_Remote\"><a target=\"_self\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{FileLink}1\">Next</a></center>")
fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n")
htmlReportTableHeader = stash.Setting('htmlReportTableHeader')
SceneTableHeader = htmlReportTableHeader
@@ -1164,34 +1171,46 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
if QtyTagForDelPaginate >= htmlReportPaginate:
QtyTagForDelPaginate = 0
fileHtmlReport.write("</table>\n")
homeHtmReportLink = f"<a target=\"_self\" id=\"HomePage\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{pathlib.Path(htmlReportNameHomePage).name}\">Home</a>"
homeHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{pathlib.Path(htmlReportNameHomePage).name}\">Home</a>"
prevHtmReportLink = ""
prevRemoteLink = f"<a target=\"_self\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{FileLink}{PaginateId-1}\">Prev</a>"
homeRemoteLink = f"<a target=\"_self\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{FileLink}0\">Home</a>"
if PaginateId > 0:
if PaginateId > 1:
prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html")
else:
prevHtmReport = htmlReportNameHomePage
prevHtmReportLink = f"<a target=\"_self\" id=\"PrevPage\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{pathlib.Path(prevHtmReport).name}\">Prev</a>"
prevHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{pathlib.Path(prevHtmReport).name}\">Prev</a>"
nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html")
nextHtmReportLink = f"<a target=\"_self\" id=\"NextPage\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{pathlib.Path(nextHtmReport).name}\">Next</a>"
fileHtmlReport.write(f"<center><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td><td>{nextHtmReportLink}</td></tr></table></center>")
nextHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{pathlib.Path(nextHtmReport).name}\">Next</a>"
nextRemoteLink = f"<a target=\"_self\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{FileLink}{PaginateId+1}\">Next</a>"
if PaginateId > 0:
fileHtmlReport.write(f"<center id=\"ID_NextPage_Bottom\"><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td><td>{nextHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Bottom_Remote\"><table><tr><td>{homeRemoteLink}</td><td>{prevRemoteLink}</td><td>{nextRemoteLink}</td></tr></table></center>")
else:
fileHtmlReport.write(f"<center id=\"ID_NextPage_Bottom\">{nextHtmReportLink}</center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Bottom_Remote\">{nextRemoteLink}</center>")
fileHtmlReport.write(f"{stash.Setting('htmlReportPostfix')}")
fileHtmlReport.close()
PaginateId+=1
fileHtmlReport = open(nextHtmReport, "w")
fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n")
prevRemoteLink = f"<a target=\"_self\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{FileLink}{PaginateId-1}\">Prev</a>"
nextRemoteLink = f"<a target=\"_self\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{FileLink}{PaginateId+1}\">Next</a>"
if PaginateId > 1:
prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html")
else:
prevHtmReport = htmlReportNameHomePage
prevHtmReportLink = f"<a target=\"_self\" id=\"PrevPage_Top\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{pathlib.Path(prevHtmReport).name}\">Prev</a>"
prevHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{pathlib.Path(prevHtmReport).name}\">Prev</a>"
if len(DupFileSets) > (QtyTagForDel + htmlReportPaginate):
nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html")
nextHtmReportLink = f"<a target=\"_self\" id=\"NextPage_Top\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{pathlib.Path(nextHtmReport).name}\">Next</a>"
fileHtmlReport.write(f"<center><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td><td>{nextHtmReportLink}</td></tr></table></center>")
nextHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-next\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Next Page\" href=\"{pathlib.Path(nextHtmReport).name}\">Next</a>"
fileHtmlReport.write(f"<center class=\"ID_NextPage_Top\" id=\"ID_NextPage_Top\"><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td><td>{nextHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Top_Remote\"><table><tr><td>{homeRemoteLink}</td><td>{prevRemoteLink}</td><td>{nextRemoteLink}</td></tr></table></center>")
else:
stash.Debug(f"DupFileSets Qty = {len(DupFileSets)}; DupFileDetailList Qty = {len(DupFileDetailList)}; QtyTagForDel = {QtyTagForDel}; htmlReportPaginate = {htmlReportPaginate}; QtyTagForDel + htmlReportPaginate = {QtyTagForDel+htmlReportPaginate}")
fileHtmlReport.write(f"<center><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"<center class=\"ID_NextPage_Top\" id=\"ID_NextPage_Top\"><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Top_Remote\"><table><tr><td>{homeRemoteLink}</td><td>{prevRemoteLink}</td></tr></table></center>")
fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n")
if htmlIncludeVideoPreview or htmlIncludeImagePreview:
fileHtmlReport.write(f"{htmlReportTableRow}{SceneTableHeader}Scene</th>{htmlReportTableHeader}Duplicate to Delete</th>{SceneTableHeader}Scene-ToKeep</th>{htmlReportTableHeader}Duplicate to Keep</th></tr>\n")
@@ -1215,18 +1234,21 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack
if fileHtmlReport != None:
fileHtmlReport.write("</table>\n")
if PaginateId > 0:
homeHtmReportLink = f"<a target=\"_self\" id=\"HomePage_Top\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{pathlib.Path(htmlReportNameHomePage).name}\">Home</a>"
homeHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{pathlib.Path(htmlReportNameHomePage).name}\">Home</a>"
homeRemoteLink = f"<a target=\"_self\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{FileLink}0\">Home</a>"
prevRemoteLink = f"<a target=\"_self\" iconCls=\"icon-home\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Home Page\" href=\"{FileLink}{PaginateId-1}\">Home</a>"
if PaginateId > 1:
prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html")
else:
prevHtmReport = htmlReportNameHomePage
prevHtmReportLink = f"<a target=\"_self\" iconCls=\"icon-prev\" class=\"easyui-linkbutton easyui-tooltip\" title=\"Previous Page\" href=\"{pathlib.Path(prevHtmReport).name}\">Prev</a>"
fileHtmlReport.write(f"<center><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Bottom\"><table><tr><td>{homeHtmReportLink}</td><td>{prevHtmReportLink}</td></tr></table></center>")
fileHtmlReport.write(f"<center id=\"ID_NextPage_Bottom_Remote\"><table><tr><td>{homeRemoteLink}</td><td>{prevRemoteLink}</td></tr></table></center>")
fileHtmlReport.write(f"<h2>Total Tagged for Deletion {QtyTagForDel}</h2>\n")
fileHtmlReport.write(f"{stash.Setting('htmlReportPostfix')}")
fileHtmlReport.close()
if PaginateId == 0:
modifyPropertyToSceneClassToAllFiles("NextPage_Top", "{display : none;}")
modifyPropertyToSceneClassToAllFiles("NextPage_Top", "{display:none;}")
stash.Log(f"************************************************************", printTo = stash.LogTo.STASH)
stash.Log(f"************************************************************", printTo = stash.LogTo.STASH)
stash.Log(f"View Stash duplicate report using Stash->Settings->Tools->[Duplicate File Report]", printTo = stash.LogTo.STASH)
@@ -1763,7 +1785,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}' {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')}' {apikey_json}" + "}"
stash.Log(f"Sending json value {jsonReturn}")
sys.stdout.write(jsonReturn)
@@ -2093,6 +2115,37 @@ def flagScene():
return
sys.stdout.write("{" + f"{stash.PLUGIN_TASK_NAME} : 'complete', scene: '{scene}', flagType: '{flagType}'" + "}")
def getReport():
if 'Target' not in stash.JSON_INPUT['args']:
stash.Error(f"Could not find Target in JSON_INPUT ({stash.JSON_INPUT['args']})")
return
PageNo = int(stash.JSON_INPUT['args']['Target'])
fileName = htmlReportName
if PageNo > 0:
fileName = fileName.replace(".html", f"_{PageNo}.html")
lines = None
stash.Log(f"Getting file {fileName}")
with open(fileName, 'r') as file:
lines = file.read()
if PageNo > 0 or lines.find(".ID_NextPage_Top{display:none;}") == -1:
lines = lines.replace("#ID_NextPage_Top_Remote{display:none;}", ".ID_NextPage_Top{display:none;}")
lines = lines.replace("#ID_NextPage_Bottom_Remote{display:none;}", "#ID_NextPage_Bottom{display:none;}")
# strToSrch = "<!-- StartOfBody -->"
# pos = lines.find(strToSrch)
# if pos > -1:
# lines = lines[pos + len(strToSrch):]
sys.stdout.write(lines)
stash.Log(f"Done getting file {fileName}.")
def getAdvanceMenu():
fileName = f"{DupFileManagerFolder}{os.sep}advance_options.html"
lines = None
stash.Log(f"Getting file {fileName}")
with open(fileName, 'r') as file:
lines = file.read()
sys.stdout.write(lines)
stash.Log(f"Done getting file {fileName}.")
# ToDo: Add to UI menu
# Remove all Dup tagged files (Just remove from stash, and leave file)
# Clear GraylistMarkForDel tag
@@ -2132,6 +2185,12 @@ try:
elif stash.PLUGIN_TASK_NAME == "generate_phash_task":
stash.metadata_generate({"phashes": True})
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "getReport":
getReport()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "getAdvanceMenu":
getAdvanceMenu()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
elif stash.PLUGIN_TASK_NAME == "deleteScene":
deleteScene()
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
@@ -2238,10 +2297,10 @@ try:
manageDuplicatesTaggedOrInReport(deleteScenes=True, advanceMenuOptionSelected=True)
stash.Debug(f"{stash.PLUGIN_TASK_NAME} EXIT")
else:
stash.Log(f"Nothing to do!!! (PLUGIN_ARGS_MODE={stash.PLUGIN_TASK_NAME})")
stash.Error(f"Invalid task name {stash.PLUGIN_TASK_NAME};")
stash.Log(f"Error: Nothing to do!!! (PLUGIN_ARGS_MODE={stash.PLUGIN_TASK_NAME})")
except Exception as e:
tb = traceback.format_exc()
stash.Error(f"Exception while running DupFileManager Task({stash.PLUGIN_TASK_NAME}); Error: {e}\nTraceBack={tb}")
killScanningJobs()
stash.convertToAscii = False

View File

@@ -1,10 +1,16 @@
name: DupFileManager
description: Manages duplicate files.
version: 1.1.2
version: 1.1.3
url: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/DupFileManager
ui:
# css:
# - https://axter.com/js/easyui/themes/black/easyui.css
# - https://axter.com/js/easyui/themes/icon.css
# - https://www.axter.com/js/jquery.prompt.css
javascript:
- https://axter.com/js/jquery-3.7.1.min.js
# - https://axter.com/js/easyui/jquery.easyui.min.js
# - https://www.axter.com/js/jquery.prompt.js
- DupFileManager.js
settings:
matchDupDistance:

View File

@@ -0,0 +1,28 @@
h2 {text-align: center;}
table, th, td {border:1px solid black;}
.inline {
display: inline;
}
.scene-details{text-align: center;font-size: small;}
.reason-details{text-align: left;font-size: small;}
.link-items{text-align: center;font-size: small;}
ul {
padding: 0;
}
li {
list-style-type: none;
padding: 1px;
position: relative;
}
.large {
position: absolute;
left: -9999px;
}
li:hover .large {
left: 20px;
top: -150px;
}
.large-image {
border-radius: 4px;
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);;
}

View File

@@ -0,0 +1,257 @@
var OrgPrevPage = null;
var OrgNextPage = null;
var OrgHomePage = null;
var RemoveToKeepConfirmValue = null;
var RemoveValidatePromptValue = null;
let thisUrl = "" + window.location;
const isAxterCom = (thisUrl.search("axter.com") > -1);
console.log("Cookies = " + document.cookie);
const StrRemoveToKeepConfirm = "RemoveToKeepConfirm=";
const StrRemoveValidatePrompt = "RemoveValidatePrompt=";
function SetPaginateButtonChange(){
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
if (chkBxRemoveValid.checked)
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableDeleteConfirm.checked)
RemoveValidatePromptValue = StrRemoveValidatePrompt + "true";
document.cookie = RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue + "; SameSite=None; Secure";
console.log("Cookies = " + document.cookie);
}
function trim(str, ch) {
var start = 0, end = str.length;
while(start < end && str[start] === ch) ++start;
while(end > start && str[end - 1] === ch) --end;
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
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);
return;
}
if (asyncAjax){
$('html').addClass('wait');
$("body").css("cursor", "progress");
}
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
$.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: asyncAjax, async: asyncAjax,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){
console.log(result);
if (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")))
window.location.reload();
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){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
}
});
}
function GetStashTabUrl(Tab){
var Url = GraphQl_URL;
Url = Url.replace("graphql", "settings?tab=" + Tab);
console.log("Url = " + Url);
return Url;
}
function SetFlagOnScene(flagType, ActionID){
if (flagType === "yellow highlight")
$('.ID_' + ActionID).css('background','yellow');
else if (flagType === "green highlight")
$('.ID_' + ActionID).css('background','#00FF00');
else if (flagType === "orange highlight")
$('.ID_' + ActionID).css('background','orange');
else if (flagType === "cyan highlight")
$('.ID_' + ActionID).css('background','cyan');
else if (flagType === "pink highlight")
$('.ID_' + ActionID).css('background','pink');
else if (flagType === "red highlight")
$('.ID_' + ActionID).css('background','red');
else if (flagType === "strike-through")
$('.ID_' + ActionID).css('text-decoration', 'line-through');
else if (flagType === "disable-scene")
$('.ID_' + ActionID).css({ 'background' : 'gray', 'pointer-events' : 'none' });
else if (flagType === "remove all flags")
$('.ID_' + ActionID).removeAttr('style'); //.css({ 'background' : '', 'text-decoration' : '', 'pointer-events' : '' });
else
return false;
return true;
}
function selectMarker(Mode, ActionID, button){
$('<p>Select desire marker type <select><option>yellow highlight</option><option>green highlight</option><option>orange highlight</option><option>cyan highlight</option><option>pink highlight</option><option>red highlight</option><option>strike-through</option><option>disable-scene</option><option>remove all flags</option></select></p>').confirm(function(answer){
if(answer.response){
console.log("Selected " + $('select',this).val());
var flagType = $('select',this).val();
if (flagType == null){
console.log("Invalid flagType");
return;
}
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID);
RunPluginOperation(Mode, ActionID, button, false);
}
else console.log("Not valid response");
});
}
$(document).ready(function(){
OrgPrevPage = $("#PrevPage").attr('href');
OrgNextPage = $("#NextPage").attr('href');
OrgHomePage = $("#HomePage").attr('href');
console.log("OrgNextPage = " + OrgNextPage);
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
console.log("urlParams = " + urlParams);
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
var FetchCookies = true;
if (urlParams.get('RemoveToKeepConfirm') != null && urlParams.get('RemoveToKeepConfirm') !== ""){
FetchCookies = false;
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + urlParams.get('RemoveToKeepConfirm');
if (urlParams.get('RemoveToKeepConfirm') === "true")
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (urlParams.get('RemoveValidatePrompt') != null && urlParams.get('RemoveValidatePrompt') !== ""){
FetchCookies = false;
RemoveValidatePromptValue = StrRemoveValidatePrompt + urlParams.get('RemoveValidatePrompt');
console.log("RemoveValidatePromptValue = " + RemoveValidatePromptValue);
if (urlParams.get('RemoveValidatePrompt') === "true")
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
if (FetchCookies){
console.log("Cookies = " + document.cookie);
var cookies = document.cookie;
if (cookies.indexOf(StrRemoveToKeepConfirm) > -1){
var idx = cookies.indexOf(StrRemoveToKeepConfirm) + StrRemoveToKeepConfirm.length;
var s = cookies.substring(idx);
console.log("StrRemoveToKeepConfirm Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (cookies.indexOf(StrRemoveValidatePrompt) > -1){
var idx = cookies.indexOf(StrRemoveValidatePrompt) + StrRemoveValidatePrompt.length;
var s = cookies.substring(idx);
console.log("StrRemoveValidatePrompt Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
}
SetPaginateButtonChange();
function ProcessClick(This_){
if (This_ == null)
return;
const ID = This_.id;
var Value = This_.getAttribute("value");
if ((ID == null || ID === "") && (Value == null || Value === ""))
return;
if (Value == null) Value = "";
var Mode = Value;
var ActionID = ID;
console.log("Mode = " + Mode + "; ActionID =" + ActionID);
if (Mode === "DoNothing")
return;
if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{
var newUrl = window.location.href;
if (isAxterCom)
newUrl = newUrl.replace("/file.html", "/advance_options.html");
else
newUrl = newUrl.replace(/report\/DuplicateTagScenes[_0-9]*.html/g, "advance_options.html?GQL=" + GraphQl_URL + "&apiKey=" + apiKey);
window.open(newUrl, "_blank");
return;
}
if (Mode.startsWith("deleteScene") || Mode === "removeScene"){
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
question = "Are you sure you want to delete this file and remove scene from stash?";
if (Mode !== "deleteScene" && Mode.startsWith("deleteScene")) question = "Are you sure you want to delete all the flagged files and remove them from stash?";
if (Mode === "removeScene") question = "Are you sure you want to remove scene from stash?";
if (!chkBxDisableDeleteConfirm.checked && !confirm(question))
return;
if (Mode === "deleteScene" || Mode === "removeScene"){
$('.ID_' + ActionID).css('background-color','gray');
$('.ID_' + ActionID).css('pointer-events','none');
}
}
else if (ID === "viewStashPlugin")
window.open(GetStashTabUrl("plugins"), "_blank");
else if (ID === "viewStashTools")
window.open(GetStashTabUrl("tools"), "_blank");
else if (Mode === "newName" || Mode === "renameFile"){
var myArray = ActionID.split(":");
var promptStr = "Enter new name for scene ID " + myArray[0] + ", or press escape to cancel.";
if (Mode === "renameFile")
promptStr = "Press enter to rename scene ID " + myArray[0] + ", or press escape to cancel.";
var newName=prompt(promptStr,trim(myArray[1], "'"));
if (newName === null)
return;
ActionID = myArray[0] + ":" + newName;
Mode = "renameFile";
}
else if (Mode === "flagScene"){
selectMarker(Mode, ActionID, This_);
return;
}
else if (Mode.startsWith("flagScene")){
var flagType = Mode.substring(9);
Mode = "flagScene";
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID);
}
RunPluginOperation(Mode, ActionID, This_, true);
}
$("button").click(function(){
ProcessClick(this);
});
$("a").click(function(){
if (this.id.startsWith("btn_mnu"))
return;
if (this.id === "reload"){
window.location.reload();
return;
}
if (this.id === "PrevPage" || this.id === "NextPage" || this.id === "HomePage" || this.id === "PrevPage_Top" || this.id === "NextPage_Top" || this.id === "HomePage_Top"){
return;
}
ProcessClick(this);
});
$("div").click(function(){
if (this.id.startsWith("btn_mnu"))
return;
if (this.id === "reload"){
window.location.reload();
return;
}
ProcessClick(this);
});
$("#RemoveValidatePrompt").change(function() {
console.log("checkbox clicked");
SetPaginateButtonChange();
});
$("#RemoveToKeepConfirm").change(function() {
SetPaginateButtonChange();
});
});

View File

@@ -45,4 +45,8 @@ report_config = {
"htmlReportName" : "DuplicateTagScenes.html",
# If enabled, create an HTML report when tagging duplicate files
"createHtmlReport" : True,
# To use a private or an alternate site to access report and advance menu
"remoteReportDirURL" : "https://stash.axter.com/1.1/",
# To use a private or an alternate site to access jquery, easyui, and jquery.prompt
"js_DirURL" : "https://www.axter.com/js/",
}

View File

@@ -3,304 +3,29 @@
<head>
<title>Stash Duplicate Report</title>
<style>
h2 {text-align: center;}
table, th, td {border:1px solid black;}
.inline {
display: inline;
}
.scene-details{text-align: center;font-size: small;}
.reason-details{text-align: left;font-size: small;}
.link-items{text-align: center;font-size: small;}
ul {
padding: 0;
}
li {
list-style-type: none;
padding: 1px;
position: relative;
}
.large {
position: absolute;
left: -9999px;
}
li:hover .large {
left: 20px;
top: -150px;
}
.large-image {
border-radius: 4px;
box-shadow: 1px 1px 3px 3px rgba(127, 127, 127, 0.15);;
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/black/easyui.css">
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/icon.css">
<script type="text/javascript" src="https://axter.com/js/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="https://axter.com/js/easyui/jquery.easyui.min.js"></script>
<script src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" href="https://www.axter.com/js/jquery.prompt.css"/>
<script>
var apiKey = "";
var GraphQl_URL = "http://localhost:9999/graphql";
var OrgPrevPage = null;
var OrgNextPage = null;
var OrgHomePage = null;
var RemoveToKeepConfirmValue = null;
var RemoveValidatePromptValue = null;
console.log("Cookies = " + document.cookie);
const StrRemoveToKeepConfirm = "RemoveToKeepConfirm=";
const StrRemoveValidatePrompt = "RemoveValidatePrompt=";
function SetPaginateButtonChange(){
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
if (chkBxRemoveValid.checked)
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "true";
if (chkBxDisableDeleteConfirm.checked)
RemoveValidatePromptValue = StrRemoveValidatePrompt + "true";
document.cookie = RemoveToKeepConfirmValue + "&" + RemoveValidatePromptValue + ";";
console.log("Cookies = " + document.cookie);
}
function trim(str, ch) {
var start = 0, end = str.length;
while(start < end && str[start] === ch) ++start;
while(end > start && str[end - 1] === ch) --end;
return (start > 0 || end < str.length) ? str.substring(start, end) : str;
}
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);
return;
}
if (asyncAjax){
$('html').addClass('wait');
$("body").css("cursor", "progress");
}
var chkBxRemoveValid = document.getElementById("RemoveValidatePrompt");
if (apiKey !== "")
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
$.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: "text", cache: asyncAjax, async: asyncAjax,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : ActionID, "mode":Mode}},
}), success: function(result){
console.log(result);
if (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")))
window.location.reload();
if (!chkBxRemoveValid.checked && Mode !== "flagScene") alert("Action " + Mode + " for scene(s) ID# " + ActionID + " complete.\\n\\nResults=" + result);
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
if (asyncAjax){
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
}
});
}
function GetStashTabUrl(Tab){
var Url = GraphQl_URL;
Url = Url.replace("graphql", "settings?tab=" + Tab);
console.log("Url = " + Url);
return Url;
}
function SetFlagOnScene(flagType, ActionID){
if (flagType === "yellow highlight")
$('.ID_' + ActionID).css('background','yellow');
else if (flagType === "green highlight")
$('.ID_' + ActionID).css('background','#00FF00');
else if (flagType === "orange highlight")
$('.ID_' + ActionID).css('background','orange');
else if (flagType === "cyan highlight")
$('.ID_' + ActionID).css('background','cyan');
else if (flagType === "pink highlight")
$('.ID_' + ActionID).css('background','pink');
else if (flagType === "red highlight")
$('.ID_' + ActionID).css('background','red');
else if (flagType === "strike-through")
$('.ID_' + ActionID).css('text-decoration', 'line-through');
else if (flagType === "disable-scene")
$('.ID_' + ActionID).css({ 'background' : 'gray', 'pointer-events' : 'none' });
else if (flagType === "remove all flags")
$('.ID_' + ActionID).removeAttr('style'); //.css({ 'background' : '', 'text-decoration' : '', 'pointer-events' : '' });
else
return false;
return true;
}
function selectMarker(Mode, ActionID, button){
$('<p>Select desire marker type <select><option>yellow highlight</option><option>green highlight</option><option>orange highlight</option><option>cyan highlight</option><option>pink highlight</option><option>red highlight</option><option>strike-through</option><option>disable-scene</option><option>remove all flags</option></select></p>').confirm(function(answer){
if(answer.response){
console.log("Selected " + $('select',this).val());
var flagType = $('select',this).val();
if (flagType == null){
console.log("Invalid flagType");
return;
}
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID);
RunPluginOperation(Mode, ActionID, button, false);
}
else console.log("Not valid response");
});
}
$(document).ready(function(){
OrgPrevPage = $("#PrevPage").attr('href');
OrgNextPage = $("#NextPage").attr('href');
OrgHomePage = $("#HomePage").attr('href');
console.log("OrgNextPage = " + OrgNextPage);
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
console.log("urlParams = " + urlParams);
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + "false";
RemoveValidatePromptValue = StrRemoveValidatePrompt + "false";
var FetchCookies = true;
if (urlParams.get('RemoveToKeepConfirm') != null && urlParams.get('RemoveToKeepConfirm') !== ""){
FetchCookies = false;
RemoveToKeepConfirmValue = StrRemoveToKeepConfirm + urlParams.get('RemoveToKeepConfirm');
if (urlParams.get('RemoveToKeepConfirm') === "true")
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (urlParams.get('RemoveValidatePrompt') != null && urlParams.get('RemoveValidatePrompt') !== ""){
FetchCookies = false;
RemoveValidatePromptValue = StrRemoveValidatePrompt + urlParams.get('RemoveValidatePrompt');
console.log("RemoveValidatePromptValue = " + RemoveValidatePromptValue);
if (urlParams.get('RemoveValidatePrompt') === "true")
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
if (FetchCookies){
console.log("Cookies = " + document.cookie);
var cookies = document.cookie;
if (cookies.indexOf(StrRemoveToKeepConfirm) > -1){
var idx = cookies.indexOf(StrRemoveToKeepConfirm) + StrRemoveToKeepConfirm.length;
var s = cookies.substring(idx);
console.log("StrRemoveToKeepConfirm Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveToKeepConfirm" ).prop("checked", true);
else
$( "#RemoveToKeepConfirm" ).prop("checked", false);
}
if (cookies.indexOf(StrRemoveValidatePrompt) > -1){
var idx = cookies.indexOf(StrRemoveValidatePrompt) + StrRemoveValidatePrompt.length;
var s = cookies.substring(idx);
console.log("StrRemoveValidatePrompt Cookie = " + s);
if (s.startsWith("true"))
$( "#RemoveValidatePrompt" ).prop("checked", true);
else
$( "#RemoveValidatePrompt" ).prop("checked", false);
}
}
SetPaginateButtonChange();
function ProcessClick(This_){
if (This_ == null)
return;
const ID = This_.id;
var Value = This_.getAttribute("value");
if ((ID == null || ID === "") && (Value == null || Value === ""))
return;
if (Value == null) Value = "";
var Mode = Value;
var ActionID = ID;
console.log("Mode = " + Mode + "; ActionID =" + ActionID);
if (Mode === "DoNothing")
return;
if (ActionID === "AdvanceMenu" || ActionID === "AdvanceMenu_")
{
var newUrl = window.location.href;
newUrl = newUrl.replace(/report\/DuplicateTagScenes[_0-9]*.html/g, "advance_options.html?GQL=" + GraphQl_URL + "&apiKey=" + apiKey);
window.open(newUrl, "_blank");
return;
}
if (Mode.startsWith("deleteScene") || Mode === "removeScene"){
var chkBxDisableDeleteConfirm = document.getElementById("RemoveToKeepConfirm");
question = "Are you sure you want to delete this file and remove scene from stash?";
if (Mode !== "deleteScene" && Mode.startsWith("deleteScene")) question = "Are you sure you want to delete all the flagged files and remove them from stash?";
if (Mode === "removeScene") question = "Are you sure you want to remove scene from stash?";
if (!chkBxDisableDeleteConfirm.checked && !confirm(question))
return;
if (Mode === "deleteScene" || Mode === "removeScene"){
$('.ID_' + ActionID).css('background-color','gray');
$('.ID_' + ActionID).css('pointer-events','none');
}
}
else if (ID === "viewStashPlugin")
window.open(GetStashTabUrl("plugins"), "_blank");
else if (ID === "viewStashTools")
window.open(GetStashTabUrl("tools"), "_blank");
else if (Mode === "newName" || Mode === "renameFile"){
var myArray = ActionID.split(":");
var promptStr = "Enter new name for scene ID " + myArray[0] + ", or press escape to cancel.";
if (Mode === "renameFile")
promptStr = "Press enter to rename scene ID " + myArray[0] + ", or press escape to cancel.";
var newName=prompt(promptStr,trim(myArray[1], "'"));
if (newName === null)
return;
ActionID = myArray[0] + ":" + newName;
Mode = "renameFile";
}
else if (Mode === "flagScene"){
selectMarker(Mode, ActionID, This_);
return;
}
else if (Mode.startsWith("flagScene")){
var flagType = Mode.substring(9);
Mode = "flagScene";
if (!SetFlagOnScene(flagType, ActionID))
return;
ActionID = ActionID + ":" + flagType;
console.log("ActionID = " + ActionID);
}
RunPluginOperation(Mode, ActionID, This_, true);
}
$("button").click(function(){
ProcessClick(this);
});
$("a").click(function(){
if (this.id.startsWith("btn_mnu"))
return;
if (this.id === "reload"){
window.location.reload();
return;
}
if (this.id === "PrevPage" || this.id === "NextPage" || this.id === "HomePage" || this.id === "PrevPage_Top" || this.id === "NextPage_Top" || this.id === "HomePage_Top"){
return;
}
ProcessClick(this);
});
$("div").click(function(){
if (this.id.startsWith("btn_mnu"))
return;
if (this.id === "reload"){
window.location.reload();
return;
}
ProcessClick(this);
});
$("#RemoveValidatePrompt").change(function() {
console.log("checkbox clicked");
SetPaginateButtonChange();
});
$("#RemoveToKeepConfirm").change(function() {
SetPaginateButtonChange();
});
});
</script>
<link rel="stylesheet" type="text/css" href="[remoteReportDirURL]DupFileManager_report.css">
<!-- <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> -->
<link rel="stylesheet" type="text/css" href="[js_DirURL]easyui/themes/black/easyui.css">
<link rel="stylesheet" type="text/css" href="[js_DirURL]easyui/themes/icon.css">
<link rel="stylesheet" type="text/css" href="[js_DirURL]jquery.prompt.css"/>
<script type="text/javascript" src="[js_DirURL]jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="[js_DirURL]easyui/jquery.easyui.min.js"></script>
<script type="text/javascript" src="[js_DirURL]jquery.prompt.js"></script>
<script type="text/javascript" src="[remoteReportDirURL]DupFileManager_report.js"></script>
<style>
#ID_NextPage_Top_Remote{display:none;}
#ID_NextPage_Bottom_Remote{display:none;}
</style>
</head>
<!-- StartOfBody -->
<body>
<div style="background-color:BackgroundColorPlaceHolder;color:TextColorPlaceHolder;">
<center><table style="color:darkgreen;background-color:powderblue;">
<center><table id="top_report_menu" style="color:darkgreen;background-color:powderblue;">
<tr><th>Report Info</th><th>Report Options</th></tr>
<tr>
<td><table><tr>
@@ -313,7 +38,7 @@ $(document).ready(function(){
<a id="btn_mnu" class="easyui-menubutton" menu="#btn_mnu1">Menu</a>
</div>
<div id="btn_mnu1">
<div iconCls="icon-add" id="AdvanceMenu" title="Open [Advance Duplicate File Deletion Menu] on a new tab in the browser." name="AdvanceMenu">Advance Duplicate File Deletion Menu</div>
<div iconCls="icon-add" id="AdvanceMenu" title="Open [Advance Duplicate File Menu] on a new tab in the browser." name="AdvanceMenu">Advance Duplicate File Menu</div>
<div iconCls="icon-reload" id="reload" title="Reload (refresh) this page." name="reload">Reload Page</div>
<div iconCls="icon-menu1" id="viewStashPlugin" title="View Stash plugins menu.">Stash Plugins</div>
<div iconCls="icon-menu-blue" id="viewStashTools" title="View Stash tools menu.">Stash Tools</div>

View File

@@ -1,11 +1,11 @@
# DupFileManager: Ver 1.1.2 (By David Maisonave)
# DupFileManager: Ver 1.1.3 (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.
### Features
- Creates a duplicate file report which can be accessed from the settings->tools menu options.The report is created as an HTML file and stored in local path under plugins\DupFileManager\report\DuplicateTagScenes.html.
- Creates a duplicate file report which can be accessed from the Stash->Settings->Tools menu options.The report is created as an HTML file and stored in local path under plugins\DupFileManager\report\DuplicateTagScenes.html.
- See screenshot at the bottom of this page for example report.
- Items on the left side of the report are the primary duplicates designated for deletion. By default, these duplicates are given a special _duplicate tag.
- Items on the right side of the report are designated as primary duplicates to keep. They usually have higher resolution, duration and/or preferred paths.
@@ -24,8 +24,7 @@ It has both **task** and **tools-UI** components.
- Normally when Stash searches the file name for tag names, performers, and studios, it only does so using the primary file.
- Advance menu
- ![Screenshot 2024-12-13 164930](https://github.com/user-attachments/assets/10098a4d-de2f-4e83-94ce-5988c5935404)
- Advance menu can be access from the Settings->Tools->**[DupFileManager Tools and Utilities]** menu or from the **reports**.
- Only access Advance Menu from the report when using Stash setup requiring a password.
- Advance menu can be access from the Stash->Settings->Tools->**[Duplicate File Report]** menu or from the **DupFileManager Tools & Util**.
- Here are **some** of the options available in the **Advance Menu**.
- Delete specially tagged duplicates in blacklist path.
- Delete duplicates with specified file path.
@@ -39,11 +38,11 @@ It has both **task** and **tools-UI** components.
- Bottom extended portion of the Advanced Menu is for customizing the report.
- ![Screenshot 2024-12-13 165238](https://github.com/user-attachments/assets/9ba9ab12-cd60-4be4-bc26-6ffe76a68edc)
- Delete duplicate file task with the following options:
- Tasks (Settings->Task->[Plugin Tasks]->DupFileManager)
- Tasks (Stash->Settings->Task->[Plugin Tasks]->DupFileManager)
- **Tag Duplicates** - Set tag DuplicateMarkForDeletion to the duplicates with lower resolution, duration, file name length, and/or black list path.
- **Delete Tagged Duplicates** - Delete scenes having DuplicateMarkForDeletion tag.
- **Delete Duplicates** - Deletes duplicate files. Performs deletion without first tagging.
- Plugin UI options (Settings->Plugins->Plugins->[DupFileManager])
- Plugin UI options (Stash->Settings->Plugins->Plugins->[DupFileManager])
- Has a 3 tier path selection to determine which duplicates to keep, and which should be candidates for deletions.
- **Whitelist** - List of paths NOT to be deleted.
- E.g. C:\Favorite\,E:\MustKeep\
@@ -80,8 +79,39 @@ That's it!!!
### Options
- Options are accessible in the GUI via Settings->Plugins->Plugins->[DupFileManager].
- More options available in DupFileManager_config.py.
- Options are accessible in the GUI via Stash->Settings->Plugins->Plugins->[DupFileManager].
- Also see:
- Stash->Settings->Tools->[Duplicate File Report]
- Stash->Settings->Tools->[DupFileManager Tools and Utilities]
- More options available on the following link:
- [advance_options.html](https://stash.axter.com/1.1/advance_options.html)
- When using a Stash installation that requires a password or that is not using port 9999...
- Access above link from Stash->Settings->Tools->[Duplicate File Report]->[**Advance Duplicate File Menu**]
- Or add the GQL and apiKey as parameters to the URL.
- Example: https://stash.axter.com/1.1/advance_options.html?GQL=http://localhost:9999/graphql&apiKey=1234567890abcdefghijklmnop
- See following for more details: [Stash Password](README.md#Stash-Password)
### Advanced Options
Users can setup a private or alternate remote site by changing variables **remoteReportDirURL** and **js_DirURL** in file DupFileManager_report_config.py.
- The following files are needed at the remote site that is pointed to by **remoteReportDirURL**.
- DupFileManager_report.js
- DupFileManager_report.css
- file.html
- advance_options.html
- The **js_DirURL** path requires the following:
- jquery-3.7.1.min.js
- EasyUI associated files
- jquery.prompt.js and jquery.prompt.css
### Stash Password
- Stash installation configured with a password, need to generate an API-Key.
- To generate an API-Key:
- Go to Stash->Settings->Security->Authentication->[API Key]
- Click on [Generate API-Key]
- Once the API key is generated, DupFileManager will automatically fetch the key.
### Screenshots
@@ -103,6 +133,7 @@ That's it!!!
### Future Planned Features, Changes, or Fixes
- 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.
@@ -121,12 +152,13 @@ That's it!!!
- Fix errors on HTML page listed in https://validator.w3.org.
- Add logic to merge performers and galaries seperatly from tag merging on report.
- Add logic to merge group metadata when selecting merge option on report.
- Add advanced menu directly to the Settings->Tools menu.
- Add report directly to the Settings->Tools menu.
- Add advanced menu directly to the Stash->Settings->Tools menu. (This change does not look doable!!!)
- Add report directly to the Stash->Settings->Tools menu. (This change does not look doable!!!)
- Create cookies for the options in the [**Advance Duplicate File Menu**].
- Add doulbe strike-through option to flagging.
- Add option to report to avoid reloading page after updating report.
- Add option to report to automatically strip width & height from name on rename.
- Add link to version history to [**Advance Duplicate File Menu**] and to [DupFileManager Tools and Utilities]
- Move [Merge Duplicate Tags], [Whitelist Delete In Same Folder], and [Swap Better **] field options from the Stash->Plugins GUI to the advance menu.

View File

@@ -10,13 +10,12 @@ table, th, td {border:1px solid black;}
}
html.wait, html.wait * { cursor: wait !important; }
</style>
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/black/easyui.css"> <!-- black || material-blue-->
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/black/easyui.css">
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/icon.css">
<!-- <script type="text/javascript" src="https://axter.com/js/easyui/jquery.min.js"></script> -->
<script type="text/javascript" src="https://axter.com/js/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="https://axter.com/js/easyui/jquery.easyui.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 type="text/javascript" src="https://www.axter.com/js/jquery.prompt.js"></script>
<link rel="stylesheet" type="text/css" href="https://www.axter.com/js/jquery.prompt.css"/>
<script>
const isChrome = !!window.chrome;
const urlParams = new URLSearchParams(window.location.search);
@@ -28,16 +27,17 @@ function getParam(ParamName, DefaultValue = ""){
return DefaultValue;
}
const apiKey = getParam("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
const GraphQl_URL = getParam("GQL", "http://localhost:9999/graphql");// For Stash installations with non-standard ports or URL's, populate this variable with actual URL; ----- Or pass in the URL at the command line using GQL param. Example: advance_options.html?GQL=http://localhost:9900/graphql
const GQL = getParam("GQL", "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 ReportUrlDir = getParam("ReportUrlDir")
const IsDebugMode = (getParam("DebugMode", "false") === "true")
const IS_DOCKER = getParam("IS_DOCKER") === "True";
var ReportUrl = "";
// let stash_site = { GQL: GraphQl_URL, apiKey: apiKey, ReportUrlDir: ReportUrlDir, };
// document.cookie = 'stash_site=' + JSON.stringify(stash_site);
console.log(urlParams);
console.log("GQL = " + GraphQl_URL + "; apiKey = " + apiKey + "; ReportUrlDir = " + ReportUrlDir + "; isChrome = " + isChrome + "; isAxterCom = " + isAxterCom + "; IS_DOCKER = " + IS_DOCKER + "; Cookies = " + document.cookie);
// DockerWarning = "<p><b>Warning: </b>The current version of DupFileManager does not support accessing report files from Docker Stash setup.</p><p>The link in the bottom of this window will not work unless you're using a browser in the Docker OS.</p>Consider installing Firefox by using instructions in following link:<a href=\"https://collabnix.com/running-firefox-in-docker-container/\" target=\"_blank\" id=\"advance_options\">Firefox-in-Docker-Container</a><p>...</p>";
if (isAxterCom == true){
let stash_site = { GQL: GQL, apiKey: apiKey, ReportUrlDir: ReportUrlDir, };
document.cookie = 'stash_site=' + JSON.stringify(stash_site) + "; SameSite=None; Secure";
}
if (IsDebugMode) console.log(urlParams);
if (IsDebugMode) console.log("GQL = " + GQL + "; ReportUrlDir = " + ReportUrlDir + "; isChrome = " + isChrome + "; isAxterCom = " + isAxterCom + "; IS_DOCKER = " + IS_DOCKER);
DockerWarning = "<p><b>Warning: </b>The current version of DupFileManager does not support accessing report files from Docker Stash setup.</p><p>The following link will not work unless you're using a browser in the Docker OS.</p>";
function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes = false, DataType = "text") {
@@ -46,12 +46,12 @@ function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes =
$("body").css("cursor", "progress");
if (TagOnlyScenes)
Param += ":TagOnlyScenes";
console.log("GraphQl_URL = " + GraphQl_URL + "; Mode = " + Mode + "; Param = " + Param + "; DataType = " + DataType);
console.log("GQL = " + GQL + "; Mode = " + Mode + "; Param = " + Param + "; DataType = " + DataType);
if (apiKey !== ""){
console.log("Using apiKey = " + apiKey);
if (IsDebugMode) console.log("Using apiKey = " + apiKey);
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
}
const AjaxData = $.ajax({method: "POST", url: GraphQl_URL, contentType: "application/json", dataType: DataType, cache: Async, async: Async,
const AjaxData = $.ajax({method: "POST", url: GQL, contentType: "application/json", dataType: DataType, cache: Async, async: Async,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": "DupFileManager", "args": { "Target" : Param, "mode":Mode}},
@@ -74,29 +74,34 @@ function RunPluginDupFileManager(Mode, Param = 0, Async = false, TagOnlyScenes =
else{
var Notice = "";
var Instructions = "<p>Click the below link to open report in your browser.</p>";
if (IS_DOCKER)
Instructions = DockerWarning;
if (isAxterCom && isChrome)
Notice = "<p>Note: If your browser does not support opening local file links from a non-local URL, copy and paste the above link to your browser address field.</p>";
$("<h2>Report complete!</h2>" + Instructions + "<a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>" + Notice).alert();
var ReportUrlLinkDisplay = ReportUrl;
if (isAxterCom)
ReportUrlLinkDisplay = "Report";
else{
if (IS_DOCKER)
Instructions = DockerWarning;
else if (isChrome)
Notice = "<p>Note: If your browser does not support opening local file links from a non-local URL, copy and paste the above link to your browser address field.</p>";
}
$("<h2>Report complete!</h2>" + Instructions + "<a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrlLinkDisplay + "</a>" + Notice).alert();
}
}
}
else
console.log("Ajax JSON results = " + JSON.stringify(result));
if (IsDebugMode) console.log("Ajax JSON results = " + JSON.stringify(result));
}, error: function(XMLHttpRequest, textStatus, errorThrown) {
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
$('html').removeClass('wait');
$("body").css("cursor", "default");
}
});
console.log("Setting default cursor");
if (IsDebugMode) console.log("Setting default cursor");
if (DataType == "text"){
console.log(AjaxData.responseText);
return AjaxData.responseText;
}
JsonStr = AjaxData.responseJSON.data.runPluginOperation.replaceAll("'", "\"");
console.log("JSON runPluginOperation = " + JsonStr);
if (IsDebugMode) console.log("JSON runPluginOperation = " + JsonStr);
return JSON.parse(JsonStr);
}
@@ -105,6 +110,8 @@ function GetReportUrlDir(){
console.log("LocalDuplicateReport.LocalDupReportExist = " + LocalDuplicateReport.LocalDupReportExist);
console.log("LocalDuplicateReport.Path = " + LocalDuplicateReport.Path);
ReportUrl = LocalDuplicateReport.ReportUrl;
if (isAxterCom)
ReportUrl = thisUrl.replace("advance_options.html", "file.html") ;//"file.html"; //?GQL=" + GQL + "&apiKey=" + apiKey;
console.log("ReportUrl = " + ReportUrl);
return LocalDuplicateReport.ReportUrlDir;
}
@@ -113,7 +120,7 @@ if (ReportUrlDir === "")
console.log("ReportUrlDir = " + ReportUrlDir);
function GetStashTabUrl(Tab){
var Url = GraphQl_URL;
var Url = GQL;
Url = Url.replace("graphql", "settings?tab=" + Tab);
console.log("Url = " + Url);
return Url;
@@ -157,14 +164,15 @@ function ProcessClick(This_){
}
else if (ID === "viewreport")
{
if (IS_DOCKER)
$(DockerWarning + "<a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>").alert();
else if (isAxterCom){
if (isChrome)
$("<p>This browser does not support local file links from a non-local URL. To open the report, copy and paste the following link to your browser address bar.</p><a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>").alert();
else
$("<p>If this browser supports local file links from a non-local URL, you can click on the following link to open your report. Other wise to open the report, copy and paste the link to your browser address bar.</p><a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>").alert();
if (isAxterCom){
//if (isChrome)
// $("<p>This browser does not support local file links from a non-local URL. To open the report, copy and paste the following link to your browser address bar.</p><a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>").alert();
//else
// $("<p>If this browser supports local file links from a non-local URL, you can click on the following link to open your report. Other wise to open the report, copy and paste the link to your browser address bar.</p><a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>").alert();
window.open(ReportUrl, "_blank");
}
else if (IS_DOCKER)
$(DockerWarning + "<a href=\"" + ReportUrl + "\" target=\"_blank\" id=\"advance_options\">" + ReportUrl + "</a>").alert();
else
window.open(ReportUrl, "_blank");
}

View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html>
<head>
<title>Duplicate Files Report</title>
<style>
h2 {text-align: center;}
table, th, td {border:1px solid black;}
.inline {
display: inline;
}
html.wait, html.wait * { cursor: wait !important; }
</style>
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/black/easyui.css"> <!-- black || material-blue-->
<link rel="stylesheet" type="text/css" href="https://axter.com/js/easyui/themes/icon.css">
<script type="text/javascript" src="https://axter.com/js/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="https://axter.com/js/easyui/jquery.easyui.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>
class StashPlugin {
#urlParams = new URLSearchParams(window.location.search);
#apiKey = "";
#doApiLog = true;
constructor(PluginID, doApiLog = true, DataType = "json", Async = false) {
this.#doApiLog = doApiLog;
this.PluginID = PluginID;
this.DataType = DataType;
this.Async = Async;
this.#apiKey = this.getParam("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
this.GraphQl_URL = this.getParam("GQL", "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
console.log("GQL = " + this.GraphQl_URL + "; apiKey = " + this.#apiKey + "; urlParams = " + this.#urlParams + "; Cookies = " + document.cookie);
}
getParam(ParamName, DefaultValue = ""){
if (this.#urlParams.get(ParamName) != null && this.#urlParams.get(ParamName) !== "")
return this.#urlParams.get(ParamName);
return DefaultValue;
}
CallBackOnSuccess(result, Args, This){ // Only called on asynchronous calls
console.log("Ajax success.");
}
CallBackOnFail(textStatus, errorThrown, This){
console.log("Ajax failed with Status: " + textStatus + "; Error: " + errorThrown);
alert("Error on StashPlugin Ajax call!!!\nReturn-Status: " + textStatus + "\nThrow-Error: " + errorThrown);
}
RunPluginOperation(Args = {}, OnSuccess = this.CallBackOnSuccess, OnFail = this.CallBackOnFail) {
console.log("PluginID = " + this.PluginID + "; Args = " + Args + "; GQL = " + this.GraphQl_URL + "; DataType = " + this.DataType + "; Async = " + this.Async);
if (this.#apiKey !== ""){
if (this.#doApiLog) console.log("Using apiKey = " + this.#apiKey);
const apiKey = this.#apiKey;
$.ajaxSetup({beforeSend: function(xhr) {xhr.setRequestHeader('apiKey', apiKey);}});
}
const AjaxData = $.ajax({method: "POST", url: this.GraphQl_URL, contentType: "application/json", dataType: this.DataType, cache: this.Async, async: this.Async,
data: JSON.stringify({
query: `mutation RunPluginOperation($plugin_id:ID!,$args:Map!){runPluginOperation(plugin_id:$plugin_id,args:$args)}`,
variables: {"plugin_id": this.PluginID, "args": Args},
}), success: function(result){
if (this.Async == true) OnSuccess(result, Args, this);
}, error: function(jqXHR, textStatus, errorThrown) {
OnFail(textStatus, errorThrown, this);
}
});
if (this.Async == true) // Make sure to use callback functions for asynchronous calls.
return;
if (this.DataType == "text")
return AjaxData.responseText;
return AjaxData.responseJSON.data.runPluginOperation;
}
};
class PluginDupFileManager extends StashPlugin{
constructor() {
super("DupFileManager");
this.IS_DOCKER = this.getParam("IS_DOCKER") === "True";
this.PageNo = parseInt(this.getParam("PageNo", "0"));
}
GetFile(Mode = "getReport") {
var results = this.RunPluginOperation({ "Target" : this.PageNo, "mode":Mode});
const strResults = JSON.stringify(results);
if (strResults.indexOf("INF: 'Error: Nothing to do!!!") > -1){
console.log("Ajax failed for function " + Mode +" and page " + this.PageNo + " with results = " + JSON.stringify(results));
return "<p>Failed to get report do to ajax error!!!</p>";
}
return results;
}
}
var plugindupfilemanager = new PluginDupFileManager();
const html = plugindupfilemanager.GetFile();
$(document).ready(function(){
$( "#report" ).append( html );
//$( "#top_report_menu" ).remove();
//$( ".ID_NextPage_Top" ).remove();
//$( ".ID_NextPage_Bottom" ).remove();
});
</script>
</head>
<body>
<div id="report"></div>
</body></html>

View File

@@ -90,15 +90,18 @@
- On browsers like FireFox, a button is displayed instead, and no note is displayed.
- Removed *.css and *.map files, which were not being used.
### 1.1.2
- Moved link to [**Advance Duplicate File Menu**] to https://stash.axter.com/1.1.2/advance_options.html
- Moved link to [**Advance Duplicate File Menu**] to https://stash.axter.com/1.1/advance_options.html
- This allows the Advance Menu to be accessed by Chrome, Edge and other Chrome based browsers which don't allow accessing local links from a non-local URL.
- Added additional warnings when detecting Chrome based browsers and when moving from non-local link to local link.
- Moved htmlReportPrefix field from the DupFileManager_report_config.py to DupFileManager_report_header.
- This was needed because Python on Docker gives an error when using tripple quoted strings.
- Made advance_options.html HTML5 compliance.
- Added additional details returned by getLocalDupReportPath to include (IS_DOCKER, ReportUrl, AdvMenuUrl, apikey, & LocalDir).
### 1.1.3
- Added access to report from https://stash.axter.com/1.1/file.html
- This allows access to report from any browser and access to report from a Docker Stash setup.
- On Stash installation using passwords or non-standard URL, the file.html link should be accessed from the advance menu or from the Stash->Tools->[DupFileManager Report Menu].
- Added fields remoteReportDirURL and js_DirURL to allow users to setup their own private or alternate remote path for javascript files.
- On Stash installations having password, the Advance Menu can now be accessed from the Stash->Tools->[DupFileManager Report Menu].