Author SHA1 Message Date
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 233 additions and 100 deletions
+8 -7
View File
@@ -55,32 +55,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] 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
+206 -82
View File
@@ -4,16 +4,19 @@ import logging
import os
from pathlib import Path
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
__version__ = "2026.3.24.0"
__version__ = "2026.3.25.0"
# Helper for logging
def setup_logging(mode="syslog", logfile=None):
@@ -47,44 +50,49 @@ def main():
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")
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("-p", type=float, help="Specify already running Chrome Driver port", default=54321)
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=10)
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, 2: large, 3: both)", default=0, choices=[0, 1, 2, 3])
parser.add_argument("-v", action='store_true', help="Display version information")
args = parser.parse_args()
if args.version:
if args.v:
sys.exit(f"v{__version__}\n")
setup_logging(args.log, args.logfile)
setup_logging(args.l, args.lf)
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
download_dir = args.d
url_file = args.u
chrome_port = args.p
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}")
logging.info(f"Using ChromeD Port: {chrome_port}")
emessage = f""
# URLs to process
try:
with open(url_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}!")
except Exception as e:
emessage = f"Could not find url file: {e}!"
logging.error(emessage)
sys.exit(emessage)
urls = list(dict.fromkeys(urls))
url_length = len(urls)
cur_url = 1
ess = "s"
@@ -153,7 +161,7 @@ def main():
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"):
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)
@@ -165,6 +173,7 @@ def main():
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).")
return last_size, tsec
def wait_for_file(pattern, delay=1, max=15):
#print(f"Waiting for file matching: {pattern}...")
i = 0
@@ -176,70 +185,185 @@ def main():
# Return the first matching file
return glob.glob(pattern)[0]
# For each URL in the file, run program
total_size = 0
total_time = 0
failed_urls = []
for url in urls:
def download_it(monitor, wait_time, download_dir, total_size, total_time, cur_url, dtitle, durl, cover, ddisc=0):
def download_img(download_dir, title, itype, baseurl):
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'{url}'
}
response = requests.get(iurl, headers=headers, allow_redirects=True, stream=True)
response.raise_for_status()
if response.status_code == 200:
fext = 'avif'
if iform == 2:
fext = 'webp'
save_path = os.path.join(download_dir, f"{title}.{fext}")
with open(save_path, 'wb') as file:
for chunk in response.iter_content(1024):
file.write(chunk)
else:
logging.warning(f"Could not download box image: {response.status_code} - {response.reason}")
pass
try:
img_element = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//img[@alt="Box"]'))
)
if itype in [1,3]:
img_url = img_element.get_attribute('src')
if img_url:
save_img(download_dir, title, itype, img_url, baseurl, 1)
else:
logging.warning("No box image url found.")
if itype in [2,3]:
body_element = driver.find_element(By.TAG_NAME, "body")
img_element.click()
try:
dialog_element = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.ID, "imageDialog"))
)
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()
except Exception as e:
logging.warning(f"No large box image found! {e}")
except:
logging.warning("No box image exists. Skipping...")
# Get Vimm file size
size = 0
try:
size_element = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.ID, "dl_size"))
)
size_raw = size_element.text
except:
logging.warning("Could not determine download size from webpage")
size_raw = '1 GB'
pass
logging.info(f"{title} {size_raw}")
# Download box cover
if cover > 0:
raw_title = driver.title.replace("The Vault: ", "")
download_img(download_dir, raw_title, cover, url)
# Click Download button
try:
download_button = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.XPATH, "//button[text()='Download']"))
)
download_button.click()
except:
logging.warning(f"Download button not found for {title}!")
failed_urls.append(url)
pass
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 * 1024
# Try to click Continue if it appears
try:
continue_button = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.XPATH, "//input[@value='Continue']"))
)
continue_button.click()
except:
pass
# Monitor current download
argmonitor = monitor
argwait = wait_time
ctime = 0
csize = 0
if size < 33554432:
monitor = True
wait_time = 15
if total_size > 0 and total_time > 0:
wait_time = min((size / total_size / total_time) * 1.25, wait_time)
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
else:
logging.warning(f"{time.time() - tempTime}")
time.sleep(refresh_rate)
if monitor == False:
csize, ctime = monitor_download(download_dir, size*1.01)
total_size += csize
total_time += ctime
# 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)
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
# 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']"))
)
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(
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
# Multiple discs?
try:
continue_button = WebDriverWait(driver, 4).until(
EC.element_to_be_clickable((By.XPATH, "//input[@value='Continue']"))
)
continue_button.click()
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(monitor, wait_time, download_dir, total_size, total_time, cur_url, title, url, cover, disc_value)
total_size += osize
total_time += otime
cur_url += 1
except:
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
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
time.sleep(wait_time)
cur_url += 1
monitor = argmonitor
wait_time = argwait
title = driver.title.replace("The Vault: ", f"{cur_url}/{url_length}: ")
osize, otime = download_it(monitor, wait_time, download_dir, total_size, total_time, cur_url, title, url, cover)
total_size += osize
total_time += otime
cur_url += 1
# Close Chrome session
driver.quit()
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 failed_urls:
# Add URL links to a file
fn = f"failed_downloads.txt"
logging.warning(f"Writing 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()
+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,3,25,0),
prodvers=(2026,3,25,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.3.25.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.3.25.0')])
]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
+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,3,25,0),
prodvers=(2026,3,25,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 UG: create a list of Vimm URLs'),
StringStruct(u'FileVersion', u'2026.3.24.0'),
StringStruct(u'FileVersion', u'2026.3.25.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.25.0')])
]),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
+10 -3
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.25.0"
# Helper for logging
def setup_logging(mode="syslog", logfile=None):
@@ -98,7 +98,10 @@ def main():
# 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 +152,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