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 2fffcd1..0000000 Binary files a/templates/1_150_slow_download.png and /dev/null differ diff --git a/templates/2_80_click_here.png b/templates/2_80_click_here.png deleted file mode 100644 index 660b4d8..0000000 Binary files a/templates/2_80_click_here.png and /dev/null differ diff --git a/templates/3_30_vortex_download.png b/templates/3_30_vortex_download.png deleted file mode 100644 index 5cf5c41..0000000 Binary files a/templates/3_30_vortex_download.png and /dev/null differ