From b06144d562e549dabeaf1db7d52ca3c0fe25a8f1 Mon Sep 17 00:00:00 2001 From: Parsiad Azimzadeh Date: Mon, 27 May 2024 15:20:59 -0400 Subject: [PATCH] Rewrite --- Makefile | 21 +---- README.md | 19 +++- index.html | 26 ++---- nexus_autodl.py | 141 ++++++++++++----------------- pyrightconfig.json | 4 + requirements.txt | 5 + style.yapf | 2 - templates/1_150_slow_download.png | Bin 9292 -> 0 bytes templates/2_80_click_here.png | Bin 10290 -> 0 bytes templates/3_30_vortex_download.png | Bin 1406 -> 0 bytes 10 files changed, 99 insertions(+), 119 deletions(-) mode change 100644 => 100755 nexus_autodl.py create mode 100644 pyrightconfig.json create mode 100644 requirements.txt delete mode 100644 style.yapf delete mode 100644 templates/1_150_slow_download.png delete mode 100644 templates/2_80_click_here.png delete mode 100644 templates/3_30_vortex_download.png diff --git a/Makefile b/Makefile index f10967c..05145e5 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,11 @@ NAME:=nexus_autodl -ifeq ($(OS),Windows_NT) - PATHSEP:=; -else - PATHSEP:=: -endif - -all: yapf lint mypy build +all: build build: $(NAME).py - pyinstaller --clean -F --add-data 'templates$(PATHSEP)templates' $< + pyinstaller --clean -F $< clean: $(RM) -r build dist *.spec -lint: $(NAME).py - pylint --max-line-length 120 $< - -mypy: $(NAME).py - mypy $< - -yapf: $(NAME).py - yapf -i --style style.yapf $< - -.PHONY: build clean lint mypy yapf +.PHONY: build clean diff --git a/README.md b/README.md index 089bbe7..95a6500 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,23 @@ Since modlists supported by tools like [Wabbajack](https://www.wabbajack.org) an Nexus AutoDL is an autoclicker (a.k.a., autodownloader, bot) that helps automate this process for you. Specifically, while Nexus AutoDL is running, any time a [mod](https://raw.githubusercontent.com/parsiad/nexus-autodl/master/assets/mod_download_page.jpg) or [collection](https://raw.githubusercontent.com/parsiad/nexus-autodl/master/assets/vortex_download_page.jpg) download page is visible on your screen, Nexus AutoDL will attempt to click the download button. +If you like Nexus AutoDL, please leave a star on GitHub to help others find it. + ## Download -👉 [Visit the website](https://parsiad.github.io/nexus-autodl) 👈 to download +A Windows binary is available on the [releases page](https://github.com/parsiad/nexus-autodl/releases). +Download it and double-click on it to start Nexus AutoDL. +The first time you run the application, you will be presented with some instructions. +Follow the instructions and relaunch it. +This spawns a terminal window which you can close when you are done downloading mods. + +Users on other platforms can download the source code on GitHub. + +## Caution + +Using a bot to download from Nexus is in direct violation of their TOS: + +> Attempting to download files or otherwise record data offered through our services (including but not limited to the Nexus Mods website and the Nexus Mods API) in a fashion that drastically exceeds the expected average, through the use of software automation or otherwise, is prohibited without expressed permission. +> Users found in violation of this policy will have their account suspended. + +Use this at your own risk. diff --git a/index.html b/index.html index 4e13e45..4aeb61d 100644 --- a/index.html +++ b/index.html @@ -31,30 +31,22 @@

About

Nexus AutoDL is an autoclicker (a.k.a., autodownloader, bot) that helps automate this process for you. Specifically, while Nexus AutoDL is running, any time a mod download page is visible on your screen, Nexus AutoDL will attempt to click the download button.

-

Download

- A Windows binary is available below. - Download it and double-click on it to start Nexus AutoDL. - This spawns a terminal window which you can close when you are done downloading mods. + If you like Nexus AutoDL, please leave a star on GitHub to help others find it:

- - - - - - - - - -
NamePlatform
nexus_autodl.exeWindows x64

- Users on other platforms can download the source code on GitHub. + Star nexus-autodl on GitHub

+

Download

- If you like Nexus AutoDL, please leave a star on GitHub to help others find it: + A Windows binary is available on the releases page. + Download it and double-click on it to start Nexus AutoDL. + The first time you run the application, you will be presented with some instructions. + Follow the instructions and relaunch it. + This spawns a terminal window which you can close when you are done downloading mods.

- Star nexus-autodl on GitHub + Users on other platforms can download the source code on GitHub.

Caution

diff --git a/nexus_autodl.py b/nexus_autodl.py old mode 100644 new mode 100755 index ed3ac26..4417df5 --- a/nexus_autodl.py +++ b/nexus_autodl.py @@ -1,99 +1,78 @@ #!/usr/bin/env python -# pylint: disable=missing-module-docstring - -from typing import List, NamedTuple -import os import logging import random -import re import sys import time +from pathlib import Path -from numpy import ndarray as NDArray import click -import cv2 as cv # type: ignore -import numpy as np -import PIL # type: ignore -import PIL.ImageOps # type: ignore -import pyautogui # type: ignore +import pyautogui +from PIL import UnidentifiedImageError +from PIL.Image import Image, open as open_image +from pyautogui import ImageNotFoundException +from pyscreeze import Box + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s") @click.command() -@click.option('--sleep_max', default=5.) -@click.option('--sleep_min', default=0.) -def run(sleep_max: float, sleep_min: float) -> None: # pylint: disable=missing-function-docstring - logging.basicConfig( - datefmt='%m/%d/%Y %I:%M:%S %p', - format='%(asctime)s [%(levelname)s] %(message)s', - level=logging.INFO, - ) - templates = _get_templates() - while True: - sleep_seconds = random.uniform(sleep_min, sleep_max) - logging.info('Sleeping for %f seconds', sleep_seconds) - time.sleep(sleep_seconds) +@click.option("--confidence", default=0.7, show_default=True) +@click.option("--grayscale/--color", default=True, show_default=True) +@click.option("--min-sleep-interval", default=1, show_default=True) +@click.option("--max-sleep-interval", default=5, show_default=True) +@click.option("--templates-path", default=Path.cwd() / "templates", show_default=True) +def main( + confidence: float, + grayscale: bool, + min_sleep_interval: int, + max_sleep_interval: int, + templates_path: str, +) -> None: + templates_path_ = Path(templates_path) + templates: dict[Path, Image] = {} + for template_path in templates_path_.rglob("*"): try: - _find_and_click(templates) - except cv.error: # pylint: disable=no-member - logging.info('Ignoring OpenCV error') - + templates[template_path] = open_image(template_path) + except UnidentifiedImageError: + logging.info(f"{template_path} is not a valid image; skipping") -class _Template(NamedTuple): - array: NDArray - name: str - threshold: int + if len(templates) == 0: + logging.error( + f"No images found in {templates_path_.absolute()}. " + f"If this is your first time running, take a screenshot and crop " + f"(WIN+S on Windows) the item on the screen you want to click on, " + f"placing the result in the {templates_path_.absolute()} directory." + ) + input("Press ENTER to exit.") + sys.exit(1) + while True: + screenshot = pyautogui.screenshot() -def _find_and_click(templates: List[_Template]) -> None: - screenshot_image = pyautogui.screenshot() - screenshot = _image_to_grayscale_array(screenshot_image) - for template in templates: - sift = cv.SIFT_create() # pylint: disable=no-member - _, template_descriptors = sift.detectAndCompute(template.array, mask=None) - screenshot_keypoints, screenshot_descriptors = sift.detectAndCompute(screenshot, mask=None) - matcher = cv.BFMatcher() # pylint: disable=no-member - matches = matcher.knnMatch(template_descriptors, screenshot_descriptors, k=2) - points = np.array([screenshot_keypoints[m.trainIdx].pt for m, _ in matches if m.distance < template.threshold]) - if points.shape[0] == 0: - continue - point = np.median(points, axis=0) - current_mouse_pos = pyautogui.position() - logging.info('Saving current mouse position at x=%f y=%f', *current_mouse_pos) - pyautogui.click(*point) - logging.info('Clicking on %s at coordinates x=%f y=%f', template.name, *point) - pyautogui.moveTo(*current_mouse_pos) - return - logging.info('No matches found') - - -def _get_templates() -> List[_Template]: # pylint: disable=too-many-locals - templates = [] - try: - root_dir = sys._MEIPASS # type: ignore # pylint: disable=no-member,protected-access - except AttributeError: - root_dir = '.' - templates_dir = os.path.join(root_dir, 'templates') - pattern = re.compile(r'^([1-9][0-9]*)_([1-9][0-9]*)_(.+)\.png$') - basenames = os.listdir(templates_dir) - matches = (pattern.match(basename) for basename in basenames) - filtered_matches = (match for match in matches if match is not None) - groups = (match.groups() for match in filtered_matches) - sorted_groups = sorted(groups, key=lambda t: int(t[0])) - for index, threshold, name in sorted_groups: - path = os.path.join(templates_dir, f'{index}_{threshold}_{name}.png') - image = PIL.Image.open(path) # pylint: disable=no-member - array = _image_to_grayscale_array(image) - template = _Template(array=array, name=name, threshold=int(threshold)) - templates.append(template) - return templates - + for template_path, template_image in templates.items(): + logging.info(f"Attempting to match {template_path}.") + box: Box | None = None + try: + box = pyautogui.locate( + template_image, + screenshot, + grayscale=grayscale, + confidence=confidence, + ) + except ImageNotFoundException: + pass + if not isinstance(box, Box): + continue + match_x, match_y = pyautogui.center(box) + pyautogui.click(match_x, match_y) + logging.info(f"Matched at ({match_x}, {match_y}).") + break -def _image_to_grayscale_array(image: PIL.Image.Image) -> NDArray: - image = PIL.ImageOps.grayscale(image) - array = np.array(image) - return array + sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval) + logging.info(f"Waiting for {sleep_interval:.2f} seconds.") + time.sleep(sleep_interval) -if __name__ == '__main__': - run() # pylint: disable=no-value-for-parameter +if __name__ == "__main__": + main() diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..79396a8 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,4 @@ +{ + "venvPath": ".", + "venv": "venv" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a82ce12 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pyautogui +click +pillow +opencv-python + diff --git a/style.yapf b/style.yapf deleted file mode 100644 index aacbf3e..0000000 --- a/style.yapf +++ /dev/null @@ -1,2 +0,0 @@ -[style] -column_limit = 120 diff --git a/templates/1_150_slow_download.png b/templates/1_150_slow_download.png deleted file mode 100644 index 2fffcd1596ddccfcf942d7c73fb0e04bb54f05fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9292 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 Qyj6iTRdrPglx#!(2fjfB?*IS* diff --git a/templates/2_80_click_here.png b/templates/2_80_click_here.png deleted file mode 100644 index 660b4d88ed9291f3bee4de0a356f8dc3eab3ca16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10290 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 CBVW@1 diff --git a/templates/3_30_vortex_download.png b/templates/3_30_vortex_download.png deleted file mode 100644 index 5cf5c41cb2757b18b056e477fe31295b65ee4f98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1406 zcmV-^1%djBP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGmbN~PnbOGLGA9w%&1rtd`K~!i%?O0uG z6lD}WJF_#p+ud!K(to5<3KRK|?g*K?4R86ZJu#d^0h=YfRKcUZh4X zCWb_eF+>v~EkA;-Vxfg9ltQsTNZak~c7JEqdvB+uO{{Kr)EDL?yR-9kzPa~1_nv!i z>gjoBXAUk3hGJh`6bP3mq3Sb33Q0RodW~)+YsKgA0EwbujF8* zCy^fLgwfFoYjV_H|9>&Ic*iT3vqr5B|KFThv32_;)! znnmoLi@XT5%rp|0+mQV27^27CMxysyXcemvT({eP;?9NFzAgxDfBjua~cc;xC38Du-UPLDtmL zS|Kz&+#~l&pae=$1jgBeh<6=YI))+*F$Yxmd zzF5a+NL>1k-q%VQGbq`^#LNZDbf{ zWp35cB1>p*m?U>i>`Ws|1W|~gP?ocdH6nV^V{f2xZ!3Kc!8fb?D4{asayjvFp(uzz zj<}))L~|=jp^+S?!P4R)Y{sW%MX7@{L+aIQ zq{wrvV*=R)y=n~w!w{)26d7tXyw|&^cFtJkjod}wL`v+YloI+$<>h;6Y7ELvxiZWs zCxHig*@~%!1t2d16;+qPlA{>e{e?mSUJ*ZwH@`rczD~+Svi~%! zq(O)9QkUN${zaN!mF^BY7bn8BAAYLWp?mkpIRlMPP(XaPmHCkufo!aJ>&xWWe#x>F zITdaHVCKN_b6+4vosyeG$(9#MGEc#z$Qx%4&WsTZsc|$@W^eV$z0^h;cc#V;&c?!E zoNYmpnwDPKD1Nd+I6%e^ABhbPH2cg`0Q;v^+=KK;k91kC-|QogTvjqFau09ZCw^L~ zxNTv6xc}03h=OXMi`#GsbO@I~hj0mW2$w*Ia0zq>mq3Sb33LSDPa0}C#7+zI`v3p{ M07*qoM6N<$g8A^1`Tzg`