forked from Github/Axter-Stash
1.1.3
### 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:
@@ -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]"))}),
|
||||
)),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
28
plugins/DupFileManager/DupFileManager_report.css
Normal file
28
plugins/DupFileManager/DupFileManager_report.css
Normal 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);;
|
||||
}
|
||||
257
plugins/DupFileManager/DupFileManager_report.js
Normal file
257
plugins/DupFileManager/DupFileManager_report.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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/",
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
- 
|
||||
- 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.
|
||||
- 
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
101
plugins/DupFileManager/file.html
Normal file
101
plugins/DupFileManager/file.html
Normal 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>
|
||||
|
||||
|
||||
@@ -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].
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user