mirror of
https://github.com/Benexl/FastAnime.git
synced 2026-01-06 17:53:40 -08:00
Add AniList download command and download service integration
- Implemented a new command for downloading anime episodes using the AniList API. - Created a DownloadService to manage episode downloads and track their status in the media registry. - Added comprehensive command-line options for filtering and selecting anime based on various criteria. - Integrated feedback mechanisms to inform users about download progress and issues. - Established validation for command options to ensure correct input. - Enhanced error handling and logging for better debugging and user experience. - Included functionality for managing multiple downloads concurrently.
This commit is contained in:
836
fastanime/cli/commands/anilist/commands/download.py
Normal file
836
fastanime/cli/commands/anilist/commands/download.py
Normal file
@@ -0,0 +1,836 @@
|
||||
"""AniList download command using the modern download service."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from .....core.config import AppConfig
|
||||
from .....core.exceptions import FastAnimeError
|
||||
from .....libs.media_api.api import create_api_client
|
||||
from .....libs.media_api.params import MediaSearchParams
|
||||
from .....libs.media_api.types import (
|
||||
MediaFormat,
|
||||
MediaGenre,
|
||||
MediaSeason,
|
||||
MediaSort,
|
||||
MediaStatus,
|
||||
MediaTag,
|
||||
MediaType,
|
||||
MediaYear,
|
||||
)
|
||||
from .....libs.provider.anime.provider import create_provider
|
||||
from .....libs.provider.anime.params import SearchParams, AnimeParams
|
||||
from .....libs.selectors import create_selector
|
||||
from ....service.download import DownloadService
|
||||
from ....service.feedback import FeedbackService
|
||||
from ....service.registry import MediaRegistryService
|
||||
from ....utils.completion import anime_titles_shell_complete
|
||||
from ....utils import parse_episode_range
|
||||
from .. import examples
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
from typing_extensions import Unpack
|
||||
|
||||
class DownloadOptions(TypedDict, total=False):
|
||||
title: str | None
|
||||
episode_range: str | None
|
||||
quality: str | None
|
||||
force_redownload: bool
|
||||
page: int
|
||||
per_page: int | None
|
||||
season: str | None
|
||||
status: tuple[str, ...]
|
||||
status_not: tuple[str, ...]
|
||||
sort: str | None
|
||||
genres: tuple[str, ...]
|
||||
genres_not: tuple[str, ...]
|
||||
tags: tuple[str, ...]
|
||||
tags_not: tuple[str, ...]
|
||||
media_format: tuple[str, ...]
|
||||
media_type: str | None
|
||||
year: str | None
|
||||
popularity_greater: int | None
|
||||
popularity_lesser: int | None
|
||||
score_greater: int | None
|
||||
score_lesser: int | None
|
||||
start_date_greater: int | None
|
||||
start_date_lesser: int | None
|
||||
end_date_greater: int | None
|
||||
end_date_lesser: int | None
|
||||
on_list: bool | None
|
||||
max_concurrent: int | None
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Download anime episodes using AniList API for search and provider integration",
|
||||
short_help="Download anime episodes",
|
||||
epilog=examples.download,
|
||||
)
|
||||
@click.option(
|
||||
"--title",
|
||||
"-t",
|
||||
shell_complete=anime_titles_shell_complete,
|
||||
help="Title of the anime to search for"
|
||||
)
|
||||
@click.option(
|
||||
"--episode-range",
|
||||
"-r",
|
||||
help="Range of episodes to download (e.g., '1:5', '3:', ':5', '1:10:2')",
|
||||
)
|
||||
@click.option(
|
||||
"--quality",
|
||||
"-q",
|
||||
type=click.Choice(["360", "480", "720", "1080", "best"]),
|
||||
help="Preferred download quality",
|
||||
)
|
||||
@click.option(
|
||||
"--force-redownload",
|
||||
"-f",
|
||||
is_flag=True,
|
||||
help="Force redownload even if episode already exists",
|
||||
)
|
||||
@click.option(
|
||||
"--page",
|
||||
"-p",
|
||||
type=click.IntRange(min=1),
|
||||
default=1,
|
||||
help="Page number for search pagination",
|
||||
)
|
||||
@click.option(
|
||||
"--per-page",
|
||||
type=click.IntRange(min=1, max=50),
|
||||
help="Number of results per page (max 50)",
|
||||
)
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice([season.value for season in MediaSeason]),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice([status.value for status in MediaStatus]),
|
||||
)
|
||||
@click.option(
|
||||
"--status-not",
|
||||
help="Exclude media with these statuses",
|
||||
multiple=True,
|
||||
type=click.Choice([status.value for status in MediaStatus]),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice([sort.value for sort in MediaSort]),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice([genre.value for genre in MediaGenre]),
|
||||
)
|
||||
@click.option(
|
||||
"--genres-not",
|
||||
multiple=True,
|
||||
help="Exclude these genres",
|
||||
type=click.Choice([genre.value for genre in MediaGenre]),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
"-T",
|
||||
multiple=True,
|
||||
help="the tags to filter by",
|
||||
type=click.Choice([tag.value for tag in MediaTag]),
|
||||
)
|
||||
@click.option(
|
||||
"--tags-not",
|
||||
multiple=True,
|
||||
help="Exclude these tags",
|
||||
type=click.Choice([tag.value for tag in MediaTag]),
|
||||
)
|
||||
@click.option(
|
||||
"--media-format",
|
||||
"-F",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice([format.value for format in MediaFormat]),
|
||||
)
|
||||
@click.option(
|
||||
"--media-type",
|
||||
help="Media type (ANIME or MANGA)",
|
||||
type=click.Choice([media_type.value for media_type in MediaType]),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice([year.value for year in MediaYear]),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
"--popularity-greater",
|
||||
type=click.IntRange(min=0),
|
||||
help="Minimum popularity score",
|
||||
)
|
||||
@click.option(
|
||||
"--popularity-lesser",
|
||||
type=click.IntRange(min=0),
|
||||
help="Maximum popularity score",
|
||||
)
|
||||
@click.option(
|
||||
"--score-greater",
|
||||
type=click.IntRange(min=0, max=100),
|
||||
help="Minimum average score (0-100)",
|
||||
)
|
||||
@click.option(
|
||||
"--score-lesser",
|
||||
type=click.IntRange(min=0, max=100),
|
||||
help="Maximum average score (0-100)",
|
||||
)
|
||||
@click.option(
|
||||
"--start-date-greater",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Minimum start date (YYYYMMDD format, e.g., 20240101)",
|
||||
)
|
||||
@click.option(
|
||||
"--start-date-lesser",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Maximum start date (YYYYMMDD format, e.g., 20241231)",
|
||||
)
|
||||
@click.option(
|
||||
"--end-date-greater",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Minimum end date (YYYYMMDD format, e.g., 20240101)",
|
||||
)
|
||||
@click.option(
|
||||
"--end-date-lesser",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Maximum end date (YYYYMMDD format, e.g., 20241231)",
|
||||
)
|
||||
@click.option(
|
||||
"--on-list/--not-on-list",
|
||||
"-L/-no-L",
|
||||
help="Whether the anime should be in your list or not",
|
||||
type=bool,
|
||||
)
|
||||
@click.option(
|
||||
"--max-concurrent",
|
||||
"-c",
|
||||
type=click.IntRange(min=1, max=10),
|
||||
help="Maximum number of concurrent downloads",
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
|
||||
"""Download anime episodes using AniList search and provider integration."""
|
||||
feedback = FeedbackService(config.general.icons)
|
||||
|
||||
try:
|
||||
# Extract and validate options
|
||||
title = options.get("title")
|
||||
episode_range = options.get("episode_range")
|
||||
quality = options.get("quality")
|
||||
force_redownload = options.get("force_redownload", False)
|
||||
max_concurrent = options.get("max_concurrent", config.downloads.max_concurrent)
|
||||
|
||||
_validate_options(options)
|
||||
|
||||
# Initialize services
|
||||
feedback.info("Initializing services...")
|
||||
api_client, provider, selector, media_registry, download_service = _initialize_services(config)
|
||||
feedback.info(f"Using provider: {provider.__class__.__name__}")
|
||||
feedback.info(f"Using media API: {config.general.media_api}")
|
||||
feedback.info(f"Translation type: {config.stream.translation_type}")
|
||||
|
||||
# Search for anime
|
||||
search_params = _build_search_params(options, config)
|
||||
search_result = _search_anime(api_client, search_params, feedback)
|
||||
|
||||
# Let user select anime (single or multiple)
|
||||
selected_anime_list = _select_anime(search_result, selector, feedback)
|
||||
if not selected_anime_list:
|
||||
feedback.info("No anime selected. Exiting.")
|
||||
return
|
||||
|
||||
# Process each selected anime
|
||||
for selected_anime in selected_anime_list:
|
||||
feedback.info(f"Processing: {selected_anime.title.english or selected_anime.title.romaji}")
|
||||
feedback.info(f"AniList ID: {selected_anime.id}")
|
||||
|
||||
# Get available episodes from provider
|
||||
episodes_result = _get_available_episodes(provider, selected_anime, config, feedback)
|
||||
if not episodes_result:
|
||||
feedback.warning(f"No episodes found for {selected_anime.title.english or selected_anime.title.romaji}")
|
||||
_suggest_alternatives(selected_anime, provider, config, feedback)
|
||||
continue
|
||||
|
||||
# Unpack the result
|
||||
if len(episodes_result) == 2:
|
||||
available_episodes, provider_anime_data = episodes_result
|
||||
else:
|
||||
# Fallback for backwards compatibility
|
||||
available_episodes = episodes_result
|
||||
provider_anime_data = None
|
||||
|
||||
# Determine episodes to download
|
||||
episodes_to_download = _determine_episodes_to_download(
|
||||
episode_range, available_episodes, selector, feedback
|
||||
)
|
||||
if not episodes_to_download:
|
||||
feedback.warning("No episodes selected for download")
|
||||
continue
|
||||
|
||||
feedback.info(f"About to download {len(episodes_to_download)} episodes: {', '.join(episodes_to_download)}")
|
||||
|
||||
# Test stream availability before attempting download (using provider anime data)
|
||||
if episodes_to_download and provider_anime_data:
|
||||
test_episode = episodes_to_download[0]
|
||||
feedback.info(f"Testing stream availability for episode {test_episode}...")
|
||||
success = _test_episode_stream_availability(provider, provider_anime_data, test_episode, config, feedback)
|
||||
if not success:
|
||||
feedback.warning(f"Stream test failed for episode {test_episode}.")
|
||||
feedback.info("Possible solutions:")
|
||||
feedback.info("1. Try a different provider (check your config)")
|
||||
feedback.info("2. Check if the episode number is correct")
|
||||
feedback.info("3. Try a different translation type (sub/dub)")
|
||||
feedback.info("4. The anime might not be available on this provider")
|
||||
|
||||
# Ask user if they want to continue anyway
|
||||
continue_anyway = input("\nContinue with download anyway? (y/N): ").strip().lower()
|
||||
if continue_anyway not in ['y', 'yes']:
|
||||
feedback.info("Download cancelled by user")
|
||||
continue
|
||||
|
||||
# Download episodes (using provider anime data if available, otherwise AniList data)
|
||||
anime_for_download = provider_anime_data if provider_anime_data else selected_anime
|
||||
_download_episodes(
|
||||
download_service, anime_for_download, episodes_to_download,
|
||||
quality, force_redownload, max_concurrent, feedback
|
||||
)
|
||||
|
||||
# Show final statistics
|
||||
_show_final_statistics(download_service, feedback)
|
||||
|
||||
except FastAnimeError as e:
|
||||
feedback.error("Download failed", str(e))
|
||||
raise click.Abort()
|
||||
except Exception as e:
|
||||
feedback.error("Unexpected error occurred", str(e))
|
||||
raise click.Abort()
|
||||
|
||||
|
||||
def _validate_options(options: "DownloadOptions") -> None:
|
||||
"""Validate command line options."""
|
||||
score_greater = options.get("score_greater")
|
||||
score_lesser = options.get("score_lesser")
|
||||
popularity_greater = options.get("popularity_greater")
|
||||
popularity_lesser = options.get("popularity_lesser")
|
||||
start_date_greater = options.get("start_date_greater")
|
||||
start_date_lesser = options.get("start_date_lesser")
|
||||
end_date_greater = options.get("end_date_greater")
|
||||
end_date_lesser = options.get("end_date_lesser")
|
||||
|
||||
# Score validation
|
||||
if score_greater is not None and score_lesser is not None and score_greater > score_lesser:
|
||||
raise FastAnimeError("Minimum score cannot be higher than maximum score")
|
||||
|
||||
# Popularity validation
|
||||
if popularity_greater is not None and popularity_lesser is not None and popularity_greater > popularity_lesser:
|
||||
raise FastAnimeError("Minimum popularity cannot be higher than maximum popularity")
|
||||
|
||||
# Date validation
|
||||
if start_date_greater is not None and start_date_lesser is not None and start_date_greater > start_date_lesser:
|
||||
raise FastAnimeError("Minimum start date cannot be after maximum start date")
|
||||
|
||||
if end_date_greater is not None and end_date_lesser is not None and end_date_greater > end_date_lesser:
|
||||
raise FastAnimeError("Minimum end date cannot be after maximum end date")
|
||||
|
||||
|
||||
def _initialize_services(config: AppConfig) -> tuple:
|
||||
"""Initialize all required services."""
|
||||
api_client = create_api_client(config.general.media_api, config)
|
||||
provider = create_provider(config.general.provider)
|
||||
selector = create_selector(config)
|
||||
media_registry = MediaRegistryService(config.general.media_api, config.media_registry)
|
||||
download_service = DownloadService(config, media_registry, provider)
|
||||
|
||||
return api_client, provider, selector, media_registry, download_service
|
||||
|
||||
|
||||
def _build_search_params(options: "DownloadOptions", config: AppConfig) -> MediaSearchParams:
|
||||
"""Build MediaSearchParams from command options."""
|
||||
return MediaSearchParams(
|
||||
query=options.get("title"),
|
||||
page=options.get("page", 1),
|
||||
per_page=options.get("per_page") or config.anilist.per_page or 50,
|
||||
sort=MediaSort(options.get("sort")) if options.get("sort") else None,
|
||||
status_in=[MediaStatus(s) for s in options.get("status", ())] if options.get("status") else None,
|
||||
status_not_in=[MediaStatus(s) for s in options.get("status_not", ())] if options.get("status_not") else None,
|
||||
genre_in=[MediaGenre(g) for g in options.get("genres", ())] if options.get("genres") else None,
|
||||
genre_not_in=[MediaGenre(g) for g in options.get("genres_not", ())] if options.get("genres_not") else None,
|
||||
tag_in=[MediaTag(t) for t in options.get("tags", ())] if options.get("tags") else None,
|
||||
tag_not_in=[MediaTag(t) for t in options.get("tags_not", ())] if options.get("tags_not") else None,
|
||||
format_in=[MediaFormat(f) for f in options.get("media_format", ())] if options.get("media_format") else None,
|
||||
type=MediaType(options.get("media_type")) if options.get("media_type") else None,
|
||||
season=MediaSeason(options.get("season")) if options.get("season") else None,
|
||||
seasonYear=int(year) if (year := options.get("year")) 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"),
|
||||
)
|
||||
|
||||
|
||||
def _search_anime(api_client, search_params, feedback):
|
||||
"""Search for anime using the API client."""
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
|
||||
# Check if we have any search criteria at all
|
||||
has_criteria = any([
|
||||
search_params.query,
|
||||
search_params.genre_in,
|
||||
search_params.tag_in,
|
||||
search_params.status_in,
|
||||
search_params.season,
|
||||
search_params.seasonYear,
|
||||
search_params.format_in,
|
||||
search_params.popularity_greater,
|
||||
search_params.averageScore_greater,
|
||||
])
|
||||
|
||||
if not has_criteria:
|
||||
raise FastAnimeError("Please provide at least one search criterion (title, genre, tag, status, etc.)")
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
transient=True,
|
||||
) as progress:
|
||||
progress.add_task("Searching for anime...", total=None)
|
||||
search_result = api_client.search_media(search_params)
|
||||
|
||||
if not search_result or not search_result.media:
|
||||
raise FastAnimeError("No anime found matching your search criteria")
|
||||
|
||||
return search_result
|
||||
|
||||
|
||||
def _select_anime(search_result, selector, feedback):
|
||||
"""Let user select anime from search results."""
|
||||
if len(search_result.media) == 1:
|
||||
selected_anime = search_result.media[0]
|
||||
feedback.info(f"Auto-selected: {selected_anime.title.english or selected_anime.title.romaji}")
|
||||
return [selected_anime]
|
||||
|
||||
# Create choice strings with additional info
|
||||
choices = []
|
||||
for i, anime in enumerate(search_result.media, 1):
|
||||
title = anime.title.english or anime.title.romaji or "Unknown"
|
||||
year = str(anime.start_date.year) if anime.start_date else "N/A"
|
||||
score = f"{anime.average_score}%" if anime.average_score else "N/A"
|
||||
status = anime.status.value if anime.status else "N/A"
|
||||
choices.append(f"{i:2d}. {title} ({year}) [Score: {score}, Status: {status}]")
|
||||
|
||||
# Use multi-selection
|
||||
selected_choices = selector.choose_multiple(
|
||||
prompt="Select anime to download",
|
||||
choices=choices,
|
||||
header="Use TAB to select multiple anime, ENTER to confirm",
|
||||
)
|
||||
|
||||
if not selected_choices:
|
||||
return []
|
||||
|
||||
# Extract anime objects from selections
|
||||
selected_anime_list = []
|
||||
for choice in selected_choices:
|
||||
# Extract index from choice string (format: "XX. Title...")
|
||||
try:
|
||||
index = int(choice.split(".")[0].strip()) - 1
|
||||
selected_anime_list.append(search_result.media[index])
|
||||
except (ValueError, IndexError):
|
||||
feedback.error(f"Invalid selection: {choice}")
|
||||
continue
|
||||
|
||||
return selected_anime_list
|
||||
|
||||
|
||||
def _get_available_episodes(provider, anime, config, feedback):
|
||||
"""Get available episodes from provider."""
|
||||
try:
|
||||
# Search for anime in provider first
|
||||
media_title = anime.title.english or anime.title.romaji
|
||||
feedback.info(f"Searching provider '{provider.__class__.__name__}' for: '{media_title}'")
|
||||
feedback.info(f"Using translation type: '{config.stream.translation_type}'")
|
||||
|
||||
provider_search_results = provider.search(
|
||||
SearchParams(query=media_title, translation_type=config.stream.translation_type)
|
||||
)
|
||||
|
||||
if not provider_search_results or not provider_search_results.results:
|
||||
feedback.warning(f"Could not find '{media_title}' on provider '{provider.__class__.__name__}'")
|
||||
return []
|
||||
|
||||
feedback.info(f"Found {len(provider_search_results.results)} results on provider")
|
||||
|
||||
# Show the first few results for debugging
|
||||
for i, result in enumerate(provider_search_results.results[:3]):
|
||||
feedback.info(f"Result {i+1}: ID={result.id}, Title='{getattr(result, 'title', 'Unknown')}'")
|
||||
|
||||
# Get the first result (could be enhanced with fuzzy matching)
|
||||
first_result = provider_search_results.results[0]
|
||||
feedback.info(f"Using first result: ID={first_result.id}")
|
||||
|
||||
# Now get the full anime data using the PROVIDER'S ID, not AniList ID
|
||||
provider_anime_data = provider.get(
|
||||
AnimeParams(id=first_result.id, query=media_title)
|
||||
)
|
||||
|
||||
if not provider_anime_data:
|
||||
feedback.warning(f"Failed to get anime details from provider")
|
||||
return []
|
||||
|
||||
# Check all available translation types
|
||||
translation_types = ['sub', 'dub']
|
||||
for trans_type in translation_types:
|
||||
episodes = getattr(provider_anime_data.episodes, trans_type, [])
|
||||
feedback.info(f"Translation '{trans_type}': {len(episodes)} episodes available")
|
||||
|
||||
available_episodes = getattr(
|
||||
provider_anime_data.episodes, config.stream.translation_type, []
|
||||
)
|
||||
|
||||
if not available_episodes:
|
||||
feedback.warning(f"No '{config.stream.translation_type}' episodes found")
|
||||
# Suggest alternative translation type if available
|
||||
for trans_type in translation_types:
|
||||
if trans_type != config.stream.translation_type:
|
||||
other_episodes = getattr(provider_anime_data.episodes, trans_type, [])
|
||||
if other_episodes:
|
||||
feedback.info(f"Suggestion: Try using translation type '{trans_type}' (has {len(other_episodes)} episodes)")
|
||||
return []
|
||||
|
||||
feedback.info(f"Found {len(available_episodes)} episodes available for download")
|
||||
|
||||
# Return both episodes and the provider anime data for later use
|
||||
return available_episodes, provider_anime_data
|
||||
|
||||
except Exception as e:
|
||||
feedback.error(f"Error getting episodes from provider: {e}")
|
||||
import traceback
|
||||
feedback.error("Full traceback", traceback.format_exc())
|
||||
return []
|
||||
|
||||
|
||||
def _determine_episodes_to_download(episode_range, available_episodes, selector, feedback):
|
||||
"""Determine which episodes to download based on range or user selection."""
|
||||
if not available_episodes:
|
||||
feedback.warning("No episodes available to download")
|
||||
return []
|
||||
|
||||
if episode_range:
|
||||
try:
|
||||
episodes_to_download = list(parse_episode_range(episode_range, available_episodes))
|
||||
feedback.info(f"Episodes from range '{episode_range}': {', '.join(episodes_to_download)}")
|
||||
return episodes_to_download
|
||||
except (ValueError, IndexError) as e:
|
||||
feedback.error(f"Invalid episode range '{episode_range}': {e}")
|
||||
return []
|
||||
else:
|
||||
# Let user select episodes
|
||||
selected_episodes = selector.choose_multiple(
|
||||
prompt="Select episodes to download",
|
||||
choices=available_episodes,
|
||||
header="Use TAB to select multiple episodes, ENTER to confirm",
|
||||
)
|
||||
|
||||
if selected_episodes:
|
||||
feedback.info(f"Selected episodes: {', '.join(selected_episodes)}")
|
||||
|
||||
return selected_episodes
|
||||
|
||||
|
||||
def _suggest_alternatives(anime, provider, config, feedback):
|
||||
"""Suggest alternatives when episodes are not found."""
|
||||
feedback.info("Troubleshooting suggestions:")
|
||||
feedback.info(f"1. Current provider: {provider.__class__.__name__}")
|
||||
feedback.info(f"2. AniList ID being used: {anime.id}")
|
||||
feedback.info(f"3. Translation type: {config.stream.translation_type}")
|
||||
|
||||
# Special message for AllAnime provider
|
||||
if provider.__class__.__name__ == "AllAnimeProvider":
|
||||
feedback.info("4. AllAnime ID mismatch: AllAnime uses different IDs than AniList")
|
||||
feedback.info(" The provider searches by title, but episodes use AniList ID")
|
||||
feedback.info(" This can cause episodes to not be found even if the anime exists")
|
||||
|
||||
# Check if provider has different ID mapping
|
||||
anime_titles = []
|
||||
if anime.title.english:
|
||||
anime_titles.append(anime.title.english)
|
||||
if anime.title.romaji:
|
||||
anime_titles.append(anime.title.romaji)
|
||||
if anime.title.native:
|
||||
anime_titles.append(anime.title.native)
|
||||
|
||||
feedback.info(f"5. Available titles: {', '.join(anime_titles)}")
|
||||
feedback.info("6. Possible solutions:")
|
||||
feedback.info(" - Try a different provider (GogoAnime, 9anime, etc.)")
|
||||
feedback.info(" - Check provider configuration")
|
||||
feedback.info(" - Try different translation type (sub/dub)")
|
||||
feedback.info(" - Manual search on the provider website")
|
||||
feedback.info(" - Check if anime is available in your region")
|
||||
|
||||
|
||||
def _download_episodes(download_service, anime, episodes, quality, force_redownload, max_concurrent, feedback):
|
||||
"""Download the specified episodes."""
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from rich.console import Console
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
MofNCompleteColumn,
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TaskProgressColumn,
|
||||
TextColumn,
|
||||
TimeElapsedColumn,
|
||||
)
|
||||
import logging
|
||||
|
||||
console = Console()
|
||||
anime_title = anime.title.english or anime.title.romaji
|
||||
|
||||
console.print(f"\n[bold green]Starting downloads for: {anime_title}[/bold green]")
|
||||
|
||||
# Set up logging capture to get download errors
|
||||
log_messages = []
|
||||
class ListHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
log_messages.append(self.format(record))
|
||||
|
||||
handler = ListHandler()
|
||||
handler.setLevel(logging.ERROR)
|
||||
logger = logging.getLogger('fastanime')
|
||||
logger.addHandler(handler)
|
||||
|
||||
try:
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
MofNCompleteColumn(),
|
||||
TaskProgressColumn(),
|
||||
TimeElapsedColumn(),
|
||||
) as progress:
|
||||
|
||||
task = progress.add_task("Downloading episodes...", total=len(episodes))
|
||||
|
||||
if max_concurrent == 1:
|
||||
# Sequential downloads
|
||||
results = {}
|
||||
for episode in episodes:
|
||||
progress.update(task, description=f"Downloading episode {episode}...")
|
||||
|
||||
# Clear previous log messages for this episode
|
||||
log_messages.clear()
|
||||
|
||||
try:
|
||||
success = download_service.download_episode(
|
||||
media_item=anime,
|
||||
episode_number=episode,
|
||||
quality=quality,
|
||||
force_redownload=force_redownload,
|
||||
)
|
||||
results[episode] = success
|
||||
|
||||
if not success:
|
||||
# Try to get more detailed error from registry
|
||||
error_msg = _get_episode_error_details(download_service, anime, episode)
|
||||
if error_msg:
|
||||
feedback.error(f"Episode {episode}", error_msg)
|
||||
elif log_messages:
|
||||
# Show any log messages that were captured
|
||||
for msg in log_messages[-3:]: # Show last 3 error messages
|
||||
feedback.error(f"Episode {episode}", msg)
|
||||
else:
|
||||
feedback.error(f"Episode {episode}", "Download failed - check logs for details")
|
||||
|
||||
except Exception as e:
|
||||
results[episode] = False
|
||||
feedback.error(f"Episode {episode} failed", str(e))
|
||||
progress.advance(task)
|
||||
else:
|
||||
# Concurrent downloads
|
||||
results = {}
|
||||
with ThreadPoolExecutor(max_workers=max_concurrent) as executor:
|
||||
# Submit all download tasks
|
||||
future_to_episode = {
|
||||
executor.submit(
|
||||
download_service.download_episode,
|
||||
media_item=anime,
|
||||
episode_number=episode,
|
||||
server=None,
|
||||
quality=quality,
|
||||
force_redownload=force_redownload,
|
||||
): episode
|
||||
for episode in episodes
|
||||
}
|
||||
|
||||
# Process completed downloads
|
||||
for future in as_completed(future_to_episode):
|
||||
episode = future_to_episode[future]
|
||||
try:
|
||||
success = future.result()
|
||||
results[episode] = success
|
||||
if not success:
|
||||
# Try to get more detailed error from registry
|
||||
error_msg = _get_episode_error_details(download_service, anime, episode)
|
||||
if error_msg:
|
||||
feedback.error(f"Episode {episode}", error_msg)
|
||||
else:
|
||||
feedback.error(f"Episode {episode}", "Download failed - check logs for details")
|
||||
except Exception as e:
|
||||
results[episode] = False
|
||||
feedback.error(f"Download failed for episode {episode}", str(e))
|
||||
|
||||
progress.advance(task)
|
||||
finally:
|
||||
# Remove the log handler
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# Display results
|
||||
_display_download_results(console, results, anime)
|
||||
|
||||
|
||||
def _get_episode_error_details(download_service, anime, episode_number):
|
||||
"""Get detailed error information from the registry for a failed episode."""
|
||||
try:
|
||||
# Get the media record from registry
|
||||
media_record = download_service.media_registry.get_record(anime.id)
|
||||
if not media_record:
|
||||
return None
|
||||
|
||||
# Find the episode in the record
|
||||
for episode_record in media_record.episodes:
|
||||
if episode_record.episode_number == episode_number:
|
||||
if episode_record.error_message:
|
||||
error_msg = episode_record.error_message
|
||||
|
||||
# Provide more helpful error messages for common issues
|
||||
if "Failed to get server for episode" in error_msg:
|
||||
return f"Episode {episode_number} not available on current provider. Try a different provider or check episode number."
|
||||
elif "NoneType" in error_msg or "not subscriptable" in error_msg:
|
||||
return f"Episode {episode_number} data not found on provider (API returned null). Episode may not exist or be accessible."
|
||||
else:
|
||||
return error_msg
|
||||
elif episode_record.download_status:
|
||||
return f"Download status: {episode_record.download_status.value}"
|
||||
break
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _test_episode_stream_availability(provider, anime, episode_number, config, feedback):
|
||||
"""Test if streams are available for a specific episode."""
|
||||
try:
|
||||
from .....libs.provider.anime.params import EpisodeStreamsParams
|
||||
|
||||
media_title = anime.title.english or anime.title.romaji
|
||||
feedback.info(f"Testing stream availability for '{media_title}' episode {episode_number}")
|
||||
|
||||
# Test episode streams
|
||||
streams = provider.episode_streams(
|
||||
EpisodeStreamsParams(
|
||||
anime_id=str(anime.id),
|
||||
query=media_title,
|
||||
episode=episode_number,
|
||||
translation_type=config.stream.translation_type,
|
||||
)
|
||||
)
|
||||
|
||||
if not streams:
|
||||
feedback.warning(f"No streams found for episode {episode_number}")
|
||||
return False
|
||||
|
||||
# Convert to list to check actual availability
|
||||
stream_list = list(streams)
|
||||
if not stream_list:
|
||||
feedback.warning(f"No stream servers available for episode {episode_number}")
|
||||
return False
|
||||
|
||||
feedback.info(f"Found {len(stream_list)} stream server(s) for episode {episode_number}")
|
||||
|
||||
# Show details about the first server for debugging
|
||||
first_server = stream_list[0]
|
||||
feedback.info(f"First server: name='{first_server.name}', type='{type(first_server).__name__}'")
|
||||
|
||||
return True
|
||||
|
||||
except TypeError as e:
|
||||
if "'NoneType' object is not subscriptable" in str(e):
|
||||
feedback.warning(f"Episode {episode_number} not available on provider (API returned null)")
|
||||
feedback.info("This usually means the episode doesn't exist on this provider or isn't accessible")
|
||||
return False
|
||||
else:
|
||||
feedback.error(f"Type error testing stream availability: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
feedback.error(f"Error testing stream availability: {e}")
|
||||
import traceback
|
||||
feedback.error("Stream test traceback", traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
def _display_download_results(console, results: dict[str, bool], anime):
|
||||
"""Display download results in a formatted table."""
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title=f"Download Results for {anime.title.english or anime.title.romaji}")
|
||||
table.add_column("Episode", justify="center", style="cyan")
|
||||
table.add_column("Status", justify="center")
|
||||
|
||||
for episode, success in sorted(results.items(), key=lambda x: float(x[0])):
|
||||
status = "[green]✓ Success[/green]" if success else "[red]✗ Failed[/red]"
|
||||
table.add_row(episode, status)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Summary
|
||||
total = len(results)
|
||||
successful = sum(results.values())
|
||||
failed = total - successful
|
||||
|
||||
if failed == 0:
|
||||
console.print(f"\n[bold green]All {total} episodes downloaded successfully![/bold green]")
|
||||
else:
|
||||
console.print(f"\n[yellow]Download complete: {successful}/{total} successful, {failed} failed[/yellow]")
|
||||
|
||||
|
||||
def _show_final_statistics(download_service, feedback):
|
||||
"""Show final download statistics."""
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
stats = download_service.get_download_statistics()
|
||||
|
||||
if stats:
|
||||
console.print(f"\n[bold blue]Overall Download Statistics:[/bold blue]")
|
||||
console.print(f"Total episodes tracked: {stats.get('total_episodes', 0)}")
|
||||
console.print(f"Successfully downloaded: {stats.get('downloaded', 0)}")
|
||||
console.print(f"Failed downloads: {stats.get('failed', 0)}")
|
||||
console.print(f"Queued downloads: {stats.get('queued', 0)}")
|
||||
|
||||
if stats.get('total_size_bytes', 0) > 0:
|
||||
size_mb = stats['total_size_bytes'] / (1024 * 1024)
|
||||
if size_mb > 1024:
|
||||
console.print(f"Total size: {size_mb/1024:.2f} GB")
|
||||
else:
|
||||
console.print(f"Total size: {size_mb:.2f} MB")
|
||||
@@ -1,408 +0,0 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
from .data import (
|
||||
genres_available,
|
||||
media_formats_available,
|
||||
media_statuses_available,
|
||||
seasons_available,
|
||||
sorts_available,
|
||||
tags_available_list,
|
||||
years_available,
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="download anime using anilists api to get the titles",
|
||||
short_help="download anime with anilist intergration",
|
||||
)
|
||||
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice(seasons_available),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice(media_statuses_available),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice(sorts_available),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice(genres_available),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
"-T",
|
||||
multiple=True,
|
||||
help="the tags to filter by",
|
||||
type=click.Choice(tags_available_list),
|
||||
)
|
||||
@click.option(
|
||||
"--media-format",
|
||||
"-f",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice(media_formats_available),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice(years_available),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
"--on-list/--not-on-list",
|
||||
"-L/-no-L",
|
||||
help="Whether the anime should be in your list or not",
|
||||
type=bool,
|
||||
)
|
||||
@click.option(
|
||||
"--episode-range",
|
||||
"-r",
|
||||
help="A range of episodes to download (start-end)",
|
||||
)
|
||||
@click.option(
|
||||
"--force-unknown-ext",
|
||||
"-F",
|
||||
help="This option forces yt-dlp to download extensions its not aware of",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--silent/--no-silent",
|
||||
"-q/-V",
|
||||
type=bool,
|
||||
help="Download silently (during download)",
|
||||
default=True,
|
||||
)
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
||||
@click.option(
|
||||
"--merge", "-m", is_flag=True, help="Merge the subfile with video using ffmpeg"
|
||||
)
|
||||
@click.option(
|
||||
"--clean",
|
||||
"-c",
|
||||
is_flag=True,
|
||||
help="After merging delete the original files",
|
||||
)
|
||||
@click.option(
|
||||
"--wait-time",
|
||||
"-w",
|
||||
type=int,
|
||||
help="The amount of time to wait after downloading is complete before the screen is completely cleared",
|
||||
default=60,
|
||||
)
|
||||
@click.option(
|
||||
"--prompt/--no-prompt",
|
||||
help="Whether to prompt for anything instead just do the best thing",
|
||||
default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--force-ffmpeg",
|
||||
is_flag=True,
|
||||
help="Force the use of FFmpeg for downloading (supports large variety of streams but slower)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-mpegts",
|
||||
is_flag=True,
|
||||
help="Use mpegts for hls streams, resulted in .ts file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-h264",
|
||||
is_flag=True,
|
||||
help="Use H.264 (MP4) for hls streams, resulted in .mp4 file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--no-check-certificates",
|
||||
is_flag=True,
|
||||
help="Suppress HTTPS certificate validation",
|
||||
)
|
||||
@click.option(
|
||||
"--max-results", "-M", type=int, help="The maximum number of results to show"
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(
|
||||
config,
|
||||
title,
|
||||
season,
|
||||
status,
|
||||
sort,
|
||||
genres,
|
||||
tags,
|
||||
media_format,
|
||||
year,
|
||||
on_list,
|
||||
episode_range,
|
||||
force_unknown_ext,
|
||||
silent,
|
||||
verbose,
|
||||
merge,
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
no_check_certificates,
|
||||
max_results,
|
||||
):
|
||||
from rich import print
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
force_ffmpeg |= hls_use_mpegts or hls_use_h264
|
||||
|
||||
success, anilist_search_results = AniList.search(
|
||||
query=title,
|
||||
sort=sort,
|
||||
status_in=list(status),
|
||||
genre_in=list(genres),
|
||||
season=season,
|
||||
tag_in=list(tags),
|
||||
seasonYear=year,
|
||||
format_in=list(media_format),
|
||||
on_list=on_list,
|
||||
max_results=max_results,
|
||||
)
|
||||
if success:
|
||||
import time
|
||||
|
||||
from rich.progress import Progress
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ....libs.anime_provider.types import Anime
|
||||
from ....libs.fzf import fzf
|
||||
from ....Utility.data import anime_normalizer
|
||||
from ....Utility.downloader.downloader import downloader
|
||||
from ...utils.tools import exit_app
|
||||
from ...utils.utils import (
|
||||
filter_by_quality,
|
||||
fuzzy_inquirer,
|
||||
move_preferred_subtitle_lang_to_top,
|
||||
)
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
anilist_anime_info = None
|
||||
|
||||
translation_type = config.translation_type
|
||||
download_dir = config.downloads_dir
|
||||
anime_titles = [
|
||||
(anime["title"]["romaji"] or anime["title"]["english"])
|
||||
for anime in anilist_search_results["data"]["Page"]["media"]
|
||||
]
|
||||
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||
for i, anime_title in enumerate(anime_titles):
|
||||
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||
# ---- search for anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, translation_type=translation_type
|
||||
)
|
||||
if not search_results:
|
||||
print(
|
||||
"No search results found from provider for {}".format(anime_title)
|
||||
)
|
||||
continue
|
||||
search_results = search_results["results"]
|
||||
if not search_results:
|
||||
print("Nothing muches your search term")
|
||||
continue
|
||||
search_results_ = {
|
||||
search_result["title"]: search_result
|
||||
for search_result in search_results
|
||||
}
|
||||
|
||||
if config.auto_select:
|
||||
selected_anime_title = max(
|
||||
search_results_.keys(),
|
||||
key=lambda title: fuzz.ratio(
|
||||
anime_normalizer.get(title, title), anime_title
|
||||
),
|
||||
)
|
||||
print("[cyan]Auto selecting:[/] ", selected_anime_title)
|
||||
else:
|
||||
choices = list(search_results_.keys())
|
||||
if config.use_fzf:
|
||||
selected_anime_title = fzf.run(
|
||||
choices, "Please Select title", "FastAnime"
|
||||
)
|
||||
else:
|
||||
selected_anime_title = fuzzy_inquirer(
|
||||
choices,
|
||||
"Please Select title",
|
||||
)
|
||||
|
||||
# ---- fetch anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Anime...", total=None)
|
||||
anime: Anime | None = anime_provider.get_anime(
|
||||
search_results_[selected_anime_title]["id"]
|
||||
)
|
||||
if not anime:
|
||||
print("Failed to fetch anime {}".format(selected_anime_title))
|
||||
continue
|
||||
|
||||
episodes = sorted(
|
||||
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||
)
|
||||
# where the magic happens
|
||||
if episode_range:
|
||||
if ":" in episode_range:
|
||||
ep_range_tuple = episode_range.split(":")
|
||||
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
episodes_range = episodes[
|
||||
int(episodes_start) : int(episodes_end)
|
||||
]
|
||||
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end, step = ep_range_tuple
|
||||
episodes_range = episodes[
|
||||
int(episodes_start) : int(episodes_end) : int(step)
|
||||
]
|
||||
else:
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
if episodes_start.strip():
|
||||
episodes_range = episodes[int(episodes_start) :]
|
||||
elif episodes_end.strip():
|
||||
episodes_range = episodes[: int(episodes_end)]
|
||||
else:
|
||||
episodes_range = episodes
|
||||
else:
|
||||
episodes_range = episodes[int(episode_range) :]
|
||||
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||
|
||||
else:
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
|
||||
if config.normalize_titles:
|
||||
anilist_anime_info = anilist_search_results["data"]["Page"]["media"][i]
|
||||
|
||||
# lets download em
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
episode = str(episode)
|
||||
if episode not in episodes:
|
||||
print(
|
||||
f"[cyan]Warning[/]: Episode {episode} not found, skipping"
|
||||
)
|
||||
continue
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Episode Streams...", total=None)
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime["id"], episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
print("No streams skipping")
|
||||
continue
|
||||
# ---- fetch servers ----
|
||||
if config.server == "top":
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server_name = next(streams, None)
|
||||
if not server_name:
|
||||
print("Sth went wrong when fetching the server")
|
||||
continue
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, server_name["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = server_name["headers"]
|
||||
episode_title = server_name["episode_title"]
|
||||
subtitles = server_name["subtitles"]
|
||||
else:
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching servers", total=None)
|
||||
# prompt for server selection
|
||||
servers = {server["server"]: server for server in streams}
|
||||
servers_names = list(servers.keys())
|
||||
if config.server in servers_names:
|
||||
server_name = config.server
|
||||
else:
|
||||
if config.use_fzf:
|
||||
server_name = fzf.run(servers_names, "Select an link")
|
||||
else:
|
||||
server_name = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server_name]["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = servers[server_name]["headers"]
|
||||
|
||||
subtitles = servers[server_name]["subtitles"]
|
||||
episode_title = servers[server_name]["episode_title"]
|
||||
|
||||
if anilist_anime_info:
|
||||
selected_anime_title = (
|
||||
anilist_anime_info["title"][config.preferred_language]
|
||||
or anilist_anime_info["title"]["romaji"]
|
||||
or anilist_anime_info["title"]["english"]
|
||||
)
|
||||
import re
|
||||
|
||||
for episode_detail in anilist_anime_info["streamingEpisodes"]:
|
||||
if re.match(
|
||||
f".*Episode {episode} .*", episode_detail["title"]
|
||||
):
|
||||
episode_title = episode_detail["title"]
|
||||
break
|
||||
print(f"[purple]Now Downloading:[/] {episode_title}")
|
||||
subtitles = move_preferred_subtitle_lang_to_top(
|
||||
subtitles, config.sub_lang
|
||||
)
|
||||
downloader._download_file(
|
||||
link,
|
||||
selected_anime_title,
|
||||
episode_title,
|
||||
download_dir,
|
||||
silent,
|
||||
vid_format=config.format,
|
||||
force_unknown_ext=force_unknown_ext,
|
||||
verbose=verbose,
|
||||
headers=provider_headers,
|
||||
sub=subtitles[0]["url"] if subtitles else "",
|
||||
merge=merge,
|
||||
clean=clean,
|
||||
prompt=prompt,
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
hls_use_h264=hls_use_h264,
|
||||
nocheckcertificate=no_check_certificates,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
print("Done Downloading")
|
||||
time.sleep(wait_time)
|
||||
exit_app()
|
||||
else:
|
||||
from sys import exit
|
||||
|
||||
print("Failed to search for anime", anilist_search_results)
|
||||
exit(1)
|
||||
@@ -7,35 +7,35 @@ download = """
|
||||
# Download specific episodes
|
||||
fastanime anilist download -t "One Piece" --episode-range "1-10"
|
||||
\b
|
||||
# Download with auto-selection (no prompts)
|
||||
fastanime anilist download -t "Naruto" --auto-select --silent
|
||||
# Download single episode
|
||||
fastanime anilist download -t "Death Note" --episode-range "1"
|
||||
\b
|
||||
# Download multiple specific episodes
|
||||
fastanime anilist download -t "Naruto" --episode-range "1,5,10"
|
||||
\b
|
||||
# Download with quality preference
|
||||
fastanime anilist download -t "Death Note" --quality 1080p --episode-range "1-5"
|
||||
fastanime anilist download -t "Death Note" --quality 1080 --episode-range "1-5"
|
||||
\b
|
||||
# Download with multiple filters
|
||||
fastanime anilist download -g Action -T Isekai --score-greater 80 --status RELEASING
|
||||
\b
|
||||
# Download recent episodes only
|
||||
fastanime anilist download -t "Demon Slayer" --episode-range "20:"
|
||||
# Download with concurrent downloads
|
||||
fastanime anilist download -t "Demon Slayer" --episode-range "1-5" --max-concurrent 3
|
||||
\b
|
||||
# Download with subtitle merging
|
||||
fastanime anilist download -t "Your Name" --merge --clean
|
||||
\b
|
||||
# Download using FFmpeg with HLS options
|
||||
fastanime anilist download -t "Spirited Away" --force-ffmpeg --hls-use-h264
|
||||
# Force redownload existing episodes
|
||||
fastanime anilist download -t "Your Name" --episode-range "1" --force-redownload
|
||||
\b
|
||||
# Download from a specific season and year
|
||||
fastanime anilist download --season WINTER --year 2024 -s POPULARITY_DESC --auto-select
|
||||
fastanime anilist download --season WINTER --year 2024 -s POPULARITY_DESC
|
||||
\b
|
||||
# Download with genre filtering
|
||||
fastanime anilist download -g Action -g Adventure --score-greater 75
|
||||
\b
|
||||
# Download only completed series
|
||||
fastanime anilist download -g Fantasy --status FINISHED --score-greater 75 --auto-select
|
||||
\b
|
||||
# Download with verbose output and no certificate checking
|
||||
fastanime anilist download -t "Akira" --verbose --no-check-certificates
|
||||
fastanime anilist download -g Fantasy --status FINISHED --score-greater 75
|
||||
\b
|
||||
# Download movies only
|
||||
fastanime anilist download -f MOVIE -s SCORE_DESC --auto-select --quality best
|
||||
fastanime anilist download -F MOVIE -s SCORE_DESC --quality best
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user