Files
FastAnime/fastanime/cli/utils/parser.py
2025-07-26 10:56:26 +03:00

135 lines
4.9 KiB
Python

"""Episode range parsing utilities for FastAnime CLI commands."""
from typing import Iterator
def parse_episode_range(
episode_range_str: str | None, available_episodes: list[str]
) -> Iterator[str]:
"""
Parse an episode range string and return an iterator of episode numbers.
This function handles various episode range formats:
- Single episode: "5" -> episodes from index 5 onwards
- Range with start and end: "5:10" -> episodes from index 5 to 10 (exclusive)
- Range with step: "5:10:2" -> episodes from index 5 to 10 with step 2
- Start only: "5:" -> episodes from index 5 onwards
- End only: ":10" -> episodes from beginning to index 10
- All episodes: ":" -> all episodes
Args:
episode_range_str: The episode range string to parse (e.g., "5:10", "5:", ":10", "5")
available_episodes: List of available episode numbers/identifiers
Returns:
Iterator over the selected episode numbers
Raises:
ValueError: If the episode range format is invalid
IndexError: If the specified indices are out of range
Examples:
>>> episodes = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
>>> list(parse_episode_range("2:5", episodes))
['3', '4', '5']
>>> list(parse_episode_range("5:", episodes))
['6', '7', '8', '9', '10']
>>> list(parse_episode_range(":3", episodes))
['1', '2', '3']
>>> list(parse_episode_range("2:8:2", episodes))
['3', '5', '7']
"""
if not episode_range_str:
# No range specified, return all episodes
return iter(available_episodes)
# Sort episodes numerically for consistent ordering
episodes = sorted(available_episodes, key=float)
if ":" in episode_range_str:
# Handle colon-separated ranges
parts = episode_range_str.split(":")
if len(parts) == 3:
# Format: start:end:step
start_str, end_str, step_str = parts
if not all([start_str, end_str, step_str]):
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"When using 3 parts (start:end:step), all parts must be non-empty."
)
try:
start_idx = int(start_str)
end_idx = int(end_str)
step = int(step_str)
if step <= 0:
raise ValueError("Step value must be positive")
return iter(episodes[start_idx:end_idx:step])
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"All parts must be valid integers."
) from e
raise
elif len(parts) == 2:
# Format: start:end or start: or :end
start_str, end_str = parts
if start_str and end_str:
# Both start and end specified: start:end
try:
start_idx = int(start_str)
end_idx = int(end_str)
return iter(episodes[start_idx:end_idx])
except ValueError as e:
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"Start and end must be valid integers."
) from e
elif start_str and not end_str:
# Only start specified: start:
try:
start_idx = int(start_str)
return iter(episodes[start_idx:])
except ValueError as e:
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"Start must be a valid integer."
) from e
elif not start_str and end_str:
# Only end specified: :end
try:
end_idx = int(end_str)
return iter(episodes[:end_idx])
except ValueError as e:
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"End must be a valid integer."
) from e
else:
# Both empty: ":"
return iter(episodes)
else:
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"Too many colon separators."
)
else:
# Single number: start from that index onwards
try:
start_idx = int(episode_range_str)
return iter(episodes[start_idx:])
except ValueError as e:
raise ValueError(
f"Invalid episode range format: '{episode_range_str}'. "
"Must be a valid integer."
) from e