Add support for downloading bundles

This commit is contained in:
Jack Wilsdon 2022-06-19 13:07:31 +01:00
parent 49ad7a719c
commit 9e006543c2
3 changed files with 27 additions and 7 deletions

View File

@ -2,7 +2,7 @@
Bulk download games from [itch.io](https://itch.io/). Bulk download games from [itch.io](https://itch.io/).
- Can download game jams, browse pages (popular, newest, browse by tag...) and individual games. - Can download game jams, browse pages (popular, newest, browse by tag...), bundles and individual games.
- Requires Python 3.8+, grab it from PyPI: `pip install itch-dl` - Requires Python 3.8+, grab it from PyPI: `pip install itch-dl`
- For development, use [Poetry](https://python-poetry.org/). - For development, use [Poetry](https://python-poetry.org/).
@ -19,9 +19,9 @@ More arguments are available - check them out with `itch-dl --help`.
The downloader is able to grab more or less everything you can download via the itch app. The downloader is able to grab more or less everything you can download via the itch app.
The input URL can be any "Browse" page (top, popular, newest, filtered by tags, etc) or any The input URL can be any "Browse" page (top, popular, newest, filtered by tags, etc), any
game jam. The input can also be a path to a itch.io JSON file with game jam entries, or just game jam or any bundle. The input can also be a path to a itch.io JSON file with game jam
a list of itch.io game URLs (not browse/jam pages!) to download. entries, or just a list of itch.io game URLs (not browse pages!) to download.
**It's expected that the downloader output will not be complete** - logs are stupidly verbose **It's expected that the downloader output will not be complete** - logs are stupidly verbose
and it prints a report on failed downloads and external URLs (links to files that are not on and it prints a report on failed downloads and external URLs (links to files that are not on

View File

@ -15,7 +15,7 @@ logging.getLogger().setLevel(logging.INFO)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Bulk download stuff from Itch.io.") parser = argparse.ArgumentParser(description="Bulk download stuff from Itch.io.")
parser.add_argument("url_or_path", parser.add_argument("url_or_path",
help="itch.io URL or path to a game jam entries.json file") help="itch.io URL or path to either a game jam entries.json file or bundle games.json file")
parser.add_argument("--api-key", metavar="key", default=None, parser.add_argument("--api-key", metavar="key", default=None,
help="itch.io API key - https://itch.io/user/settings/api-keys") help="itch.io API key - https://itch.io/user/settings/api-keys")
parser.add_argument("--profile", metavar="profile", default=None, parser.add_argument("--profile", metavar="profile", default=None,

View File

@ -83,6 +83,21 @@ def get_jobs_for_browse_url(url: str, client: ItchApiClient) -> List[str]:
return list(found_urls) return list(found_urls)
def get_jobs_for_bundle_json(bundle_json: dict) -> List[str]:
if 'games' not in bundle_json:
raise Exception("Provided JSON is not a valid itch.io bundle JSON.")
return [g['url'] for g in bundle_json['games']]
def get_bundle_json(bundle_id: str, client: ItchApiClient) -> dict:
r = client.get(f"{ITCH_URL}/bundle/{bundle_id}/games.json")
if not r.ok:
raise ItchDownloadError(f"Could not download game list: {r.status_code} {r.reason}")
return r.json()
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")
@ -116,7 +131,8 @@ def get_jobs_for_itch_url(url: str, client: ItchApiClient) -> List[str]:
return get_jobs_for_browse_url(clean_browse_url, client) return get_jobs_for_browse_url(clean_browse_url, client)
elif site in ("b", "bundle"): # Bundles elif site in ("b", "bundle"): # Bundles
raise NotImplementedError("itch-dl cannot download bundles yet.") bundle_json = get_bundle_json(url_path_parts[1], client)
return get_jobs_for_bundle_json(bundle_json)
elif site in ("j", "jobs"): # Jobs... elif site in ("j", "jobs"): # Jobs...
raise ValueError("itch-dl cannot download a job.") raise ValueError("itch-dl cannot download a job.")
@ -149,7 +165,7 @@ def get_jobs_for_itch_url(url: str, client: ItchApiClient) -> List[str]:
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 or Bundle Games JSON?
with open(path, "rb") as f: with open(path, "rb") as f:
json_data = json.load(f) json_data = json.load(f)
@ -159,6 +175,10 @@ def get_jobs_for_path(path: str) -> List[str]:
if 'jam_games' in json_data: if 'jam_games' in json_data:
logging.info("Parsing provided file as a Game Jam Entries JSON...") logging.info("Parsing provided file as a Game Jam Entries JSON...")
return get_jobs_for_game_jam_json(json_data) return get_jobs_for_game_jam_json(json_data)
if 'games' in json_data:
logging.info("Parsing provided file as Bundle Games JSON...")
return get_jobs_for_bundle_json(json_data)
except json.JSONDecodeError: except json.JSONDecodeError:
pass # Not a valid JSON, okay... pass # Not a valid JSON, okay...