From cbc1ceccbbb16248eeea6ef00c4d86e5c1f38ac5 Mon Sep 17 00:00:00 2001 From: Benexl Date: Mon, 18 Aug 2025 02:14:56 +0300 Subject: [PATCH] feat(cli): auto check for updates --- viu_media/cli/cli.py | 43 +++++++++ viu_media/cli/commands/update.py | 121 +++++++++----------------- viu_media/cli/utils/update.py | 21 +++++ viu_media/core/config/defaults.py | 1 + viu_media/core/config/descriptions.py | 1 + viu_media/core/config/model.py | 4 + 6 files changed, 109 insertions(+), 82 deletions(-) diff --git a/viu_media/cli/cli.py b/viu_media/cli/cli.py index 492f51d..7a6c36a 100644 --- a/viu_media/cli/cli.py +++ b/viu_media/cli/cli.py @@ -108,6 +108,49 @@ def cli(ctx: click.Context, **options: "Unpack[Options]"): else loader.load(cli_overrides) ) ctx.obj = config + + if config.general.check_for_updates: + import time + + from ..core.constants import APP_CACHE_DIR + + last_updated_at_file = APP_CACHE_DIR / "last_update" + should_check_for_update = False + if last_updated_at_file.exists(): + try: + last_updated_at_time = float( + last_updated_at_file.read_text(encoding="utf-8") + ) + if ( + time.time() - last_updated_at_time + ) > config.general.update_check_interval * 3600: + should_check_for_update = True + + except Exception as e: + logger.warning(f"Failed to check for update: {e}") + + else: + should_check_for_update = True + if should_check_for_update: + last_updated_at_file.write_text(str(time.time()), encoding="utf-8") + from .service.feedback import FeedbackService + from .utils.update import check_for_updates, print_release_json, update_app + + feedback = FeedbackService(config) + feedback.info("Checking for updates...") + is_latest, release_json = check_for_updates() + if not is_latest: + from ..libs.selectors.selector import create_selector + + selector = create_selector(config) + if release_json and selector.confirm( + "Theres an update available would you like to see the release notes before deciding to update?" + ): + print_release_json(release_json) + selector.ask("Enter to continue...") + if selector.confirm("Would you like to update?"): + update_app() + if ctx.invoked_subcommand is None: from .commands.anilist import cmd diff --git a/viu_media/cli/commands/update.py b/viu_media/cli/commands/update.py index 658ef96..cfb768c 100644 --- a/viu_media/cli/commands/update.py +++ b/viu_media/cli/commands/update.py @@ -5,10 +5,8 @@ from typing import TYPE_CHECKING import click from rich import print -from rich.console import Console -from rich.markdown import Markdown -from ..utils.update import check_for_updates, update_app +from ..utils.update import check_for_updates, print_release_json, update_app if TYPE_CHECKING: from ...core.config import AppConfig @@ -74,87 +72,46 @@ def update( check_only: Whether to only check for updates without updating release_notes: Whether to show release notes for the latest version """ - try: - if release_notes: - print("[cyan]Fetching latest release notes...[/]") - is_latest, release_json = check_for_updates() + if release_notes: + print("[cyan]Fetching latest release notes...[/]") + is_latest, release_json = check_for_updates() - if not release_json: - print( - "[yellow]Could not fetch release information. Please check your internet connection.[/]" - ) - sys.exit(1) - - version = release_json.get("tag_name", "unknown") - release_name = release_json.get("name", version) - release_body = release_json.get("body", "No release notes available.") - published_at = release_json.get("published_at", "unknown") - - console = Console() - - print(f"[bold cyan]Release: {release_name}[/]") - print(f"[dim]Version: {version}[/]") - print(f"[dim]Published: {published_at}[/]") - print() - - # Display release notes as markdown if available - if release_body.strip(): - markdown = Markdown(release_body) - console.print(markdown) - else: - print("[dim]No release notes available for this version.[/]") - - return - - elif check_only: - print("[cyan]Checking for updates...[/]") - is_latest, release_json = check_for_updates() - - if not release_json: - print( - "[yellow]Could not check for updates. Please check your internet connection.[/]" - ) - sys.exit(1) - - if is_latest: - print("[green]Viu is up to date![/]") - print( - f"[dim]Current version: {release_json.get('tag_name', 'unknown')}[/]" - ) - else: - latest_version = release_json.get("tag_name", "unknown") - print(f"[yellow]Update available: {latest_version}[/]") - print("[dim]Run 'viu update' to update[/]") - sys.exit(1) + if not release_json: + print( + "[yellow]Could not fetch release information. Please check your internet connection.[/]" + ) else: - print("[cyan]Checking for updates and updating if necessary...[/]") - success, release_json = update_app(force=force) + print_release_json(release_json) - if not release_json: - print( - "[red]Could not check for updates. Please check your internet connection.[/]" - ) - sys.exit(1) + return - if success: - latest_version = release_json.get("tag_name", "unknown") - print(f"[green]Successfully updated to version {latest_version}![/]") - else: - if force: - print( - "[red]Update failed. Please check the error messages above.[/]" - ) - sys.exit(1) - # If not forced and update failed, it might be because already up to date - # The update_app function already prints appropriate messages + elif check_only: + print("[cyan]Checking for updates...[/]") + is_latest, release_json = check_for_updates() - except KeyboardInterrupt: - print("\n[yellow]Update cancelled by user.[/]") - sys.exit(1) - except Exception as e: - print(f"[red]An error occurred during update: {e}[/]") - # Get trace option from parent context - trace = ctx.parent.params.get("trace", False) if ctx.parent else False - if trace: - raise - sys.exit(1) + if not release_json: + print( + "[yellow]Could not check for updates. Please check your internet connection.[/]" + ) + + if is_latest: + print("[green]Viu is up to date![/]") + print(f"[dim]Current version: {release_json.get('tag_name', 'unknown')}[/]") + else: + latest_version = release_json.get("tag_name", "unknown") + print(f"[yellow]Update available: {latest_version}[/]") + print("[dim]Run 'viu update' to update[/]") + else: + print("[cyan]Checking for updates and updating if necessary...[/]") + success, release_json = update_app(force=force) + + if not release_json: + print( + "[red]Could not check for updates. Please check your internet connection.[/]" + ) + if success: + latest_version = release_json.get("tag_name", "unknown") + print(f"[green]Successfully updated to version {latest_version}![/]") + else: + if force: + print("[red]Update failed. Please check the error messages above.[/]") diff --git a/viu_media/cli/utils/update.py b/viu_media/cli/utils/update.py index 158deb2..7bc991e 100644 --- a/viu_media/cli/utils/update.py +++ b/viu_media/cli/utils/update.py @@ -8,6 +8,8 @@ import sys from httpx import get from rich import print +from rich.console import Console +from rich.markdown import Markdown from ...core.constants import ( AUTHOR, @@ -20,6 +22,25 @@ from ...core.constants import ( API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{CLI_NAME_LOWER}/releases/latest" +def print_release_json(release_json): + version = release_json.get("tag_name", "unknown") + release_name = release_json.get("name", version) + release_body = release_json.get("body", "No release notes available.") + published_at = release_json.get("published_at", "unknown") + + console = Console() + + print(f"[bold cyan]Release: {release_name}[/]") + print(f"[dim]Version: {version}[/]") + print(f"[dim]Published: {published_at}[/]") + print() + + # Display release notes as markdown if available + if release_body and release_body.strip(): + markdown = Markdown(release_body) + console.print(markdown) + + def check_for_updates(): USER_AGENT = f"{CLI_NAME_LOWER} user" try: diff --git a/viu_media/core/config/defaults.py b/viu_media/core/config/defaults.py index 7a3b917..b057f45 100644 --- a/viu_media/core/config/defaults.py +++ b/viu_media/core/config/defaults.py @@ -32,6 +32,7 @@ def GENERAL_IMAGE_RENDERER(): GENERAL_MANGA_VIEWER = "feh" GENERAL_CHECK_FOR_UPDATES = True +GENERAL_UPDATE_CHECK_INTERVAL = 12 GENERAL_CACHE_REQUESTS = True GENERAL_MAX_CACHE_LIFETIME = "03:00:00" GENERAL_NORMALIZE_TITLES = True diff --git a/viu_media/core/config/descriptions.py b/viu_media/core/config/descriptions.py index fdb8114..9801f0e 100644 --- a/viu_media/core/config/descriptions.py +++ b/viu_media/core/config/descriptions.py @@ -24,6 +24,7 @@ GENERAL_IMAGE_RENDERER = ( ) GENERAL_MANGA_VIEWER = "The external application to use for viewing manga pages." GENERAL_CHECK_FOR_UPDATES = "Automatically check for new versions of Viu on startup." +GENERAL_UPDATE_CHECK_INTERVAL = "The interval in hours to check for updates" GENERAL_CACHE_REQUESTS = ( "Enable caching of network requests to speed up subsequent operations." ) diff --git a/viu_media/core/config/model.py b/viu_media/core/config/model.py index ecab9ac..d2da931 100644 --- a/viu_media/core/config/model.py +++ b/viu_media/core/config/model.py @@ -190,6 +190,10 @@ class GeneralConfig(BaseModel): default=defaults.GENERAL_CHECK_FOR_UPDATES, description=desc.GENERAL_CHECK_FOR_UPDATES, ) + update_check_interval: float = Field( + default=defaults.GENERAL_UPDATE_CHECK_INTERVAL, + description=desc.GENERAL_UPDATE_CHECK_INTERVAL, + ) cache_requests: bool = Field( default=defaults.GENERAL_CACHE_REQUESTS, description=desc.GENERAL_CACHE_REQUESTS,