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:
-
- 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`