From 31729076fe9c6d53064ed45a0954d012b42b5bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 28 Sep 2023 04:45:09 +0200 Subject: [PATCH 01/33] [0001] [ndf-script] [version-upgrade] Upgrade to Python 3.11 : requirements --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 276df9a..1c6aba5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -PyAutoGUI~=0.9.53 -opencv-python~=4.5.5.64 -mss~=6.1.0 \ No newline at end of file +PyAutoGUI==0.9.54 +opencv-python==4.8.0.76 +mss==9.0.1 \ No newline at end of file From 52e2fdd6ce217b227ce26919d5c48c58bd5cab26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 28 Sep 2023 04:48:38 +0200 Subject: [PATCH 02/33] TODOs + init templates function --- main.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 87edc61..96df39d 100644 --- a/main.py +++ b/main.py @@ -7,14 +7,25 @@ from config.definitions import assets_dir + +# TODO: use DRY principle +# TODO: use type hints +# TODO: use doc comments for every function +# TODO: add logs through the script and generate a log file based on the running day +# TODO: may add unit test + +def init_templates() -> list: + return [cv2.imread(os.path.join(assets_dir, 'template1.png')), + cv2.imread(os.path.join(assets_dir, 'template2.png')), + cv2.imread(os.path.join(assets_dir, 'template3.png'))] + + if __name__ == '__main__': - print('NexusDownloadFlow 2022 starting...') + print('NexusDownloadFlow is starting...') print('Do not forget to replace the assets templates (1, 2 & 3) in order to match with the screenshots ' 'taken from your monitor!') try: - templates = [cv2.imread(os.path.join(assets_dir, 'template1.png')), - cv2.imread(os.path.join(assets_dir, 'template2.png')), - cv2.imread(os.path.join(assets_dir, 'template3.png'))] + templates: list = init_templates() with mss() as sct: while True: for i in range(1, 4): From 979316f1e3ce1f3f565bea909803eff6af4b5377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sun, 1 Oct 2023 19:25:19 +0200 Subject: [PATCH 03/33] [0002] [ndf-script] [version-upgrade] Upgrade to Python 3.11 : code --- main.py | 63 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/main.py b/main.py index 96df39d..87c2391 100644 --- a/main.py +++ b/main.py @@ -1,55 +1,70 @@ import os import time +from typing import Sequence -import pyautogui import cv2 +import pyautogui +from cv2.typing import MatLike from mss import mss +from mss.base import MSSBase from config.definitions import assets_dir -# TODO: use DRY principle -# TODO: use type hints # TODO: use doc comments for every function # TODO: add logs through the script and generate a log file based on the running day # TODO: may add unit test -def init_templates() -> list: +def init_templates() -> list[MatLike]: return [cv2.imread(os.path.join(assets_dir, 'template1.png')), cv2.imread(os.path.join(assets_dir, 'template2.png')), cv2.imread(os.path.join(assets_dir, 'template3.png'))] -if __name__ == '__main__': +def click_on_target(match_location: Sequence[int], template_shape: tuple[int, ...]) -> None: + top_left_x: int + top_left_y: int + top_left_x, top_left_y = match_location + target = (top_left_x + template_shape[1] / 2, top_left_y + template_shape[0] / 2) + pyautogui.leftClick(target) + + +def search_template(mss_instance: MSSBase, threshold: float) -> None: + template: MatLike + for template in init_templates(): + template_gray: MatLike = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) + screenshot: str = next(mss_instance.save(mon=-1, output='screenshot.png')) + screenshot_gray: MatLike = cv2.cvtColor(cv2.imread(screenshot), cv2.COLOR_BGR2GRAY) + match_template: MatLike = cv2.matchTemplate(screenshot_gray, template_gray, cv2.TM_SQDIFF) + min_value: float + min_loc: Sequence[int] + min_value, _, min_loc, _ = cv2.minMaxLoc(match_template) + if min_value < threshold: + print('Matching template!') + click_on_target(min_loc, template_gray.shape) + break + + +def main() -> None: print('NexusDownloadFlow is starting...') print('Do not forget to replace the assets templates (1, 2 & 3) in order to match with the screenshots ' 'taken from your monitor!') try: - templates: list = init_templates() - with mss() as sct: + threshold: int = 3000 + with mss() as mss_instance: while True: - for i in range(1, 4): - template = templates[i - 1] - template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) - screenshot = cv2.imread(sct.shot()) - screenshot_gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) - res = cv2.matchTemplate(screenshot_gray, template_gray, cv2.TM_SQDIFF) - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) - threshold = 3000 - if min_val < threshold: - print('Matching template!') - top_left = min_loc - target = (top_left[0] + template_gray.shape[1] / 2, top_left[1] + template_gray.shape[0] / 2) - pyautogui.leftClick(target) - break + search_template(mss_instance, threshold) time.sleep(6) except SystemExit: print('Exiting the program...') raise finally: - time.sleep(5) - if os.path.exists("monitor-1.png"): - os.remove("monitor-1.png") + if os.path.exists("screenshot.png"): + os.remove("screenshot.png") else: print("The file does not exist") print('Program ended') + + +if __name__ == '__main__': + main() From 4bdf76da476c791edac65fd64b1331884a855796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sun, 1 Oct 2023 20:56:42 +0200 Subject: [PATCH 04/33] [0003] [ndf-script] [version-upgrade] Upgrade to Python 3.11 : configuration --- config/config.toml | 12 ++++++++++++ config/definitions.py | 5 ++--- main.py | 10 ++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 config/config.toml diff --git a/config/config.toml b/config/config.toml new file mode 100644 index 0000000..6c286f5 --- /dev/null +++ b/config/config.toml @@ -0,0 +1,12 @@ +# Configuration file +name = "NexusDownloadFlow" +version = "2.0.0" +author = "greg-ynx" +release_date = 2023-10-01 + +[github] +owner = "greg-ynx" +repository = "https://github.com/greg-ynx/NexusDownloadFlow" + +[python] +version = "3.11.5" diff --git a/config/definitions.py b/config/definitions.py index 8bdc1a8..3e149e6 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -2,6 +2,5 @@ ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) -main_path = os.path.join(ROOT_DIR, 'main.py') -assets_dir = os.path.join(ROOT_DIR, 'assets') - +MAIN_PATH = os.path.join(ROOT_DIR, 'main.py') +ASSETS_DIR = os.path.join(ROOT_DIR, 'assets') diff --git a/main.py b/main.py index 87c2391..4b48de1 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from mss import mss from mss.base import MSSBase -from config.definitions import assets_dir +from config.definitions import ASSETS_DIR # TODO: use doc comments for every function @@ -16,17 +16,19 @@ # TODO: may add unit test def init_templates() -> list[MatLike]: - return [cv2.imread(os.path.join(assets_dir, 'template1.png')), - cv2.imread(os.path.join(assets_dir, 'template2.png')), - cv2.imread(os.path.join(assets_dir, 'template3.png'))] + return [cv2.imread(os.path.join(ASSETS_DIR, 'template1.png')), + cv2.imread(os.path.join(ASSETS_DIR, 'template2.png')), + cv2.imread(os.path.join(ASSETS_DIR, 'template3.png'))] def click_on_target(match_location: Sequence[int], template_shape: tuple[int, ...]) -> None: top_left_x: int top_left_y: int top_left_x, top_left_y = match_location + original_position = pyautogui.position() target = (top_left_x + template_shape[1] / 2, top_left_y + template_shape[0] / 2) pyautogui.leftClick(target) + pyautogui.moveTo(original_position) def search_template(mss_instance: MSSBase, threshold: float) -> None: From d8ad3a0f01f38d9f4cd5a490c2c9c143541b7630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Tue, 3 Oct 2023 19:11:24 +0200 Subject: [PATCH 05/33] change version to dev SNAPSHOT --- config/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.toml b/config/config.toml index 6c286f5..785c307 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,6 +1,6 @@ # Configuration file name = "NexusDownloadFlow" -version = "2.0.0" +version = "2.0.0-SNAPSHOT" author = "greg-ynx" release_date = 2023-10-01 From 2035b6ed4cdee1de02fe65b31122359de657a7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 6 Oct 2023 03:03:16 +0200 Subject: [PATCH 06/33] ruff linter added --- pyproject.toml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..826da3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[tool.ruff] +extend-select = [ + "W", + "I", + "N", + "D", + "S", + "T20", + "C4", + "SIM", + "TCH" +] +ignore = [ + "D203", + "D212" +] +fix = true +show-fixes = true +show-source = false +line-length = 120 +target-version = "py311" From a02fa88a36bdbf3e73623c89c4f7406acc4f0193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 6 Oct 2023 03:03:51 +0200 Subject: [PATCH 07/33] config file edited --- config/config.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.toml b/config/config.toml index 785c307..fc8070d 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,8 +1,9 @@ -# Configuration file +[project] name = "NexusDownloadFlow" version = "2.0.0-SNAPSHOT" author = "greg-ynx" -release_date = 2023-10-01 +release-date = 2023-10-01 +requires-python = ">=3.11" [github] owner = "greg-ynx" From f437ec6b1bc2b3649cb25ae1d0407da3d0061e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 6 Oct 2023 03:04:24 +0200 Subject: [PATCH 08/33] reformat --- config/definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/config/definitions.py b/config/definitions.py index 3e149e6..d6923ec 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -1,6 +1,5 @@ import os - ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) MAIN_PATH = os.path.join(ROOT_DIR, 'main.py') ASSETS_DIR = os.path.join(ROOT_DIR, 'assets') From 48dea73926386ef3fd43dbf3de075882b7ee4dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sat, 7 Oct 2023 14:11:23 +0200 Subject: [PATCH 09/33] [0013] [ndf-script] Project setup file --- config/config.toml | 13 ------------- pyproject.toml | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) delete mode 100644 config/config.toml diff --git a/config/config.toml b/config/config.toml deleted file mode 100644 index fc8070d..0000000 --- a/config/config.toml +++ /dev/null @@ -1,13 +0,0 @@ -[project] -name = "NexusDownloadFlow" -version = "2.0.0-SNAPSHOT" -author = "greg-ynx" -release-date = 2023-10-01 -requires-python = ">=3.11" - -[github] -owner = "greg-ynx" -repository = "https://github.com/greg-ynx/NexusDownloadFlow" - -[python] -version = "3.11.5" diff --git a/pyproject.toml b/pyproject.toml index 826da3b..1eae245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,21 @@ +[project] +name = "NexusDownloadFlow" +version = "2.0.0-SNAPSHOT" +authors = [ + {name = "Gregory Ployart", email = "greg.ynx@gmail.com", alias = "greg-ynx"}, +] +description = "Auto-clicker program to automate Nexus modlist downloads for free." +readme = "README.md" +requires-python = ">=3.11" +release-date = 2023-10-01 + +[github] +owner = "greg-ynx" +repository = "https://github.com/greg-ynx/NexusDownloadFlow" + +[python] +version = "3.11.5" + [tool.ruff] extend-select = [ "W", From 082ea6e6c6d9a59c159c94e2ad55e776ca49a938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sat, 7 Oct 2023 14:22:19 +0200 Subject: [PATCH 10/33] [0005] [ndf-script] [version-upgrade] Upgrade to Python 3.11 : documentation --- main.py | 62 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index 4b48de1..ce50394 100644 --- a/main.py +++ b/main.py @@ -10,18 +10,34 @@ from config.definitions import ASSETS_DIR - # TODO: use doc comments for every function # TODO: add logs through the script and generate a log file based on the running day # TODO: may add unit test + +SCREENSHOT: str = "screenshot.png" + + def init_templates() -> list[MatLike]: - return [cv2.imread(os.path.join(ASSETS_DIR, 'template1.png')), - cv2.imread(os.path.join(ASSETS_DIR, 'template2.png')), - cv2.imread(os.path.join(ASSETS_DIR, 'template3.png'))] + """ + Return the list of templates. + + :return: list of templates + """ + return [ + cv2.imread(os.path.join(ASSETS_DIR, "template1.png")), + cv2.imread(os.path.join(ASSETS_DIR, "template2.png")), + cv2.imread(os.path.join(ASSETS_DIR, "template3.png")) + ] def click_on_target(match_location: Sequence[int], template_shape: tuple[int, ...]) -> None: + """ + Click on the target that has been identified and move the cursor to its previous location. + + :param Sequence[int] match_location: the coordinates of the pixels located at the top left of the matched image. + :param tuple[int, ...] template_shape: the shape of the corresponding template + """ top_left_x: int top_left_y: int top_left_x, top_left_y = match_location @@ -32,41 +48,57 @@ def click_on_target(match_location: Sequence[int], template_shape: tuple[int, .. def search_template(mss_instance: MSSBase, threshold: float) -> None: + """ + Search and identify an image matching any templates with the specified threshold. + + :param MSSBase mss_instance: an instance of MSSBase + :param float threshold: the threshold required to identify a match. + """ template: MatLike for template in init_templates(): template_gray: MatLike = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) - screenshot: str = next(mss_instance.save(mon=-1, output='screenshot.png')) + screenshot: str = next(mss_instance.save(mon=-1, output=SCREENSHOT)) screenshot_gray: MatLike = cv2.cvtColor(cv2.imread(screenshot), cv2.COLOR_BGR2GRAY) match_template: MatLike = cv2.matchTemplate(screenshot_gray, template_gray, cv2.TM_SQDIFF) min_value: float min_loc: Sequence[int] min_value, _, min_loc, _ = cv2.minMaxLoc(match_template) if min_value < threshold: - print('Matching template!') + print("Matching template!") click_on_target(min_loc, template_gray.shape) + time.sleep(6) break def main() -> None: - print('NexusDownloadFlow is starting...') - print('Do not forget to replace the assets templates (1, 2 & 3) in order to match with the screenshots ' - 'taken from your monitor!') + """ + NexusDownloadFlow main function. + + :raise SystemExit: raised when the window is closed + """ + print("NexusDownloadFlow is starting...") + print( + "Do not forget to replace the assets templates (1, 2 & 3) in order to match with the screenshots " + "taken from your monitor!" + ) try: threshold: int = 3000 with mss() as mss_instance: while True: search_template(mss_instance, threshold) - time.sleep(6) except SystemExit: - print('Exiting the program...') + print("Exiting the program...") raise + # except FailSafeException: + # # log error + # raise finally: - if os.path.exists("screenshot.png"): - os.remove("screenshot.png") + if os.path.exists(SCREENSHOT): + os.remove(SCREENSHOT) else: print("The file does not exist") - print('Program ended') + print("Program ended") -if __name__ == '__main__': +if __name__ == "__main__": main() From 545eb40b2527187c781feb0d3b7a24f3aa3b0f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sat, 7 Oct 2023 15:05:59 +0200 Subject: [PATCH 11/33] [0004] [ndf-script] [version-upgrade] Upgrade to Python 3.11 : templates --- assets/template1.png | Bin 2314 -> 2159 bytes assets/template2.png | Bin 2350 -> 1644 bytes assets/template3.png | Bin 2150 -> 1373 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/template1.png b/assets/template1.png index 98a699f664e368b7e7eca4ab267a430958de2f5f..30f0e690494774d9b209a6435244713cfdc329d1 100644 GIT binary patch delta 2144 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 P00000NkvXXu0mjfTEmEj diff --git a/assets/template3.png b/assets/template3.png index 0bbd9a313a14e5719e4e603e656871f30ffcd18e..b04bd5cf26725238d0f979792d2e538c112b9b03 100644 GIT binary patch literal 1373 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 Date: Sat, 7 Oct 2023 16:02:47 +0200 Subject: [PATCH 12/33] [0006] [ndf-script] [version-upgrade] Upgrade to Python 3.11 : readme --- README.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4fdc92a..3ad76c7 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,33 @@ -# NexusDownloadFlow 2022 : Auto clicker script using computer vision +# NexusDownloadFlow: Auto-downloader for Nexus Mods -NexusDownloaderFlow (NDF) 2022 is a script that take screenshots and classify if any template match with the current -screenshot taken. It was made in order to automate process with `Wabbajack modlist installation of Nexus' mods` in which -you have to manually click on `Slow download` button is your NexusMods account is not premium. +NexusDownloadFlow (NDF) is a program that automates the download process with `Wabbajack modlist installation of Nexus +Mods` in which you have to manually click on `Slow download` button if your Nexus Mods account is not premium. -## How to use NDF 2022 ? +## How to use NexusDownloadFlow? -Just execute `NexusDownloadFlow 2022.exe` and open your NexusMods' download page. +### Without Wabbajack +Execute `NexusDownloadFlow.exe` and open your Nexus Mods download page. -## Auto clicker is not clicking +### With Wabbajack +Execute `NexusDownloadFlow.exe` while the mod list is downloading. -Do not worry, you have to replace the templates files where you installed NDF with the one you will screenshot: +## Auto-clicker is not clicking + +Do not worry, you have to replace the template files where you installed NDF with the one you will screenshot: `NexusDownloadFlow 2022/assets/template{x}.png` + `template1.png` is the raw `Slow download` button + `template2.png` is the `Slow download` button with mouse over + `template3.png` is the `Click here` link appearing five seconds after clicking on `Slow download` button -## Credits +If your issue persists, maximize the Nexus Mods page. + +## Your issue still persist? -Thanks to @parsiad for inspiring me with his repository named `parsiad/nexus-autodl` -(I could not download his auto clicker). +Open an issue here, and if possible, give the scenario in which you had this issue, which version of NDF you are using +and provide a screenshot of your logs or the contents of your current `nexus-download-flow-logs.log` file. + +## Credits -Requirements used for this script are : -+ PyAutoGUI~=0.9.53 -+ opencv-python==4.5.5.64 -+ mss~=6.1.0 \ No newline at end of file +Thanks to [parsiad](https://github.com/parsiad) for inspiring me with his repository named +[`parsiad/nexus-autodl`](https://github.com/parsiad/nexus-autodl). From c706cd45e415fb5d7e4d80f5551bf68833608ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sat, 7 Oct 2023 16:05:46 +0200 Subject: [PATCH 13/33] reformat readme file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ad76c7..84c8e33 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NexusDownloadFlow: Auto-downloader for Nexus Mods -NexusDownloadFlow (NDF) is a program that automates the download process with `Wabbajack modlist installation of Nexus +NexusDownloadFlow (NDF) is a program that automates the download process with `Wabbajack modlist installation of Nexus Mods` in which you have to manually click on `Slow download` button if your Nexus Mods account is not premium. ## How to use NexusDownloadFlow? From 8507ea4ff0cdaa568eb8ba0244b9b90d04c5e176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sat, 7 Oct 2023 16:07:08 +0200 Subject: [PATCH 14/33] link for issue in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84c8e33..0aad65e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If your issue persists, maximize the Nexus Mods page. ## Your issue still persist? -Open an issue here, and if possible, give the scenario in which you had this issue, which version of NDF you are using +Open an issue [here](https://github.com/greg-ynx/NexusDownloadFlow/issues/new), and if possible, give the scenario in which you had this issue, which version of NDF you are using and provide a screenshot of your logs or the contents of your current `nexus-download-flow-logs.log` file. ## Credits From 6d4449a8f4d32bc70c32670cf3853fd238f75d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sat, 7 Oct 2023 16:08:45 +0200 Subject: [PATCH 15/33] 3rd level titles refactor --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0aad65e..f0f81f4 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,11 @@ Mods` in which you have to manually click on `Slow download` button if your Nexu ## How to use NexusDownloadFlow? ### Without Wabbajack + Execute `NexusDownloadFlow.exe` and open your Nexus Mods download page. ### With Wabbajack + Execute `NexusDownloadFlow.exe` while the mod list is downloading. ## Auto-clicker is not clicking From f6e6d5d1bf88a093b71c8adaba5c9c0bd9604c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sun, 8 Oct 2023 00:59:56 +0200 Subject: [PATCH 16/33] [0011] [ndf-script] ASCII art --- config/ascii_art.py | 29 +++++++++++++++++++++++++++++ config/definitions.py | 10 +++++++--- main.py | 10 ++++++---- pyproject.toml | 3 ++- 4 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 config/ascii_art.py diff --git a/config/ascii_art.py b/config/ascii_art.py new file mode 100644 index 0000000..bc11f34 --- /dev/null +++ b/config/ascii_art.py @@ -0,0 +1,29 @@ +"""Print NexusDownloadFlow ascii art.""" +import sys +from typing import Any + +from config.definitions import PYPROJECT_DATA + +ASCII_COLOR: str = "\033[33m" +ASCII_TEXT: str = """ + _ _ +| \\ | | +| \\| | _____ ___ _ ___ +| . ` |/ _ \\ \\/ / | | / __| +| |\\ | __/> <| |_| \\__ \\ +\\_| \\_/\\___/_/\\_\\__,_|___/ +______ _ _ ______ _ +| _ \\ | | | | | ___| | +| | | |_____ ___ __ | | ___ __ _ __| | | |_ | | _____ __ +| | | / _ \\ \\ /\\ / / '_ \\| |/ _ \\ / _` |/ _` | | _| | |/ _ \\ \\ /\\ / / +| |/ / (_) \\ V V /| | | | | (_) | (_| | (_| | | | | | (_) \\ V V / +|___/ \\___/ \\_/\\_/ |_| |_|_|\\___/ \\__,_|\\__,_| \\_| |_|\\___/ \\_/\\_/\ +""" + +PROJECT_DATA: Any = PYPROJECT_DATA.get("project") +PROJECT_VERSION: str = "v{0}".format(str(PROJECT_DATA.get("version"))) + + +def print_ascii_art() -> None: + sys.stdout.write(ASCII_COLOR + ASCII_TEXT + PROJECT_VERSION + "\033[0m\n") +print_ascii_art() diff --git a/config/definitions.py b/config/definitions.py index d6923ec..90bd840 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -1,5 +1,9 @@ import os +import tomllib +from typing import Any -ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) -MAIN_PATH = os.path.join(ROOT_DIR, 'main.py') -ASSETS_DIR = os.path.join(ROOT_DIR, 'assets') +ROOT_DIRECTORY: str = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) +MAIN_PATH: str = os.path.join(ROOT_DIRECTORY, 'main.py') +ASSETS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, 'assets') + +PYPROJECT_DATA: dict[str, Any] = tomllib.load(open(ROOT_DIRECTORY + "/pyproject.toml", "rb")) diff --git a/main.py b/main.py index ce50394..a5f3955 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,8 @@ from mss import mss from mss.base import MSSBase -from config.definitions import ASSETS_DIR +from config.ascii_art import print_ascii_art +from config.definitions import ASSETS_DIRECTORY # TODO: use doc comments for every function # TODO: add logs through the script and generate a log file based on the running day @@ -25,9 +26,9 @@ def init_templates() -> list[MatLike]: :return: list of templates """ return [ - cv2.imread(os.path.join(ASSETS_DIR, "template1.png")), - cv2.imread(os.path.join(ASSETS_DIR, "template2.png")), - cv2.imread(os.path.join(ASSETS_DIR, "template3.png")) + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")) ] @@ -76,6 +77,7 @@ def main() -> None: :raise SystemExit: raised when the window is closed """ + print_ascii_art() print("NexusDownloadFlow is starting...") print( "Do not forget to replace the assets templates (1, 2 & 3) in order to match with the screenshots " diff --git a/pyproject.toml b/pyproject.toml index 1eae245..19615f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "2.0.0-SNAPSHOT" authors = [ {name = "Gregory Ployart", email = "greg.ynx@gmail.com", alias = "greg-ynx"}, ] -description = "Auto-clicker program to automate Nexus modlist downloads for free." +description = "Auto-downloader program to automate Nexus modlist downloads for free." readme = "README.md" requires-python = ">=3.11" release-date = 2023-10-01 @@ -12,6 +12,7 @@ release-date = 2023-10-01 [github] owner = "greg-ynx" repository = "https://github.com/greg-ynx/NexusDownloadFlow" +issues = "https://github.com/greg-ynx/NexusDownloadFlow/issues" [python] version = "3.11.5" From 5dfd8d4169791670d729d4676a245cf09f8733b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Sun, 8 Oct 2023 18:58:55 +0200 Subject: [PATCH 17/33] [0010] [ndf-script] Work on multiple monitors --- config/ascii_art.py | 2 +- config/definitions.py | 10 ++++++---- main.py | 7 ++++++- pyproject.toml | 15 +++++++++++++-- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/config/ascii_art.py b/config/ascii_art.py index bc11f34..a0f59fa 100644 --- a/config/ascii_art.py +++ b/config/ascii_art.py @@ -25,5 +25,5 @@ def print_ascii_art() -> None: + """Print NexusDownloadFlow ascii art with project version.""" sys.stdout.write(ASCII_COLOR + ASCII_TEXT + PROJECT_VERSION + "\033[0m\n") -print_ascii_art() diff --git a/config/definitions.py b/config/definitions.py index 90bd840..69a2fbd 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -1,9 +1,11 @@ +"""Define global constants used in the project.""" import os import tomllib from typing import Any -ROOT_DIRECTORY: str = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) -MAIN_PATH: str = os.path.join(ROOT_DIRECTORY, 'main.py') -ASSETS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, 'assets') +ROOT_DIRECTORY: str = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) +MAIN_PATH: str = os.path.join(ROOT_DIRECTORY, "main.py") +ASSETS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, "assets") -PYPROJECT_DATA: dict[str, Any] = tomllib.load(open(ROOT_DIRECTORY + "/pyproject.toml", "rb")) +with open(ROOT_DIRECTORY + "/pyproject.toml", "rb") as pyproject: + PYPROJECT_DATA: dict[str, Any] = tomllib.load(pyproject) diff --git a/main.py b/main.py index a5f3955..0a22d35 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ +"""Main executable file of NexusDownloadFlow.""" import os import time -from typing import Sequence +from typing import Optional, Sequence import cv2 import pyautogui @@ -57,6 +58,8 @@ def search_template(mss_instance: MSSBase, threshold: float) -> None: """ template: MatLike for template in init_templates(): + monitors_size: dict[str, int] = mss_instance.monitors[0] + monitors_left_top: Sequence[Optional[int]] = (monitors_size.get('left'), monitors_size.get('top')) template_gray: MatLike = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) screenshot: str = next(mss_instance.save(mon=-1, output=SCREENSHOT)) screenshot_gray: MatLike = cv2.cvtColor(cv2.imread(screenshot), cv2.COLOR_BGR2GRAY) @@ -64,6 +67,8 @@ def search_template(mss_instance: MSSBase, threshold: float) -> None: min_value: float min_loc: Sequence[int] min_value, _, min_loc, _ = cv2.minMaxLoc(match_template) + if monitors_left_top[0] is not None and monitors_left_top[1] is not None: + min_loc = (min_loc[0] + monitors_left_top[0], min_loc[1] + monitors_left_top[1]) if min_value < threshold: print("Matching template!") click_on_target(min_loc, template_gray.shape) diff --git a/pyproject.toml b/pyproject.toml index 19615f2..a1832d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,13 +9,24 @@ readme = "README.md" requires-python = ">=3.11" release-date = 2023-10-01 +[python] +version = "3.11.5" + [github] owner = "greg-ynx" repository = "https://github.com/greg-ynx/NexusDownloadFlow" issues = "https://github.com/greg-ynx/NexusDownloadFlow/issues" -[python] -version = "3.11.5" +[tool.mypy] +python_version = "3.11" +disallow_untyped_defs = true +disallow_any_unimported = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = true + [tool.ruff] extend-select = [ From fc53b309df45c050dbd405fd276cacec8003bf5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Wed, 11 Oct 2023 21:30:56 +0200 Subject: [PATCH 18/33] working snapshot of ndf --- README.md | 2 +- main.py | 29 ++++++++++++++++++++++------- pyproject.toml | 1 - 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f0f81f4..6ba94e6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Execute `NexusDownloadFlow.exe` while the mod list is downloading. ## Auto-clicker is not clicking Do not worry, you have to replace the template files where you installed NDF with the one you will screenshot: -`NexusDownloadFlow 2022/assets/template{x}.png` +`NexusDownloadFlow/assets/template{x}.png` + `template1.png` is the raw `Slow download` button + `template2.png` is the `Slow download` button with mouse over diff --git a/main.py b/main.py index 0a22d35..fc93697 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,10 @@ """Main executable file of NexusDownloadFlow.""" import os import time -from typing import Optional, Sequence +from typing import Any, Optional, Sequence import cv2 +import numpy as np import pyautogui from cv2.typing import MatLike from mss import mss @@ -12,15 +13,21 @@ from config.ascii_art import print_ascii_art from config.definitions import ASSETS_DIRECTORY -# TODO: use doc comments for every function # TODO: add logs through the script and generate a log file based on the running day # TODO: may add unit test SCREENSHOT: str = "screenshot.png" +CHUNK_SLICES: list[float] = [ + 1., .95789474, .91578947, .87368421, .83157895, .78947368, + .74736842, .70526316, .66315789, .62105263, .57894737, .53684211, + .49473684, .45263158, .41052632, .36842105, .32631579, .28421053, + .24210526, .2 +] +THRESHOLD: int = 3000 -def init_templates() -> list[MatLike]: +def load_templates() -> list[MatLike]: """ Return the list of templates. @@ -29,10 +36,19 @@ def init_templates() -> list[MatLike]: return [ cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")) + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), ] +def init_templates() -> list[MatLike]: + """ + Return the list of edges for each template. + + :return: list of templates' edges + """ + return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in load_templates().copy()] + + def click_on_target(match_location: Sequence[int], template_shape: tuple[int, ...]) -> None: """ Click on the target that has been identified and move the cursor to its previous location. @@ -57,7 +73,7 @@ def search_template(mss_instance: MSSBase, threshold: float) -> None: :param float threshold: the threshold required to identify a match. """ template: MatLike - for template in init_templates(): + for template in load_templates(): monitors_size: dict[str, int] = mss_instance.monitors[0] monitors_left_top: Sequence[Optional[int]] = (monitors_size.get('left'), monitors_size.get('top')) template_gray: MatLike = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) @@ -89,10 +105,9 @@ def main() -> None: "taken from your monitor!" ) try: - threshold: int = 3000 with mss() as mss_instance: while True: - search_template(mss_instance, threshold) + search_template(mss_instance, THRESHOLD) except SystemExit: print("Exiting the program...") raise diff --git a/pyproject.toml b/pyproject.toml index a1832d3..5840f78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ warn_return_any = true show_error_codes = true warn_unused_ignores = true - [tool.ruff] extend-select = [ "W", From 9ff0ee68214f966a59876871198844dcbce1de13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Mon, 30 Oct 2023 19:58:02 +0100 Subject: [PATCH 19/33] [0019] [ndf-script] [test] grayscale template matching algorithm demo --- test/demo/ndf_1.0.0_template_matching_demo.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/demo/ndf_1.0.0_template_matching_demo.py diff --git a/test/demo/ndf_1.0.0_template_matching_demo.py b/test/demo/ndf_1.0.0_template_matching_demo.py new file mode 100644 index 0000000..8b6d0e3 --- /dev/null +++ b/test/demo/ndf_1.0.0_template_matching_demo.py @@ -0,0 +1,76 @@ +""" +This test file is used to test NexusDownloadFlow's v1.0.0 algorithm. +The algorithm used is the grayscale template matching and the TM_SQDIFF comparison method from OpenCV. +""" +import os +import time + +import cv2 +import pyautogui +from cv2.typing import MatLike +from mss import mss + +from config.definitions import ASSETS_DIRECTORY + + +TEST_TEXT: str = "[TEST] [1.0.0] " +SCREENSHOT: str = "screenshot.png" +THRESHOLD: int = 3000 + + +def logging_test(text: str) -> None: + print(TEST_TEXT + text) + + +def load_templates() -> list[MatLike]: + return [ + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), + ] + + +def init_templates() -> list[MatLike]: + return [cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) for template in load_templates()] + + +def test_algorithm() -> None: + logging_test("ndf-1.0.0-grayscale-template-matching.") + logging_test("Comparaison method: TM_SQDIFF.") + try: + with mss() as mss_instance: + while True: + monitors_size = mss_instance.monitors[0] + monitors_left_top = (monitors_size.get("left"), monitors_size.get("top")) + screenshot = cv2.imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) + screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) + for template in init_templates(): + match_template = cv2.matchTemplate(screenshot, template, cv2.TM_SQDIFF) + min_value, _, min_location, _ = cv2.minMaxLoc(match_template) + if min_value < THRESHOLD: + logging_test("Match found!") + match_left_top_location = ( + min_location[0] + monitors_left_top[0], + min_location[1] + monitors_left_top[1], + ) + template_height, template_width = template.shape + target = ( + match_left_top_location[0] + template_width / 2, + match_left_top_location[1] + template_height / 2, + ) + pyautogui.moveTo(target) + time.sleep(6) + break + except SystemExit: + logging_test("Exiting the program...") + raise + finally: + if os.path.exists(SCREENSHOT): + os.remove(SCREENSHOT) + else: + logging_test("The file does not exist") + logging_test("Program ended") + + +if __name__ == "__main__": + test_algorithm() From 31e22f589131d195a1857cd6e67fa57121731d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Mon, 30 Oct 2023 19:59:08 +0100 Subject: [PATCH 20/33] [0020] [ndf-script] [test] edges template matching algorithm demo --- ...f_2.0.0-snapshot_template_matching_demo.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/demo/ndf_2.0.0-snapshot_template_matching_demo.py diff --git a/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py b/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py new file mode 100644 index 0000000..b850a1a --- /dev/null +++ b/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py @@ -0,0 +1,77 @@ +""" +This test file is used to test NexusDownloadFlow's v2.0.0-SNAPSHOT algorithm. +The algorithm used is the edges template matching and the TM_CCOEFF_NORMED comparison method from OpenCV. +""" +import os +import time + +import cv2 +import pyautogui +from cv2.typing import MatLike +from mss import mss + +from config.definitions import ASSETS_DIRECTORY + + +SCREENSHOT: str = "screenshot.png" +TEST_TEXT: str = "[TEST] [2.0.0-SNAPSHOT] " +THRESHOLD: float = .65 + + +def logging_test(text: str) -> None: + print(TEST_TEXT + text) + + +def load_templates() -> list[MatLike]: + return [ + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), + ] + + +def init_templates() -> list[MatLike]: + return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in load_templates()] + + +def test_algorithm() -> None: + logging_test("ndf-2.0.0-snapshot-edges-template-matching.") + logging_test("Comparaison method: TM_CCOEFF_NORMED.") + try: + with mss() as mss_instance: + while True: + monitors_size = mss_instance.monitors[0] + monitors_left_top = (monitors_size.get("left"), monitors_size.get("top")) + screenshot = cv2.imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) + screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) + screenshot = cv2.Canny(screenshot, 50, 200) + for template in init_templates(): + match_template = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED) + _, max_value, _, max_location = cv2.minMaxLoc(match_template) + if max_value > THRESHOLD: + logging_test("Match found!") + match_left_top_location = ( + max_location[0] + monitors_left_top[0], + max_location[1] + monitors_left_top[1], + ) + template_height, template_width = template.shape + target = ( + match_left_top_location[0] + template_width / 2, + match_left_top_location[1] + template_height / 2, + ) + pyautogui.moveTo(target) + time.sleep(6) + break + except SystemExit: + logging_test("Exiting the program...") + raise + finally: + if os.path.exists(SCREENSHOT): + os.remove(SCREENSHOT) + else: + logging_test("The file does not exist") + logging_test("Program ended") + + +if __name__ == "__main__": + test_algorithm() From a0163029ea2d13292a79b452ee76c060314a845a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Mon, 30 Oct 2023 20:00:36 +0100 Subject: [PATCH 21/33] [0021] [ndf-script] [test] multiscaled template matching algorithm demo --- test/demo/ndf_2.0.0_template_matching_demo.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/demo/ndf_2.0.0_template_matching_demo.py diff --git a/test/demo/ndf_2.0.0_template_matching_demo.py b/test/demo/ndf_2.0.0_template_matching_demo.py new file mode 100644 index 0000000..6ee2d21 --- /dev/null +++ b/test/demo/ndf_2.0.0_template_matching_demo.py @@ -0,0 +1,106 @@ +""" +This test file is used to test NexusDownloadFlow's v2.0.0 algorithm. +The algorithm used is the multiscale template matching and the TM_CCOEFF_NORMED comparison method from OpenCV. +""" +import os +import time +from typing import Any + +import cv2 +import pyautogui +from cv2.typing import MatLike +from mss import mss + +from config.definitions import ASSETS_DIRECTORY + +CHUNK_SLICES: list[float] = [ + 1.0, + 0.95789474, + 0.91578947, + 0.87368421, + 0.83157895, + 0.78947368, + 0.74736842, + 0.70526316, + 0.66315789, + 0.62105263, + 0.57894737, + 0.53684211, + 0.49473684, + 0.45263158, + 0.41052632, + 0.36842105, + 0.32631579, + 0.28421053, + 0.24210526, + 0.2, +] +TEST_TEXT: str = "[TEST] [2.0.0] " +THRESHOLD: float = .65 +SCREENSHOT: str = "screenshot.png" + + +def logging_test(text: str) -> None: + print(TEST_TEXT + text) + + +def load_templates() -> list[MatLike]: + return [ + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), + ] + + +def init_templates() -> list[MatLike]: + return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in load_templates()] + + +def multiscale_template_matching(screenshot: Any, left_top_coordinates: Any) -> None: + for template in init_templates(): + for scale in CHUNK_SLICES: + dsize_x, dsize_y = (int(screenshot.shape[1] * scale), int(screenshot.shape[0] * scale)) + resized_screenshot = cv2.resize(screenshot, (dsize_x, dsize_y)) + edges = cv2.Canny(resized_screenshot, 50, 200) + match_template = cv2.matchTemplate(edges, template, cv2.TM_CCOEFF_NORMED) + _, max_value, _, max_location = cv2.minMaxLoc(match_template) + if max_value > THRESHOLD: + logging_test("Match found!") + match_left_top_location = ( + max_location[0] + left_top_coordinates[0], + max_location[1] + left_top_coordinates[1], + ) + template_height, template_width = template.shape + target = ( + match_left_top_location[0] + template_width / 2, + match_left_top_location[1] + template_height / 2, + ) + pyautogui.moveTo(target) + time.sleep(6) + return + + +def test_algorithm() -> None: + logging_test("ndf-2.0.0-multiscale-template-matching.") + logging_test("Comparaison method: TM_CCOEFF_NORMED.") + try: + with mss() as mss_instance: + while True: + monitors_size = mss_instance.monitors[0] + monitors_left_top = (monitors_size.get("left"), monitors_size.get("top")) + screenshot = cv2.imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) + screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) + multiscale_template_matching(screenshot, monitors_left_top) + except SystemExit: + logging_test("Exiting the program...") + raise + finally: + if os.path.exists(SCREENSHOT): + os.remove(SCREENSHOT) + else: + logging_test("The file does not exist") + logging_test("Program ended") + + +if __name__ == "__main__": + test_algorithm() From f01bf572abda635f75eaadfc5b28cfeb4bfa9958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 2 Nov 2023 03:27:15 +0100 Subject: [PATCH 22/33] [0021] [ndf-script] [test] multiscale template matching algorithm demo --- test/demo/ndf_1.0.0_template_matching_demo.py | 36 ++--- ...f_2.0.0-snapshot_template_matching_demo.py | 35 ++--- test/demo/ndf_2.0.0_template_matching_demo.py | 133 +++++++++++------- 3 files changed, 121 insertions(+), 83 deletions(-) diff --git a/test/demo/ndf_1.0.0_template_matching_demo.py b/test/demo/ndf_1.0.0_template_matching_demo.py index 8b6d0e3..6c42370 100644 --- a/test/demo/ndf_1.0.0_template_matching_demo.py +++ b/test/demo/ndf_1.0.0_template_matching_demo.py @@ -1,8 +1,11 @@ """ -This test file is used to test NexusDownloadFlow's v1.0.0 algorithm. +Test file is used to test NexusDownloadFlow's v1.0.0 algorithm. + The algorithm used is the grayscale template matching and the TM_SQDIFF comparison method from OpenCV. """ + import os +import sys import time import cv2 @@ -12,17 +15,16 @@ from config.definitions import ASSETS_DIRECTORY - -TEST_TEXT: str = "[TEST] [1.0.0] " SCREENSHOT: str = "screenshot.png" +TEST_TEXT: str = "[TEST] [1.0.0] " THRESHOLD: int = 3000 -def logging_test(text: str) -> None: - print(TEST_TEXT + text) +def _logging_test(text: str) -> None: + sys.stdout.write(TEST_TEXT + text + "\n") -def load_templates() -> list[MatLike]: +def _load_templates() -> list[MatLike]: return [ cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), @@ -30,13 +32,13 @@ def load_templates() -> list[MatLike]: ] -def init_templates() -> list[MatLike]: - return [cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) for template in load_templates()] +def _init_templates() -> list[MatLike]: + return [cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) for template in _load_templates()] -def test_algorithm() -> None: - logging_test("ndf-1.0.0-grayscale-template-matching.") - logging_test("Comparaison method: TM_SQDIFF.") +def _test_algorithm() -> None: + _logging_test("ndf-1.0.0-grayscale-template-matching.") + _logging_test("Comparaison method: TM_SQDIFF.") try: with mss() as mss_instance: while True: @@ -44,11 +46,11 @@ def test_algorithm() -> None: monitors_left_top = (monitors_size.get("left"), monitors_size.get("top")) screenshot = cv2.imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) - for template in init_templates(): + for template in _init_templates(): match_template = cv2.matchTemplate(screenshot, template, cv2.TM_SQDIFF) min_value, _, min_location, _ = cv2.minMaxLoc(match_template) if min_value < THRESHOLD: - logging_test("Match found!") + _logging_test("Match found!") match_left_top_location = ( min_location[0] + monitors_left_top[0], min_location[1] + monitors_left_top[1], @@ -62,15 +64,15 @@ def test_algorithm() -> None: time.sleep(6) break except SystemExit: - logging_test("Exiting the program...") + _logging_test("Exiting the program...") raise finally: if os.path.exists(SCREENSHOT): os.remove(SCREENSHOT) else: - logging_test("The file does not exist") - logging_test("Program ended") + _logging_test("The file does not exist") + _logging_test("Program ended") if __name__ == "__main__": - test_algorithm() + _test_algorithm() diff --git a/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py b/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py index b850a1a..a4064b3 100644 --- a/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py +++ b/test/demo/ndf_2.0.0-snapshot_template_matching_demo.py @@ -1,8 +1,10 @@ """ -This test file is used to test NexusDownloadFlow's v2.0.0-SNAPSHOT algorithm. +Test file is used to test NexusDownloadFlow's v2.0.0-SNAPSHOT algorithm. + The algorithm used is the edges template matching and the TM_CCOEFF_NORMED comparison method from OpenCV. """ import os +import sys import time import cv2 @@ -12,17 +14,16 @@ from config.definitions import ASSETS_DIRECTORY - SCREENSHOT: str = "screenshot.png" TEST_TEXT: str = "[TEST] [2.0.0-SNAPSHOT] " -THRESHOLD: float = .65 +THRESHOLD: float = 0.65 -def logging_test(text: str) -> None: - print(TEST_TEXT + text) +def _logging_test(text: str) -> None: + sys.stdout.write(TEST_TEXT + text + "\n") -def load_templates() -> list[MatLike]: +def _load_templates() -> list[MatLike]: return [ cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), @@ -30,13 +31,13 @@ def load_templates() -> list[MatLike]: ] -def init_templates() -> list[MatLike]: - return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in load_templates()] +def _init_templates() -> list[MatLike]: + return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in _load_templates()] -def test_algorithm() -> None: - logging_test("ndf-2.0.0-snapshot-edges-template-matching.") - logging_test("Comparaison method: TM_CCOEFF_NORMED.") +def _test_algorithm() -> None: + _logging_test("ndf-2.0.0-snapshot-edges-template-matching.") + _logging_test("Comparaison method: TM_CCOEFF_NORMED.") try: with mss() as mss_instance: while True: @@ -45,11 +46,11 @@ def test_algorithm() -> None: screenshot = cv2.imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) screenshot = cv2.Canny(screenshot, 50, 200) - for template in init_templates(): + for template in _init_templates(): match_template = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED) _, max_value, _, max_location = cv2.minMaxLoc(match_template) if max_value > THRESHOLD: - logging_test("Match found!") + _logging_test("Match found!") match_left_top_location = ( max_location[0] + monitors_left_top[0], max_location[1] + monitors_left_top[1], @@ -63,15 +64,15 @@ def test_algorithm() -> None: time.sleep(6) break except SystemExit: - logging_test("Exiting the program...") + _logging_test("Exiting the program...") raise finally: if os.path.exists(SCREENSHOT): os.remove(SCREENSHOT) else: - logging_test("The file does not exist") - logging_test("Program ended") + _logging_test("The file does not exist") + _logging_test("Program ended") if __name__ == "__main__": - test_algorithm() + _test_algorithm() diff --git a/test/demo/ndf_2.0.0_template_matching_demo.py b/test/demo/ndf_2.0.0_template_matching_demo.py index 6ee2d21..68020f0 100644 --- a/test/demo/ndf_2.0.0_template_matching_demo.py +++ b/test/demo/ndf_2.0.0_template_matching_demo.py @@ -1,19 +1,23 @@ """ -This test file is used to test NexusDownloadFlow's v2.0.0 algorithm. +Test file is used to test NexusDownloadFlow's v2.0.0 algorithm. + The algorithm used is the multiscale template matching and the TM_CCOEFF_NORMED comparison method from OpenCV. """ import os -import time -from typing import Any +import sys +from time import sleep +from typing import Sequence, cast -import cv2 -import pyautogui +from cv2 import COLOR_BGR2GRAY, TM_CCOEFF_NORMED, Canny, cvtColor, imread, matchTemplate, minMaxLoc, resize from cv2.typing import MatLike from mss import mss +from pyautogui import moveTo from config.definitions import ASSETS_DIRECTORY -CHUNK_SLICES: list[float] = [ +EDGE_MIN_VALUE: int = 50 +EDGE_MAX_VALUE: int = 200 +SCALES: list[float] = [ 1.0, 0.95789474, 0.91578947, @@ -35,72 +39,103 @@ 0.24210526, 0.2, ] -TEST_TEXT: str = "[TEST] [2.0.0] " -THRESHOLD: float = .65 SCREENSHOT: str = "screenshot.png" +TEMPLATES: list[MatLike] = [ + imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), +] +TEST_TEXT: str = "[TEST] [2.0.0] " +THRESHOLD: float = 0.65 + + +def _logging_test(text: str) -> None: + sys.stdout.write(TEST_TEXT + text + "\n") + + +def _init_templates() -> list[MatLike]: + return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] + + +def _resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: + new_width: int = int(screenshot.shape[1] * scale) + new_height: int = int(screenshot.shape[0] * scale) + return cast(MatLike, resize(screenshot, (new_width, new_height))) -def logging_test(text: str) -> None: - print(TEST_TEXT + text) +def _get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, Sequence[int]]: + matches: MatLike = matchTemplate(screenshot, template, TM_CCOEFF_NORMED) + potential_match: tuple[float, float, Sequence[int], Sequence[int]] = minMaxLoc(matches) + max_value: float = potential_match[1] + max_location: Sequence[int] = potential_match[3] + return max_value, max_location -def load_templates() -> list[MatLike]: - return [ - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), - ] +def _if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, int]: + monitors_left: int | None = monitors_size.get("left") + monitors_top: int | None = monitors_size.get("top") + if monitors_left is None: + raise ValueError("monitors_size 'left' value is None") + if monitors_top is None: + raise ValueError("monitors_size 'top' value is None") + return monitors_left, monitors_top -def init_templates() -> list[MatLike]: - return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in load_templates()] +def _is_match_found(match_value: float) -> bool: + return match_value > THRESHOLD -def multiscale_template_matching(screenshot: Any, left_top_coordinates: Any) -> None: - for template in init_templates(): - for scale in CHUNK_SLICES: - dsize_x, dsize_y = (int(screenshot.shape[1] * scale), int(screenshot.shape[0] * scale)) - resized_screenshot = cv2.resize(screenshot, (dsize_x, dsize_y)) - edges = cv2.Canny(resized_screenshot, 50, 200) - match_template = cv2.matchTemplate(edges, template, cv2.TM_CCOEFF_NORMED) - _, max_value, _, max_location = cv2.minMaxLoc(match_template) - if max_value > THRESHOLD: - logging_test("Match found!") - match_left_top_location = ( - max_location[0] + left_top_coordinates[0], - max_location[1] + left_top_coordinates[1], +def _multiscale_match_template( + templates: list[MatLike], screenshot: MatLike, left_top_coordinates: tuple[int, int] +) -> None: + for scale in SCALES: + resized_screenshot: MatLike = _resize_screenshot(screenshot, scale) + edged_screenshot: MatLike = Canny(resized_screenshot, 50, 200) + for template in templates: + potential_match: tuple[float, Sequence[int]] = _get_potential_match(edged_screenshot, template) + potential_match_value: float = potential_match[0] + potential_match_location: Sequence[int] = potential_match[1] + if _is_match_found(potential_match_value): + _logging_test("Match found!") + match_location_x: int = potential_match_location[0] + match_location_y: int = potential_match_location[1] + match_left_top_location: tuple[int, int] = ( + match_location_x + left_top_coordinates[0], + match_location_y + left_top_coordinates[1], ) - template_height, template_width = template.shape - target = ( + template_height: int = template.shape[0] + template_width: int = template.shape[1] + target: tuple[float, float] = ( match_left_top_location[0] + template_width / 2, match_left_top_location[1] + template_height / 2, ) - pyautogui.moveTo(target) - time.sleep(6) + moveTo(target) + sleep(6) return -def test_algorithm() -> None: - logging_test("ndf-2.0.0-multiscale-template-matching.") - logging_test("Comparaison method: TM_CCOEFF_NORMED.") +def _test_algorithm() -> None: + _logging_test("ndf-2.0.0-multiscale-template-matching.") + _logging_test("Comparaison method: TM_CCOEFF_NORMED.") + edged_templates: list[MatLike] = _init_templates() try: with mss() as mss_instance: while True: - monitors_size = mss_instance.monitors[0] - monitors_left_top = (monitors_size.get("left"), monitors_size.get("top")) - screenshot = cv2.imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) - screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY) - multiscale_template_matching(screenshot, monitors_left_top) - except SystemExit: - logging_test("Exiting the program...") - raise + monitors_size: dict[str, int] = mss_instance.monitors[0] + monitors_left_top: tuple[int, int] = _if_monitors_left_top_present(monitors_size) + screenshot: MatLike = imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) + grayscale_screenshot: MatLike = cvtColor(screenshot, COLOR_BGR2GRAY) + _multiscale_match_template(edged_templates, grayscale_screenshot, monitors_left_top) + except (SystemExit, KeyboardInterrupt): + _logging_test("Exiting the program...") + sys.exit(0) finally: if os.path.exists(SCREENSHOT): os.remove(SCREENSHOT) else: - logging_test("The file does not exist") - logging_test("Program ended") + _logging_test("The file does not exist") + _logging_test("Program ended") if __name__ == "__main__": - test_algorithm() + _test_algorithm() From 3c1a6874b01e7dab5bf4867cd22a7e0e77c7687d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 2 Nov 2023 03:28:15 +0100 Subject: [PATCH 23/33] [0008] [ndf-script] More efficient matching template algorithm --- main.py | 211 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 141 insertions(+), 70 deletions(-) diff --git a/main.py b/main.py index fc93697..7c2daef 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,13 @@ """Main executable file of NexusDownloadFlow.""" import os -import time -from typing import Any, Optional, Sequence +import sys +from time import sleep +from typing import Sequence, cast -import cv2 -import numpy as np -import pyautogui +from cv2 import COLOR_BGR2GRAY, TM_CCOEFF_NORMED, Canny, cvtColor, imread, matchTemplate, minMaxLoc, resize from cv2.typing import MatLike from mss import mss -from mss.base import MSSBase +from pyautogui import Point, leftClick, moveTo, position from config.ascii_art import print_ascii_art from config.definitions import ASSETS_DIRECTORY @@ -17,100 +16,172 @@ # TODO: may add unit test +EDGE_MIN_VALUE: int = 50 +EDGE_MAX_VALUE: int = 200 +SCALES: list[float] = [ + 1.0, + 0.95789474, + 0.91578947, + 0.87368421, + 0.83157895, + 0.78947368, + 0.74736842, + 0.70526316, + 0.66315789, + 0.62105263, + 0.57894737, + 0.53684211, + 0.49473684, + 0.45263158, + 0.41052632, + 0.36842105, + 0.32631579, + 0.28421053, + 0.24210526, + 0.2, +] SCREENSHOT: str = "screenshot.png" -CHUNK_SLICES: list[float] = [ - 1., .95789474, .91578947, .87368421, .83157895, .78947368, - .74736842, .70526316, .66315789, .62105263, .57894737, .53684211, - .49473684, .45263158, .41052632, .36842105, .32631579, .28421053, - .24210526, .2 +TEMPLATES: list[MatLike] = [ + imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), ] -THRESHOLD: int = 3000 +THRESHOLD: float = 0.65 -def load_templates() -> list[MatLike]: +def init_templates() -> list[MatLike]: """ - Return the list of templates. + Return the list of edged templates. - :return: list of templates + :return: List of edged templates """ - return [ - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), - cv2.imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), - ] + return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] -def init_templates() -> list[MatLike]: +def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: + """ + Resize the input screenshot. + + :param screenshot: Screenshot to resize + :param scale: Factor to resize the screenshot + :return: resized screenshot. + """ + new_width: int = int(screenshot.shape[1] * scale) + new_height: int = int(screenshot.shape[0] * scale) + return cast(MatLike, resize(screenshot, (new_width, new_height))) + + +def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, Sequence[int]]: + """ + Get the potential match value and its location. + + :param screenshot: Source for template matching + :param template: template to match + :return: potential match value and location. + """ + matches: MatLike = matchTemplate(screenshot, template, TM_CCOEFF_NORMED) + potential_match: tuple[float, float, Sequence[int], Sequence[int]] = minMaxLoc(matches) + max_value: float = potential_match[1] + max_location: Sequence[int] = potential_match[3] + return max_value, max_location + + +def if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, int]: """ - Return the list of edges for each template. + Handle Optional of monitors_left_top (if_present like). - :return: list of templates' edges + :param monitors_size: Dictionary containing left and top properties of the system's monitor(s) + :return: if present, the left-top pixel's coordinates of the system's monitor(s). """ - return [cv2.Canny(cv2.cvtColor(template, cv2.COLOR_BGR2GRAY), 50, 200) for template in load_templates().copy()] + monitors_left: int | None = monitors_size.get("left") + monitors_top: int | None = monitors_size.get("top") + if monitors_left is None: + raise ValueError("monitors_size 'left' value is None") + if monitors_top is None: + raise ValueError("monitors_size 'top' value is None") + return monitors_left, monitors_top + +def is_match_found(match_value: float) -> bool: + """ + Check if a match is found. -def click_on_target(match_location: Sequence[int], template_shape: tuple[int, ...]) -> None: + :param match_value: Value of the match to check + :return: Whether the match is found or not. + """ + return match_value > THRESHOLD + + +def multiscale_match_template( + templates: list[MatLike], screenshot: MatLike, left_top_coordinates: tuple[int, int] +) -> None: + """ + Apply multiscale template matching algorithm. + + :param templates: List of edged templates to match + :param screenshot: screenshot where the search is running + :param left_top_coordinates: left-top pixel of the system monitor(s) + :return: + """ + for scale in SCALES: + resized_screenshot: MatLike = resize_screenshot(screenshot, scale) + edged_screenshot: MatLike = Canny(resized_screenshot, 50, 200) + for template in templates: + potential_match: tuple[float, Sequence[int]] = get_potential_match(edged_screenshot, template) + potential_match_value: float = potential_match[0] + potential_match_location: Sequence[int] = potential_match[1] + if is_match_found(potential_match_value): + print("Match found!") + match_location_x: int = potential_match_location[0] + match_location_y: int = potential_match_location[1] + match_left_top_location: tuple[int, int] = ( + match_location_x + left_top_coordinates[0], + match_location_y + left_top_coordinates[1], + ) + template_height: int = template.shape[0] + template_width: int = template.shape[1] + target: tuple[float, float] = ( + match_left_top_location[0] + template_width / 2, + match_left_top_location[1] + template_height / 2, + ) + click_on_target(target) + sleep(6) + return + + +def click_on_target(target_location: tuple[float, float]) -> None: """ Click on the target that has been identified and move the cursor to its previous location. - :param Sequence[int] match_location: the coordinates of the pixels located at the top left of the matched image. - :param tuple[int, ...] template_shape: the shape of the corresponding template - """ - top_left_x: int - top_left_y: int - top_left_x, top_left_y = match_location - original_position = pyautogui.position() - target = (top_left_x + template_shape[1] / 2, top_left_y + template_shape[0] / 2) - pyautogui.leftClick(target) - pyautogui.moveTo(original_position) - - -def search_template(mss_instance: MSSBase, threshold: float) -> None: - """ - Search and identify an image matching any templates with the specified threshold. - - :param MSSBase mss_instance: an instance of MSSBase - :param float threshold: the threshold required to identify a match. - """ - template: MatLike - for template in load_templates(): - monitors_size: dict[str, int] = mss_instance.monitors[0] - monitors_left_top: Sequence[Optional[int]] = (monitors_size.get('left'), monitors_size.get('top')) - template_gray: MatLike = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) - screenshot: str = next(mss_instance.save(mon=-1, output=SCREENSHOT)) - screenshot_gray: MatLike = cv2.cvtColor(cv2.imread(screenshot), cv2.COLOR_BGR2GRAY) - match_template: MatLike = cv2.matchTemplate(screenshot_gray, template_gray, cv2.TM_SQDIFF) - min_value: float - min_loc: Sequence[int] - min_value, _, min_loc, _ = cv2.minMaxLoc(match_template) - if monitors_left_top[0] is not None and monitors_left_top[1] is not None: - min_loc = (min_loc[0] + monitors_left_top[0], min_loc[1] + monitors_left_top[1]) - if min_value < threshold: - print("Matching template!") - click_on_target(min_loc, template_gray.shape) - time.sleep(6) - break + :param target_location: Target coordinates + """ + original_position: Point | tuple[int, int] = position() + leftClick(target_location) + moveTo(original_position) def main() -> None: """ NexusDownloadFlow main function. - :raise SystemExit: raised when the window is closed + :raises SystemExit: raised when closing program + :raises KeyboardInterrupt: raised when the user interrupts the program """ print_ascii_art() print("NexusDownloadFlow is starting...") - print( - "Do not forget to replace the assets templates (1, 2 & 3) in order to match with the screenshots " - "taken from your monitor!" - ) + edged_templates: list[MatLike] = init_templates() try: with mss() as mss_instance: + print("NexusDownloadFlow is running") while True: - search_template(mss_instance, THRESHOLD) - except SystemExit: + monitors_size: dict[str, int] = mss_instance.monitors[0] + monitors_left_top: tuple[int, int] = if_monitors_left_top_present(monitors_size) + screenshot: MatLike = imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) + grayscale_screenshot: MatLike = cvtColor(screenshot, COLOR_BGR2GRAY) + multiscale_match_template(edged_templates, grayscale_screenshot, monitors_left_top) + except (SystemExit, KeyboardInterrupt): print("Exiting the program...") - raise + sys.exit(0) # except FailSafeException: # # log error # raise From d0a17f9bdbd7d45c4439623514abd0c982ca789f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 2 Nov 2023 14:03:06 +0100 Subject: [PATCH 24/33] [0007] [ndf-script] Logging --- config/definitions.py | 1 + config/ndf_logging.py | 73 +++++++++++++++++++++++++++++++++++++++++++ main.py | 40 ++++++++++++++---------- 3 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 config/ndf_logging.py diff --git a/config/definitions.py b/config/definitions.py index 69a2fbd..9564226 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -6,6 +6,7 @@ ROOT_DIRECTORY: str = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) MAIN_PATH: str = os.path.join(ROOT_DIRECTORY, "main.py") ASSETS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, "assets") +LOGS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, "logs") with open(ROOT_DIRECTORY + "/pyproject.toml", "rb") as pyproject: PYPROJECT_DATA: dict[str, Any] = tomllib.load(pyproject) diff --git a/config/ndf_logging.py b/config/ndf_logging.py new file mode 100644 index 0000000..87cde61 --- /dev/null +++ b/config/ndf_logging.py @@ -0,0 +1,73 @@ +"""Logging configuration.""" + +import logging +import os +import sys +import time +from logging import Handler +from typing import Iterable + +from config.definitions import LOGS_DIRECTORY + +_LOG_EXTENSION: str = ".log" +_NDF_STR: str = "ndf" +_LOGFILE_NAME: str = time.strftime("%Y_%m_%d_") + _NDF_STR + _LOG_EXTENSION + + +def _setup_logfile_path() -> str: + """ + Set up logfile. + + :return: logfile path + """ + return os.path.join(LOGS_DIRECTORY, _LOGFILE_NAME) + + +def _stop_logging() -> None: + """ + Shut down the logger listener. + + :return: None + """ + logging.shutdown() + + +HANDLERS: Iterable[Handler] = [logging.FileHandler(_setup_logfile_path()), logging.StreamHandler(sys.stdout)] + + +def get_logfile_path() -> str: + """ + Getter for the current logfile path. + + :return: logfile path + """ + return _setup_logfile_path() + + +def setup_logging() -> None: + """ + Set up logging configuration. + + :return: None + """ + logging.basicConfig( + level=logging.INFO, + handlers=HANDLERS, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%d/%m/%Y - %H:%M:%S", + ) + logging.debug("Logger setup complete.") + + +def delete_logfile() -> None: + """ + Delete the logfile. + + :return: None + """ + logging.debug("Try to delete the current logfile...") + logfile_path: str = get_logfile_path() + _stop_logging() + if os.path.exists(logfile_path): + os.remove(path=logfile_path) + logging.debug("Logfile deleted.") diff --git a/main.py b/main.py index 7c2daef..439931b 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ """Main executable file of NexusDownloadFlow.""" +import logging import os import sys from time import sleep @@ -11,6 +12,7 @@ from config.ascii_art import print_ascii_art from config.definitions import ASSETS_DIRECTORY +from config.ndf_logging import delete_logfile, setup_logging # TODO: add logs through the script and generate a log file based on the running day # TODO: may add unit test @@ -53,7 +55,7 @@ def init_templates() -> list[MatLike]: """ Return the list of edged templates. - :return: List of edged templates + :return: list of edged templates """ return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] @@ -62,8 +64,8 @@ def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: """ Resize the input screenshot. - :param screenshot: Screenshot to resize - :param scale: Factor to resize the screenshot + :param screenshot: screenshot to resize + :param scale: the scale factor to resize the screenshot :return: resized screenshot. """ new_width: int = int(screenshot.shape[1] * scale) @@ -75,7 +77,7 @@ def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, """ Get the potential match value and its location. - :param screenshot: Source for template matching + :param screenshot: source for template matching :param template: template to match :return: potential match value and location. """ @@ -90,7 +92,7 @@ def if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, in """ Handle Optional of monitors_left_top (if_present like). - :param monitors_size: Dictionary containing left and top properties of the system's monitor(s) + :param monitors_size: dictionary containing left and top properties of the system's monitor(s) :return: if present, the left-top pixel's coordinates of the system's monitor(s). """ monitors_left: int | None = monitors_size.get("left") @@ -106,8 +108,8 @@ def is_match_found(match_value: float) -> bool: """ Check if a match is found. - :param match_value: Value of the match to check - :return: Whether the match is found or not. + :param match_value: value of the match to check + :return: whether the match is found or not. """ return match_value > THRESHOLD @@ -118,10 +120,10 @@ def multiscale_match_template( """ Apply multiscale template matching algorithm. - :param templates: List of edged templates to match + :param templates: list of edged templates to match :param screenshot: screenshot where the search is running :param left_top_coordinates: left-top pixel of the system monitor(s) - :return: + :return: None """ for scale in SCALES: resized_screenshot: MatLike = resize_screenshot(screenshot, scale) @@ -131,7 +133,7 @@ def multiscale_match_template( potential_match_value: float = potential_match[0] potential_match_location: Sequence[int] = potential_match[1] if is_match_found(potential_match_value): - print("Match found!") + logging.info("Match found!") match_location_x: int = potential_match_location[0] match_location_y: int = potential_match_location[1] match_left_top_location: tuple[int, int] = ( @@ -153,7 +155,8 @@ def click_on_target(target_location: tuple[float, float]) -> None: """ Click on the target that has been identified and move the cursor to its previous location. - :param target_location: Target coordinates + :param target_location: tuple of target coordinates + :return: None """ original_position: Point | tuple[int, int] = position() leftClick(target_location) @@ -166,13 +169,16 @@ def main() -> None: :raises SystemExit: raised when closing program :raises KeyboardInterrupt: raised when the user interrupts the program + :return: None """ + setup_logging() + keep_logfile: bool = False print_ascii_art() - print("NexusDownloadFlow is starting...") + logging.info("NexusDownloadFlow is starting...") edged_templates: list[MatLike] = init_templates() try: with mss() as mss_instance: - print("NexusDownloadFlow is running") + logging.info("NexusDownloadFlow is running") while True: monitors_size: dict[str, int] = mss_instance.monitors[0] monitors_left_top: tuple[int, int] = if_monitors_left_top_present(monitors_size) @@ -180,7 +186,7 @@ def main() -> None: grayscale_screenshot: MatLike = cvtColor(screenshot, COLOR_BGR2GRAY) multiscale_match_template(edged_templates, grayscale_screenshot, monitors_left_top) except (SystemExit, KeyboardInterrupt): - print("Exiting the program...") + logging.info("Exiting the program...") sys.exit(0) # except FailSafeException: # # log error @@ -189,8 +195,10 @@ def main() -> None: if os.path.exists(SCREENSHOT): os.remove(SCREENSHOT) else: - print("The file does not exist") - print("Program ended") + logging.warning("The screenshot does not exist") + logging.info("Program ended") + if not keep_logfile: + delete_logfile() if __name__ == "__main__": From 0de3273a3e290429b999edd826a9b4d4fed7ef3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 2 Nov 2023 15:48:49 +0100 Subject: [PATCH 25/33] [0022] [ndf-script] Better exceptions handling --- config/ascii_art.py | 2 +- config/ndf_logging.py | 10 +++---- main.py | 70 ++++++++++++++++++++++++++----------------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/config/ascii_art.py b/config/ascii_art.py index a0f59fa..6018489 100644 --- a/config/ascii_art.py +++ b/config/ascii_art.py @@ -25,5 +25,5 @@ def print_ascii_art() -> None: - """Print NexusDownloadFlow ascii art with project version.""" + """Print NexusDownloadFlow ascii art with the project version.""" sys.stdout.write(ASCII_COLOR + ASCII_TEXT + PROJECT_VERSION + "\033[0m\n") diff --git a/config/ndf_logging.py b/config/ndf_logging.py index 87cde61..22f92c8 100644 --- a/config/ndf_logging.py +++ b/config/ndf_logging.py @@ -18,7 +18,7 @@ def _setup_logfile_path() -> str: """ Set up logfile. - :return: logfile path + :return: Logfile path. """ return os.path.join(LOGS_DIRECTORY, _LOGFILE_NAME) @@ -27,7 +27,7 @@ def _stop_logging() -> None: """ Shut down the logger listener. - :return: None + :return: None. """ logging.shutdown() @@ -39,7 +39,7 @@ def get_logfile_path() -> str: """ Getter for the current logfile path. - :return: logfile path + :return: Logfile path. """ return _setup_logfile_path() @@ -48,7 +48,7 @@ def setup_logging() -> None: """ Set up logging configuration. - :return: None + :return: None. """ logging.basicConfig( level=logging.INFO, @@ -63,7 +63,7 @@ def delete_logfile() -> None: """ Delete the logfile. - :return: None + :return: None. """ logging.debug("Try to delete the current logfile...") logfile_path: str = get_logfile_path() diff --git a/main.py b/main.py index 439931b..d61efc4 100644 --- a/main.py +++ b/main.py @@ -8,11 +8,11 @@ from cv2 import COLOR_BGR2GRAY, TM_CCOEFF_NORMED, Canny, cvtColor, imread, matchTemplate, minMaxLoc, resize from cv2.typing import MatLike from mss import mss -from pyautogui import Point, leftClick, moveTo, position +from pyautogui import FailSafeException, Point, leftClick, moveTo, position from config.ascii_art import print_ascii_art from config.definitions import ASSETS_DIRECTORY -from config.ndf_logging import delete_logfile, setup_logging +from config.ndf_logging import delete_logfile, get_logfile_path, setup_logging # TODO: add logs through the script and generate a log file based on the running day # TODO: may add unit test @@ -55,7 +55,7 @@ def init_templates() -> list[MatLike]: """ Return the list of edged templates. - :return: list of edged templates + :return: List of edged templates. """ return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] @@ -64,9 +64,9 @@ def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: """ Resize the input screenshot. - :param screenshot: screenshot to resize - :param scale: the scale factor to resize the screenshot - :return: resized screenshot. + :param screenshot: Screenshot to resize. + :param scale: The scale factor to resize the screenshot. + :return: Resized screenshot. """ new_width: int = int(screenshot.shape[1] * scale) new_height: int = int(screenshot.shape[0] * scale) @@ -77,9 +77,9 @@ def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, """ Get the potential match value and its location. - :param screenshot: source for template matching - :param template: template to match - :return: potential match value and location. + :param screenshot: Source for template matching. + :param template: Template to match. + :return: Potential match value and location. """ matches: MatLike = matchTemplate(screenshot, template, TM_CCOEFF_NORMED) potential_match: tuple[float, float, Sequence[int], Sequence[int]] = minMaxLoc(matches) @@ -92,15 +92,19 @@ def if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, in """ Handle Optional of monitors_left_top (if_present like). - :param monitors_size: dictionary containing left and top properties of the system's monitor(s) - :return: if present, the left-top pixel's coordinates of the system's monitor(s). + :param monitors_size: Dictionary containing left and top properties of the system's monitor(s). + :return: If present, the left-top pixel's coordinates of the system's monitor(s). """ + + def error_message(_key: str) -> str: + return f"Monitors' size '{_key}' value is None." + monitors_left: int | None = monitors_size.get("left") monitors_top: int | None = monitors_size.get("top") if monitors_left is None: - raise ValueError("monitors_size 'left' value is None") + raise ValueError(error_message("left")) if monitors_top is None: - raise ValueError("monitors_size 'top' value is None") + raise ValueError(error_message("top")) return monitors_left, monitors_top @@ -108,8 +112,8 @@ def is_match_found(match_value: float) -> bool: """ Check if a match is found. - :param match_value: value of the match to check - :return: whether the match is found or not. + :param match_value: Value of the match to check. + :return: Whether the match is found or not. """ return match_value > THRESHOLD @@ -120,9 +124,9 @@ def multiscale_match_template( """ Apply multiscale template matching algorithm. - :param templates: list of edged templates to match - :param screenshot: screenshot where the search is running - :param left_top_coordinates: left-top pixel of the system monitor(s) + :param templates: List of edged templates to match. + :param screenshot: Screenshot where the search is running. + :param left_top_coordinates: Left-top pixel of the system monitor(s). :return: None """ for scale in SCALES: @@ -155,7 +159,7 @@ def click_on_target(target_location: tuple[float, float]) -> None: """ Click on the target that has been identified and move the cursor to its previous location. - :param target_location: tuple of target coordinates + :param target_location: Tuple of target coordinates. :return: None """ original_position: Point | tuple[int, int] = position() @@ -167,8 +171,10 @@ def main() -> None: """ NexusDownloadFlow main function. - :raises SystemExit: raised when closing program - :raises KeyboardInterrupt: raised when the user interrupts the program + :raises SystemExit: Raised when closing the program. + :raises KeyboardInterrupt: Raised when the user interrupts the program. + :raises ValueError: Should not be raised (open an issue on GitHub if it happens). + :raises Exception: For currently unknown exceptions (open an issue on GitHub if it happens). :return: None """ setup_logging() @@ -178,7 +184,7 @@ def main() -> None: edged_templates: list[MatLike] = init_templates() try: with mss() as mss_instance: - logging.info("NexusDownloadFlow is running") + logging.info("NexusDownloadFlow is running.") while True: monitors_size: dict[str, int] = mss_instance.monitors[0] monitors_left_top: tuple[int, int] = if_monitors_left_top_present(monitors_size) @@ -188,16 +194,24 @@ def main() -> None: except (SystemExit, KeyboardInterrupt): logging.info("Exiting the program...") sys.exit(0) - # except FailSafeException: - # # log error - # raise + except FailSafeException: + logging.error("Fail-safe triggered from mouse moving to a corner of the screen.") + keep_logfile = True + except ValueError as e: + logging.error(e) + keep_logfile = True + except Exception as e: + logging.exception(e) + keep_logfile = True finally: if os.path.exists(SCREENSHOT): os.remove(SCREENSHOT) else: - logging.warning("The screenshot does not exist") - logging.info("Program ended") - if not keep_logfile: + logging.warning("The screenshot does not exist.") + logging.info("Program ended.") + if keep_logfile: + logging.info(f"Find logfile at: { get_logfile_path() }") + else: delete_logfile() From b52c1ab341bc405eaa143a69086d2a241660024d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Thu, 2 Nov 2023 23:51:37 +0100 Subject: [PATCH 26/33] [0009] [ndf-script] keep logfile --- config/ndf_logging.py | 12 +++ main.py | 207 ++-------------------------------------- scripts/ndf_params.py | 21 ++++ scripts/ndf_run.py | 217 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 201 deletions(-) create mode 100644 scripts/ndf_params.py create mode 100644 scripts/ndf_run.py diff --git a/config/ndf_logging.py b/config/ndf_logging.py index 22f92c8..26eee63 100644 --- a/config/ndf_logging.py +++ b/config/ndf_logging.py @@ -71,3 +71,15 @@ def delete_logfile() -> None: if os.path.exists(logfile_path): os.remove(path=logfile_path) logging.debug("Logfile deleted.") + + +def logging_report() -> None: + """ + Log report to open an issue on the project's repository. + + :return: None. + """ + logging.critical( + "Please report this exception to our repository on GitHub: " + "https://github.com/greg-ynx/NexusDownloadFlow/issues?q=is%3Aissue+is%3Aopen" + ) diff --git a/main.py b/main.py index d61efc4..b47a9cb 100644 --- a/main.py +++ b/main.py @@ -1,218 +1,23 @@ """Main executable file of NexusDownloadFlow.""" import logging -import os -import sys -from time import sleep -from typing import Sequence, cast - -from cv2 import COLOR_BGR2GRAY, TM_CCOEFF_NORMED, Canny, cvtColor, imread, matchTemplate, minMaxLoc, resize -from cv2.typing import MatLike -from mss import mss -from pyautogui import FailSafeException, Point, leftClick, moveTo, position from config.ascii_art import print_ascii_art -from config.definitions import ASSETS_DIRECTORY -from config.ndf_logging import delete_logfile, get_logfile_path, setup_logging - -# TODO: add logs through the script and generate a log file based on the running day -# TODO: may add unit test - - -EDGE_MIN_VALUE: int = 50 -EDGE_MAX_VALUE: int = 200 -SCALES: list[float] = [ - 1.0, - 0.95789474, - 0.91578947, - 0.87368421, - 0.83157895, - 0.78947368, - 0.74736842, - 0.70526316, - 0.66315789, - 0.62105263, - 0.57894737, - 0.53684211, - 0.49473684, - 0.45263158, - 0.41052632, - 0.36842105, - 0.32631579, - 0.28421053, - 0.24210526, - 0.2, -] -SCREENSHOT: str = "screenshot.png" -TEMPLATES: list[MatLike] = [ - imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), - imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), - imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), -] -THRESHOLD: float = 0.65 - - -def init_templates() -> list[MatLike]: - """ - Return the list of edged templates. - - :return: List of edged templates. - """ - return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] - - -def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: - """ - Resize the input screenshot. - - :param screenshot: Screenshot to resize. - :param scale: The scale factor to resize the screenshot. - :return: Resized screenshot. - """ - new_width: int = int(screenshot.shape[1] * scale) - new_height: int = int(screenshot.shape[0] * scale) - return cast(MatLike, resize(screenshot, (new_width, new_height))) - - -def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, Sequence[int]]: - """ - Get the potential match value and its location. - - :param screenshot: Source for template matching. - :param template: Template to match. - :return: Potential match value and location. - """ - matches: MatLike = matchTemplate(screenshot, template, TM_CCOEFF_NORMED) - potential_match: tuple[float, float, Sequence[int], Sequence[int]] = minMaxLoc(matches) - max_value: float = potential_match[1] - max_location: Sequence[int] = potential_match[3] - return max_value, max_location - - -def if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, int]: - """ - Handle Optional of monitors_left_top (if_present like). - - :param monitors_size: Dictionary containing left and top properties of the system's monitor(s). - :return: If present, the left-top pixel's coordinates of the system's monitor(s). - """ - - def error_message(_key: str) -> str: - return f"Monitors' size '{_key}' value is None." - - monitors_left: int | None = monitors_size.get("left") - monitors_top: int | None = monitors_size.get("top") - if monitors_left is None: - raise ValueError(error_message("left")) - if monitors_top is None: - raise ValueError(error_message("top")) - return monitors_left, monitors_top - - -def is_match_found(match_value: float) -> bool: - """ - Check if a match is found. - - :param match_value: Value of the match to check. - :return: Whether the match is found or not. - """ - return match_value > THRESHOLD - - -def multiscale_match_template( - templates: list[MatLike], screenshot: MatLike, left_top_coordinates: tuple[int, int] -) -> None: - """ - Apply multiscale template matching algorithm. - - :param templates: List of edged templates to match. - :param screenshot: Screenshot where the search is running. - :param left_top_coordinates: Left-top pixel of the system monitor(s). - :return: None - """ - for scale in SCALES: - resized_screenshot: MatLike = resize_screenshot(screenshot, scale) - edged_screenshot: MatLike = Canny(resized_screenshot, 50, 200) - for template in templates: - potential_match: tuple[float, Sequence[int]] = get_potential_match(edged_screenshot, template) - potential_match_value: float = potential_match[0] - potential_match_location: Sequence[int] = potential_match[1] - if is_match_found(potential_match_value): - logging.info("Match found!") - match_location_x: int = potential_match_location[0] - match_location_y: int = potential_match_location[1] - match_left_top_location: tuple[int, int] = ( - match_location_x + left_top_coordinates[0], - match_location_y + left_top_coordinates[1], - ) - template_height: int = template.shape[0] - template_width: int = template.shape[1] - target: tuple[float, float] = ( - match_left_top_location[0] + template_width / 2, - match_left_top_location[1] + template_height / 2, - ) - click_on_target(target) - sleep(6) - return - - -def click_on_target(target_location: tuple[float, float]) -> None: - """ - Click on the target that has been identified and move the cursor to its previous location. - - :param target_location: Tuple of target coordinates. - :return: None - """ - original_position: Point | tuple[int, int] = position() - leftClick(target_location) - moveTo(original_position) +from config.ndf_logging import setup_logging +from scripts.ndf_params import ask_to_keep_logfile +from scripts.ndf_run import try_run def main() -> None: """ NexusDownloadFlow main function. - :raises SystemExit: Raised when closing the program. - :raises KeyboardInterrupt: Raised when the user interrupts the program. - :raises ValueError: Should not be raised (open an issue on GitHub if it happens). - :raises Exception: For currently unknown exceptions (open an issue on GitHub if it happens). - :return: None + :return: None. """ setup_logging() - keep_logfile: bool = False print_ascii_art() logging.info("NexusDownloadFlow is starting...") - edged_templates: list[MatLike] = init_templates() - try: - with mss() as mss_instance: - logging.info("NexusDownloadFlow is running.") - while True: - monitors_size: dict[str, int] = mss_instance.monitors[0] - monitors_left_top: tuple[int, int] = if_monitors_left_top_present(monitors_size) - screenshot: MatLike = imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) - grayscale_screenshot: MatLike = cvtColor(screenshot, COLOR_BGR2GRAY) - multiscale_match_template(edged_templates, grayscale_screenshot, monitors_left_top) - except (SystemExit, KeyboardInterrupt): - logging.info("Exiting the program...") - sys.exit(0) - except FailSafeException: - logging.error("Fail-safe triggered from mouse moving to a corner of the screen.") - keep_logfile = True - except ValueError as e: - logging.error(e) - keep_logfile = True - except Exception as e: - logging.exception(e) - keep_logfile = True - finally: - if os.path.exists(SCREENSHOT): - os.remove(SCREENSHOT) - else: - logging.warning("The screenshot does not exist.") - logging.info("Program ended.") - if keep_logfile: - logging.info(f"Find logfile at: { get_logfile_path() }") - else: - delete_logfile() + keep_logfile: bool = ask_to_keep_logfile() + try_run(keep_logfile) if __name__ == "__main__": diff --git a/scripts/ndf_params.py b/scripts/ndf_params.py new file mode 100644 index 0000000..5d852ea --- /dev/null +++ b/scripts/ndf_params.py @@ -0,0 +1,21 @@ +"""Parameters file.""" +import logging + + +def ask_to_keep_logfile() -> bool: + """ + Ask if the user wants to keep the logfile. + + :return: Whether to keep logfile. + """ + while True: + keep: str = str(input("Would you like to save the logfile? (y/n)\n")) + match keep: + case "y" | "Y": + logging.info("Logfile will be saved.") + return True + case "n" | "N": + logging.info("Logfile will be saved only if an exception/error occurred.") + return False + case _: + continue diff --git a/scripts/ndf_run.py b/scripts/ndf_run.py new file mode 100644 index 0000000..4f6d869 --- /dev/null +++ b/scripts/ndf_run.py @@ -0,0 +1,217 @@ +"""Run file.""" + +import logging +import os +from time import sleep +from typing import Sequence, cast + +from cv2 import COLOR_BGR2GRAY, TM_CCOEFF_NORMED, Canny, cvtColor, imread, matchTemplate, minMaxLoc, resize +from cv2.typing import MatLike +from mss import mss +from pyautogui import FailSafeException, Point, leftClick, moveTo, position + +from config.definitions import ASSETS_DIRECTORY +from config.ndf_logging import delete_logfile, get_logfile_path, logging_report + +EDGE_MIN_VALUE: int = 50 +EDGE_MAX_VALUE: int = 200 +SCALES: list[float] = [ + 1.0, + 0.95789474, + 0.91578947, + 0.87368421, + 0.83157895, + 0.78947368, + 0.74736842, + 0.70526316, + 0.66315789, + 0.62105263, + 0.57894737, + 0.53684211, + 0.49473684, + 0.45263158, + 0.41052632, + 0.36842105, + 0.32631579, + 0.28421053, + 0.24210526, + 0.2, +] +SCREENSHOT: str = "screenshot.png" +TEMPLATES: list[MatLike] = [ + imread(os.path.join(ASSETS_DIRECTORY, "template1.png")), + imread(os.path.join(ASSETS_DIRECTORY, "template2.png")), + imread(os.path.join(ASSETS_DIRECTORY, "template3.png")), +] +THRESHOLD: float = 0.65 + + +def init_templates() -> list[MatLike]: + """ + Return the list of edged templates. + + :return: List of edged templates. + """ + return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] + + +def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: + """ + Resize the input screenshot. + + :param screenshot: Screenshot to resize. + :param scale: The scale factor to resize the screenshot. + :return: Resized screenshot. + """ + new_width: int = int(screenshot.shape[1] * scale) + new_height: int = int(screenshot.shape[0] * scale) + return cast(MatLike, resize(screenshot, (new_width, new_height))) + + +def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, Sequence[int]]: + """ + Get the potential match value and its location. + + :param screenshot: Source for template matching. + :param template: Template to match. + :return: Potential match value and location. + """ + matches: MatLike = matchTemplate(screenshot, template, TM_CCOEFF_NORMED) + potential_match: tuple[float, float, Sequence[int], Sequence[int]] = minMaxLoc(matches) + max_value: float = potential_match[1] + max_location: Sequence[int] = potential_match[3] + return max_value, max_location + + +def if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, int]: + """ + Handle Optional of monitors_left_top (if_present like). + + :param monitors_size: Dictionary containing left and top properties of the system's monitor(s). + :return: If present, the left-top pixel's coordinates of the system's monitor(s). + """ + + def error_message(_key: str) -> str: + return f"Monitors' size '{_key}' value is None." + + monitors_left: int | None = monitors_size.get("left") + monitors_top: int | None = monitors_size.get("top") + if monitors_left is None: + raise ValueError(error_message("left")) + if monitors_top is None: + raise ValueError(error_message("top")) + return monitors_left, monitors_top + + +def is_match_found(match_value: float) -> bool: + """ + Check if a match is found. + + :param match_value: Value of the match to check. + :return: Whether the match is found or not. + """ + return match_value > THRESHOLD + + +def click_on_target(target_location: tuple[float, float]) -> None: + """ + Click on the target that has been identified and move the cursor to its previous location. + + :param target_location: Tuple of target coordinates. + :return: None. + """ + original_position: Point | tuple[int, int] = position() + leftClick(target_location) + moveTo(original_position) + + +def multiscale_match_template( + templates: list[MatLike], screenshot: MatLike, left_top_coordinates: tuple[int, int] +) -> None: + """ + Apply multiscale template matching algorithm. + + :param templates: List of edged templates to match. + :param screenshot: Screenshot where the search is running. + :param left_top_coordinates: Left-top pixel of the system monitor(s). + :return: None. + """ + for scale in SCALES: + resized_screenshot: MatLike = resize_screenshot(screenshot, scale) + edged_screenshot: MatLike = Canny(resized_screenshot, 50, 200) + for template in templates: + potential_match: tuple[float, Sequence[int]] = get_potential_match(edged_screenshot, template) + potential_match_value: float = potential_match[0] + potential_match_location: Sequence[int] = potential_match[1] + if is_match_found(potential_match_value): + logging.info("Match found!") + match_location_x: int = potential_match_location[0] + match_location_y: int = potential_match_location[1] + match_left_top_location: tuple[int, int] = ( + match_location_x + left_top_coordinates[0], + match_location_y + left_top_coordinates[1], + ) + template_height: int = template.shape[0] + template_width: int = template.shape[1] + target: tuple[float, float] = ( + match_left_top_location[0] + template_width / 2, + match_left_top_location[1] + template_height / 2, + ) + click_on_target(target) + sleep(6) + return + + +def run() -> None: + """ + Run the auto-downloader. + + :return: None. + """ + logging.info("NexusDownloadFlow is running.") + edged_templates: list[MatLike] = init_templates() + with mss() as mss_instance: + while True: + monitors_size: dict[str, int] = mss_instance.monitors[0] + monitors_left_top: tuple[int, int] = if_monitors_left_top_present(monitors_size) + screenshot: MatLike = imread(next(mss_instance.save(mon=-1, output=SCREENSHOT))) + grayscale_screenshot: MatLike = cvtColor(screenshot, COLOR_BGR2GRAY) + multiscale_match_template(edged_templates, grayscale_screenshot, monitors_left_top) + + +def try_run(keep_logfile: bool = False) -> None: + """ + Try to run the auto-downloader. + + :param keep_logfile: If the logfile should be kept on program exit. + :raises SystemExit: Raised when closing the program. + :raises KeyboardInterrupt: Raised when the user interrupts the program. + :raises ValueError: Should not be raised (open an issue on GitHub if it happens). + :raises Exception: For currently unknown exceptions (open an issue on GitHub if it happens). + :return: None. + """ + try: + run() + except KeyboardInterrupt: + logging.info("Exiting the program...") + except FailSafeException: + logging.error("Fail-safe triggered from mouse moving to a corner of the screen.") + keep_logfile = True + except ValueError as e: + logging.error(e) + logging_report() + keep_logfile = True + except Exception as e: + logging.exception(e) + logging_report() + keep_logfile = True + finally: + if os.path.exists(SCREENSHOT): + os.remove(SCREENSHOT) + else: + logging.warning("The screenshot does not exist.") + logging.info("Program ended.") + if keep_logfile: + logging.info(f"Find logfile at: { get_logfile_path() }") + else: + delete_logfile() From d6b61d929f6f9247f1def0e62045f2ca7a434898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 06:06:45 +0100 Subject: [PATCH 27/33] update git ignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 53f054f..567dc28 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec # Installer logs pip-log.txt From 959b8a90139433879bf72ae5fda9ac051f23e582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 06:08:21 +0100 Subject: [PATCH 28/33] [0012] [ndf-script] Temp directory handling --- config/definitions.py | 27 ++++++++++++++++++++++----- config/ndf_logging.py | 13 +++++++++++-- main.py | 4 +--- scripts/ndf_run.py | 5 ++++- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/config/definitions.py b/config/definitions.py index 9564226..fda781e 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -1,12 +1,29 @@ """Define global constants used in the project.""" import os +import sys import tomllib from typing import Any -ROOT_DIRECTORY: str = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) -MAIN_PATH: str = os.path.join(ROOT_DIRECTORY, "main.py") -ASSETS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, "assets") -LOGS_DIRECTORY: str = os.path.join(ROOT_DIRECTORY, "logs") +_TEMP_DIRECTORY: str +_EXE_DIRECTORY: str = os.path.realpath(os.path.join(sys.executable, "..")) +_DEV_DIRECTORY: str = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) +MAIN_PATH: str +ASSETS_DIRECTORY: str +LOGS_DIRECTORY: str +PYPROJECT_DIRECTORY: str -with open(ROOT_DIRECTORY + "/pyproject.toml", "rb") as pyproject: +if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + _TEMP_DIRECTORY = os.path.join(sys._MEIPASS) + MAIN_PATH = os.path.join(_TEMP_DIRECTORY, "main.py") + ASSETS_DIRECTORY = os.path.join(_TEMP_DIRECTORY, "assets") + LOGS_DIRECTORY = os.path.join(_EXE_DIRECTORY, "logs") + PYPROJECT_DIRECTORY = os.path.join(_TEMP_DIRECTORY, "pyproject.toml") +else: + MAIN_PATH = os.path.join(_DEV_DIRECTORY, "main.py") + ASSETS_DIRECTORY = os.path.join(_DEV_DIRECTORY, "assets") + LOGS_DIRECTORY = os.path.join(_DEV_DIRECTORY, "logs") + PYPROJECT_DIRECTORY = os.path.join(_DEV_DIRECTORY, "pyproject.toml") + + +with open(PYPROJECT_DIRECTORY, "rb") as pyproject: PYPROJECT_DATA: dict[str, Any] = tomllib.load(pyproject) diff --git a/config/ndf_logging.py b/config/ndf_logging.py index 26eee63..7f716dd 100644 --- a/config/ndf_logging.py +++ b/config/ndf_logging.py @@ -32,7 +32,13 @@ def _stop_logging() -> None: logging.shutdown() -HANDLERS: Iterable[Handler] = [logging.FileHandler(_setup_logfile_path()), logging.StreamHandler(sys.stdout)] +def _logs_directory_exists() -> bool: + """ + Check if the logs directory exists. + + :return: Whether the logs directory exists. + """ + return os.path.exists(LOGS_DIRECTORY) def get_logfile_path() -> str: @@ -50,9 +56,12 @@ def setup_logging() -> None: :return: None. """ + if not _logs_directory_exists(): + os.makedirs(LOGS_DIRECTORY) + _HANDLERS: Iterable[Handler] = [logging.FileHandler(_setup_logfile_path()), logging.StreamHandler(sys.stdout)] logging.basicConfig( level=logging.INFO, - handlers=HANDLERS, + handlers=_HANDLERS, format="%(asctime)s | %(levelname)s | %(message)s", datefmt="%d/%m/%Y - %H:%M:%S", ) diff --git a/main.py b/main.py index b47a9cb..87ba026 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,6 @@ from config.ascii_art import print_ascii_art from config.ndf_logging import setup_logging -from scripts.ndf_params import ask_to_keep_logfile from scripts.ndf_run import try_run @@ -16,8 +15,7 @@ def main() -> None: setup_logging() print_ascii_art() logging.info("NexusDownloadFlow is starting...") - keep_logfile: bool = ask_to_keep_logfile() - try_run(keep_logfile) + try_run() if __name__ == "__main__": diff --git a/scripts/ndf_run.py b/scripts/ndf_run.py index 4f6d869..11b269e 100644 --- a/scripts/ndf_run.py +++ b/scripts/ndf_run.py @@ -12,6 +12,7 @@ from config.definitions import ASSETS_DIRECTORY from config.ndf_logging import delete_logfile, get_logfile_path, logging_report +from scripts.ndf_params import ask_to_keep_logfile EDGE_MIN_VALUE: int = 50 EDGE_MAX_VALUE: int = 200 @@ -179,7 +180,7 @@ def run() -> None: multiscale_match_template(edged_templates, grayscale_screenshot, monitors_left_top) -def try_run(keep_logfile: bool = False) -> None: +def try_run() -> None: """ Try to run the auto-downloader. @@ -190,7 +191,9 @@ def try_run(keep_logfile: bool = False) -> None: :raises Exception: For currently unknown exceptions (open an issue on GitHub if it happens). :return: None. """ + keep_logfile: bool = False try: + keep_logfile = ask_to_keep_logfile() run() except KeyboardInterrupt: logging.info("Exiting the program...") From 8503c82934adf3d060b585fba4de4da8dc2d5162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 06:38:59 +0100 Subject: [PATCH 29/33] [0016] [ndf-script] [pl] pyinstaller tool to exe --- build.bat | 1 + build.spec | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 build.bat create mode 100644 build.spec diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..90aeef6 --- /dev/null +++ b/build.bat @@ -0,0 +1 @@ +pyinstaller build.spec --clean --workpath ./target --distpath ./target/dist \ No newline at end of file diff --git a/build.spec b/build.spec new file mode 100644 index 0000000..c169116 --- /dev/null +++ b/build.spec @@ -0,0 +1,41 @@ +# -*- mode: python ; coding: utf-8 -*- + +extra_files = [ + ('assets/*', 'assets'), + ('pyproject.toml', '.') +] + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=extra_files, + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='NexusDownloadFlow', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) From b8ad915c8a30236ad50b4a1094fed1425c48a3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 08:03:44 +0100 Subject: [PATCH 30/33] [0014] [ndf-script] documentation --- config/ndf_logging.py | 78 +++++++++++++++++-------------------------- main.py | 6 +--- scripts/ndf_params.py | 7 ++-- scripts/ndf_run.py | 63 +++++++++++++++------------------- 4 files changed, 64 insertions(+), 90 deletions(-) diff --git a/config/ndf_logging.py b/config/ndf_logging.py index 7f716dd..bff3965 100644 --- a/config/ndf_logging.py +++ b/config/ndf_logging.py @@ -14,66 +14,31 @@ _LOGFILE_NAME: str = time.strftime("%Y_%m_%d_") + _NDF_STR + _LOG_EXTENSION -def _setup_logfile_path() -> str: - """ - Set up logfile. - - :return: Logfile path. - """ - return os.path.join(LOGS_DIRECTORY, _LOGFILE_NAME) - - -def _stop_logging() -> None: - """ - Shut down the logger listener. - - :return: None. - """ - logging.shutdown() - - def _logs_directory_exists() -> bool: """ Check if the logs directory exists. - :return: Whether the logs directory exists. + :return: Bool value indicating if the logs directory exists. """ return os.path.exists(LOGS_DIRECTORY) -def get_logfile_path() -> str: +def _setup_logfile_path() -> str: """ - Getter for the current logfile path. + Set up log file. - :return: Logfile path. + :return: String representing the log file path. """ - return _setup_logfile_path() + return os.path.join(LOGS_DIRECTORY, _LOGFILE_NAME) -def setup_logging() -> None: - """ - Set up logging configuration. - - :return: None. - """ - if not _logs_directory_exists(): - os.makedirs(LOGS_DIRECTORY) - _HANDLERS: Iterable[Handler] = [logging.FileHandler(_setup_logfile_path()), logging.StreamHandler(sys.stdout)] - logging.basicConfig( - level=logging.INFO, - handlers=_HANDLERS, - format="%(asctime)s | %(levelname)s | %(message)s", - datefmt="%d/%m/%Y - %H:%M:%S", - ) - logging.debug("Logger setup complete.") +def _stop_logging() -> None: + """Shut down the logger.""" + logging.shutdown() def delete_logfile() -> None: - """ - Delete the logfile. - - :return: None. - """ + """Delete the log file.""" logging.debug("Try to delete the current logfile...") logfile_path: str = get_logfile_path() _stop_logging() @@ -82,13 +47,32 @@ def delete_logfile() -> None: logging.debug("Logfile deleted.") -def logging_report() -> None: +def get_logfile_path() -> str: """ - Log report to open an issue on the project's repository. + Getter for the current log file path. - :return: None. + :return: Log file path. """ + return _setup_logfile_path() + + +def logging_report() -> None: + """Log report to open an issue on the project's repository.""" logging.critical( "Please report this exception to our repository on GitHub: " "https://github.com/greg-ynx/NexusDownloadFlow/issues?q=is%3Aissue+is%3Aopen" ) + + +def setup_logging() -> None: + """Set up logging configuration.""" + if not _logs_directory_exists(): + os.makedirs(LOGS_DIRECTORY) + _handlers: Iterable[Handler] = [logging.FileHandler(_setup_logfile_path()), logging.StreamHandler(sys.stdout)] + logging.basicConfig( + level=logging.INFO, + handlers=_handlers, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%d/%m/%Y - %H:%M:%S", + ) + logging.debug("Logger setup complete.") diff --git a/main.py b/main.py index 87ba026..3cb85e0 100644 --- a/main.py +++ b/main.py @@ -7,11 +7,7 @@ def main() -> None: - """ - NexusDownloadFlow main function. - - :return: None. - """ + """NexusDownloadFlow main function.""" setup_logging() print_ascii_art() logging.info("NexusDownloadFlow is starting...") diff --git a/scripts/ndf_params.py b/scripts/ndf_params.py index 5d852ea..a75f9e7 100644 --- a/scripts/ndf_params.py +++ b/scripts/ndf_params.py @@ -4,9 +4,12 @@ def ask_to_keep_logfile() -> bool: """ - Ask if the user wants to keep the logfile. + Ask if the user wants to keep the log file. - :return: Whether to keep logfile. + :return: Bool value representing whether to keep the log file or not. + True, if user's answer is "y" or "Y". + False, if user's answer is "n" or "N". + Will repeat if the input value is not valid. """ while True: keep: str = str(input("Would you like to save the logfile? (y/n)\n")) diff --git a/scripts/ndf_run.py b/scripts/ndf_run.py index 11b269e..5c869f0 100644 --- a/scripts/ndf_run.py +++ b/scripts/ndf_run.py @@ -47,26 +47,15 @@ THRESHOLD: float = 0.65 -def init_templates() -> list[MatLike]: - """ - Return the list of edged templates. - - :return: List of edged templates. - """ - return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] - - -def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: +def click_on_target(target_location: tuple[float, float]) -> None: """ - Resize the input screenshot. + Click on the target that has been identified and move the cursor to its previous location. - :param screenshot: Screenshot to resize. - :param scale: The scale factor to resize the screenshot. - :return: Resized screenshot. + :param target_location: Tuple of target coordinates. """ - new_width: int = int(screenshot.shape[1] * scale) - new_height: int = int(screenshot.shape[0] * scale) - return cast(MatLike, resize(screenshot, (new_width, new_height))) + original_position: Point | tuple[int, int] = position() + leftClick(target_location) + moveTo(original_position) def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, Sequence[int]]: @@ -75,7 +64,7 @@ def get_potential_match(screenshot: MatLike, template: MatLike) -> tuple[float, :param screenshot: Source for template matching. :param template: Template to match. - :return: Potential match value and location. + :return: Tuple of potential match value and location. """ matches: MatLike = matchTemplate(screenshot, template, TM_CCOEFF_NORMED) potential_match: tuple[float, float, Sequence[int], Sequence[int]] = minMaxLoc(matches) @@ -89,7 +78,7 @@ def if_monitors_left_top_present(monitors_size: dict[str, int]) -> tuple[int, in Handle Optional of monitors_left_top (if_present like). :param monitors_size: Dictionary containing left and top properties of the system's monitor(s). - :return: If present, the left-top pixel's coordinates of the system's monitor(s). + :return: If present, tuple representing the left-top pixel's coordinates of the system's monitor(s). """ def error_message(_key: str) -> str: @@ -104,26 +93,36 @@ def error_message(_key: str) -> str: return monitors_left, monitors_top +def init_templates() -> list[MatLike]: + """ + Return the list of edged templates. + + :return: List of edged templates. + """ + return [Canny(cvtColor(template, COLOR_BGR2GRAY), EDGE_MIN_VALUE, EDGE_MAX_VALUE) for template in TEMPLATES] + + def is_match_found(match_value: float) -> bool: """ Check if a match is found. :param match_value: Value of the match to check. - :return: Whether the match is found or not. + :return: Bool value indicating whether a match is found or not. """ return match_value > THRESHOLD -def click_on_target(target_location: tuple[float, float]) -> None: +def resize_screenshot(screenshot: MatLike, scale: float) -> MatLike: """ - Click on the target that has been identified and move the cursor to its previous location. + Resize the input screenshot. - :param target_location: Tuple of target coordinates. - :return: None. + :param screenshot: Screenshot to resize. + :param scale: The scale factor to resize the screenshot. + :return: Resized screenshot. """ - original_position: Point | tuple[int, int] = position() - leftClick(target_location) - moveTo(original_position) + new_width: int = int(screenshot.shape[1] * scale) + new_height: int = int(screenshot.shape[0] * scale) + return cast(MatLike, resize(screenshot, (new_width, new_height))) def multiscale_match_template( @@ -135,7 +134,6 @@ def multiscale_match_template( :param templates: List of edged templates to match. :param screenshot: Screenshot where the search is running. :param left_top_coordinates: Left-top pixel of the system monitor(s). - :return: None. """ for scale in SCALES: resized_screenshot: MatLike = resize_screenshot(screenshot, scale) @@ -164,11 +162,7 @@ def multiscale_match_template( def run() -> None: - """ - Run the auto-downloader. - - :return: None. - """ + """Run the auto-downloader.""" logging.info("NexusDownloadFlow is running.") edged_templates: list[MatLike] = init_templates() with mss() as mss_instance: @@ -184,12 +178,9 @@ def try_run() -> None: """ Try to run the auto-downloader. - :param keep_logfile: If the logfile should be kept on program exit. - :raises SystemExit: Raised when closing the program. :raises KeyboardInterrupt: Raised when the user interrupts the program. :raises ValueError: Should not be raised (open an issue on GitHub if it happens). :raises Exception: For currently unknown exceptions (open an issue on GitHub if it happens). - :return: None. """ keep_logfile: bool = False try: From c9e6e0b37e0bf4f938dd8217bb8bd4d79df7e8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 08:06:35 +0100 Subject: [PATCH 31/33] [0015] [ndf-script] readme --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 6ba94e6..b4b7956 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,6 @@ Execute `NexusDownloadFlow.exe` while the mod list is downloading. ## Auto-clicker is not clicking -Do not worry, you have to replace the template files where you installed NDF with the one you will screenshot: -`NexusDownloadFlow/assets/template{x}.png` - -+ `template1.png` is the raw `Slow download` button -+ `template2.png` is the `Slow download` button with mouse over -+ `template3.png` is the `Click here` link appearing five seconds after clicking on `Slow download` button - -If your issue persists, maximize the Nexus Mods page. - -## Your issue still persist? - Open an issue [here](https://github.com/greg-ynx/NexusDownloadFlow/issues/new), and if possible, give the scenario in which you had this issue, which version of NDF you are using and provide a screenshot of your logs or the contents of your current `nexus-download-flow-logs.log` file. From 308db51ee2ba90d8030ea0ccfa3ea4d93418c0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 08:08:05 +0100 Subject: [PATCH 32/33] [0015] [ndf-script] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b4b7956..47ab411 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Execute `NexusDownloadFlow.exe` while the mod list is downloading. ## Auto-clicker is not clicking Open an issue [here](https://github.com/greg-ynx/NexusDownloadFlow/issues/new), and if possible, give the scenario in which you had this issue, which version of NDF you are using -and provide a screenshot of your logs or the contents of your current `nexus-download-flow-logs.log` file. +and provide a screenshot of your logs or the contents of your current `{date}_ndf.log` file. ## Credits From ccc0165ed6b58802f7a37f525ecf3a00e9e6529a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Ployart?= Date: Fri, 3 Nov 2023 08:21:05 +0100 Subject: [PATCH 33/33] [release] [ndf-script] release version 2.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5840f78..aa681a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "NexusDownloadFlow" -version = "2.0.0-SNAPSHOT" +version = "2.0.0" authors = [ {name = "Gregory Ployart", email = "greg.ynx@gmail.com", alias = "greg-ynx"}, ]