diff --git a/fastanime/cli/cli.py b/fastanime/cli/cli.py index f0f5b83..a3db373 100644 --- a/fastanime/cli/cli.py +++ b/fastanime/cli/cli.py @@ -31,6 +31,7 @@ commands = { "search": "search.search", "anilist": "anilist.anilist", "download": "download.download", + "update": "update.update", } diff --git a/fastanime/cli/commands/update.py b/fastanime/cli/commands/update.py new file mode 100644 index 0000000..125d31b --- /dev/null +++ b/fastanime/cli/commands/update.py @@ -0,0 +1,144 @@ +"""Update command for FastAnime CLI.""" + +import sys +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 + +if TYPE_CHECKING: + from ...core.config import AppConfig + + +@click.command( + help="Update FastAnime to the latest version", + short_help="Update FastAnime", + epilog=""" +\b +\b\bExamples: + # Check for updates and update if available + fastanime update +\b + # Force update even if already up to date + fastanime update --force +\b + # Only check for updates without updating + fastanime update --check-only +\b + # Show release notes for the latest version + fastanime update --release-notes +""", +) +@click.option( + "--force", + "-f", + is_flag=True, + help="Force update even if already up to date", +) +@click.option( + "--check-only", + "-c", + is_flag=True, + help="Only check for updates without updating", +) +@click.option( + "--release-notes", + "-r", + is_flag=True, + help="Show release notes for the latest version", +) +@click.pass_context +@click.pass_obj +def update(config: "AppConfig", ctx: click.Context, force: bool, check_only: bool, release_notes: bool) -> None: + """ + Update FastAnime to the latest version. + + This command checks for available updates and optionally updates + the application to the latest version from the configured sources + (pip, uv, pipx, git, or nix depending on installation method). + + Args: + config: The application configuration object + ctx: The click context containing CLI options + force: Whether to force update even if already up to date + 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 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]FastAnime 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(f"[dim]Run 'fastanime update' to update[/]") + sys.exit(1) + 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.[/]") + sys.exit(1) + + 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 + + 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) diff --git a/fastanime/cli/utils/update.py b/fastanime/cli/utils/update.py index 9738ec1..547f888 100644 --- a/fastanime/cli/utils/update.py +++ b/fastanime/cli/utils/update.py @@ -9,13 +9,13 @@ import sys import requests from rich import print -from ... import APP_NAME, AUTHOR, GIT_REPO, __version__ +from ...core.constants import AUTHOR, GIT_REPO, PROJECT_NAME_LOWER, __version__ -API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{APP_NAME}/releases/latest" +API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{PROJECT_NAME_LOWER}/releases/latest" def check_for_updates(): - USER_AGENT = f"{APP_NAME} user" + USER_AGENT = f"{PROJECT_NAME_LOWER} user" try: request = requests.get( API_URL, @@ -96,9 +96,9 @@ def update_app(force=False): return False, release_json process = subprocess.run( - [NIX, "profile", "upgrade", APP_NAME.lower()], check=False + [NIX, "profile", "upgrade", PROJECT_NAME_LOWER], check=False ) - elif is_git_repo(AUTHOR, APP_NAME): + elif is_git_repo(AUTHOR, PROJECT_NAME_LOWER) : GIT_EXECUTABLE = shutil.which("git") args = [ GIT_EXECUTABLE, @@ -117,9 +117,9 @@ def update_app(force=False): ) elif UV := shutil.which("uv"): - process = subprocess.run([UV, "tool", "upgrade", APP_NAME], check=False) + process = subprocess.run([UV, "tool", "upgrade", PROJECT_NAME_LOWER], check=False) elif PIPX := shutil.which("pipx"): - process = subprocess.run([PIPX, "upgrade", APP_NAME], check=False) + process = subprocess.run([PIPX, "upgrade", PROJECT_NAME_LOWER], check=False) else: PYTHON_EXECUTABLE = sys.executable @@ -128,7 +128,7 @@ def update_app(force=False): "-m", "pip", "install", - APP_NAME, + PROJECT_NAME_LOWER, "-U", "--no-warn-script-location", ] diff --git a/fastanime/core/constants.py b/fastanime/core/constants.py index 4d60d4a..99c9311 100644 --- a/fastanime/core/constants.py +++ b/fastanime/core/constants.py @@ -6,6 +6,7 @@ from pathlib import Path PLATFORM = sys.platform PROJECT_NAME = "FASTANIME" +PROJECT_NAME_LOWER = "fastanime" APP_NAME = os.environ.get(f"{PROJECT_NAME}_APP_NAME", PROJECT_NAME.lower()) USER_NAME = os.environ.get("USERNAME", "User")