mirror of
https://github.com/User0332/rewards-farmer.git
synced 2026-09-16 09:31:36 +00:00
normalize indentation to tabs
This commit is contained in:
+84
-84
@@ -9,116 +9,116 @@ from selenium import webdriver
|
|||||||
from selenium.common.exceptions import SessionNotCreatedException
|
from selenium.common.exceptions import SessionNotCreatedException
|
||||||
|
|
||||||
HEADLESS = os.environ.get("REWARDS_HEADLESS", "").strip().lower() in (
|
HEADLESS = os.environ.get("REWARDS_HEADLESS", "").strip().lower() in (
|
||||||
"1",
|
"1",
|
||||||
"true",
|
"true",
|
||||||
"yes",
|
"yes",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def build_options(account: accounts.Account) -> webdriver.EdgeOptions:
|
def build_options(account: accounts.Account) -> webdriver.EdgeOptions:
|
||||||
options = webdriver.EdgeOptions()
|
options = webdriver.EdgeOptions()
|
||||||
|
|
||||||
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||||
options.add_experimental_option("useAutomationExtension", False)
|
options.add_experimental_option("useAutomationExtension", False)
|
||||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
options.add_argument(f"--user-data-dir={account.user_data_dir}")
|
options.add_argument(f"--user-data-dir={account.user_data_dir}")
|
||||||
options.add_argument(f"--profile-directory={account.profile_name}")
|
options.add_argument(f"--profile-directory={account.profile_name}")
|
||||||
|
|
||||||
if HEADLESS:
|
if HEADLESS:
|
||||||
# A container has no display. The window size is set explicitly because
|
# A container has no display. The window size is set explicitly because
|
||||||
# the pointer code works in viewport coordinates, and the default
|
# the pointer code works in viewport coordinates, and the default
|
||||||
# headless window is small enough to put cards out of reach.
|
# headless window is small enough to put cards out of reach.
|
||||||
options.add_argument("--headless=new")
|
options.add_argument("--headless=new")
|
||||||
options.add_argument("--window-size=1920,1080")
|
options.add_argument("--window-size=1920,1080")
|
||||||
options.add_argument("--no-sandbox")
|
options.add_argument("--no-sandbox")
|
||||||
options.add_argument("--disable-dev-shm-usage")
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def run_account(account: accounts.Account) -> bool:
|
def run_account(account: accounts.Account) -> bool:
|
||||||
"""Work one account. Returns whether the browser started."""
|
"""Work one account. Returns whether the browser started."""
|
||||||
try:
|
try:
|
||||||
driver = webdriver.Edge(options=build_options(account))
|
driver = webdriver.Edge(options=build_options(account))
|
||||||
except SessionNotCreatedException as exc:
|
except SessionNotCreatedException as exc:
|
||||||
# Chromium allows one process per user data directory. When the profile
|
# Chromium allows one process per user data directory. When the profile
|
||||||
# is already open the driver's copy exits during startup, and selenium
|
# is already open the driver's copy exits during startup, and selenium
|
||||||
# reports it as the browser crashing with a message that names neither
|
# reports it as the browser crashing with a message that names neither
|
||||||
# the profile nor the other window.
|
# the profile nor the other window.
|
||||||
logger.error("[FAIL] %s: could not start Edge with this profile.", account.name)
|
logger.error("[FAIL] %s: could not start Edge with this profile.", account.name)
|
||||||
logger.error(" profile directory: %s", account.user_data_dir)
|
logger.error(" profile directory: %s", account.user_data_dir)
|
||||||
logger.error(
|
logger.error(
|
||||||
" The usual cause is that this profile is already open in another"
|
" The usual cause is that this profile is already open in another"
|
||||||
)
|
)
|
||||||
logger.error(" Edge window, including one left over from a previous run.")
|
logger.error(" Edge window, including one left over from a previous run.")
|
||||||
logger.error(" driver said: %s", log_utils.exception_summary(exc))
|
logger.error(" driver said: %s", log_utils.exception_summary(exc))
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rewards = rewards_tasks.RewardsTaskUtils(driver)
|
rewards = rewards_tasks.RewardsTaskUtils(driver)
|
||||||
rewards.complete_all_tasks()
|
rewards.complete_all_tasks()
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
driver.quit()
|
driver.quit()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# quit() raises when the browser is already gone. Letting it out
|
# quit() raises when the browser is already gone. Letting it out
|
||||||
# here would replace whatever actually went wrong with the tidy-up's
|
# here would replace whatever actually went wrong with the tidy-up's
|
||||||
# own error, and the process it is meant to end is dead anyway.
|
# own error, and the process it is meant to end is dead anyway.
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"%s: the driver did not shut down cleanly: %s",
|
"%s: the driver did not shut down cleanly: %s",
|
||||||
account.name,
|
account.name,
|
||||||
log_utils.exception_summary(exc),
|
log_utils.exception_summary(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
log_utils.setup_logging()
|
log_utils.setup_logging()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
configured = accounts.configured()
|
configured = accounts.configured()
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
logger.error("[FAIL] %s", exc)
|
logger.error("[FAIL] %s", exc)
|
||||||
|
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
started = 0
|
started = 0
|
||||||
|
|
||||||
for account in configured:
|
for account in configured:
|
||||||
if len(configured) > 1:
|
if len(configured) > 1:
|
||||||
logger.info("=== account: %s ===", account.name)
|
logger.info("=== account: %s ===", account.name)
|
||||||
|
|
||||||
# One account must not be able to end the batch. complete_all_tasks
|
# One account must not be able to end the batch. complete_all_tasks
|
||||||
# already contains a task that fails, and run_account names the profile
|
# already contains a task that fails, and run_account names the profile
|
||||||
# that is already open, but everything else - a driver that will not
|
# that is already open, but everything else - a driver that will not
|
||||||
# start for some other reason, the browser dying mid-run, a page that
|
# start for some other reason, the browser dying mid-run, a page that
|
||||||
# never loads - reached here and took the remaining accounts with it.
|
# never loads - reached here and took the remaining accounts with it.
|
||||||
# KeyboardInterrupt is deliberately not caught: Ctrl-C means stop.
|
# KeyboardInterrupt is deliberately not caught: Ctrl-C means stop.
|
||||||
try:
|
try:
|
||||||
if run_account(account):
|
if run_account(account):
|
||||||
started += 1
|
started += 1
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
"[FAIL] %s: %s: %s",
|
"[FAIL] %s: %s: %s",
|
||||||
account.name,
|
account.name,
|
||||||
type(exc).__name__,
|
type(exc).__name__,
|
||||||
log_utils.exception_summary(exc),
|
log_utils.exception_summary(exc),
|
||||||
exc_info=logger.isEnabledFor(logging.DEBUG),
|
exc_info=logger.isEnabledFor(logging.DEBUG),
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(configured) > 1:
|
if len(configured) > 1:
|
||||||
logger.info("%s/%s accounts ran", started, len(configured))
|
logger.info("%s/%s accounts ran", started, len(configured))
|
||||||
|
|
||||||
# Nothing is watching a container, and stdin is not a terminal there.
|
# Nothing is watching a container, and stdin is not a terminal there.
|
||||||
if not HEADLESS:
|
if not HEADLESS:
|
||||||
input("Press Enter to exit...")
|
input("Press Enter to exit...")
|
||||||
|
|
||||||
return 0 if started else 1
|
return 0 if started else 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ MAX_BACKOFF = 300
|
|||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
|
|
||||||
session.headers.update({
|
session.headers.update({
|
||||||
"User-Agent": (
|
"User-Agent": (
|
||||||
"RandomVisualSearchImage/1.1 "
|
"RandomVisualSearchImage/1.1 "
|
||||||
"(contact: 12345rfdz@gmail.com)"
|
"(contact: 12345rfdz@gmail.com)"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -55,17 +55,17 @@ session.headers.update({
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def clean_url(url):
|
def clean_url(url):
|
||||||
"""Remove query parameters from Wikimedia URLs."""
|
"""Remove query parameters from Wikimedia URLs."""
|
||||||
|
|
||||||
parts = urlsplit(url)
|
parts = urlsplit(url)
|
||||||
|
|
||||||
return urlunsplit((
|
return urlunsplit((
|
||||||
parts.scheme,
|
parts.scheme,
|
||||||
parts.netloc,
|
parts.netloc,
|
||||||
parts.path,
|
parts.path,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -73,32 +73,32 @@ def clean_url(url):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def wait_after_429(response, attempt):
|
def wait_after_429(response, attempt):
|
||||||
"""Wait according to Wikimedia's Retry-After header."""
|
"""Wait according to Wikimedia's Retry-After header."""
|
||||||
|
|
||||||
retry_after = response.headers.get("Retry-After")
|
retry_after = response.headers.get("Retry-After")
|
||||||
|
|
||||||
if retry_after:
|
if retry_after:
|
||||||
try:
|
try:
|
||||||
wait_time = int(retry_after)
|
wait_time = int(retry_after)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
wait_time = min(
|
wait_time = min(
|
||||||
2 ** attempt,
|
2 ** attempt,
|
||||||
MAX_BACKOFF,
|
MAX_BACKOFF,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
wait_time = min(
|
wait_time = min(
|
||||||
2 ** attempt,
|
2 ** attempt,
|
||||||
MAX_BACKOFF,
|
MAX_BACKOFF,
|
||||||
)
|
)
|
||||||
|
|
||||||
wait_time = max(5, wait_time)
|
wait_time = max(5, wait_time)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"Rate limited. Waiting "
|
f"Rate limited. Waiting "
|
||||||
f"{wait_time} seconds..."
|
f"{wait_time} seconds..."
|
||||||
)
|
)
|
||||||
|
|
||||||
time.sleep(wait_time)
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -106,51 +106,51 @@ def wait_after_429(response, attempt):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def download_image(url):
|
def download_image(url):
|
||||||
"""Download image bytes from Wikimedia."""
|
"""Download image bytes from Wikimedia."""
|
||||||
|
|
||||||
url = clean_url(url)
|
url = clean_url(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = session.get(
|
response = session.get(
|
||||||
url,
|
url,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
allow_redirects=True,
|
allow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"Download failed: {e}")
|
print(f"Download failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if response.status_code == 429:
|
if response.status_code == 429:
|
||||||
wait_after_429(response, 1)
|
wait_after_429(response, 1)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if response.status_code == 403:
|
if response.status_code == 403:
|
||||||
print("Wikimedia returned 403 Forbidden.")
|
print("Wikimedia returned 403 Forbidden.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"HTTP error: {e}")
|
print(f"HTTP error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
content_type = response.headers.get(
|
content_type = response.headers.get(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
"",
|
"",
|
||||||
).lower()
|
).lower()
|
||||||
|
|
||||||
if not content_type.startswith("image/"):
|
if not content_type.startswith("image/"):
|
||||||
print(
|
print(
|
||||||
f"Not an image: {content_type}"
|
f"Not an image: {content_type}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not response.content:
|
if not response.content:
|
||||||
print("Downloaded image is empty.")
|
print("Downloaded image is empty.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return response.content
|
return response.content
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -158,40 +158,40 @@ def download_image(url):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def convert_to_jpeg(image_data):
|
def convert_to_jpeg(image_data):
|
||||||
"""Convert downloaded image bytes to JPEG."""
|
"""Convert downloaded image bytes to JPEG."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Image.open(
|
with Image.open(
|
||||||
io.BytesIO(image_data)
|
io.BytesIO(image_data)
|
||||||
) as image:
|
) as image:
|
||||||
|
|
||||||
# JPEG does not support alpha (transparency).
|
# JPEG does not support alpha (transparency).
|
||||||
# If the image has transparency (RGBA or LA), paste it over a white background.
|
# If the image has transparency (RGBA or LA), paste it over a white background.
|
||||||
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
|
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
|
||||||
background = Image.new("RGB", image.size, (255, 255, 255))
|
background = Image.new("RGB", image.size, (255, 255, 255))
|
||||||
if image.mode == "P":
|
if image.mode == "P":
|
||||||
image = image.convert("RGBA")
|
image = image.convert("RGBA")
|
||||||
background.paste(image, mask=image.split()[-1])
|
background.paste(image, mask=image.split()[-1])
|
||||||
jpeg_image = background
|
jpeg_image = background
|
||||||
else:
|
else:
|
||||||
jpeg_image = image.convert("RGB")
|
jpeg_image = image.convert("RGB")
|
||||||
|
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
|
|
||||||
jpeg_image.save(
|
jpeg_image.save(
|
||||||
output,
|
output,
|
||||||
format="JPEG",
|
format="JPEG",
|
||||||
quality=90,
|
quality=90,
|
||||||
optimize=True,
|
optimize=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return output.getvalue()
|
return output.getvalue()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"JPEG conversion failed: {e}"
|
f"JPEG conversion failed: {e}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -200,308 +200,308 @@ def convert_to_jpeg(image_data):
|
|||||||
|
|
||||||
def get_random_image():
|
def get_random_image():
|
||||||
|
|
||||||
for attempt in range(
|
for attempt in range(
|
||||||
1,
|
1,
|
||||||
MAX_ATTEMPTS + 1,
|
MAX_ATTEMPTS + 1,
|
||||||
):
|
):
|
||||||
|
|
||||||
if attempt > 1:
|
if attempt > 1:
|
||||||
time.sleep(REQUEST_DELAY)
|
time.sleep(REQUEST_DELAY)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"\nAttempt "
|
f"\nAttempt "
|
||||||
f"{attempt}/{MAX_ATTEMPTS}"
|
f"{attempt}/{MAX_ATTEMPTS}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# RANDOM FILE
|
# RANDOM FILE
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"action": "query",
|
"action": "query",
|
||||||
"format": "json",
|
"format": "json",
|
||||||
|
|
||||||
"generator": "random",
|
"generator": "random",
|
||||||
"grnnamespace": 6,
|
"grnnamespace": 6,
|
||||||
"grnlimit": 1,
|
"grnlimit": 1,
|
||||||
|
|
||||||
"prop": "imageinfo",
|
"prop": "imageinfo",
|
||||||
|
|
||||||
"iiprop": (
|
"iiprop": (
|
||||||
"url|size|mime|dimensions"
|
"url|size|mime|dimensions"
|
||||||
),
|
),
|
||||||
|
|
||||||
"iiurlwidth": THUMBNAIL_WIDTH,
|
"iiurlwidth": THUMBNAIL_WIDTH,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = session.get(
|
response = session.get(
|
||||||
API_URL,
|
API_URL,
|
||||||
params=params,
|
params=params,
|
||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"API request failed: {e}")
|
print(f"API request failed: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# API RATE LIMIT
|
# API RATE LIMIT
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
if response.status_code == 429:
|
if response.status_code == 429:
|
||||||
wait_after_429(
|
wait_after_429(
|
||||||
response,
|
response,
|
||||||
attempt,
|
attempt,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
except (
|
except (
|
||||||
requests.RequestException,
|
requests.RequestException,
|
||||||
ValueError,
|
ValueError,
|
||||||
) as e:
|
) as e:
|
||||||
print(f"API error: {e}")
|
print(f"API error: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# GET PAGE
|
# GET PAGE
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
pages = (
|
pages = (
|
||||||
data
|
data
|
||||||
.get("query", {})
|
.get("query", {})
|
||||||
.get("pages", {})
|
.get("pages", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
if not pages:
|
if not pages:
|
||||||
print("No page returned.")
|
print("No page returned.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
page = next(
|
page = next(
|
||||||
iter(pages.values())
|
iter(pages.values())
|
||||||
)
|
)
|
||||||
|
|
||||||
title = page.get(
|
title = page.get(
|
||||||
"title",
|
"title",
|
||||||
"Unknown",
|
"Unknown",
|
||||||
)
|
)
|
||||||
|
|
||||||
imageinfo = page.get(
|
imageinfo = page.get(
|
||||||
"imageinfo"
|
"imageinfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not imageinfo:
|
if not imageinfo:
|
||||||
print(
|
print(
|
||||||
"No image information."
|
"No image information."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
info = imageinfo[0]
|
info = imageinfo[0]
|
||||||
|
|
||||||
mime = info.get(
|
mime = info.get(
|
||||||
"mime",
|
"mime",
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
width = info.get(
|
width = info.get(
|
||||||
"width",
|
"width",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
height = info.get(
|
height = info.get(
|
||||||
"height",
|
"height",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
size = info.get(
|
size = info.get(
|
||||||
"size",
|
"size",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
thumbnail_url = info.get(
|
thumbnail_url = info.get(
|
||||||
"thumburl"
|
"thumburl"
|
||||||
)
|
)
|
||||||
|
|
||||||
original_url = info.get(
|
original_url = info.get(
|
||||||
"url"
|
"url"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# FILTER
|
# FILTER
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
if mime not in {
|
if mime not in {
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/webp",
|
"image/webp",
|
||||||
}:
|
}:
|
||||||
print(
|
print(
|
||||||
f"Skipping unsupported type: "
|
f"Skipping unsupported type: "
|
||||||
f"{mime}"
|
f"{mime}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if width < MIN_WIDTH or height < MIN_HEIGHT:
|
if width < MIN_WIDTH or height < MIN_HEIGHT:
|
||||||
print(
|
print(
|
||||||
f"Skipping small image: "
|
f"Skipping small image: "
|
||||||
f"{width}x{height}"
|
f"{width}x{height}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if size > MAX_FILE_SIZE:
|
if size > MAX_FILE_SIZE:
|
||||||
print(
|
print(
|
||||||
f"Skipping large image: "
|
f"Skipping large image: "
|
||||||
f"{size / 1024 / 1024:.1f} MB"
|
f"{size / 1024 / 1024:.1f} MB"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not thumbnail_url:
|
if not thumbnail_url:
|
||||||
print("No thumbnail URL.")
|
print("No thumbnail URL.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# FOUND
|
# FOUND
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
print(f"Found: {title}")
|
print(f"Found: {title}")
|
||||||
print(
|
print(
|
||||||
f"Size: {width}x{height}"
|
f"Size: {width}x{height}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# DOWNLOAD THUMBNAIL
|
# DOWNLOAD THUMBNAIL
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
image_data = download_image(
|
image_data = download_image(
|
||||||
thumbnail_url
|
thumbnail_url
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# FALLBACK TO ORIGINAL
|
# FALLBACK TO ORIGINAL
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
if image_data is None and original_url:
|
if image_data is None and original_url:
|
||||||
print(
|
print(
|
||||||
"Trying original..."
|
"Trying original..."
|
||||||
)
|
)
|
||||||
|
|
||||||
image_data = download_image(
|
image_data = download_image(
|
||||||
original_url
|
original_url
|
||||||
)
|
)
|
||||||
|
|
||||||
if image_data is None:
|
if image_data is None:
|
||||||
print(
|
print(
|
||||||
"Couldn't download image."
|
"Couldn't download image."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# CONVERT TO JPEG
|
# CONVERT TO JPEG
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
print("Converting to JPEG...")
|
print("Converting to JPEG...")
|
||||||
|
|
||||||
jpeg_data = convert_to_jpeg(
|
jpeg_data = convert_to_jpeg(
|
||||||
image_data
|
image_data
|
||||||
)
|
)
|
||||||
|
|
||||||
if jpeg_data is None:
|
if jpeg_data is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# SAVE JPEG
|
# SAVE JPEG
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
try:
|
try:
|
||||||
OUTPUT_FILE.write_bytes(
|
OUTPUT_FILE.write_bytes(
|
||||||
jpeg_data
|
jpeg_data
|
||||||
)
|
)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(
|
print(
|
||||||
f"Couldn't save image: {e}"
|
f"Couldn't save image: {e}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# SAVE METADATA
|
# SAVE METADATA
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"source": "Wikimedia Commons",
|
"source": "Wikimedia Commons",
|
||||||
"output_format": "JPEG",
|
"output_format": "JPEG",
|
||||||
|
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
|
||||||
"original_mime": mime,
|
"original_mime": mime,
|
||||||
|
|
||||||
"original_size": size,
|
"original_size": size,
|
||||||
|
|
||||||
"jpeg_size": len(
|
"jpeg_size": len(
|
||||||
jpeg_data
|
jpeg_data
|
||||||
),
|
),
|
||||||
|
|
||||||
"original_url": (
|
"original_url": (
|
||||||
clean_url(original_url)
|
clean_url(original_url)
|
||||||
if original_url
|
if original_url
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
|
||||||
"thumbnail_url": (
|
"thumbnail_url": (
|
||||||
clean_url(thumbnail_url)
|
clean_url(thumbnail_url)
|
||||||
if thumbnail_url
|
if thumbnail_url
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
METADATA_FILE.write_text(
|
METADATA_FILE.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
metadata,
|
metadata,
|
||||||
indent=4,
|
indent=4,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(
|
print(
|
||||||
f"Warning: couldn't save "
|
f"Warning: couldn't save "
|
||||||
f"metadata: {e}"
|
f"metadata: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# DONE
|
# DONE
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("SUCCESS")
|
print("SUCCESS")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print(
|
print(
|
||||||
f"Image: "
|
f"Image: "
|
||||||
f"{OUTPUT_FILE.absolute()}"
|
f"{OUTPUT_FILE.absolute()}"
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f"Size: "
|
f"Size: "
|
||||||
f"{len(jpeg_data) / 1024:.1f} KB"
|
f"{len(jpeg_data) / 1024:.1f} KB"
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f"Source: {title}"
|
f"Source: {title}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Unable to obtain a suitable "
|
"Unable to obtain a suitable "
|
||||||
"Wikimedia image."
|
"Wikimedia image."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -509,4 +509,4 @@ def get_random_image():
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
get_random_image()
|
get_random_image()
|
||||||
@@ -19,87 +19,87 @@ Point = tuple[int, int]
|
|||||||
|
|
||||||
|
|
||||||
def make_paths() -> tuple[callable, callable]:
|
def make_paths() -> tuple[callable, callable]:
|
||||||
"""Return independent paths with the same start and end points."""
|
"""Return independent paths with the same start and end points."""
|
||||||
start = (150, 535)
|
start = (150, 535)
|
||||||
end = (950, 535)
|
end = (950, 535)
|
||||||
base_path = get_bezier_path(start, end, intermediate_radius_interval=(150, 210))
|
base_path = get_bezier_path(start, end, intermediate_radius_interval=(150, 210))
|
||||||
distorted_path = get_distorted_bezier_path(
|
distorted_path = get_distorted_bezier_path(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
intermediate_radius_interval=(150, 210),
|
intermediate_radius_interval=(150, 210),
|
||||||
distortion_zone_time_length=0.08,
|
distortion_zone_time_length=0.08,
|
||||||
distortion_frequency=1.0,
|
distortion_frequency=1.0,
|
||||||
deviation_interval=(10, 18),
|
deviation_interval=(10, 18),
|
||||||
)
|
)
|
||||||
|
|
||||||
return base_path, distorted_path
|
return base_path, distorted_path
|
||||||
|
|
||||||
|
|
||||||
def sample_path(path: callable, steps: int = 360) -> list[Point]:
|
def sample_path(path: callable, steps: int = 360) -> list[Point]:
|
||||||
return [
|
return [
|
||||||
(round(point[0]), round(point[1]))
|
(round(point[0]), round(point[1]))
|
||||||
for point in (path(index / (steps - 1)) for index in range(steps))
|
for point in (path(index / (steps - 1)) for index in range(steps))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def draw_label(screen: pygame.Surface, font: pygame.font.Font, text: str, position: tuple[int, int], color: tuple[int, int, int]) -> None:
|
def draw_label(screen: pygame.Surface, font: pygame.font.Font, text: str, position: tuple[int, int], color: tuple[int, int, int]) -> None:
|
||||||
screen.blit(font.render(text, True, color), position)
|
screen.blit(font.render(text, True, color), position)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
pygame.init()
|
pygame.init()
|
||||||
screen = pygame.display.set_mode(WINDOW_SIZE)
|
screen = pygame.display.set_mode(WINDOW_SIZE)
|
||||||
pygame.display.set_caption("Bezier Path Distortion")
|
pygame.display.set_caption("Bezier Path Distortion")
|
||||||
clock = pygame.time.Clock()
|
clock = pygame.time.Clock()
|
||||||
title_font = pygame.font.SysFont("Segoe UI", 28, bold=True)
|
title_font = pygame.font.SysFont("Segoe UI", 28, bold=True)
|
||||||
body_font = pygame.font.SysFont("Segoe UI", 19)
|
body_font = pygame.font.SysFont("Segoe UI", 19)
|
||||||
button_font = pygame.font.SysFont("Segoe UI", 18, bold=True)
|
button_font = pygame.font.SysFont("Segoe UI", 18, bold=True)
|
||||||
|
|
||||||
base_path, distorted_path = make_paths()
|
base_path, distorted_path = make_paths()
|
||||||
base_points = sample_path(base_path)
|
base_points = sample_path(base_path)
|
||||||
distorted_points = sample_path(distorted_path)
|
distorted_points = sample_path(distorted_path)
|
||||||
show_distorted = False
|
show_distorted = False
|
||||||
button = pygame.Rect(405, 595, 290, 52)
|
button = pygame.Rect(405, 595, 290, 52)
|
||||||
|
|
||||||
running = True
|
running = True
|
||||||
while running:
|
while running:
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
if event.type == pygame.QUIT:
|
if event.type == pygame.QUIT:
|
||||||
running = False
|
running = False
|
||||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||||
if button.collidepoint(event.pos):
|
if button.collidepoint(event.pos):
|
||||||
show_distorted = not show_distorted
|
show_distorted = not show_distorted
|
||||||
|
|
||||||
screen.fill(BACKGROUND)
|
screen.fill(BACKGROUND)
|
||||||
draw_label(screen, title_font, "Bezier path comparison", (38, 28), PATH_COLOR)
|
draw_label(screen, title_font, "Bezier path comparison", (38, 28), PATH_COLOR)
|
||||||
draw_label(
|
draw_label(
|
||||||
screen,
|
screen,
|
||||||
body_font,
|
body_font,
|
||||||
"The red path adds temporary offsets to the same underlying curve.",
|
"The red path adds temporary offsets to the same underlying curve.",
|
||||||
(40, 70),
|
(40, 70),
|
||||||
(91, 97, 102),
|
(91, 97, 102),
|
||||||
)
|
)
|
||||||
|
|
||||||
pygame.draw.lines(screen, (184, 188, 190), False, base_points, 1)
|
pygame.draw.lines(screen, (184, 188, 190), False, base_points, 1)
|
||||||
pygame.draw.lines(screen, DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR, False, distorted_points if show_distorted else base_points, 4)
|
pygame.draw.lines(screen, DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR, False, distorted_points if show_distorted else base_points, 4)
|
||||||
pygame.draw.circle(screen, POINT_COLOR, base_points[0], 10)
|
pygame.draw.circle(screen, POINT_COLOR, base_points[0], 10)
|
||||||
pygame.draw.circle(screen, POINT_COLOR, base_points[-1], 10)
|
pygame.draw.circle(screen, POINT_COLOR, base_points[-1], 10)
|
||||||
|
|
||||||
draw_label(screen, body_font, "A", (base_points[0][0] - 8, base_points[0][1] + 18), PATH_COLOR)
|
draw_label(screen, body_font, "A", (base_points[0][0] - 8, base_points[0][1] + 18), PATH_COLOR)
|
||||||
draw_label(screen, body_font, "B", (base_points[-1][0] - 8, base_points[-1][1] + 18), PATH_COLOR)
|
draw_label(screen, body_font, "B", (base_points[-1][0] - 8, base_points[-1][1] + 18), PATH_COLOR)
|
||||||
draw_label(screen, body_font, "DISTORTED" if show_distorted else "UNDISTORTED", (20, 535), DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR)
|
draw_label(screen, body_font, "DISTORTED" if show_distorted else "UNDISTORTED", (20, 535), DISTORTED_PATH_COLOR if show_distorted else PATH_COLOR)
|
||||||
|
|
||||||
button_color = BUTTON_HOVER_COLOR if button.collidepoint(pygame.mouse.get_pos()) else BUTTON_COLOR
|
button_color = BUTTON_HOVER_COLOR if button.collidepoint(pygame.mouse.get_pos()) else BUTTON_COLOR
|
||||||
pygame.draw.rect(screen, button_color, button, border_radius=7)
|
pygame.draw.rect(screen, button_color, button, border_radius=7)
|
||||||
button_text = "Show undistorted path" if show_distorted else "Show distorted path"
|
button_text = "Show undistorted path" if show_distorted else "Show distorted path"
|
||||||
text_surface = button_font.render(button_text, True, BUTTON_TEXT_COLOR)
|
text_surface = button_font.render(button_text, True, BUTTON_TEXT_COLOR)
|
||||||
screen.blit(text_surface, text_surface.get_rect(center=button.center))
|
screen.blit(text_surface, text_surface.get_rect(center=button.center))
|
||||||
|
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
clock.tick(60)
|
clock.tick(60)
|
||||||
|
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user