mirror of
https://github.com/DragoonAethis/itch-dl.git
synced 2025-02-28 18:43:38 +01:00
Upgrade to Python 3.10+
We can use fancier type annotations now, newer deps, more checkers.
This commit is contained in:
parent
d307ae8db7
commit
cb08443778
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional, Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests import Session
|
from requests import Session
|
||||||
@ -9,7 +9,7 @@ from .consts import ITCH_API
|
|||||||
|
|
||||||
|
|
||||||
class ItchApiClient:
|
class ItchApiClient:
|
||||||
def __init__(self, api_key: str, user_agent: str, base_url: Optional[str] = None) -> None:
|
def __init__(self, api_key: str, user_agent: str, base_url: str | None = None) -> None:
|
||||||
self.base_url = base_url or ITCH_API
|
self.base_url = base_url or ITCH_API
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import logging
|
|||||||
import platform
|
import platform
|
||||||
import argparse
|
import argparse
|
||||||
from dataclasses import dataclass, fields
|
from dataclasses import dataclass, fields
|
||||||
from typing import Optional, Any, get_type_hints
|
from typing import Any, get_type_hints
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -16,16 +16,16 @@ class Settings:
|
|||||||
"""Available settings for itch-dl. Make sure all of them
|
"""Available settings for itch-dl. Make sure all of them
|
||||||
have default values, as the config file may not exist."""
|
have default values, as the config file may not exist."""
|
||||||
|
|
||||||
api_key: Optional[str] = None
|
api_key: str | None = None
|
||||||
user_agent: str = f"python-requests/{requests.__version__} itch-dl/{__version__}"
|
user_agent: str = f"python-requests/{requests.__version__} itch-dl/{__version__}"
|
||||||
|
|
||||||
download_to: Optional[str] = None
|
download_to: str | None = None
|
||||||
mirror_web: bool = False
|
mirror_web: bool = False
|
||||||
urls_only: bool = False
|
urls_only: bool = False
|
||||||
parallel: int = 1
|
parallel: int = 1
|
||||||
|
|
||||||
filter_files_glob: Optional[str] = None
|
filter_files_glob: str | None = None
|
||||||
filter_files_regex: Optional[str] = None
|
filter_files_regex: str | None = None
|
||||||
|
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ def clean_config(config_data: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return cleaned_config
|
return cleaned_config
|
||||||
|
|
||||||
|
|
||||||
def load_config(args: argparse.Namespace, profile: Optional[str] = None) -> Settings:
|
def load_config(args: argparse.Namespace, profile: str | None = None) -> Settings:
|
||||||
"""Loads the configuration from the file system if it exists,
|
"""Loads the configuration from the file system if it exists,
|
||||||
the returns a Settings object."""
|
the returns a Settings object."""
|
||||||
config_path = create_and_get_config_path()
|
config_path = create_and_get_config_path()
|
||||||
|
@ -6,7 +6,7 @@ import logging
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import zipfile
|
import zipfile
|
||||||
import tarfile
|
import tarfile
|
||||||
from typing import List, Dict, TypedDict, Optional, Union, Any
|
from typing import TypedDict, Any
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from requests.exceptions import HTTPError, JSONDecodeError
|
from requests.exceptions import HTTPError, JSONDecodeError
|
||||||
@ -30,7 +30,7 @@ TARGET_PATHS = {
|
|||||||
|
|
||||||
|
|
||||||
class DownloadResult:
|
class DownloadResult:
|
||||||
def __init__(self, url: str, success: bool, errors: Optional[List[str]], external_urls: List[str]) -> None:
|
def __init__(self, url: str, success: bool, errors: list[str] | None, external_urls: list[str]) -> None:
|
||||||
self.url = url
|
self.url = url
|
||||||
self.success = success
|
self.success = success
|
||||||
self.errors = errors or []
|
self.errors = errors or []
|
||||||
@ -42,17 +42,17 @@ class GameMetadata(TypedDict, total=False):
|
|||||||
title: str
|
title: str
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
errors: List[str]
|
errors: list[str]
|
||||||
external_downloads: List[str]
|
external_downloads: list[str]
|
||||||
|
|
||||||
author: str
|
author: str
|
||||||
author_url: str
|
author_url: str
|
||||||
|
|
||||||
cover_url: Optional[str]
|
cover_url: str | None
|
||||||
screenshots: List[str]
|
screenshots: list[str]
|
||||||
description: Optional[str]
|
description: str | None
|
||||||
|
|
||||||
rating: Dict[str, Union[float, int]]
|
rating: dict[str, float | int]
|
||||||
extra: InfoboxMetadata
|
extra: InfoboxMetadata
|
||||||
|
|
||||||
created_at: str
|
created_at: str
|
||||||
@ -62,13 +62,13 @@ class GameMetadata(TypedDict, total=False):
|
|||||||
|
|
||||||
|
|
||||||
class GameDownloader:
|
class GameDownloader:
|
||||||
def __init__(self, settings: Settings, keys: Dict[int, str]) -> None:
|
def __init__(self, settings: Settings, keys: dict[int, str]) -> None:
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.download_keys = keys
|
self.download_keys = keys
|
||||||
self.client = ItchApiClient(settings.api_key, settings.user_agent)
|
self.client = ItchApiClient(settings.api_key, settings.user_agent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_rating_json(site: BeautifulSoup) -> Optional[dict]:
|
def get_rating_json(site: BeautifulSoup) -> dict | None:
|
||||||
for ldjson_node in site.find_all("script", type="application/ld+json"):
|
for ldjson_node in site.find_all("script", type="application/ld+json"):
|
||||||
try:
|
try:
|
||||||
ldjson: dict = json.loads(ldjson_node.text.strip())
|
ldjson: dict = json.loads(ldjson_node.text.strip())
|
||||||
@ -80,7 +80,7 @@ class GameDownloader:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_meta(site: BeautifulSoup, **kwargs: Any) -> Optional[str]: # noqa: ANN401
|
def get_meta(site: BeautifulSoup, **kwargs: Any) -> str | None: # noqa: ANN401
|
||||||
"""Grabs <meta property="xyz" content="value"/> values."""
|
"""Grabs <meta property="xyz" content="value"/> values."""
|
||||||
node = site.find("meta", attrs=kwargs)
|
node = site.find("meta", attrs=kwargs)
|
||||||
if not node:
|
if not node:
|
||||||
@ -89,7 +89,7 @@ class GameDownloader:
|
|||||||
return node.get("content")
|
return node.get("content")
|
||||||
|
|
||||||
def get_game_id(self, url: str, site: BeautifulSoup) -> int:
|
def get_game_id(self, url: str, site: BeautifulSoup) -> int:
|
||||||
game_id: Optional[int] = None
|
game_id: int | None = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Headers: <meta name="itch:path" content="games/12345" />
|
# Headers: <meta name="itch:path" content="games/12345" />
|
||||||
@ -134,14 +134,14 @@ class GameDownloader:
|
|||||||
return game_id
|
return game_id
|
||||||
|
|
||||||
def extract_metadata(self, game_id: int, url: str, site: BeautifulSoup) -> GameMetadata:
|
def extract_metadata(self, game_id: int, url: str, site: BeautifulSoup) -> GameMetadata:
|
||||||
rating_json: Optional[dict] = self.get_rating_json(site)
|
rating_json: dict | None = self.get_rating_json(site)
|
||||||
title = rating_json.get("name") if rating_json else None
|
title = rating_json.get("name") if rating_json else None
|
||||||
|
|
||||||
description: Optional[str] = self.get_meta(site, property="og:description")
|
description: str | None = self.get_meta(site, property="og:description")
|
||||||
if not description:
|
if not description:
|
||||||
description = self.get_meta(site, name="description")
|
description = self.get_meta(site, name="description")
|
||||||
|
|
||||||
screenshot_urls: List[str] = []
|
screenshot_urls: list[str] = []
|
||||||
screenshots_node = site.find("div", class_="screenshot_list")
|
screenshots_node = site.find("div", class_="screenshot_list")
|
||||||
if screenshots_node:
|
if screenshots_node:
|
||||||
screenshot_urls = [a["href"] for a in screenshots_node.find_all("a")]
|
screenshot_urls = [a["href"] for a in screenshots_node.find_all("a")]
|
||||||
@ -193,7 +193,7 @@ class GameDownloader:
|
|||||||
|
|
||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
def download_file(self, url: str, download_path: Optional[str], credentials: dict) -> str:
|
def download_file(self, url: str, download_path: str | None, credentials: dict) -> str:
|
||||||
"""Performs a request to download a given file, optionally saves the
|
"""Performs a request to download a given file, optionally saves the
|
||||||
file to the provided path and returns the final URL that was downloaded."""
|
file to the provided path and returns the final URL that was downloaded."""
|
||||||
try:
|
try:
|
||||||
@ -216,7 +216,7 @@ class GameDownloader:
|
|||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
raise ItchDownloadError(f"Unrecoverable download error: {e}") from e
|
raise ItchDownloadError(f"Unrecoverable download error: {e}") from e
|
||||||
|
|
||||||
def download_file_by_upload_id(self, upload_id: int, download_path: Optional[str], credentials: dict) -> str:
|
def download_file_by_upload_id(self, upload_id: int, download_path: str | None, credentials: dict) -> str:
|
||||||
"""Performs a request to download a given upload by its ID."""
|
"""Performs a request to download a given upload by its ID."""
|
||||||
return self.download_file(f"/uploads/{upload_id}/download", download_path, credentials)
|
return self.download_file(f"/uploads/{upload_id}/download", download_path, credentials)
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ class GameDownloader:
|
|||||||
download_path = os.path.join(self.settings.download_to, author, game)
|
download_path = os.path.join(self.settings.download_to, author, game)
|
||||||
os.makedirs(download_path, exist_ok=True)
|
os.makedirs(download_path, exist_ok=True)
|
||||||
|
|
||||||
paths: Dict[str, str] = {k: os.path.join(download_path, v) for k, v in TARGET_PATHS.items()}
|
paths: dict[str, str] = {k: os.path.join(download_path, v) for k, v in TARGET_PATHS.items()}
|
||||||
|
|
||||||
if os.path.exists(paths["metadata"]) and skip_downloaded:
|
if os.path.exists(paths["metadata"]) and skip_downloaded:
|
||||||
# As metadata is the final file we write, all the files
|
# As metadata is the final file we write, all the files
|
||||||
@ -405,9 +405,9 @@ class GameDownloader:
|
|||||||
|
|
||||||
|
|
||||||
def drive_downloads(
|
def drive_downloads(
|
||||||
jobs: List[str],
|
jobs: list[str],
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
keys: Dict[int, str],
|
keys: dict[int, str],
|
||||||
) -> None:
|
) -> None:
|
||||||
downloader = GameDownloader(settings, keys)
|
downloader = GameDownloader(settings, keys)
|
||||||
tqdm_args = {
|
tqdm_args = {
|
||||||
|
@ -2,7 +2,6 @@ import json
|
|||||||
import os.path
|
import os.path
|
||||||
import logging
|
import logging
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import List, Set, Optional
|
|
||||||
|
|
||||||
from http.client import responses
|
from http.client import responses
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@ -14,7 +13,7 @@ from .config import Settings
|
|||||||
from .keys import get_owned_games
|
from .keys import get_owned_games
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_game_jam_json(game_jam_json: dict) -> List[str]:
|
def get_jobs_for_game_jam_json(game_jam_json: dict) -> list[str]:
|
||||||
if "jam_games" not in game_jam_json:
|
if "jam_games" not in game_jam_json:
|
||||||
raise Exception("Provided JSON is not a valid itch.io jam JSON.")
|
raise Exception("Provided JSON is not a valid itch.io jam JSON.")
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ def get_game_jam_json(jam_url: str, client: ItchApiClient) -> dict:
|
|||||||
if not r.ok:
|
if not r.ok:
|
||||||
raise ItchDownloadError(f"Could not download the game jam site: {r.status_code} {r.reason}")
|
raise ItchDownloadError(f"Could not download the game jam site: {r.status_code} {r.reason}")
|
||||||
|
|
||||||
jam_id: Optional[int] = get_int_after_marker_in_json(r.text, "I.ViewJam", "id")
|
jam_id: int | None = get_int_after_marker_in_json(r.text, "I.ViewJam", "id")
|
||||||
if jam_id is None:
|
if jam_id is None:
|
||||||
raise ItchDownloadError(
|
raise ItchDownloadError(
|
||||||
"Provided site did not contain the Game Jam ID. Provide "
|
"Provided site did not contain the Game Jam ID. Provide "
|
||||||
@ -42,7 +41,7 @@ def get_game_jam_json(jam_url: str, client: ItchApiClient) -> dict:
|
|||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_browse_url(url: str, client: ItchApiClient) -> List[str]:
|
def get_jobs_for_browse_url(url: str, client: ItchApiClient) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Every browser page has a hidden RSS feed that can be accessed by
|
Every browser page has a hidden RSS feed that can be accessed by
|
||||||
appending .xml to its URL. An optional "page" argument lets us
|
appending .xml to its URL. An optional "page" argument lets us
|
||||||
@ -53,7 +52,7 @@ def get_jobs_for_browse_url(url: str, client: ItchApiClient) -> List[str]:
|
|||||||
.xml?page=N suffix and iterate until we've caught 'em all.
|
.xml?page=N suffix and iterate until we've caught 'em all.
|
||||||
"""
|
"""
|
||||||
page = 1
|
page = 1
|
||||||
found_urls: Set[str] = set()
|
found_urls: set[str] = set()
|
||||||
logging.info("Scraping game URLs from RSS feeds for %s", url)
|
logging.info("Scraping game URLs from RSS feeds for %s", url)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@ -87,9 +86,9 @@ def get_jobs_for_browse_url(url: str, client: ItchApiClient) -> List[str]:
|
|||||||
return list(found_urls)
|
return list(found_urls)
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_collection_json(url: str, client: ItchApiClient) -> List[str]:
|
def get_jobs_for_collection_json(url: str, client: ItchApiClient) -> list[str]:
|
||||||
page = 1
|
page = 1
|
||||||
found_urls: Set[str] = set()
|
found_urls: set[str] = set()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
logging.info("Downloading page %d (found %d URLs total)", page, len(found_urls))
|
logging.info("Downloading page %d (found %d URLs total)", page, len(found_urls))
|
||||||
@ -118,7 +117,7 @@ def get_jobs_for_collection_json(url: str, client: ItchApiClient) -> List[str]:
|
|||||||
return list(found_urls)
|
return list(found_urls)
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_creator(creator: str, client: ItchApiClient) -> List[str]:
|
def get_jobs_for_creator(creator: str, client: ItchApiClient) -> list[str]:
|
||||||
logging.info("Downloading public games for creator %s", creator)
|
logging.info("Downloading public games for creator %s", creator)
|
||||||
r = client.get(f"https://{ITCH_BASE}/profile/{creator}", append_api_key=False)
|
r = client.get(f"https://{ITCH_BASE}/profile/{creator}", append_api_key=False)
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
@ -139,7 +138,7 @@ def get_jobs_for_creator(creator: str, client: ItchApiClient) -> List[str]:
|
|||||||
return sorted(game_links)
|
return sorted(game_links)
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_itch_url(url: str, client: ItchApiClient) -> List[str]:
|
def get_jobs_for_itch_url(url: str, client: ItchApiClient) -> list[str]:
|
||||||
if url.startswith("http://"):
|
if url.startswith("http://"):
|
||||||
logging.info("HTTP link provided, upgrading to HTTPS")
|
logging.info("HTTP link provided, upgrading to HTTPS")
|
||||||
url = "https://" + url[7:]
|
url = "https://" + url[7:]
|
||||||
@ -149,7 +148,7 @@ def get_jobs_for_itch_url(url: str, client: ItchApiClient) -> List[str]:
|
|||||||
url = ITCH_URL + "/" + url[20:]
|
url = ITCH_URL + "/" + url[20:]
|
||||||
|
|
||||||
url_parts = urllib.parse.urlparse(url)
|
url_parts = urllib.parse.urlparse(url)
|
||||||
url_path_parts: List[str] = [x for x in str(url_parts.path).split("/") if len(x) > 0]
|
url_path_parts: list[str] = [x for x in str(url_parts.path).split("/") if len(x) > 0]
|
||||||
|
|
||||||
if url_parts.netloc == ITCH_BASE:
|
if url_parts.netloc == ITCH_BASE:
|
||||||
if len(url_path_parts) == 0:
|
if len(url_path_parts) == 0:
|
||||||
@ -209,7 +208,7 @@ def get_jobs_for_itch_url(url: str, client: ItchApiClient) -> List[str]:
|
|||||||
raise ValueError(f"Unknown domain: {url_parts.netloc}")
|
raise ValueError(f"Unknown domain: {url_parts.netloc}")
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_path(path: str) -> List[str]:
|
def get_jobs_for_path(path: str) -> list[str]:
|
||||||
try: # Game Jam Entries JSON?
|
try: # Game Jam Entries JSON?
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
json_data = json.load(f)
|
json_data = json.load(f)
|
||||||
@ -237,7 +236,7 @@ def get_jobs_for_path(path: str) -> List[str]:
|
|||||||
raise ValueError("File format is unknown - cannot read URLs to download.")
|
raise ValueError("File format is unknown - cannot read URLs to download.")
|
||||||
|
|
||||||
|
|
||||||
def get_jobs_for_url_or_path(path_or_url: str, settings: Settings) -> List[str]:
|
def get_jobs_for_url_or_path(path_or_url: str, settings: Settings) -> list[str]:
|
||||||
"""Returns a list of Game URLs for a given itch.io URL or file."""
|
"""Returns a list of Game URLs for a given itch.io URL or file."""
|
||||||
path_or_url = path_or_url.strip()
|
path_or_url = path_or_url.strip()
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TypedDict, Dict, List, Any, Tuple, Optional
|
from typing import TypedDict, Any
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
@ -9,26 +9,26 @@ class InfoboxMetadata(TypedDict, total=False):
|
|||||||
released_at: datetime
|
released_at: datetime
|
||||||
published_at: datetime
|
published_at: datetime
|
||||||
status: str
|
status: str
|
||||||
platforms: List[str] # Windows/macOS/Linux/etc
|
platforms: list[str] # Windows/macOS/Linux/etc
|
||||||
publisher: str
|
publisher: str
|
||||||
author: Dict[str, str] # See impl below!
|
author: dict[str, str] # See impl below!
|
||||||
authors: Dict[str, str] # Links
|
authors: dict[str, str] # Links
|
||||||
genre: Dict[str, str] # Links
|
genre: dict[str, str] # Links
|
||||||
tools: Dict[str, str] # Links
|
tools: dict[str, str] # Links
|
||||||
license: Dict[str, str] # Links
|
license: dict[str, str] # Links
|
||||||
asset_license: Dict[str, str] # Links
|
asset_license: dict[str, str] # Links
|
||||||
tags: Dict[str, str] # Links
|
tags: dict[str, str] # Links
|
||||||
length: str
|
length: str
|
||||||
multiplayer: Dict[str, str] # Links
|
multiplayer: dict[str, str] # Links
|
||||||
player_count: str
|
player_count: str
|
||||||
accessibility: Dict[str, str] # Links
|
accessibility: dict[str, str] # Links
|
||||||
inputs: Dict[str, str] # Links
|
inputs: dict[str, str] # Links
|
||||||
links: Dict[str, str] # Links
|
links: dict[str, str] # Links
|
||||||
mentions: Dict[str, str] # Links
|
mentions: dict[str, str] # Links
|
||||||
category: Dict[str, str] # Links
|
category: dict[str, str] # Links
|
||||||
|
|
||||||
|
|
||||||
def parse_date_block(td: BeautifulSoup) -> Optional[datetime]:
|
def parse_date_block(td: BeautifulSoup) -> datetime | None:
|
||||||
abbr = td.find("abbr")
|
abbr = td.find("abbr")
|
||||||
if not abbr or "title" not in abbr.attrs:
|
if not abbr or "title" not in abbr.attrs:
|
||||||
return None
|
return None
|
||||||
@ -39,17 +39,17 @@ def parse_date_block(td: BeautifulSoup) -> Optional[datetime]:
|
|||||||
return datetime(date.year, date.month, date.day, time.hour, time.minute)
|
return datetime(date.year, date.month, date.day, time.hour, time.minute)
|
||||||
|
|
||||||
|
|
||||||
def parse_links(td: BeautifulSoup) -> Dict[str, str]:
|
def parse_links(td: BeautifulSoup) -> dict[str, str]:
|
||||||
"""Parses blocks of comma-separated <a> blocks, returns a dict
|
"""Parses blocks of comma-separated <a> blocks, returns a dict
|
||||||
of link text -> URL it points at."""
|
of link text -> URL it points at."""
|
||||||
return {link.text.strip(): link["href"] for link in td.find_all("a")}
|
return {link.text.strip(): link["href"] for link in td.find_all("a")}
|
||||||
|
|
||||||
|
|
||||||
def parse_text_from_links(td: BeautifulSoup) -> List[str]:
|
def parse_text_from_links(td: BeautifulSoup) -> list[str]:
|
||||||
return list(parse_links(td).keys())
|
return list(parse_links(td).keys())
|
||||||
|
|
||||||
|
|
||||||
def parse_tr(name: str, content: BeautifulSoup) -> Optional[Tuple[str, Any]]:
|
def parse_tr(name: str, content: BeautifulSoup) -> tuple[str, Any] | None:
|
||||||
if name == "Updated":
|
if name == "Updated":
|
||||||
return "updated_at", parse_date_block(content)
|
return "updated_at", parse_date_block(content)
|
||||||
elif name == "Release date":
|
elif name == "Release date":
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple
|
|
||||||
|
|
||||||
from .api import ItchApiClient
|
from .api import ItchApiClient
|
||||||
|
|
||||||
KEYS_CACHED: bool = False
|
KEYS_CACHED: bool = False
|
||||||
DOWNLOAD_KEYS: Dict[int, str] = {}
|
DOWNLOAD_KEYS: dict[int, str] = {}
|
||||||
GAME_URLS: List[str] = []
|
GAME_URLS: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
def load_keys_and_urls(client: ItchApiClient) -> None:
|
def load_keys_and_urls(client: ItchApiClient) -> None:
|
||||||
@ -36,21 +35,21 @@ def load_keys_and_urls(client: ItchApiClient) -> None:
|
|||||||
KEYS_CACHED = True
|
KEYS_CACHED = True
|
||||||
|
|
||||||
|
|
||||||
def get_owned_keys(client: ItchApiClient) -> Tuple[Dict[int, str], List[str]]:
|
def get_owned_keys(client: ItchApiClient) -> tuple[dict[int, str], list[str]]:
|
||||||
if not KEYS_CACHED:
|
if not KEYS_CACHED:
|
||||||
load_keys_and_urls(client)
|
load_keys_and_urls(client)
|
||||||
|
|
||||||
return DOWNLOAD_KEYS, GAME_URLS
|
return DOWNLOAD_KEYS, GAME_URLS
|
||||||
|
|
||||||
|
|
||||||
def get_download_keys(client: ItchApiClient) -> Dict[int, str]:
|
def get_download_keys(client: ItchApiClient) -> dict[int, str]:
|
||||||
if not KEYS_CACHED:
|
if not KEYS_CACHED:
|
||||||
load_keys_and_urls(client)
|
load_keys_and_urls(client)
|
||||||
|
|
||||||
return DOWNLOAD_KEYS
|
return DOWNLOAD_KEYS
|
||||||
|
|
||||||
|
|
||||||
def get_owned_games(client: ItchApiClient) -> List[str]:
|
def get_owned_games(client: ItchApiClient) -> list[str]:
|
||||||
if not KEYS_CACHED:
|
if not KEYS_CACHED:
|
||||||
load_keys_and_urls(client)
|
load_keys_and_urls(client)
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class ItchDownloadError(Exception):
|
class ItchDownloadError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_int_after_marker_in_json(text: str, marker: str, key: str) -> Optional[int]:
|
def get_int_after_marker_in_json(text: str, marker: str, key: str) -> int | None:
|
||||||
"""
|
"""
|
||||||
Many itch.io sites use a pattern like this: Most of the HTML page
|
Many itch.io sites use a pattern like this: Most of the HTML page
|
||||||
is prerendered, but certain interactive objects are handled with
|
is prerendered, but certain interactive objects are handled with
|
||||||
@ -14,7 +13,7 @@ def get_int_after_marker_in_json(text: str, marker: str, key: str) -> Optional[i
|
|||||||
somewhere near the end of each page. Those config blocks often
|
somewhere near the end of each page. Those config blocks often
|
||||||
contain metadata like game/page IDs that we want to extract.
|
contain metadata like game/page IDs that we want to extract.
|
||||||
"""
|
"""
|
||||||
marker_line: Optional[str] = None
|
marker_line: str | None = None
|
||||||
for line in reversed(text.splitlines()):
|
for line in reversed(text.splitlines()):
|
||||||
marker_index = line.find(marker)
|
marker_index = line.find(marker)
|
||||||
if marker_index != -1:
|
if marker_index != -1:
|
||||||
|
@ -25,7 +25,7 @@ classifiers = [
|
|||||||
"Donate" = "https://ko-fi.com/dragoonaethis"
|
"Donate" = "https://ko-fi.com/dragoonaethis"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.10"
|
||||||
tqdm = "^4.67.1"
|
tqdm = "^4.67.1"
|
||||||
urllib3 = "^1.26.20"
|
urllib3 = "^1.26.20"
|
||||||
requests = "^2.32.3"
|
requests = "^2.32.3"
|
||||||
@ -41,7 +41,7 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
target-version = "py38"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# https://docs.astral.sh/ruff/rules/
|
# https://docs.astral.sh/ruff/rules/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user