Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a2ddae93a | ||
|
|
86969ffff7 | ||
|
|
90cfe009b6 | ||
|
|
4d088fa7bb | ||
|
|
1ea3e9f231 | ||
|
|
2dc5542bb2 | ||
|
|
6ec47572de | ||
|
|
a769502237 | ||
|
|
928165467c | ||
|
|
8c9c634623 | ||
|
|
34ca036897 | ||
|
|
aec6d522bc | ||
|
|
50a345e20c | ||
|
|
92cdea8775 | ||
|
|
625904bcc7 | ||
|
|
b4b796c56f | ||
|
|
06b91e771d | ||
|
|
97dd01a8fc | ||
|
|
95dcc33e84 | ||
|
|
3bb6d742e6 | ||
|
|
51a1921b12 | ||
|
|
e2ec03c354 | ||
|
|
c62962f428 | ||
|
|
3a26dcedbd |
@@ -10,12 +10,15 @@ A program to aid in queuing downloads from Vimm.net
|
|||||||
### Shared
|
### Shared
|
||||||
|
|
||||||
- [Google Chrome](https://www.google.com/chrome/)
|
- [Google Chrome](https://www.google.com/chrome/)
|
||||||
- [Chromedriver](https://googlechromelabs.github.io/chrome-for-testing/)
|
- ~~[Chromedriver](https://googlechromelabs.github.io/chrome-for-testing/)~~
|
||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
- Python 3.11 (others might work)
|
- Python 3.11 (others might work)
|
||||||
- Selenium `pip install selenium`
|
- Selenium `pip install selenium`
|
||||||
|
- Results `pip install results`
|
||||||
|
- Pathlib ` pip install pathlib`
|
||||||
|
- Pathvalidate `pip install pathvalidate`
|
||||||
|
|
||||||
## Programs
|
## 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 |
|
| -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 |
|
| -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 |
|
| -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 |
|
| -v | Display version information | NA | none |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [X] Download files
|
- [X] Download files
|
||||||
|
- [X] Handle multi-disc downloads
|
||||||
|
- [X] Download box cover art
|
||||||
- [X] Show download statistics (speed, ETA)
|
- [X] Show download statistics (speed, ETA)
|
||||||
|
- [X] More download statistics
|
||||||
- [X] Show number of URLs
|
- [X] Show number of URLs
|
||||||
- [X] Show download name/size
|
- [X] Show download name/size
|
||||||
- [X] Add headless option
|
- [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
|
- [X] No more waiting for each download to finish
|
||||||
|
|
||||||
|
|
||||||
## Upcoming features
|
## Upcoming features
|
||||||
|
|
||||||
- [ ] Proper error handling
|
- [ ] Proper error handling
|
||||||
- [ ] Show failed downloads
|
|
||||||
- [ ] Download cover art
|
|
||||||
- [ ] Download manuals
|
- [ ] Download manuals
|
||||||
- [ ] Remove duplicate URLs
|
|
||||||
- [ ] Email upon completion
|
- [ ] Email upon completion
|
||||||
- [ ] More download statistics
|
|
||||||
- [ ] Handle multi-disc downloads
|
|
||||||
- [ ] Remove dependency on external start of Chrome Driver
|
|
||||||
- [ ] Click on ads
|
- [ ] Click on ads
|
||||||
- [ ] OS agnostic (heavily Windows based as they need the most hand holding)
|
- [ ] OS agnostic (heavily Windows based as they need the most hand holding)
|
||||||
- [ ] Get # links
|
|
||||||
|
|
||||||
## Bonus
|
## Bonus
|
||||||
|
|
||||||
@@ -100,4 +104,8 @@ Download the games.
|
|||||||
python dlp.py -nm True -uh True -tw 1 -u atari26roms.txt
|
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.
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import glob
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from pathvalidate import sanitize_filename
|
||||||
import random
|
import random
|
||||||
|
import requests
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.chrome.service import Service
|
||||||
from selenium.webdriver.chrome.options import Options
|
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
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
__version__ = "2026.3.24.0"
|
__version__ = "2026.4.6.0"
|
||||||
|
|
||||||
# Helper for logging
|
# Helper for logging
|
||||||
def setup_logging(mode="syslog", logfile=None):
|
def setup_logging(folder, mode="syslog", logfile=None):
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
# remove default handlers
|
# remove default handlers
|
||||||
@@ -35,69 +39,55 @@ def setup_logging(mode="syslog", logfile=None):
|
|||||||
if mode in ("file", "all"):
|
if mode in ("file", "all"):
|
||||||
if not logfile:
|
if not logfile:
|
||||||
raise ValueError("File logging requires a logfile path")
|
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)
|
file_handler.setFormatter(formatter)
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
# Setup pass in arguments
|
# Create the parser and add a description
|
||||||
def main():
|
def args():
|
||||||
# Create the parser and add a description
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="V-dlp options and variables...",
|
description="V-dlp options and variables...",
|
||||||
epilog="End of help documentation..."
|
epilog="End of help documentation..."
|
||||||
)
|
)
|
||||||
#parser = argparse.ArgumentParser()
|
parser.add_argument("-l", type=str, help="Choose logging option", default="syslog", choices=["syslog","file","all","none"])
|
||||||
parser.add_argument("--log", "-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("--logfile", "-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("--dir_download", "-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("--file_urls", "-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("--chrome_port", "-p", type=float, help="Specify already running Chrome Driver port", default=54321)
|
parser.add_argument("-tl", type=float, help="How long to wait for webpage to load before timeout", default=8)
|
||||||
parser.add_argument("--use_headless", "-uh", type=bool, help="Use headless Chrome", default=False)
|
parser.add_argument("-r", type=float, help="How often to refresh statistics on screen", default=2)
|
||||||
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("-tw", type=float, help="Number of seconds to pause between downloads", default=4)
|
||||||
parser.add_argument("--refresh_rate", "-r", type=float, help="How often to refresh statistics on screen", default=2)
|
parser.add_argument("-nm", type=bool, help="Do not monitor download statistics", default=False)
|
||||||
parser.add_argument("--wait_time", "-tw", type=float, help="Number of seconds to pause between downloads", default=4)
|
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("--no_monitor", "-nm", type=bool, help="Do not monitor download statistics", default=False)
|
parser.add_argument("-v", action='store_true', help="Display version information")
|
||||||
parser.add_argument("--version", "-v", action='store_true', help="Display version information")
|
return parser
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.version:
|
|
||||||
sys.exit(f"v{__version__}\n")
|
|
||||||
|
|
||||||
setup_logging(args.log, args.logfile)
|
# URLs to process
|
||||||
|
def open_urls(file):
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
with open(url_file) as f:
|
with open(file) as f:
|
||||||
urls = [line.strip() for line in f]
|
urls = [line.strip() for line in f]
|
||||||
except:
|
urls = list(dict.fromkeys(urls))
|
||||||
logging.error(f"Could not find url file: {url_file}!")
|
url_length = len(urls)
|
||||||
raise FileNotFoundError(f"Could not find url file: {url_file}!")
|
ess = "s"
|
||||||
url_length = len(urls)
|
if url_length == 0:
|
||||||
cur_url = 1
|
logging.warning("There are no URLs to process!")
|
||||||
ess = "s"
|
sys.exit("There are no URLs to process!")
|
||||||
if url_length == 0:
|
if url_length == 1:
|
||||||
logging.warning("There are no URLs to process!")
|
ess = ""
|
||||||
sys.exit("There are no URLs to process!")
|
logging.info(f"Will process {url_length} URL{ess}...")
|
||||||
if url_length == 1:
|
return urls, url_length
|
||||||
ess = ""
|
except Exception as e:
|
||||||
logging.info(f"Will process {url_length} URL{ess}...")
|
exc_type, exc_obj, exc_tb = sys.exc_info()
|
||||||
# If directory does not exist, create it
|
emessage = f"Error {exc_tb.tb_lineno}: Could not find url file: {e}!"
|
||||||
os.makedirs(download_dir, exist_ok=True)
|
logging.error(emessage)
|
||||||
|
sys.exit(emessage)
|
||||||
|
|
||||||
|
# Launch Chrome
|
||||||
|
def open_chrome(headless, download_dir):
|
||||||
# Generate list of random screen resolutions
|
# Generate list of random screen resolutions
|
||||||
display_resolutions = ["2560,1440","1920,1080","1600,1200"]
|
display_resolutions = ["2560,1440","1920,1080","1600,1200"]
|
||||||
|
|
||||||
# Create and add Chrome options
|
# Create and add Chrome options
|
||||||
chrome_options = Options()
|
chrome_options = Options()
|
||||||
if headless == True:
|
if headless == True:
|
||||||
@@ -106,21 +96,20 @@ def main():
|
|||||||
chrome_options.add_argument(f"--window-size=2560,1440")
|
chrome_options.add_argument(f"--window-size=2560,1440")
|
||||||
chrome_options.add_argument("--no-sandbox")
|
chrome_options.add_argument("--no-sandbox")
|
||||||
chrome_options.add_argument("--disable-dev-shm-usage")
|
chrome_options.add_argument("--disable-dev-shm-usage")
|
||||||
prefs = {
|
chrome_options.add_argument("--simulate-outdated-no-au='Tue, 31 Dec 2099 23:59:59 GMT'")
|
||||||
"download.default_directory": download_dir,
|
chrome_options.add_argument("--disable-background-networking")
|
||||||
"download.prompt_for_download": False,
|
chrome_options.add_argument("--disable-component-update")
|
||||||
"download.directory_upgrade": True
|
prefs = {
|
||||||
}
|
"download.default_directory": download_dir,
|
||||||
chrome_options.add_experimental_option("prefs", prefs)
|
"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(options=chrome_options)
|
||||||
#driver = webdriver.Chrome(service=Service(r"C:\Tools\Standalone\chromedriver.exe"), options=chrome_options)
|
|
||||||
#driver = webdriver.Remote(
|
# Chrome sometimes blocks downloads in headless mode
|
||||||
# command_executor=f"http://127.0.0.1:{chrome_port}",
|
|
||||||
# options=chrome_options
|
|
||||||
#)
|
|
||||||
if headless == True:
|
if headless == True:
|
||||||
# Chrome sometimes blocks downloads in headless mode
|
|
||||||
driver.execute_cdp_cmd(
|
driver.execute_cdp_cmd(
|
||||||
"Page.setDownloadBehavior",
|
"Page.setDownloadBehavior",
|
||||||
{
|
{
|
||||||
@@ -128,118 +117,387 @@ def main():
|
|||||||
"downloadPath": download_dir
|
"downloadPath": download_dir
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Helper to calculate percentages
|
return driver
|
||||||
def percentage_of_total(part, whole):
|
|
||||||
if whole == 0:
|
# Helper to calculate percentages
|
||||||
return 0 # Handle division by zero case
|
def percentage_of_total(part, whole):
|
||||||
return round(((part / whole) * 100), 1)
|
if whole == 0:
|
||||||
# Display useful stats about ongoing downloads
|
return 0 # Handle division by zero case
|
||||||
def monitor_download(folder, fsize):
|
return round(((part / whole) * 100), 1)
|
||||||
downloading = True
|
|
||||||
last_size = 0
|
# Check for temp files limiting amount of time spent before failing
|
||||||
tstart = time.time()
|
def wait_for_file(download_dir, check_interval_seconds=0.25, max_wait_seconds=10):
|
||||||
while downloading:
|
start_time = time.time()
|
||||||
files = os.listdir(folder)
|
while True:
|
||||||
partial = [f for f in files if f.endswith(".crdownload")]
|
# Check if the file exists
|
||||||
if partial:
|
files = os.listdir(download_dir)
|
||||||
file_path = os.path.join(folder, partial[0])
|
partial = [f for f in files if f.endswith(".crdownload")]
|
||||||
size = os.path.getsize(file_path)
|
if partial:
|
||||||
tot_perc = percentage_of_total(size, fsize)
|
elapsed_time = time.time() - start_time
|
||||||
if size != last_size:
|
return True
|
||||||
ct = time.time()
|
# Calculate elapsed time and check if timeout is reached
|
||||||
dt = ct - tstart
|
elapsed_time = time.time() - start_time
|
||||||
if dt == 0:
|
if elapsed_time >= max_wait_seconds:
|
||||||
dt = 1
|
print(f"Timed out after {max_wait_seconds} seconds. Temp file not found at {download_dir}.")
|
||||||
speed = round(((size - last_size)/1024/1024/refresh_rate)*8, 2)
|
return False
|
||||||
etas = ((fsize - size) / (size / dt))
|
# Wait for the specified interval before the next check
|
||||||
etam, etams = divmod(etas, 60)
|
remaining_time = max_wait_seconds - elapsed_time
|
||||||
if args.log in ("syslog", "all"):
|
if remaining_time < check_interval_seconds:
|
||||||
print(F"Downloading at {speed} Mbps... {tot_perc}%. ETA: {int(etam)}m {int(etams)}s ", end="\r")
|
time_to_sleep = remaining_time
|
||||||
last_size = size
|
# Time to sleep
|
||||||
time.sleep(refresh_rate)
|
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:
|
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).")
|
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):
|
return last_size, tsec
|
||||||
#print(f"Waiting for file matching: {pattern}...")
|
|
||||||
i = 0
|
# Get cover art
|
||||||
while not glob.glob(pattern):
|
def download_img(driver, download_dir, title, itype, baseurl):
|
||||||
time.sleep(delay)
|
try:
|
||||||
i += 1
|
img_element = WebDriverWait(driver, 5).until(
|
||||||
if i > max:
|
EC.presence_of_element_located((By.XPATH, '//img[@alt="Box"]'))
|
||||||
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']"))
|
|
||||||
)
|
)
|
||||||
download_button.click()
|
if img_element:
|
||||||
# Output page title to console
|
if itype in [1,3]:
|
||||||
title = driver.title.replace("The Vault: ", f"{cur_url}/{url_length}: ")
|
img_url = img_element.get_attribute('src')
|
||||||
# Get Vimm file size
|
if img_url:
|
||||||
size_element = WebDriverWait(driver, 10).until(
|
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"))
|
EC.presence_of_element_located((By.ID, "dl_size"))
|
||||||
)
|
)
|
||||||
size_raw = size_element.text
|
size_raw = size_element.text
|
||||||
logging.info(f"{title} {size_raw}")
|
except:
|
||||||
size = 0
|
exc_type, exc_obj, exc_tb = sys.exc_info()
|
||||||
if " KB" in size_raw:
|
logging.warning(f"Error {exc_tb.tb_lineno}: Could not determine download size from webpage")
|
||||||
size = float(size_raw.replace(" KB", "")) * 1024
|
size_raw = '1 GB'
|
||||||
elif " MB" in size_raw:
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
# Monitor current download
|
logging.info(f"{dtitle} {size_raw}")
|
||||||
argmonitor = monitor
|
|
||||||
argwait = wait_time
|
# Download box cover
|
||||||
if size < 33554432:
|
if cover > 0:
|
||||||
monitor = True
|
raw_title = driver.title.replace("The Vault: ", "")
|
||||||
wait_time = 17
|
download_img(driver, download_dir, raw_title, cover, durl)
|
||||||
logging.warning("File size was too small to monitor! (Less than 32MB)")
|
|
||||||
if monitor == False:
|
# Click Download button
|
||||||
tempTime = time.time()
|
dl_start = time.time()
|
||||||
file_pattern = download_dir + '/*.crdownload'
|
try:
|
||||||
while not os.path.exists(wait_for_file(file_pattern)):
|
download_form = wait.until(
|
||||||
if time.time() - tempTime > wait_time:
|
EC.presence_of_element_located((By.ID, "dl_form"))
|
||||||
logging.warning("Never found temp file for download...")
|
)
|
||||||
break
|
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:
|
else:
|
||||||
logging.warning(f"{time.time() - tempTime}")
|
logging.warning("Vimm's Lair Error 400: An unexpected browser error has occurred (we think you might be a bot)")
|
||||||
time.sleep(refresh_rate)
|
failed_urls.append(dtitle)
|
||||||
if monitor == False:
|
failed_urls.append(durl)
|
||||||
monitor_download(download_dir, size+1024)
|
except:
|
||||||
# Wait to call next URL
|
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)
|
time.sleep(wait_time)
|
||||||
cur_url += 1
|
else:
|
||||||
monitor = argmonitor
|
if size < 33554432:
|
||||||
wait_time = argwait
|
logging.warning(f"File size was too small to monitor! (Less than 32MB).")
|
||||||
# Close Chrome session
|
|
||||||
driver.quit()
|
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__":
|
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
@@ -7,8 +7,8 @@ VSVersionInfo(
|
|||||||
ffi=FixedFileInfo(
|
ffi=FixedFileInfo(
|
||||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
# 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.
|
# Set not needed items to zero 0. Must always contain 4 elements.
|
||||||
filevers=(2026,3,24,0),
|
filevers=(2026,4,6,0),
|
||||||
prodvers=(2026,3,24,0),
|
prodvers=(2026,4,6,0),
|
||||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||||
mask=0x3f,
|
mask=0x3f,
|
||||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||||
@@ -32,12 +32,12 @@ VSVersionInfo(
|
|||||||
u'040904B0',
|
u'040904B0',
|
||||||
[StringStruct(u'CompanyName', u''),
|
[StringStruct(u'CompanyName', u''),
|
||||||
StringStruct(u'FileDescription', u'V-dlp: download a list of Vimm URLs'),
|
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'InternalName', u'V-dlp'),
|
||||||
StringStruct(u'LegalCopyright', u'© iamdoubz'),
|
StringStruct(u'LegalCopyright', u'© iamdoubz'),
|
||||||
StringStruct(u'OriginalFilename', u'V-dlp.exe'),
|
StringStruct(u'OriginalFilename', u'V-dlp.exe'),
|
||||||
StringStruct(u'ProductName', u'V-dlp'),
|
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])])
|
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ VSVersionInfo(
|
|||||||
ffi=FixedFileInfo(
|
ffi=FixedFileInfo(
|
||||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
# 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.
|
# Set not needed items to zero 0. Must always contain 4 elements.
|
||||||
filevers=(2026,3,24,0),
|
filevers=(2026,3,26,0),
|
||||||
prodvers=(2026,3,24,0),
|
prodvers=(2026,3,26,0),
|
||||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||||
mask=0x3f,
|
mask=0x3f,
|
||||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||||
@@ -31,13 +31,13 @@ VSVersionInfo(
|
|||||||
StringTable(
|
StringTable(
|
||||||
u'040904B0',
|
u'040904B0',
|
||||||
[StringStruct(u'CompanyName', u''),
|
[StringStruct(u'CompanyName', u''),
|
||||||
StringStruct(u'FileDescription', u'V-dlp UG: create a list of Vimm URLs'),
|
StringStruct(u'FileDescription', u'V-dlpUG: create a list of Vimm URLs'),
|
||||||
StringStruct(u'FileVersion', u'2026.3.24.0'),
|
StringStruct(u'FileVersion', u'2026.3.26.0'),
|
||||||
StringStruct(u'InternalName', u'V-dlpUG'),
|
StringStruct(u'InternalName', u'V-dlpUG'),
|
||||||
StringStruct(u'LegalCopyright', u'© iamdoubz'),
|
StringStruct(u'LegalCopyright', u'© iamdoubz'),
|
||||||
StringStruct(u'OriginalFilename', u'V-dlpUG.exe'),
|
StringStruct(u'OriginalFilename', u'V-dlpUG.exe'),
|
||||||
StringStruct(u'ProductName', u'V-dlpUG'),
|
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])])
|
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from selenium.webdriver.chrome.options import Options
|
|||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
__version__ = "2026.3.24.0"
|
__version__ = "2026.3.26.0"
|
||||||
|
|
||||||
# Helper for logging
|
# Helper for logging
|
||||||
def setup_logging(mode="syslog", logfile=None):
|
def setup_logging(mode="syslog", logfile=None):
|
||||||
@@ -91,14 +91,14 @@ def main():
|
|||||||
chrome_options.add_argument("--no-sandbox")
|
chrome_options.add_argument("--no-sandbox")
|
||||||
chrome_options.add_argument("--disable-dev-shm-usage")
|
chrome_options.add_argument("--disable-dev-shm-usage")
|
||||||
|
|
||||||
driver = webdriver.Remote(
|
driver = webdriver.Chrome(options=chrome_options)
|
||||||
command_executor=f"http://127.0.0.1:{chrome_port}",
|
|
||||||
options=chrome_options
|
|
||||||
)
|
|
||||||
|
|
||||||
# Function to read platform/letter and write links to file
|
# Function to read platform/letter and write links to file
|
||||||
def get_links(pf, lets):
|
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 = []
|
data = []
|
||||||
# Locate the main table
|
# Locate the main table
|
||||||
table = driver.find_element(By.CSS_SELECTOR, "table.rounded")
|
table = driver.find_element(By.CSS_SELECTOR, "table.rounded")
|
||||||
@@ -149,8 +149,12 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if letter == 'ALL':
|
if letter == 'ALL':
|
||||||
|
big_list = []
|
||||||
|
big_list.append({"platform": platform, "letter": f"?p=list&system={platform}§ion=number"})
|
||||||
for l in string.ascii_uppercase:
|
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:
|
else:
|
||||||
get_links(platform, letter)
|
get_links(platform, letter)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user