diff --git a/itch_dl/cli.py b/itch_dl/cli.py index 335dab3..52cb81f 100644 --- a/itch_dl/cli.py +++ b/itch_dl/cli.py @@ -47,6 +47,8 @@ def parse_args() -> argparse.Namespace: help="directory to save results into (default: current working dir)") parser.add_argument("--mirror-web", action="store_true", help="try to fetch assets on game sites") + parser.add_argument("--refresh-media", action="store_true", + help="force re-download cover images and screenshots (skips game files if already downloaded)") parser.add_argument("--urls-only", action="store_true", help="print scraped game URLs without downloading them") parser.add_argument("--parallel", metavar="parallel", type=int, default=None, diff --git a/itch_dl/config.py b/itch_dl/config.py index 6fdffe5..bfda1d2 100644 --- a/itch_dl/config.py +++ b/itch_dl/config.py @@ -21,6 +21,7 @@ class Settings: download_to: str | None = None mirror_web: bool = False + refresh_media: bool = False urls_only: bool = False parallel: int = 1 diff --git a/itch_dl/downloader.py b/itch_dl/downloader.py index 4a9e67f..0002793 100644 --- a/itch_dl/downloader.py +++ b/itch_dl/downloader.py @@ -259,12 +259,19 @@ class GameDownloader: 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: + # Check if we should skip or only refresh media + game_already_downloaded = os.path.exists(paths["metadata"]) + media_only_mode = game_already_downloaded and self.settings.refresh_media + + if game_already_downloaded and skip_downloaded and not self.settings.refresh_media: # As metadata is the final file we write, all the files # should already be downloaded at this point. logging.info("Skipping already-downloaded game for URL: %s", url) return DownloadResult(url, True, ["Game already downloaded."], []) + if media_only_mode: + logging.info("Refreshing media for already-downloaded game: %s", url) + try: logging.info("Downloading %s", url) r = self.client.get(url, append_api_key=False) @@ -293,82 +300,83 @@ class GameDownloader: external_urls = [] errors = [] - try: - os.makedirs(paths["files"], exist_ok=True) - for upload in game_uploads: - if any(key not in upload for key in ("id", "filename", "type", "traits", "storage")): - errors.append(f"Upload metadata incomplete: {upload}") - continue + if not media_only_mode: + try: + os.makedirs(paths["files"], exist_ok=True) + for upload in game_uploads: + if any(key not in upload for key in ("id", "filename", "type", "traits", "storage")): + errors.append(f"Upload metadata incomplete: {upload}") + continue - logging.debug(upload) - upload_id = upload["id"] - file_name = upload["filename"] - file_type = upload["type"] - file_traits = upload["traits"] - expected_size = upload.get("size") - upload_is_external = upload["storage"] == "external" + logging.debug(upload) + upload_id = upload["id"] + file_name = upload["filename"] + file_type = upload["type"] + file_traits = upload["traits"] + expected_size = upload.get("size") + upload_is_external = upload["storage"] == "external" - if self.settings.filter_files_type and file_type not in self.settings.filter_files_type: - logging.info("File '%s' has ignored type '%s', skipping", file_name, file_type) - continue + if self.settings.filter_files_type and file_type not in self.settings.filter_files_type: + logging.info("File '%s' has ignored type '%s', skipping", file_name, file_type) + continue - if ( - self.settings.filter_files_platform - and file_type == "default" - and not any(trait in self.settings.filter_files_platform for trait in file_traits) - ): - # Setup for filter_files_platform is in config.py, including the trait listing. - logging.info("File '%s' not for requested platforms, skipping", file_name) - continue + if ( + self.settings.filter_files_platform + and file_type == "default" + and not any(trait in self.settings.filter_files_platform for trait in file_traits) + ): + # Setup for filter_files_platform is in config.py, including the trait listing. + logging.info("File '%s' not for requested platforms, skipping", file_name) + continue - if should_skip_item_by_glob("File", file_name, self.settings.filter_files_glob): - continue + if should_skip_item_by_glob("File", file_name, self.settings.filter_files_glob): + continue - if should_skip_item_by_regex("File", file_name, self.settings.filter_files_regex): - continue + if should_skip_item_by_regex("File", file_name, self.settings.filter_files_regex): + continue - logging.debug( - "Downloading '%s' (%d), %s", - file_name, - upload_id, - f"{expected_size} bytes" if expected_size is not None else "unknown size", - ) - - target_path = None if upload_is_external else os.path.join(paths["files"], file_name) - - try: - target_url = self.download_file_by_upload_id(upload_id, target_path, credentials) - except ItchDownloadError as e: - errors.append(f"Download failed for upload {upload}: {e}") - continue - - if upload_is_external: - logging.debug("Found external download URL for %s: %s", title, target_url) - external_urls.append(target_url) - continue - - try: - downloaded_file_stat = os.stat(target_path) - except FileNotFoundError: - errors.append(f"Downloaded file not found for upload {upload}") - continue - - downloaded_size = downloaded_file_stat.st_size - content_size = self.get_decompressed_content_size(target_path) - - if ( - all(x is not None for x in (target_path, expected_size, downloaded_size)) - and downloaded_size != expected_size - and content_size != expected_size - ): - errors.append( - f"Downloaded file size is {downloaded_size} (content {content_size}), " - f"expected {expected_size} for upload {upload}" + logging.debug( + "Downloading '%s' (%d), %s", + file_name, + upload_id, + f"{expected_size} bytes" if expected_size is not None else "unknown size", ) - logging.debug("Done downloading files for %s", title) - except Exception as e: - errors.append(f"Download failed for {title}: {e}") + target_path = None if upload_is_external else os.path.join(paths["files"], file_name) + + try: + target_url = self.download_file_by_upload_id(upload_id, target_path, credentials) + except ItchDownloadError as e: + errors.append(f"Download failed for upload {upload}: {e}") + continue + + if upload_is_external: + logging.debug("Found external download URL for %s: %s", title, target_url) + external_urls.append(target_url) + continue + + try: + downloaded_file_stat = os.stat(target_path) + except FileNotFoundError: + errors.append(f"Downloaded file not found for upload {upload}") + continue + + downloaded_size = downloaded_file_stat.st_size + content_size = self.get_decompressed_content_size(target_path) + + if ( + all(x is not None for x in (target_path, expected_size, downloaded_size)) + and downloaded_size != expected_size + and content_size != expected_size + ): + errors.append( + f"Downloaded file size is {downloaded_size} (content {content_size}), " + f"expected {expected_size} for upload {upload}" + ) + + logging.debug("Done downloading files for %s", title) + except Exception as e: + errors.append(f"Download failed for {title}: {e}") metadata["errors"] = errors metadata["external_downloads"] = external_urls @@ -384,17 +392,34 @@ class GameDownloader: continue file_name = os.path.basename(screenshot) - try: - self.download_file(screenshot, os.path.join(paths["screenshots"], file_name), credentials={}) - except Exception as e: - errors.append(f"Screenshot download failed (this is not fatal): {e}") + screenshot_path = os.path.join(paths["screenshots"], file_name) + + # Check if screenshot already exists and if we should skip or force re-download + screenshot_exists = os.path.exists(screenshot_path) + should_download_screenshot = not screenshot_exists or self.settings.refresh_media + + if should_download_screenshot: + try: + self.download_file(screenshot, screenshot_path, credentials={}) + except Exception as e: + errors.append(f"Screenshot download failed (this is not fatal): {e}") + else: + logging.debug("Screenshot '%s' already exists, skipping download", file_name) cover_url = metadata.get("cover_url") if cover_url: - try: - self.download_file(cover_url, paths["cover"] + os.path.splitext(cover_url)[-1], credentials={}) - except Exception as e: - errors.append(f"Cover art download failed (this is not fatal): {e}") + cover_path = paths["cover"] + os.path.splitext(cover_url)[-1] + # Check if cover already exists and if we should skip or force re-download + cover_exists = os.path.exists(cover_path) + should_download_cover = not cover_exists or self.settings.refresh_media + + if should_download_cover: + try: + self.download_file(cover_url, cover_path, credentials={}) + except Exception as e: + errors.append(f"Cover art download failed (this is not fatal): {e}") + else: + logging.debug("Cover already exists, skipping download") with open(paths["site"], "wb") as f: f.write(site.prettify(encoding="utf-8"))