Skip to content

Commit

Permalink
Rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
parsiad committed May 27, 2024
1 parent fa87a17 commit b06144d
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 119 deletions.
21 changes: 3 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
26 changes: 9 additions & 17 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,22 @@ <h3>About</h3>
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 <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.
</p>
<h3>Download</h3>
<p>
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:
</p>
<table class="table">
<tr>
<th scope="col">Name</th>
<th scope="col">Platform</th>
</tr>
<tr>
<td><a href="https://rg.to/file/f5c83f7b0d68450ba2a7668d26acb2ae" target="_blank">nexus_autodl.exe</a></td>
<td>Windows x64</td>
</tr>
</table>
<p>
Users on other platforms can download the <a href="https://github.com/parsiad/nexus-autodl" target="_blank">source code on GitHub</a>.
<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>
</p>
<h3>Download</h3>
<p>
If you like Nexus AutoDL, please leave a star on GitHub to help others find it:
A Windows binary is available on the <a href="https://github.com/parsiad/nexus-autodl/releases">releases page</a>.
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.
</p>
<p>
<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>
Users on other platforms can download the <a href="https://github.com/parsiad/nexus-autodl" target="_blank">source code on GitHub</a>.
</p>
<h3>Caution</h3>
<p>
Expand Down
141 changes: 60 additions & 81 deletions nexus_autodl.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 4 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"venvPath": ".",
"venv": "venv"
}
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pyautogui
click
pillow
opencv-python

2 changes: 0 additions & 2 deletions style.yapf

This file was deleted.

Binary file removed templates/1_150_slow_download.png
Binary file not shown.
Binary file removed templates/2_80_click_here.png
Binary file not shown.
Binary file removed templates/3_30_vortex_download.png
Binary file not shown.

0 comments on commit b06144d

Please sign in to comment.