import click from viu.core.config import AppConfig from viu.core.exceptions import ViuError from viu.libs.media_api.types import ( MediaFormat, MediaGenre, MediaItem, MediaSeason, MediaSort, MediaStatus, MediaTag, MediaType, MediaYear, ) @click.command(help="Queue episodes for the background worker to download.") # Search/Filter options (mirrors 'viu anilist download') @click.option("--title", "-t") @click.option("--page", "-p", type=click.IntRange(min=1), default=1) @click.option("--per-page", type=click.IntRange(min=1, max=50)) @click.option("--season", type=click.Choice([s.value for s in MediaSeason])) @click.option( "--status", "-S", multiple=True, type=click.Choice([s.value for s in MediaStatus]) ) @click.option( "--status-not", multiple=True, type=click.Choice([s.value for s in MediaStatus]) ) @click.option("--sort", "-s", type=click.Choice([s.value for s in MediaSort])) @click.option( "--genres", "-g", multiple=True, type=click.Choice([g.value for g in MediaGenre]) ) @click.option( "--genres-not", multiple=True, type=click.Choice([g.value for g in MediaGenre]) ) @click.option("--tags", "-T", multiple=True, type=click.Choice([t.value for t in MediaTag])) @click.option("--tags-not", multiple=True, type=click.Choice([t.value for t in MediaTag])) @click.option( "--media-format", "-f", multiple=True, type=click.Choice([f.value for f in MediaFormat]), ) @click.option("--media-type", type=click.Choice([t.value for t in MediaType])) @click.option("--year", "-y", type=click.Choice([y.value for y in MediaYear])) @click.option("--popularity-greater", type=click.IntRange(min=0)) @click.option("--popularity-lesser", type=click.IntRange(min=0)) @click.option("--score-greater", type=click.IntRange(min=0, max=100)) @click.option("--score-lesser", type=click.IntRange(min=0, max=100)) @click.option("--start-date-greater", type=int) @click.option("--start-date-lesser", type=int) @click.option("--end-date-greater", type=int) @click.option("--end-date-lesser", type=int) @click.option("--on-list/--not-on-list", "-L/-no-L", type=bool, default=None) # Queue-specific options @click.option( "--episode-range", "-r", required=True, help="Range of episodes to queue (e.g., '1-10', '5', '8:12').", ) @click.option( "--yes", "-Y", is_flag=True, help="Automatically queue from all found anime without prompting for selection.", ) @click.pass_obj def queue(config: AppConfig, **options): """ Search AniList with filters, select one or more anime (or use --yes), and queue the specified episode range for background download. The background worker should be running to process the queue. """ from viu.cli.service.download.service import DownloadService from viu.cli.service.feedback import FeedbackService from viu.cli.service.registry import MediaRegistryService from viu.cli.utils.parser import parse_episode_range from viu.libs.media_api.params import MediaSearchParams from viu.libs.media_api.api import create_api_client from viu.libs.provider.anime.provider import create_provider from viu.libs.selectors import create_selector from rich.progress import Progress feedback = FeedbackService(config) selector = create_selector(config) media_api = create_api_client(config.general.media_api, config) provider = create_provider(config.general.provider) registry = MediaRegistryService(config.general.media_api, config.media_registry) download_service = DownloadService(config, registry, media_api, provider) try: # Build search params mirroring anilist download sort_val = options.get("sort") status_val = options.get("status") status_not_val = options.get("status_not") genres_val = options.get("genres") genres_not_val = options.get("genres_not") tags_val = options.get("tags") tags_not_val = options.get("tags_not") media_format_val = options.get("media_format") media_type_val = options.get("media_type") season_val = options.get("season") year_val = options.get("year") search_params = MediaSearchParams( query=options.get("title"), page=options.get("page", 1), per_page=options.get("per_page"), sort=MediaSort(sort_val) if sort_val else None, status_in=[MediaStatus(s) for s in status_val] if status_val else None, status_not_in=[MediaStatus(s) for s in status_not_val] if status_not_val else None, genre_in=[MediaGenre(g) for g in genres_val] if genres_val else None, genre_not_in=[MediaGenre(g) for g in genres_not_val] if genres_not_val else None, tag_in=[MediaTag(t) for t in tags_val] if tags_val else None, tag_not_in=[MediaTag(t) for t in tags_not_val] if tags_not_val else None, format_in=[MediaFormat(f) for f in media_format_val] if media_format_val else None, type=MediaType(media_type_val) if media_type_val else None, season=MediaSeason(season_val) if season_val else None, seasonYear=int(year_val) if year_val else None, popularity_greater=options.get("popularity_greater"), popularity_lesser=options.get("popularity_lesser"), averageScore_greater=options.get("score_greater"), averageScore_lesser=options.get("score_lesser"), startDate_greater=options.get("start_date_greater"), startDate_lesser=options.get("start_date_lesser"), endDate_greater=options.get("end_date_greater"), endDate_lesser=options.get("end_date_lesser"), on_list=options.get("on_list"), ) with Progress() as progress: progress.add_task("Searching AniList...", total=None) search_result = media_api.search_media(search_params) if not search_result or not search_result.media: raise ViuError("No anime found matching your search criteria.") if options.get("yes"): anime_to_queue = search_result.media else: choice_map: dict[str, MediaItem] = { (item.title.english or item.title.romaji or f"ID: {item.id}"): item for item in search_result.media } preview_command = None if config.general.preview != "none": from ..utils.preview import create_preview_context # type: ignore with create_preview_context() as preview_ctx: preview_command = preview_ctx.get_anime_preview( list(choice_map.values()), list(choice_map.keys()), config, ) selected_titles = selector.choose_multiple( "Select anime to queue", list(choice_map.keys()), preview=preview_command, ) else: selected_titles = selector.choose_multiple( "Select anime to queue", list(choice_map.keys()) ) if not selected_titles: feedback.warning("No anime selected. Nothing queued.") return anime_to_queue = [choice_map[title] for title in selected_titles] episode_range_str = options.get("episode_range") total_queued = 0 for media_item in anime_to_queue: available_episodes = [str(i + 1) for i in range(media_item.episodes or 0)] if not available_episodes: feedback.warning( f"No episode information for '{media_item.title.english}', skipping." ) continue try: episodes_to_queue = list( parse_episode_range(episode_range_str, available_episodes) ) if not episodes_to_queue: feedback.warning( f"Episode range '{episode_range_str}' resulted in no episodes for '{media_item.title.english}'." ) continue queued_count = 0 for ep in episodes_to_queue: if download_service.add_to_queue(media_item, ep): queued_count += 1 total_queued += queued_count feedback.success( f"Queued {queued_count} episodes for '{media_item.title.english}'." ) except (ValueError, IndexError) as e: feedback.error( f"Invalid episode range for '{media_item.title.english}': {e}" ) feedback.success( f"Done. Total of {total_queued} episode(s) queued across all selections." ) except ViuError as e: feedback.error("Queue command failed", str(e)) except Exception as e: feedback.error("An unexpected error occurred", str(e))