From 9e006543c254548a9bb15b6cba21cfabf1777c50 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 19 Jun 2022 13:07:31 +0100 Subject: [PATCH] Add support for downloading bundles --- README.md | 8 ++++---- itch_dl/cli.py | 2 +- itch_dl/handlers.py | 24 ++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 44d8bb8..24c9fef 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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` - 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 input URL can be any "Browse" page (top, popular, newest, filtered by tags, etc) or any -game jam. The input can also be a path to a itch.io JSON file with game jam entries, or just -a list of itch.io game URLs (not browse/jam pages!) to download. +The input URL can be any "Browse" page (top, popular, newest, filtered by tags, etc), any +game jam or any bundle. The input can also be a path to a itch.io JSON file with game jam +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 and it prints a report on failed downloads and external URLs (links to files that are not on diff --git a/itch_dl/cli.py b/itch_dl/cli.py index 140cc76..c528751 100644 --- a/itch_dl/cli.py +++ b/itch_dl/cli.py @@ -15,7 +15,7 @@ logging.getLogger().setLevel(logging.INFO) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Bulk download stuff from Itch.io.") 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, help="itch.io API key - https://itch.io/user/settings/api-keys") parser.add_argument("--profile", metavar="profile", default=None, diff --git a/itch_dl/handlers.py b/itch_dl/handlers.py index e45faa1..c63d41a 100644 --- a/itch_dl/handlers.py +++ b/itch_dl/handlers.py @@ -83,6 +83,21 @@ def get_jobs_for_browse_url(url: str, client: ItchApiClient) -> List[str]: 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]: if url.startswith("http://"): 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) 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... 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]: - try: # Game Jam Entries JSON? + try: # Game Jam Entries JSON or Bundle Games JSON? with open(path, "rb") as 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: logging.info("Parsing provided file as a Game Jam Entries JSON...") 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: pass # Not a valid JSON, okay...