148 lines
No EOL
6 KiB
Python
148 lines
No EOL
6 KiB
Python
import os
|
|
import urllib.request
|
|
import urllib.parse
|
|
import re
|
|
import time
|
|
import random
|
|
import datetime
|
|
import json
|
|
from threading import Thread
|
|
import queue
|
|
|
|
import tqdm
|
|
|
|
def recursive_find_links(url):
|
|
links_regex_pattern = r'(?<=<a href=")([^ ?]*)(?=">)' # Find href links that do not contain question marks or whitespace
|
|
times_regex_pattern = r'(?<=<\/a>)(\s*\d{4}-\d{2}-\d{2} \d{2}:\d{2})(?= )' # Find timestamps in UTC in the format YYYY-MM-DD HH:mm
|
|
with urllib.request.urlopen(url) as response:
|
|
html_content = response.read().decode('utf-8')
|
|
|
|
links = re.findall(links_regex_pattern, html_content)
|
|
times = re.findall(times_regex_pattern, html_content)
|
|
for i in range(len(times)):
|
|
dt = datetime.datetime.strptime(times[i].strip(), "%Y-%m-%d %H:%M")
|
|
dt.replace(tzinfo=datetime.timezone.utc)
|
|
times[i] = time.mktime(dt.timetuple())
|
|
|
|
if len(links) - 1 == len(times):
|
|
links = links[1:]
|
|
elif len(links) == len(times):
|
|
pass
|
|
else:
|
|
raise(ValueError)
|
|
|
|
for _link, _time in zip(links, times):
|
|
if _link.endswith("latest.png"):
|
|
continue
|
|
elif _link.endswith("/"):
|
|
yield from recursive_find_links(urllib.parse.urljoin(url,_link))
|
|
else:
|
|
yield urllib.parse.urljoin(url,_link), _time
|
|
|
|
|
|
# Fetch image from url and store it to path, retrying on failure
|
|
def image_fetch_worker(work_queue, result_queue, attempt_count = 1):
|
|
while True:
|
|
job = work_queue.get()
|
|
if job == None:
|
|
return
|
|
url, path, t = job
|
|
|
|
attempts = 0
|
|
while attempts < attempt_count:
|
|
try:
|
|
req = urllib.request.Request(url, data=None)
|
|
image_data = urllib.request.urlopen(req).read()
|
|
os.makedirs(os.path.split(path)[0], exist_ok=True)
|
|
open(path, 'wb').write(image_data)
|
|
result_queue.put((True, url, t))
|
|
break
|
|
except Exception as e:
|
|
if hasattr(e, "code") and e.code == 404: # This is expected if the file has been removed from the site (at least for swpc.noaa.gov)
|
|
attempts += attempt_count
|
|
elif attempts == 0:
|
|
print(f"\nA problem occurred on image: {url} | {e}")
|
|
time.sleep(1 + random.random())
|
|
attempts += 1
|
|
if (attempt_count > 1) and (attempts == attempt_count):
|
|
print(f'\nAfter {attempt_count} retries, could not fetch: {url}')
|
|
result_queue.put((False, url, t))
|
|
break
|
|
|
|
|
|
if __name__ == "__main__":
|
|
directory_url = r"https://services.swpc.noaa.gov/images/animations/suvi/"
|
|
stored_images_dir = r"..\suvi"
|
|
file_database_path = r"..\file_database.json"
|
|
fetch_interval = 30*60
|
|
nworkers = 8
|
|
|
|
file_info_cache = {}
|
|
try:
|
|
print(f"Attempting to load file records from cache: {file_database_path}")
|
|
with open(file_database_path, 'r') as f:
|
|
file_info_cache = json.loads(f.read())
|
|
print(f"File records loaded from cache: {len(file_info_cache)} records found.")
|
|
except Exception as e:
|
|
print(f"Load failed, starting with empty cache")
|
|
file_info_cache = {}
|
|
|
|
work_queue = queue.Queue(maxsize=nworkers)
|
|
result_queue = queue.Queue()
|
|
workers = []
|
|
for _ in range(nworkers):
|
|
t = Thread(target=image_fetch_worker, args=(work_queue, result_queue), daemon=True)
|
|
t.start()
|
|
workers.append(t)
|
|
try:
|
|
while True:
|
|
fetched_image_count = 0
|
|
already_had_image_count = 0
|
|
failed_image_count = 0
|
|
urllen = len(directory_url)
|
|
for l, t in tqdm.tqdm(recursive_find_links(directory_url), desc="Downloading files"):
|
|
# Collect complete work and record it
|
|
while True:
|
|
try:
|
|
r_success, r_url, r_t = result_queue.get_nowait()
|
|
if r_success:
|
|
fetched_image_count += 1
|
|
file_info_cache[r_url] = r_t
|
|
else:
|
|
failed_image_count += 1
|
|
except queue.Empty:
|
|
break
|
|
# If we dont have the file or the file at the link is newer than the one we previously fetched
|
|
if (not (l in file_info_cache)) or t > file_info_cache[l]:
|
|
file_portion_of_link = l[urllen:]
|
|
filepath = os.path.join(stored_images_dir, file_portion_of_link.replace("/", os.sep))
|
|
if os.path.exists(filepath):
|
|
if l in file_info_cache: # If we have record of this file, it must be out of date, rename it and download the new version.
|
|
os.rename(filepath, filepath + f"_{int(file_info_cache[l])}")
|
|
else: # If we have no record of this file, update the file info cache and don't redownload
|
|
file_info_cache[l] = t
|
|
already_had_image_count += 1
|
|
continue
|
|
work_queue.put((l, filepath, t))
|
|
else:
|
|
already_had_image_count += 1
|
|
with open(file_database_path, 'w') as f:
|
|
f.write(json.dumps(file_info_cache))
|
|
print(f"Downloaded {fetched_image_count} | Already had {already_had_image_count} | Failed {failed_image_count}")
|
|
time.sleep(fetch_interval)
|
|
except KeyboardInterrupt:
|
|
print("Saving file database and shutting down.")
|
|
|
|
for _ in range(nworkers):
|
|
try:
|
|
work_queue.put(None, timeout=1.0)
|
|
except:
|
|
break
|
|
|
|
for w in workers:
|
|
w.join(5.0)
|
|
|
|
with open(file_database_path, 'w') as f:
|
|
f.write(json.dumps(file_info_cache))
|
|
|
|
print(f"Downloaded {fetched_image_count} | Already had {already_had_image_count} | Failed {failed_image_count}") |