From 8e5e4519165942bcb45a66da0ebb89efd6fdc968 Mon Sep 17 00:00:00 2001 From: David Maisonave <47364845+David-Maisonave@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:39:20 -0500 Subject: [PATCH] 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]. --- Dev/index.yml | 26 ++ plugins/DupFileManager/DupFileManager.js | 116 ++++++- plugins/DupFileManager/DupFileManager.py | 99 ++++-- plugins/DupFileManager/DupFileManager.yml | 8 +- .../DupFileManager/DupFileManager_report.css | 28 ++ .../DupFileManager/DupFileManager_report.js | 257 +++++++++++++++ .../DupFileManager_report_config.py | 4 + .../DupFileManager_report_header | 307 +----------------- plugins/DupFileManager/README.md | 52 ++- plugins/DupFileManager/advance_options.html | 68 ++-- plugins/DupFileManager/file.html | 101 ++++++ .../DupFileManager/version_history/README.md | 11 +- 12 files changed, 716 insertions(+), 361 deletions(-) create mode 100644 Dev/index.yml create mode 100644 plugins/DupFileManager/DupFileManager_report.css create mode 100644 plugins/DupFileManager/DupFileManager_report.js create mode 100644 plugins/DupFileManager/file.html diff --git a/Dev/index.yml b/Dev/index.yml new file mode 100644 index 0000000..3860d09 --- /dev/null +++ b/Dev/index.yml @@ -0,0 +1,26 @@ +- id: DupFileManager + name: DupFileManager + metadata: + description: Manages duplicate files. + version: 1.0.0-3ddd7fa + date: 2024-11-29 02:38:12 + path: DupFileManager.zip + sha256: 5047dc27cfec45f64372d68d93e227a26bcc1e3590d2701ce31b039dfa5c7c89 + +- id: filemonitor + name: FileMonitor + metadata: + description: Monitors the Stash library folders, and updates Stash if any changes occurs in the Stash library paths. + version: 0.9.0-97999c1 + date: 2024-08-28 13:25:34 + path: filemonitor.zip + sha256: 8a45d4341d85e4ea6e0dbc8125282027e1c76ecb178b0c146d20cc2745ccf711 + +- id: renamefile + name: RenameFile + metadata: + description: Renames video (scene) file names when the user edits the [Title] field located in the scene [Edit] tab. + version: 0.4.6-97999c1 + date: 2024-08-28 13:25:34 + path: renamefile.zip + sha256: e0cf5105be0be6336ad6bca56ff6d7295954e1fbaec3f0e921a917a44da31f57 diff --git a/plugins/DupFileManager/DupFileManager.js b/plugins/DupFileManager/DupFileManager.js index 9d05b91..4a37b6a 100644 --- a/plugins/DupFileManager/DupFileManager.js +++ b/plugins/DupFileManager/DupFileManager.js @@ -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 "

Failed to get report do to ajax error!!!

"; + //} + //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]"))}), )), }, ]; diff --git a/plugins/DupFileManager/DupFileManager.py b/plugins/DupFileManager/DupFileManager.py index 6910b8b..c3ed1e0 100644 --- a/plugins/DupFileManager/DupFileManager.py +++ b/plugins/DupFileManager/DupFileManager.py @@ -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("
Next
") + fileHtmlReport.write("
Next
") + fileHtmlReport.write(f"
Next
") 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("\n") - homeHtmReportLink = f"Home" + homeHtmReportLink = f"Home" prevHtmReportLink = "" + prevRemoteLink = f"Prev" + homeRemoteLink = f"Home" if PaginateId > 0: if PaginateId > 1: prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html") else: prevHtmReport = htmlReportNameHomePage - prevHtmReportLink = f"Prev" + prevHtmReportLink = f"Prev" nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html") - nextHtmReportLink = f"Next" - fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}{nextHtmReportLink}
") + nextHtmReportLink = f"Next" + nextRemoteLink = f"Next" + if PaginateId > 0: + fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}{nextHtmReportLink}
") + fileHtmlReport.write(f"
{homeRemoteLink}{prevRemoteLink}{nextRemoteLink}
") + else: + fileHtmlReport.write(f"
{nextHtmReportLink}
") + fileHtmlReport.write(f"
{nextRemoteLink}
") fileHtmlReport.write(f"{stash.Setting('htmlReportPostfix')}") fileHtmlReport.close() PaginateId+=1 fileHtmlReport = open(nextHtmReport, "w") fileHtmlReport.write(f"{getHtmlReportTableRow(qtyResults, tagDuplicates)}\n") + prevRemoteLink = f"Prev" + nextRemoteLink = f"Next" if PaginateId > 1: prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html") else: prevHtmReport = htmlReportNameHomePage - prevHtmReportLink = f"Prev" + prevHtmReportLink = f"Prev" if len(DupFileSets) > (QtyTagForDel + htmlReportPaginate): nextHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId+1}.html") - nextHtmReportLink = f"Next" - fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}{nextHtmReportLink}
") + nextHtmReportLink = f"Next" + fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}{nextHtmReportLink}
") + fileHtmlReport.write(f"
{homeRemoteLink}{prevRemoteLink}{nextRemoteLink}
") else: stash.Debug(f"DupFileSets Qty = {len(DupFileSets)}; DupFileDetailList Qty = {len(DupFileDetailList)}; QtyTagForDel = {QtyTagForDel}; htmlReportPaginate = {htmlReportPaginate}; QtyTagForDel + htmlReportPaginate = {QtyTagForDel+htmlReportPaginate}") - fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}
") + fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}
") + fileHtmlReport.write(f"
{homeRemoteLink}{prevRemoteLink}
") fileHtmlReport.write(f"{stash.Setting('htmlReportTable')}\n") if htmlIncludeVideoPreview or htmlIncludeImagePreview: fileHtmlReport.write(f"{htmlReportTableRow}{SceneTableHeader}Scene{htmlReportTableHeader}Duplicate to Delete{SceneTableHeader}Scene-ToKeep{htmlReportTableHeader}Duplicate to Keep\n") @@ -1215,18 +1234,21 @@ def mangeDupFiles(merge=False, deleteDup=False, tagDuplicates=False, deleteBlack if fileHtmlReport != None: fileHtmlReport.write("\n") if PaginateId > 0: - homeHtmReportLink = f"Home" + homeHtmReportLink = f"Home" + homeRemoteLink = f"Home" + prevRemoteLink = f"Home" if PaginateId > 1: prevHtmReport = htmlReportNameHomePage.replace(".html", f"_{PaginateId-1}.html") else: prevHtmReport = htmlReportNameHomePage prevHtmReportLink = f"Prev" - fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}
") + fileHtmlReport.write(f"
{homeHtmReportLink}{prevHtmReportLink}
") + fileHtmlReport.write(f"
{homeRemoteLink}{prevRemoteLink}
") fileHtmlReport.write(f"

