Author SHA1 Message Date
iamdoubz 5a2ddae93a Bump new version for binary release 2026-04-06 16:51:56 -05:00
iamdoubz 86969ffff7 Refactor, simplify, more error handling 2026-04-06 16:49:55 -05:00
iamdoubz 90cfe009b6 Add why was this created section 2026-03-27 14:08:37 -05:00
iamdoubz 4d088fa7bb Bump binary release version 2026-03-27 14:08:06 -05:00
iamdoubz 1ea3e9f231 For small downloads better wait time logic 2026-03-27 14:07:38 -05:00
iamdoubz 2dc5542bb2 Bump binary release version 2026-03-26 09:36:45 -05:00
iamdoubz 6ec47572de Add more prerequisites for file name sanitization, retrieving covers 2026-03-26 09:36:01 -05:00
iamdoubz a769502237 Remove chrome driver dependency, bump version 2026-03-26 09:35:05 -05:00
iamdoubz 928165467c Fix bug when downloading covers, bump version 2026-03-26 09:34:12 -05:00
iamdoubz 8c9c634623 Add -gc argument to V-dlp program 2026-03-25 15:01:06 -05:00
iamdoubz 34ca036897 Bump release for binary files 2026-03-25 14:58:55 -05:00
iamdoubz aec6d522bc Update Features section to include downloading box covers 2026-03-25 14:56:27 -05:00
iamdoubz 50a345e20c Add ability to download game covers, misc. improvements 2026-03-25 14:55:18 -05:00
iamdoubz 92cdea8775 Simplify args by removing long args and keeping short ones 2026-03-25 11:51:41 -05:00
iamdoubz 625904bcc7 Add multi-disc download to feature section 2026-03-25 11:36:10 -05:00
iamdoubz b4b796c56f Add logic to download multiple discs 2026-03-25 11:35:08 -05:00
iamdoubz 06b91e771d Add urls.txt 2026-03-25 11:34:38 -05:00
iamdoubz 97dd01a8fc Add new features! 2026-03-25 10:13:28 -05:00
iamdoubz 95dcc33e84 Add ability to get # links 2026-03-25 10:12:52 -05:00
iamdoubz 3bb6d742e6 Add ability to get # links 2026-03-25 10:12:15 -05:00
iamdoubz 51a1921b12 Write failed downloads to file 2026-03-25 09:56:08 -05:00
iamdoubz e2ec03c354 Handle divide by zero error for total statistics 2026-03-25 09:51:32 -05:00
iamdoubz c62962f428 Better error handling, add total statistics 2026-03-25 09:48:28 -05:00
iamdoubz 3a26dcedbd Return unique list of URLs 2026-03-25 09:24:49 -05:00
6 changed files with 468 additions and 197 deletions
+18 -10
View File
@@ -10,12 +10,15 @@ A program to aid in queuing downloads from Vimm.net
### Shared
- [Google Chrome](https://www.google.com/chrome/)
- [Chromedriver](https://googlechromelabs.github.io/chrome-for-testing/)
- ~~[Chromedriver](https://googlechromelabs.github.io/chrome-for-testing/)~~
### Source
- Python 3.11 (others might work)
- Selenium `pip install selenium`
- Results `pip install results`
- Pathlib ` pip install pathlib`
- Pathvalidate `pip install pathvalidate`
## Programs
@@ -55,32 +58,33 @@ V-dlp will parse a file and attempt to download each one at a time.
| -tw | Number of seconds to pause between downloads | float | 4 |
| -nm | Do not monitor download statistics (set flag to True for small downloads <32MB) | boolean | False |
| -uh | Use headless Chrome (If you do not want to use, do not pass in the flag) | boolean | False |
| -gc | Download cover image (0: don't download, 1: small, 2: large, 3: both) | int | 0 |
| -v | Display version information | NA | none |
## Features
- [X] Download files
- [X] Handle multi-disc downloads
- [X] Download box cover art
- [X] Show download statistics (speed, ETA)
- [X] More download statistics
- [X] Show number of URLs
- [X] Show download name/size
- [X] Add headless option
- [X] Remove dependency of Chrome Driver
- [X] Show failed downloads
- [X] Remove duplicate URLs
- [X] Get all links (#, A-Z)
- [X] No more waiting for each download to finish
## Upcoming features
- [ ] Proper error handling
- [ ] Show failed downloads
- [ ] Download cover art
- [ ] Download manuals
- [ ] Remove duplicate URLs
- [ ] Email upon completion
- [ ] More download statistics
- [ ] Handle multi-disc downloads
- [ ] Remove dependency on external start of Chrome Driver
- [ ] Click on ads
- [ ] OS agnostic (heavily Windows based as they need the most hand holding)
- [ ] Get # links
## Bonus
@@ -100,4 +104,8 @@ Download the games.
python dlp.py -nm True -uh True -tw 1 -u atari26roms.txt
```
**NOTE**: if you are using the release, substitute "V-dlp" for "python dlp.py".
**NOTE**: if you are using the release, substitute "V-dlp" for "python dlp.py".
## Why was this created?
iamdoubz wanted to learn how to use python, selenium, and get data from a website. iamdoubz did not know where to start and generated a few lines of code using AI, then added the rest using StackOverflow and Google.
+429 -171
View File
@@ -1,22 +1,26 @@
import argparse
import glob
import logging
import os
from pathlib import Path
from pathvalidate import sanitize_filename
import random
import requests
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
import sys
import time
import traceback
__version__ = "2026.3.24.0"
__version__ = "2026.4.6.0"
# Helper for logging
def setup_logging(mode="syslog", logfile=None):
def setup_logging(folder, mode="syslog", logfile=None):
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# remove default handlers
@@ -35,69 +39,55 @@ def setup_logging(mode="syslog", logfile=None):
if mode in ("file", "all"):
if not logfile:
raise ValueError("File logging requires a logfile path")
file_handler = logging.FileHandler(logfile)
file_handler = logging.FileHandler(os.path.join(folder, logfile))
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Setup pass in arguments
def main():
# Create the parser and add a description
# Create the parser and add a description
def args():
parser = argparse.ArgumentParser(
description="V-dlp options and variables...",
epilog="End of help documentation..."
)
#parser = argparse.ArgumentParser()
parser.add_argument("--log", "-l", type=str, help="Choose logging option", default="syslog", choices=["syslog","file","all","none"])
parser.add_argument("--logfile", "-lf", help="If log is file/all need to specify file to log to")
parser.add_argument("--dir_download", "-d", type=str, help="Download directory to use", default=f"{Path.home() / 'Downloads'}")
parser.add_argument("--file_urls", "-u", type=str, help="File with links inside", default="urls.txt")
parser.add_argument("--chrome_port", "-p", type=float, help="Specify already running Chrome Driver port", default=54321)
parser.add_argument("--use_headless", "-uh", type=bool, help="Use headless Chrome", default=False)
parser.add_argument("--page_load_time", "-tl", type=float, help="How long to wait for webpage to load before timeout", default=10)
parser.add_argument("--refresh_rate", "-r", type=float, help="How often to refresh statistics on screen", default=2)
parser.add_argument("--wait_time", "-tw", type=float, help="Number of seconds to pause between downloads", default=4)
parser.add_argument("--no_monitor", "-nm", type=bool, help="Do not monitor download statistics", default=False)
parser.add_argument("--version", "-v", action='store_true', help="Display version information")
args = parser.parse_args()
if args.version:
sys.exit(f"v{__version__}\n")
parser.add_argument("-l", type=str, help="Choose logging option", default="syslog", choices=["syslog","file","all","none"])
parser.add_argument("-lf", help="If log is file/all need to specify file to log to")
parser.add_argument("-d", type=str, help="Download directory to use", default=f"{Path.home() / 'Downloads'}")
parser.add_argument("-u", type=str, help="File with links inside", default="urls.txt")
parser.add_argument("-uh", type=bool, help="Use headless Chrome", default=False)
parser.add_argument("-tl", type=float, help="How long to wait for webpage to load before timeout", default=8)
parser.add_argument("-r", type=float, help="How often to refresh statistics on screen", default=2)
parser.add_argument("-tw", type=float, help="Number of seconds to pause between downloads", default=4)
parser.add_argument("-nm", type=bool, help="Do not monitor download statistics", default=False)
parser.add_argument("-gc", type=int, help="Download cover image (0: don't download, 1: small (avif), 2: large (webp), 3: both)", default=0, choices=[0, 1, 2, 3])
parser.add_argument("-v", action='store_true', help="Display version information")
return parser
setup_logging(args.log, args.logfile)
download_dir = args.dir_download
url_file = args.file_urls
chrome_port = args.chrome_port
headless = args.use_headless
page_load_time = args.page_load_time
refresh_rate = args.refresh_rate
wait_time = args.wait_time
monitor = args.no_monitor
logging.info(f"Download Directory: {download_dir}")
logging.info(f"Reading URLs from: {url_file}")
logging.info(f"Using ChromeD Port: {chrome_port}")
# URLs to process
# URLs to process
def open_urls(file):
try:
with open(url_file) as f:
with open(file) as f:
urls = [line.strip() for line in f]
except:
logging.error(f"Could not find url file: {url_file}!")
raise FileNotFoundError(f"Could not find url file: {url_file}!")
url_length = len(urls)
cur_url = 1
ess = "s"
if url_length == 0:
logging.warning("There are no URLs to process!")
sys.exit("There are no URLs to process!")
if url_length == 1:
ess = ""
logging.info(f"Will process {url_length} URL{ess}...")
# If directory does not exist, create it
os.makedirs(download_dir, exist_ok=True)
urls = list(dict.fromkeys(urls))
url_length = len(urls)
ess = "s"
if url_length == 0:
logging.warning("There are no URLs to process!")
sys.exit("There are no URLs to process!")
if url_length == 1:
ess = ""
logging.info(f"Will process {url_length} URL{ess}...")
return urls, url_length
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
emessage = f"Error {exc_tb.tb_lineno}: Could not find url file: {e}!"
logging.error(emessage)
sys.exit(emessage)
# Launch Chrome
def open_chrome(headless, download_dir):
# Generate list of random screen resolutions
display_resolutions = ["2560,1440","1920,1080","1600,1200"]
# Create and add Chrome options
chrome_options = Options()
if headless == True:
@@ -106,21 +96,20 @@ def main():
chrome_options.add_argument(f"--window-size=2560,1440")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
prefs = {
"download.default_directory": download_dir,
"download.prompt_for_download": False,
"download.directory_upgrade": True
}
chrome_options.add_experimental_option("prefs", prefs)
chrome_options.add_argument("--simulate-outdated-no-au='Tue, 31 Dec 2099 23:59:59 GMT'")
chrome_options.add_argument("--disable-background-networking")
chrome_options.add_argument("--disable-component-update")
prefs = {
"download.default_directory": download_dir,
"download.prompt_for_download": False,
"download.directory_upgrade": True
}
chrome_options.add_experimental_option("prefs", prefs)
driver = webdriver.Chrome(options=chrome_options)
#driver = webdriver.Chrome(service=Service(r"C:\Tools\Standalone\chromedriver.exe"), options=chrome_options)
#driver = webdriver.Remote(
# command_executor=f"http://127.0.0.1:{chrome_port}",
# options=chrome_options
#)
# Chrome sometimes blocks downloads in headless mode
if headless == True:
# Chrome sometimes blocks downloads in headless mode
driver.execute_cdp_cmd(
"Page.setDownloadBehavior",
{
@@ -128,118 +117,387 @@ def main():
"downloadPath": download_dir
}
)
# Helper to calculate percentages
def percentage_of_total(part, whole):
if whole == 0:
return 0 # Handle division by zero case
return round(((part / whole) * 100), 1)
# Display useful stats about ongoing downloads
def monitor_download(folder, fsize):
downloading = True
last_size = 0
tstart = time.time()
while downloading:
files = os.listdir(folder)
partial = [f for f in files if f.endswith(".crdownload")]
if partial:
file_path = os.path.join(folder, partial[0])
size = os.path.getsize(file_path)
tot_perc = percentage_of_total(size, fsize)
if size != last_size:
ct = time.time()
dt = ct - tstart
if dt == 0:
dt = 1
speed = round(((size - last_size)/1024/1024/refresh_rate)*8, 2)
etas = ((fsize - size) / (size / dt))
etam, etams = divmod(etas, 60)
if args.log in ("syslog", "all"):
print(F"Downloading at {speed} Mbps... {tot_perc}%. ETA: {int(etam)}m {int(etams)}s ", end="\r")
last_size = size
time.sleep(refresh_rate)
return driver
# Helper to calculate percentages
def percentage_of_total(part, whole):
if whole == 0:
return 0 # Handle division by zero case
return round(((part / whole) * 100), 1)
# Check for temp files limiting amount of time spent before failing
def wait_for_file(download_dir, check_interval_seconds=0.25, max_wait_seconds=10):
start_time = time.time()
while True:
# Check if the file exists
files = os.listdir(download_dir)
partial = [f for f in files if f.endswith(".crdownload")]
if partial:
elapsed_time = time.time() - start_time
return True
# Calculate elapsed time and check if timeout is reached
elapsed_time = time.time() - start_time
if elapsed_time >= max_wait_seconds:
print(f"Timed out after {max_wait_seconds} seconds. Temp file not found at {download_dir}.")
return False
# Wait for the specified interval before the next check
remaining_time = max_wait_seconds - elapsed_time
if remaining_time < check_interval_seconds:
time_to_sleep = remaining_time
# Time to sleep
time.sleep(check_interval_seconds)
# Display useful stats about ongoing downloads
def monitor_download(folder, fsize, refresh_rate, tstart=time.time()):
downloading = True
last_size = 0
while downloading:
files = os.listdir(folder)
partial = [f for f in files if f.endswith(".crdownload")]
if partial:
file_path = os.path.join(folder, partial[0])
size = os.path.getsize(file_path)
tot_perc = percentage_of_total(size, fsize)
if size != last_size:
ct = time.time()
dt = ct - tstart
if dt == 0:
dt = 1
speed = round(((size - last_size)/1024/1024/refresh_rate)*8, 2)
etas = ((fsize - size) / (size / dt))
etam, etams = divmod(etas, 60)
if args.l in ("syslog", "all"):
print(F"Downloading at {speed} Mbps... {tot_perc}%. ETA: {int(etam)}m {int(etams)}s ", end="\r")
last_size = size
time.sleep(refresh_rate)
else:
downloading = False
tsec = time.time() - tstart
if tsec == 0:
tsec = 1
tmin, tmsec = divmod(tsec, 60)
avg_speed = round((fsize/tsec/1024/1024)*8, 1)
if avg_speed > 175:
logging.warning("Download did not start (probably)")
return -999, -999
else:
downloading = False
tsec = time.time() - tstart
if tsec == 0:
tsec = 1
tmin, tmsec = divmod(tsec, 60)
avg_speed = round((fsize/tsec/1024/1024)*8, 1)
logging.info(f"Downloaded {round((fsize/1024/1024), 2)}MB in {int(tmin)}m {int(tmsec)}s ({avg_speed} Mbps).")
def wait_for_file(pattern, delay=1, max=15):
#print(f"Waiting for file matching: {pattern}...")
i = 0
while not glob.glob(pattern):
time.sleep(delay)
i += 1
if i > max:
break
# Return the first matching file
return glob.glob(pattern)[0]
# For each URL in the file, run program
for url in urls:
# Open URL
driver.get(url)
# Wait to open URL
wait = WebDriverWait(driver, page_load_time)
# Click Download button
download_button = wait.until(
EC.element_to_be_clickable((By.XPATH, "//button[text()='Download']"))
return last_size, tsec
# Get cover art
def download_img(driver, download_dir, title, itype, baseurl):
try:
img_element = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//img[@alt="Box"]'))
)
download_button.click()
# Output page title to console
title = driver.title.replace("The Vault: ", f"{cur_url}/{url_length}: ")
# Get Vimm file size
size_element = WebDriverWait(driver, 10).until(
if img_element:
if itype in [1,3]:
img_url = img_element.get_attribute('src')
if img_url:
img_saved = save_img(download_dir, title, itype, img_url, baseurl, 1)
if not img_saved:
logging.warning("See above WARNING.")
else:
logging.warning("No box image url found.")
if itype in [2,3]:
body_element = driver.find_element(By.ID, "main")
img_element.click()
try:
dialog_element = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.ID, "imageDialog"))
)
if dialog_element:
img_element2 = dialog_element.find_element(By.TAG_NAME, "img")
img_url2 = img_element2.get_attribute('src')
if img_url2:
save_img(download_dir, title, itype, img_url2, baseurl, 2)
else:
logging.warning("No large box image found.")
actions = ActionChains(driver)
actions.move_to_element_with_offset(body_element, random.randint(1, 100), random.randint(1, 100)).click().perform()
time.sleep(1)
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
logging.warning(f"Error {exc_tb.tb_lineno}: No large box image found!")
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
logging.warning(f"Error {exc_tb.tb_lineno}: No box image exists. Skipping...")
# Save cover art
def save_img(download_dir, title, itype, iurl, baseurl, iform):
headers: dict[str, str] = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0',
'Referer': f'{iurl}'
}
response = requests.get(iurl, headers=headers, allow_redirects=True, stream=True)
response.raise_for_status()
if response.status_code == 200:
fext = 'avif'
ftitle = sanitize_filename(title)
if iform == 2:
fext = 'webp'
save_path = os.path.join(f"{download_dir}", f"{ftitle}.{fext}")
with open(save_path, 'wb') as file:
for chunk in response.iter_content(1024):
file.write(chunk)
return True
else:
logging.warning(f"Could not download box image: {response.status_code} - {response.reason}")
return False
# Download logic
def download_it(driver, wait, monitor, wait_time, download_dir, refresh_rate, total_size, total_time, cur_url, url_length, dtitle, durl, cover, failed_urls, ddisc=0):
# Get file size
size = 0
try:
size_element = wait.until(
EC.presence_of_element_located((By.ID, "dl_size"))
)
size_raw = size_element.text
logging.info(f"{title} {size_raw}")
size = 0
if " KB" in size_raw:
size = float(size_raw.replace(" KB", "")) * 1024
elif " MB" in size_raw:
size = float(size_raw.replace(" MB", "")) * 1024 ** 2
elif " GB" in size_raw:
size = float(size_raw.replace(" GB", "")) * 1024 ** 3
elif " TB" in size_raw:
size = float(size_raw.replace(" GB", "")) * 1024 ** 4
else:
size = 500 * 1024 * 1024
# Try to click Continue if it appears
try:
continue_button = WebDriverWait(driver, 4).until(
EC.element_to_be_clickable((By.XPATH, "//input[@value='Continue']"))
)
continue_button.click()
except:
pass
except:
exc_type, exc_obj, exc_tb = sys.exc_info()
logging.warning(f"Error {exc_tb.tb_lineno}: Could not determine download size from webpage")
size_raw = '1 GB'
pass
# Monitor current download
argmonitor = monitor
argwait = wait_time
if size < 33554432:
monitor = True
wait_time = 17
logging.warning("File size was too small to monitor! (Less than 32MB)")
if monitor == False:
tempTime = time.time()
file_pattern = download_dir + '/*.crdownload'
while not os.path.exists(wait_for_file(file_pattern)):
if time.time() - tempTime > wait_time:
logging.warning("Never found temp file for download...")
break
logging.info(f"{dtitle} {size_raw}")
# Download box cover
if cover > 0:
raw_title = driver.title.replace("The Vault: ", "")
download_img(driver, download_dir, raw_title, cover, durl)
# Click Download button
dl_start = time.time()
try:
download_form = wait.until(
EC.presence_of_element_located((By.ID, "dl_form"))
)
download_button = wait.until(
EC.element_to_be_clickable((By.XPATH, "//button[text()='Download']"))
)
actions = ActionChains(driver)
if download_form:
actions.move_to_element_with_offset(download_form, random.randint(1, 50), random.randint(1, 11)).perform()
time.sleep(1)
actions.move_to_element_with_offset(download_button, random.randint(1, 25), random.randint(1, 6)).perform()
time.sleep(0.5)
#time.sleep(3)
download_form = wait.until(
EC.presence_of_element_located((By.ID, "dl_form"))
)
#logging.info("Clicked form submit")
if download_form:
download_form.submit()
raw_title = driver.title
if raw_title == "Vimm's Lair: Error 400":
return -998, -998
else:
raise ValueError("Download form could not be submitted")
else:
actions.move_to_element_with_offset(download_button, random.randint(1, 13), random.randint(1, 3)).perform()
time.sleep(1)
#logging.info("Clicked download button")
download_button = wait.until(
EC.element_to_be_clickable((By.XPATH, "//button[text()='Download']"))
)
if download_button:
download_button.click()
#driver.execute_script("arguments[0].click();", download_button)
raw_title = driver.title
if raw_title == "Vimm's Lair: Error 400":
return -998, -998
else:
raise ValueError("Download button could not be submitted")
dl_start = time.time()
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
logging.warning(f"Error {exc_tb.tb_lineno}: Download button not found for {dtitle}!")
logging.error(f"{e}")
failed_urls.append(dtitle)
failed_urls.append(durl)
return 0, 0
# Convert human readable size to bytes
if " KB" in size_raw:
size = float(size_raw.replace(" KB", "")) * 1024
elif " MB" in size_raw:
size = float(size_raw.replace(" MB", "")) * 1024 ** 2
elif " GB" in size_raw:
size = float(size_raw.replace(" GB", "")) * 1024 ** 3
elif " TB" in size_raw:
size = float(size_raw.replace(" TB", "")) * 1024 ** 4
else:
size = 500 * 1024 ** 2
# Monitor current download
argmonitor = monitor
argwait = wait_time
ctime = 0
csize = 0
# If file size is >32MB, go ahead and monitor anyway
if size > 33554431 and monitor:
monitor = False
# If file size <32MB, force no monitor
if size < 33554432:
monitor = True
wait_time = 15
size_limit = 2097152
size_factor = size // size_limit
if size_factor < wait_time:
wait_time = size_factor
if size_factor == 0:
wait_time = 1
# If we are monitoring downloads
if monitor == False:
if wait_for_file(download_dir, 0.25, 8):
csize, ctime = monitor_download(download_dir, size*1.01, refresh_rate, dl_start)
if csize == -999 and ctime == -999:
failed_urls.append(dtitle)
failed_urls.append(durl)
elif csize == -998 and ctime == -998:
logging.warning("Vimm's Lair Error 400: An unexpected browser error has occurred (we think you might be a bot)")
failed_urls.append(dtitle)
failed_urls.append(durl)
else:
total_size += csize
total_time += ctime
else:
# Try to click Continue if it appears
try:
raw_title = driver.title
if raw_title != "Vimm's Lair: Error 400":
continue_button = WebDriverWait(driver, 4).until(
EC.element_to_be_clickable((By.XPATH, "//input[@value='Continue']"))
)
continue_button.click()
dl_start = time.time()
logging.info(f"DL Click: {dl_start} Check: {round((time.time() - dl_start),1)}")
if wait_for_file(files, 0.25, 8):
csize, ctime = monitor_download(download_dir, files, size*1.01, refresh_rate, dl_start)
total_size += csize
total_time += ctime
else:
logging.warning("Never found temp file for download...")
failed_urls.append(dtitle)
failed_urls.append(durl)
pass
else:
logging.warning(f"{time.time() - tempTime}")
time.sleep(refresh_rate)
if monitor == False:
monitor_download(download_dir, size+1024)
# Wait to call next URL
logging.warning("Vimm's Lair Error 400: An unexpected browser error has occurred (we think you might be a bot)")
failed_urls.append(dtitle)
failed_urls.append(durl)
except:
exc_type, exc_obj, exc_tb = sys.exc_info()
logging.warning(f"Error {exc_tb.tb_lineno}: Could not click on Continue button...")
failed_urls.append(dtitle)
failed_urls.append(durl)
pass
# Wait to call next URL
if cur_url < url_length:
if size < 33554432:
logging.warning(f"File size was too small to monitor! (Less than 32MB). Waiting {round(wait_time, 1)} seconds...")
time.sleep(wait_time)
cur_url += 1
monitor = argmonitor
wait_time = argwait
# Close Chrome session
driver.quit()
else:
if size < 33554432:
logging.warning(f"File size was too small to monitor! (Less than 32MB).")
cur_url += 1
monitor = argmonitor
wait_time = argwait
return csize, ctime
# Main program
def main(args):
download_dir = args.d
url_file = args.u
headless = args.uh
page_load_time = args.tl
refresh_rate = args.r
wait_time = args.tw
monitor = args.nm
cover = args.gc
logging.info(f"Download Directory: {download_dir}")
logging.info(f"Reading URLs from: {url_file}")
urls, url_length = open_urls(url_file)
# For each URL in the file
total_size = 0
total_time = 0
cur_url = 1
failed_urls = []
for url in urls:
# Launch chrome and open URL
driver = open_chrome(headless, download_dir)
driver.get(url)
# Wait to open URL
wait = WebDriverWait(driver, page_load_time)
# Check for multiple discs
try:
disc_element = driver.find_element(By.ID, "disc_number")
disc_select = Select(disc_element)
url_length += len(disc_select.options) - 1
for option in disc_select.options:
disc_value = option.get_attribute("value")
disc_text = option.get_attribute("text")
disc_replace = f"{cur_url}/{url_length}: ({disc_text}) "
if len(disc_select.options) == 1:
disc_replace = f"{cur_url}/{url_length}: "
disc_select.select_by_value(disc_value)
title = driver.title.replace("The Vault: ", disc_replace)
osize, otime = download_it(driver, wait, monitor, wait_time, download_dir, refresh_rate, total_size, total_time, cur_url, url_length, title, url, cover, failed_urls, disc_value)
total_size += osize
total_time += otime
cur_url += 1
except KeyboardInterrupt:
logging.info("\nSignal received. Shutting down gracefully...")
driver.close()
driver.quit()
sys.exit(67)
except Exception as e:
logging.warning(f"Multiple discs error? {e}")
title = driver.title.replace("The Vault: ", f"{cur_url}/{url_length}: ")
osize, otime = download_it(driver, wait, monitor, wait_time, download_dir, refresh_rate, total_size, total_time, cur_url, url_length, title, url, cover, failed_urls)
total_size += osize
total_time += otime
cur_url += 1
finally:
# Close and end Chrome session
driver.close()
driver.quit()
# If we were monitoring, display total statistics
if total_size > 0:
if total_time == 0:
total_time = 1
ttmin, ttmsec = divmod(total_time, 60)
tavg_speed = round((total_size/total_time/1024/1024)*8, 1)
logging.info(f"Downloaded {round((total_size/1024/1024), 2)}MB in {int(ttmin)}m {int(ttmsec)}s ({tavg_speed} Mbps).")
# If anything failed, write to file
if failed_urls:
fn = os.path.join(f"{download_dir}", "failed.txt")
logging.warning(f"Appending failed downloads to {fn}")
fnt = "a"
with open(f"{fn}", fnt) as f:
for item in failed_urls:
f.write(item + '\n')
if __name__ == "__main__":
main()
parser = args()
args = parser.parse_args()
if args.v:
sys.exit(f"v{__version__}\n")
# If directory does not exist, create it
os.makedirs(args.d, exist_ok=True)
setup_logging(args.d, args.l, args.lf)
main(args)
+4 -4
View File
@@ -7,8 +7,8 @@ VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0. Must always contain 4 elements.
filevers=(2026,3,24,0),
prodvers=(2026,3,24,0),
filevers=(2026,4,6,0),
prodvers=(2026,4,6,0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
@@ -32,12 +32,12 @@ VSVersionInfo(
u'040904B0',
[StringStruct(u'CompanyName', u''),
StringStruct(u'FileDescription', u'V-dlp: download a list of Vimm URLs'),
StringStruct(u'FileVersion', u'2026.3.24.0'),
StringStruct(u'FileVersion', u'2026.4.6.0'),
StringStruct(u'InternalName', u'V-dlp'),
StringStruct(u'LegalCopyright', u'© iamdoubz'),
StringStruct(u'OriginalFilename', u'V-dlp.exe'),
StringStruct(u'ProductName', u'V-dlp'),
StringStruct(u'ProductVersion', u'2026.3.24.0')])
StringStruct(u'ProductVersion', u'2026.4.6.0')])
]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
+5 -5
View File
@@ -7,8 +7,8 @@ VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0. Must always contain 4 elements.
filevers=(2026,3,24,0),
prodvers=(2026,3,24,0),
filevers=(2026,3,26,0),
prodvers=(2026,3,26,0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
@@ -31,13 +31,13 @@ VSVersionInfo(
StringTable(
u'040904B0',
[StringStruct(u'CompanyName', u''),
StringStruct(u'FileDescription', u'V-dlp UG: create a list of Vimm URLs'),
StringStruct(u'FileVersion', u'2026.3.24.0'),
StringStruct(u'FileDescription', u'V-dlpUG: create a list of Vimm URLs'),
StringStruct(u'FileVersion', u'2026.3.26.0'),
StringStruct(u'InternalName', u'V-dlpUG'),
StringStruct(u'LegalCopyright', u'© iamdoubz'),
StringStruct(u'OriginalFilename', u'V-dlpUG.exe'),
StringStruct(u'ProductName', u'V-dlpUG'),
StringStruct(u'ProductVersion', u'2026.3.24.0')])
StringStruct(u'ProductVersion', u'2026.3.26.0')])
]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
+11 -7
View File
@@ -7,7 +7,7 @@ from selenium.webdriver.chrome.options import Options
import string
import sys
__version__ = "2026.3.24.0"
__version__ = "2026.3.26.0"
# Helper for logging
def setup_logging(mode="syslog", logfile=None):
@@ -91,14 +91,14 @@ def main():
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Remote(
command_executor=f"http://127.0.0.1:{chrome_port}",
options=chrome_options
)
driver = webdriver.Chrome(options=chrome_options)
# Function to read platform/letter and write links to file
def get_links(pf, lets):
driver.get(f"https://vimm.net/vault/{pf}/{lets}")
if lets in string.ascii_uppercase:
driver.get(f"https://vimm.net/vault/{pf}/{lets}")
else:
driver.get(f"https://vimm.net/vault/{lets}")
data = []
# Locate the main table
table = driver.find_element(By.CSS_SELECTOR, "table.rounded")
@@ -149,8 +149,12 @@ def main():
if letter == 'ALL':
big_list = []
big_list.append({"platform": platform, "letter": f"?p=list&system={platform}&section=number"})
for l in string.ascii_uppercase:
get_links(platform, l)
big_list.append({"platform": platform, "letter": l})
for a in big_list:
get_links(a['platform'], a['letter'])
else:
get_links(platform, letter)
+1
View File
@@ -0,0 +1 @@
https://vimm.net/vault/2829