Skip to content

Commit b06144d

Browse files
committed
Rewrite
1 parent fa87a17 commit b06144d

File tree

10 files changed

+99
-119
lines changed

10 files changed

+99
-119
lines changed

Makefile

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
NAME:=nexus_autodl
22

3-
ifeq ($(OS),Windows_NT)
4-
PATHSEP:=;
5-
else
6-
PATHSEP:=:
7-
endif
8-
9-
all: yapf lint mypy build
3+
all: build
104

115
build: $(NAME).py
12-
pyinstaller --clean -F --add-data 'templates$(PATHSEP)templates' $<
6+
pyinstaller --clean -F $<
137

148
clean:
159
$(RM) -r build dist *.spec
1610

17-
lint: $(NAME).py
18-
pylint --max-line-length 120 $<
19-
20-
mypy: $(NAME).py
21-
mypy $<
22-
23-
yapf: $(NAME).py
24-
yapf -i --style style.yapf $<
25-
26-
.PHONY: build clean lint mypy yapf
11+
.PHONY: build clean

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ Since modlists supported by tools like [Wabbajack](https://www.wabbajack.org) an
99
Nexus AutoDL is an autoclicker (a.k.a., autodownloader, bot) that helps automate this process for you.
1010
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.
1111

12+
If you like Nexus AutoDL, please leave a star on GitHub to help others find it.
13+
1214
## Download
1315

14-
👉 [Visit the website](https://parsiad.github.io/nexus-autodl) 👈 to download
16+
A Windows binary is available on the [releases page](https://github.com/parsiad/nexus-autodl/releases).
17+
Download it and double-click on it to start Nexus AutoDL.
18+
The first time you run the application, you will be presented with some instructions.
19+
Follow the instructions and relaunch it.
20+
This spawns a terminal window which you can close when you are done downloading mods.
21+
22+
Users on other platforms can download the source code on GitHub.
23+
24+
## Caution
25+
26+
Using a bot to download from Nexus is in direct violation of their TOS:
27+
28+
> 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.
29+
> Users found in violation of this policy will have their account suspended.
30+
31+
Use this at your own risk.

index.html

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,22 @@ <h3>About</h3>
3131
Nexus AutoDL is an autoclicker (a.k.a., autodownloader, bot) that helps automate this process for you.
3232
Specifically, while Nexus AutoDL is running, any time a <a href="assets/mod_download_page.jpg" target="_blank">mod download page</a> is visible on your screen, Nexus AutoDL will attempt to click the download button.
3333
</p>
34-
<h3>Download</h3>
3534
<p>
36-
A Windows binary is available below.
37-
Download it and double-click on it to start Nexus AutoDL.
38-
This spawns a terminal window which you can close when you are done downloading mods.
35+
If you like Nexus AutoDL, please leave a star on GitHub to help others find it:
3936
</p>
40-
<table class="table">
41-
<tr>
42-
<th scope="col">Name</th>
43-
<th scope="col">Platform</th>
44-
</tr>
45-
<tr>
46-
<td><a href="https://rg.to/file/f5c83f7b0d68450ba2a7668d26acb2ae" target="_blank">nexus_autodl.exe</a></td>
47-
<td>Windows x64</td>
48-
</tr>
49-
</table>
5037
<p>
51-
Users on other platforms can download the <a href="https://github.com/parsiad/nexus-autodl" target="_blank">source code on GitHub</a>.
38+
<a aria-label="Star nexus-autodl on GitHub" class="github-button" href="https://github.com/parsiad/nexus-autodl" data-icon="octicon-star" data-show-count="true" data-size="large">Star nexus-autodl on GitHub</a>
5239
</p>
40+
<h3>Download</h3>
5341
<p>
54-
If you like Nexus AutoDL, please leave a star on GitHub to help others find it:
42+
A Windows binary is available on the <a href="https://github.com/parsiad/nexus-autodl/releases">releases page</a>.
43+
Download it and double-click on it to start Nexus AutoDL.
44+
The first time you run the application, you will be presented with some instructions.
45+
Follow the instructions and relaunch it.
46+
This spawns a terminal window which you can close when you are done downloading mods.
5547
</p>
5648
<p>
57-
<a aria-label="Star nexus-autodl on GitHub" class="github-button" href="https://github.com/parsiad/nexus-autodl" data-icon="octicon-star" data-show-count="true" data-size="large">Star nexus-autodl on GitHub</a>
49+
Users on other platforms can download the <a href="https://github.com/parsiad/nexus-autodl" target="_blank">source code on GitHub</a>.
5850
</p>
5951
<h3>Caution</h3>
6052
<p>

nexus_autodl.py

100644100755
Lines changed: 60 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,78 @@
11
#!/usr/bin/env python
22

3-
# pylint: disable=missing-module-docstring
4-
5-
from typing import List, NamedTuple
6-
import os
73
import logging
84
import random
9-
import re
105
import sys
116
import time
7+
from pathlib import Path
128

13-
from numpy import ndarray as NDArray
149
import click
15-
import cv2 as cv # type: ignore
16-
import numpy as np
17-
import PIL # type: ignore
18-
import PIL.ImageOps # type: ignore
19-
import pyautogui # type: ignore
10+
import pyautogui
11+
from PIL import UnidentifiedImageError
12+
from PIL.Image import Image, open as open_image
13+
from pyautogui import ImageNotFoundException
14+
from pyscreeze import Box
15+
16+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
2017

2118

2219
@click.command()
23-
@click.option('--sleep_max', default=5.)
24-
@click.option('--sleep_min', default=0.)
25-
def run(sleep_max: float, sleep_min: float) -> None: # pylint: disable=missing-function-docstring
26-
logging.basicConfig(
27-
datefmt='%m/%d/%Y %I:%M:%S %p',
28-
format='%(asctime)s [%(levelname)s] %(message)s',
29-
level=logging.INFO,
30-
)
31-
templates = _get_templates()
32-
while True:
33-
sleep_seconds = random.uniform(sleep_min, sleep_max)
34-
logging.info('Sleeping for %f seconds', sleep_seconds)
35-
time.sleep(sleep_seconds)
20+
@click.option("--confidence", default=0.7, show_default=True)
21+
@click.option("--grayscale/--color", default=True, show_default=True)
22+
@click.option("--min-sleep-interval", default=1, show_default=True)
23+
@click.option("--max-sleep-interval", default=5, show_default=True)
24+
@click.option("--templates-path", default=Path.cwd() / "templates", show_default=True)
25+
def main(
26+
confidence: float,
27+
grayscale: bool,
28+
min_sleep_interval: int,
29+
max_sleep_interval: int,
30+
templates_path: str,
31+
) -> None:
32+
templates_path_ = Path(templates_path)
33+
templates: dict[Path, Image] = {}
34+
for template_path in templates_path_.rglob("*"):
3635
try:
37-
_find_and_click(templates)
38-
except cv.error: # pylint: disable=no-member
39-
logging.info('Ignoring OpenCV error')
40-
36+
templates[template_path] = open_image(template_path)
37+
except UnidentifiedImageError:
38+
logging.info(f"{template_path} is not a valid image; skipping")
4139

42-
class _Template(NamedTuple):
43-
array: NDArray
44-
name: str
45-
threshold: int
40+
if len(templates) == 0:
41+
logging.error(
42+
f"No images found in {templates_path_.absolute()}. "
43+
f"If this is your first time running, take a screenshot and crop "
44+
f"(WIN+S on Windows) the item on the screen you want to click on, "
45+
f"placing the result in the {templates_path_.absolute()} directory."
46+
)
47+
input("Press ENTER to exit.")
48+
sys.exit(1)
4649

50+
while True:
51+
screenshot = pyautogui.screenshot()
4752

48-
def _find_and_click(templates: List[_Template]) -> None:
49-
screenshot_image = pyautogui.screenshot()
50-
screenshot = _image_to_grayscale_array(screenshot_image)
51-
for template in templates:
52-
sift = cv.SIFT_create() # pylint: disable=no-member
53-
_, template_descriptors = sift.detectAndCompute(template.array, mask=None)
54-
screenshot_keypoints, screenshot_descriptors = sift.detectAndCompute(screenshot, mask=None)
55-
matcher = cv.BFMatcher() # pylint: disable=no-member
56-
matches = matcher.knnMatch(template_descriptors, screenshot_descriptors, k=2)
57-
points = np.array([screenshot_keypoints[m.trainIdx].pt for m, _ in matches if m.distance < template.threshold])
58-
if points.shape[0] == 0:
59-
continue
60-
point = np.median(points, axis=0)
61-
current_mouse_pos = pyautogui.position()
62-
logging.info('Saving current mouse position at x=%f y=%f', *current_mouse_pos)
63-
pyautogui.click(*point)
64-
logging.info('Clicking on %s at coordinates x=%f y=%f', template.name, *point)
65-
pyautogui.moveTo(*current_mouse_pos)
66-
return
67-
logging.info('No matches found')
68-
69-
70-
def _get_templates() -> List[_Template]: # pylint: disable=too-many-locals
71-
templates = []
72-
try:
73-
root_dir = sys._MEIPASS # type: ignore # pylint: disable=no-member,protected-access
74-
except AttributeError:
75-
root_dir = '.'
76-
templates_dir = os.path.join(root_dir, 'templates')
77-
pattern = re.compile(r'^([1-9][0-9]*)_([1-9][0-9]*)_(.+)\.png$')
78-
basenames = os.listdir(templates_dir)
79-
matches = (pattern.match(basename) for basename in basenames)
80-
filtered_matches = (match for match in matches if match is not None)
81-
groups = (match.groups() for match in filtered_matches)
82-
sorted_groups = sorted(groups, key=lambda t: int(t[0]))
83-
for index, threshold, name in sorted_groups:
84-
path = os.path.join(templates_dir, f'{index}_{threshold}_{name}.png')
85-
image = PIL.Image.open(path) # pylint: disable=no-member
86-
array = _image_to_grayscale_array(image)
87-
template = _Template(array=array, name=name, threshold=int(threshold))
88-
templates.append(template)
89-
return templates
90-
53+
for template_path, template_image in templates.items():
54+
logging.info(f"Attempting to match {template_path}.")
55+
box: Box | None = None
56+
try:
57+
box = pyautogui.locate(
58+
template_image,
59+
screenshot,
60+
grayscale=grayscale,
61+
confidence=confidence,
62+
)
63+
except ImageNotFoundException:
64+
pass
65+
if not isinstance(box, Box):
66+
continue
67+
match_x, match_y = pyautogui.center(box)
68+
pyautogui.click(match_x, match_y)
69+
logging.info(f"Matched at ({match_x}, {match_y}).")
70+
break
9171

92-
def _image_to_grayscale_array(image: PIL.Image.Image) -> NDArray:
93-
image = PIL.ImageOps.grayscale(image)
94-
array = np.array(image)
95-
return array
72+
sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval)
73+
logging.info(f"Waiting for {sleep_interval:.2f} seconds.")
74+
time.sleep(sleep_interval)
9675

9776

98-
if __name__ == '__main__':
99-
run() # pylint: disable=no-value-for-parameter
77+
if __name__ == "__main__":
78+
main()

pyrightconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"venvPath": ".",
3+
"venv": "venv"
4+
}

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pyautogui
2+
click
3+
pillow
4+
opencv-python
5+

style.yapf

Lines changed: 0 additions & 2 deletions
This file was deleted.

templates/1_150_slow_download.png

-9.07 KB
Binary file not shown.

templates/2_80_click_here.png

-10 KB
Binary file not shown.

templates/3_30_vortex_download.png

-1.37 KB
Binary file not shown.

0 commit comments

Comments
 (0)