Total Tagged for Deletion {QtyTagForDel}

\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 = "" + # 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 diff --git a/plugins/DupFileManager/DupFileManager.yml b/plugins/DupFileManager/DupFileManager.yml index 5cf53bb..abb1d1b 100644 --- a/plugins/DupFileManager/DupFileManager.yml +++ b/plugins/DupFileManager/DupFileManager.yml @@ -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: diff --git a/plugins/DupFileManager/DupFileManager_report.css b/plugins/DupFileManager/DupFileManager_report.css new file mode 100644 index 0000000..ecd987d --- /dev/null +++ b/plugins/DupFileManager/DupFileManager_report.css @@ -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);; +} diff --git a/plugins/DupFileManager/DupFileManager_report.js b/plugins/DupFileManager/DupFileManager_report.js new file mode 100644 index 0000000..c746091 --- /dev/null +++ b/plugins/DupFileManager/DupFileManager_report.js @@ -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){ + $('

Select desire marker type

').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(); + }); +}); diff --git a/plugins/DupFileManager/DupFileManager_report_config.py b/plugins/DupFileManager/DupFileManager_report_config.py index 08837a7..1d0bc4f 100644 --- a/plugins/DupFileManager/DupFileManager_report_config.py +++ b/plugins/DupFileManager/DupFileManager_report_config.py @@ -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/", } diff --git a/plugins/DupFileManager/DupFileManager_report_header b/plugins/DupFileManager/DupFileManager_report_header index 425f06d..67aee0e 100644 --- a/plugins/DupFileManager/DupFileManager_report_header +++ b/plugins/DupFileManager/DupFileManager_report_header @@ -3,304 +3,29 @@ Stash Duplicate Report - - - - - - - - - + + + + + + + + + + +
-
+
Report InfoReport Options
@@ -313,7 +38,7 @@ $(document).ready(function(){ Menu
-
Advance Duplicate File Deletion Menu
+
Advance Duplicate File Menu
Reload Page
Stash Plugins
Stash Tools
diff --git a/plugins/DupFileManager/README.md b/plugins/DupFileManager/README.md index f70269b..80319d9 100644 --- a/plugins/DupFileManager/README.md +++ b/plugins/DupFileManager/README.md @@ -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. diff --git a/plugins/DupFileManager/advance_options.html b/plugins/DupFileManager/advance_options.html index ba07b16..d914edb 100644 --- a/plugins/DupFileManager/advance_options.html +++ b/plugins/DupFileManager/advance_options.html @@ -10,13 +10,12 @@ table, th, td {border:1px solid black;} } html.wait, html.wait * { cursor: wait !important; } - + - - - + + + + + + + + +
+ + + diff --git a/plugins/DupFileManager/version_history/README.md b/plugins/DupFileManager/version_history/README.md index 744506b..d63f6a7 100644 --- a/plugins/DupFileManager/version_history/README.md +++ b/plugins/DupFileManager/version_history/README.md @@ -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].