Created
June 24, 2025 20:10
-
-
Save VIRUXE/a72a615da212f6adbc6e8f4145e6232e to your computer and use it in GitHub Desktop.
python3 gta5-mods_download.py <URL> [-o <output_directory>]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from selenium import webdriver | |
from selenium.webdriver.common.by import By | |
from selenium.webdriver.support.ui import WebDriverWait | |
from selenium.webdriver.support import expected_conditions as EC | |
from selenium.common.exceptions import TimeoutException | |
import argparse | |
import time | |
import os | |
from selenium.webdriver.chrome.service import Service | |
# ------------------------------ | |
# Command-line interface | |
# ------------------------------ | |
parser = argparse.ArgumentParser(description="Download a mod file from GTA5-Mods with Selenium (head-less).") | |
parser.add_argument("url", help="The GTA5-Mods mod page or direct download page URL") | |
# Optional download directory (defaults to the working directory) | |
parser.add_argument( | |
"-o", | |
"--output", | |
default=os.getcwd(), | |
help="Directory where the file will be saved (defaults to the current working directory)", | |
) | |
args = parser.parse_args() | |
# Resolve and, if necessary, create the target download directory | |
download_dir = os.path.abspath(args.output) | |
os.makedirs(download_dir, exist_ok=True) | |
# Configure Chrome to run in headless mode for non-GUI environments | |
options = webdriver.ChromeOptions() | |
# NOTE: '--headless=new' uses the new headless implementation available in recent Chrome versions. | |
# Fall back to '--headless' if you encounter compatibility issues on older releases. | |
options.add_argument("--headless=new") | |
# Optionally disable GPU and infobars for additional stability | |
options.add_argument("--disable-gpu") | |
# Configure automatic download location without prompting. | |
options.add_experimental_option("prefs", { | |
"download.default_directory": download_dir, | |
"download.prompt_for_download": False, | |
"safebrowsing.enabled": True, | |
}) | |
options.add_argument("--disable-infobars") | |
# Reduce Chrome log noise that appears on STDERR ("blink.mojom.WidgetHost" etc.) | |
options.add_argument("--log-level=3") # 0 = INFO, 3 = FATAL | |
options.add_argument("--disable-logging") | |
# Remove automation & logging switches that can trigger extra output | |
options.add_experimental_option("excludeSwitches", ["enable-logging", "enable-automation"]) | |
# --- Linux / root-user stability tweaks -------------------------------------- | |
# When running Chrome in head-less mode on Linux (especially as the root user), | |
# the default user-data directory can become locked by another stray Chrome | |
# process, triggering the dreaded: | |
# "session not created: probably user data directory is already in use" | |
# We therefore force Chrome to use an isolated, per-process directory under | |
# /tmp which will be removed automatically by the OS on reboot. We also pass | |
# the "--no-sandbox" and "--disable-dev-shm-usage" flags to avoid sandbox/ | |
# shared-memory pitfalls that frequently appear in containerised or root | |
# environments. | |
unique_user_data_dir = f"/tmp/selenium_chrome_profile_{os.getpid()}" | |
os.makedirs(unique_user_data_dir, exist_ok=True) | |
options.add_argument(f"--user-data-dir={unique_user_data_dir}") | |
options.add_argument("--no-sandbox") # Required when running as root | |
options.add_argument("--disable-dev-shm-usage") # Mitigate /dev/shm size limits | |
# Create a Service that discards ChromeDriver's own logs | |
service = Service(log_path=os.devnull) | |
# Initialise the driver with the silent service | |
driver = webdriver.Chrome(service=service, options=options) | |
try: | |
# Navigate to URL | |
driver.get(args.url) | |
print("Navigated to the provided URL.") | |
# Attempt to accept cookie/consent banner if present | |
try: | |
accept_button = WebDriverWait(driver, 5).until( | |
EC.element_to_be_clickable( | |
(By.XPATH, "//div[contains(@class,'amc-focus-first') and normalize-space(text())='Accept']") | |
) | |
) | |
accept_button.click() | |
print("Clicked the 'Accept' consent button.") | |
except TimeoutException: | |
# Banner not present; continue normally | |
pass | |
# Define download button XPath (language-agnostic) | |
download_button_xpath = "//a[contains(@class, 'btn-download') and .//span[contains(@class, 'fa-download')]]" | |
# Check if on download page or mod page | |
current_url = driver.current_url | |
if "/download/" in current_url: | |
print("Detected download page. Clicking the download button.") | |
download_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, download_button_xpath))) | |
download_button.click() | |
else: | |
print("Detected mod page. Clicking the first download button.") | |
first_download_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, download_button_xpath))) | |
first_download_button.click() | |
# Wait for download page | |
WebDriverWait(driver, 20).until(EC.url_contains("/download/")) | |
print("Navigated to the download page.") | |
# Click second download button | |
print("Clicking the second download button.") | |
second_download_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.XPATH, download_button_xpath))) | |
second_download_button.click() | |
# Wait for download to start | |
time.sleep(5) | |
print("Download initiated. Check your default download folder.") | |
finally: | |
driver.quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment