feat: animepahe provider

This commit is contained in:
Benexl
2025-07-23 11:29:52 +03:00
parent aa50ab62b5
commit 6e9babf270
10 changed files with 192 additions and 197 deletions

View File

@@ -13,7 +13,6 @@ if TYPE_CHECKING:
from typing_extensions import Unpack
from ...libs.players.base import BasePlayer
from ...libs.providers.anime.base import BaseAnimeProvider
from ...libs.providers.anime.types import Anime
from ...libs.selectors.base import BaseSelector
@@ -116,7 +115,6 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
from ...libs.selectors.selector import create_selector
provider = create_provider(config.general.provider)
player = create_player(config)
selector = create_selector(config)
anime_titles = options["anime_title"]
@@ -149,7 +147,7 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime = provider.get(AnimeParams(id=anime_result.id))
anime = provider.get(AnimeParams(id=anime_result.id, query=anime_title))
if not anime:
raise FastAnimeError(f"Failed to fetch anime {anime_result.title}")
@@ -184,7 +182,13 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
for episode in episodes_range:
download_anime(
config, options, provider, selector, player, anime, episode
config,
options,
provider,
selector,
anime,
episode,
anime_title,
)
else:
episode = selector.choose(
@@ -193,7 +197,9 @@ def download(config: AppConfig, **options: "Unpack[Options]"):
)
if not episode:
raise FastAnimeError("No episode selected")
download_anime(config, options, provider, selector, player, anime, episode)
download_anime(
config, options, provider, selector, anime, episode, anime_title
)
def download_anime(
@@ -201,15 +207,14 @@ def download_anime(
download_options: "Options",
provider: "BaseAnimeProvider",
selector: "BaseSelector",
player: "BasePlayer",
anime: "Anime",
episode: str,
anime_title: str,
):
from rich import print
from rich.progress import Progress
from ...core.downloader import DownloadParams, create_downloader
from ...libs.players.params import PlayerParams
from ...libs.providers.anime.params import EpisodeStreamsParams
downloader = create_downloader(config.downloads)
@@ -219,6 +224,7 @@ def download_anime(
streams = provider.episode_streams(
EpisodeStreamsParams(
anime_id=anime.id,
query=anime_title,
episode=episode,
translation_type=config.stream.translation_type,
)

View File

@@ -46,7 +46,6 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
from rich.progress import Progress
from ...core.exceptions import FastAnimeError
from ...libs.players.player import create_player
from ...libs.providers.anime.params import (
AnimeParams,
SearchParams,
@@ -55,7 +54,6 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
from ...libs.selectors.selector import create_selector
provider = create_provider(config.general.provider)
player = create_player(config)
selector = create_selector(config)
anime_titles = options["anime_title"]
@@ -88,7 +86,7 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
# ---- fetch selected anime ----
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime = provider.get(AnimeParams(id=anime_result.id))
anime = provider.get(AnimeParams(id=anime_result.id, query=anime_title))
if not anime:
raise FastAnimeError(f"Failed to fetch anime {anime_result.title}")
@@ -122,7 +120,7 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
episodes_range = iter(episodes_range)
for episode in episodes_range:
stream_anime(config, provider, selector, player, anime, episode)
stream_anime(config, provider, selector, anime, episode, anime_title)
else:
episode = selector.choose(
"Select Episode",
@@ -130,28 +128,32 @@ def search(config: AppConfig, **options: "Unpack[Options]"):
)
if not episode:
raise FastAnimeError("No episode selected")
stream_anime(config, provider, selector, player, anime, episode)
stream_anime(config, provider, selector, anime, episode, anime_title)
def stream_anime(
config: AppConfig,
provider: "BaseAnimeProvider",
selector: "BaseSelector",
player: "BasePlayer",
anime: "Anime",
episode: str,
anime_title: str,
):
from rich import print
from rich.progress import Progress
from ...libs.players.params import PlayerParams
from ...libs.players.player import create_player
from ...libs.providers.anime.params import EpisodeStreamsParams
player = create_player(config)
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = provider.episode_streams(
EpisodeStreamsParams(
anime_id=anime.id,
query=anime_title,
episode=episode,
translation_type=config.stream.translation_type,
)

View File

@@ -80,7 +80,9 @@ def provider_search(ctx: Context, state: State) -> State | ControlFlow:
)
from ....libs.providers.anime.params import AnimeParams
full_provider_anime = provider.get(AnimeParams(id=selected_provider_anime.id))
full_provider_anime = provider.get(
AnimeParams(id=selected_provider_anime.id, query=anilist_title.lower())
)
if not full_provider_anime:
feedback.warning(

View File

@@ -25,6 +25,11 @@ def servers(ctx: Context, state: State) -> State | ControlFlow:
then launches the media player and transitions to post-playback controls.
"""
provider_anime = state.provider.anime
if not state.media_api.anime:
return ControlFlow.BACK
anime_title = (
state.media_api.anime.title.romaji or state.media_api.anime.title.romaji
)
episode_number = state.provider.episode_number
config = ctx.config
provider = ctx.provider
@@ -47,6 +52,7 @@ def servers(ctx: Context, state: State) -> State | ControlFlow:
server_iterator = provider.episode_streams(
EpisodeStreamsParams(
anime_id=provider_anime.id,
query=anime_title,
episode=episode_number,
translation_type=config.stream.translation_type,
)

View File

@@ -6,9 +6,9 @@ ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api"
SERVERS_AVAILABLE = ["kwik"]
REQUEST_HEADERS = {
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592",
"Host": ANIMEPAHE,
"Accept": "application , text/javascript, */*; q=0.01",
"Accept": "application, text/javascript, */*; q=0.01",
"Accept-Encoding": "Utf-8",
"Referer": ANIMEPAHE_BASE,
"DNT": "1",

View File

@@ -1,7 +1,25 @@
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from ..types import Anime, AnimeEpisodes, AnimeEpisodeInfo, PageInfo, SearchResult, SearchResults, Server, EpisodeStream, Subtitle
from .types import AnimePaheAnimePage, AnimePaheSearchResult, AnimePaheSearchPage, AnimePaheServer, AnimePaheEpisodeInfo, AnimePaheAnime, AnimePaheStreamLink
from ..types import (
Anime,
AnimeEpisodeInfo,
AnimeEpisodes,
EpisodeStream,
PageInfo,
SearchResult,
SearchResults,
Server,
Subtitle,
)
from .types import (
AnimePaheAnime,
AnimePaheAnimePage,
AnimePaheEpisodeInfo,
AnimePaheSearchPage,
AnimePaheSearchResult,
AnimePaheServer,
AnimePaheStreamLink,
)
def map_to_search_results(data: AnimePaheSearchPage) -> SearchResults:
@@ -21,6 +39,7 @@ def map_to_search_results(data: AnimePaheSearchPage) -> SearchResults:
status=result["status"],
season=result["season"],
poster=result["poster"],
year=str(result["year"]),
)
)
@@ -34,47 +53,45 @@ def map_to_search_results(data: AnimePaheSearchPage) -> SearchResults:
)
def map_to_anime_result(data: AnimePaheAnime) -> Anime:
def map_to_anime_result(
search_result: SearchResult, anime: AnimePaheAnimePage
) -> Anime:
episodes_info = []
for ep_info in data["episodesInfo"]:
episodes = []
for ep_info in anime["data"]:
episodes.append(str(ep_info["episode"]))
episodes_info.append(
AnimeEpisodeInfo(
id=ep_info["id"],
id=str(ep_info["id"]),
session_id=ep_info["session"],
episode=str(ep_info["episode"]),
title=ep_info["title"],
poster=ep_info["poster"],
duration=ep_info["duration"],
poster=ep_info["snapshot"],
duration=str(ep_info["duration"]),
)
)
return Anime(
id=data["id"],
title=data["title"],
id=search_result.id,
title=search_result.title,
episodes=AnimeEpisodes(
sub=data["availableEpisodesDetail"]["sub"],
dub=data["availableEpisodesDetail"]["dub"],
raw=data["availableEpisodesDetail"]["raw"],
sub=episodes,
dub=episodes,
),
year=str(data["year"]),
poster=data["poster"],
year=str(search_result.year),
poster=search_result.poster,
episodes_info=episodes_info,
)
def map_to_server(data: AnimePaheServer) -> Server:
links = []
for link in data["links"]:
links.append(
EpisodeStream(
link=link["link"],
quality=link["quality"],
translation_type=link["translation_type"],
)
def map_to_server(
episode: AnimeEpisodeInfo, translation_type: Any, quality: Any, stream_link: Any
) -> Server:
links = [
EpisodeStream(
link=stream_link,
quality=quality,
translation_type=translation_type,
)
return Server(
name=data["server"],
links=links,
episode_title=data["episode_title"],
subtitles=data["subtitles"],
headers=data["headers"],
)
]
return Server(name="kwik", links=links, episode_title=episode.title)

View File

@@ -1,7 +1,8 @@
import logging
import random
import time
from typing import TYPE_CHECKING
from functools import lru_cache
from typing import TYPE_CHECKING, Iterator, Optional, Union
from yt_dlp.utils import (
extract_attributes,
@@ -11,7 +12,7 @@ from yt_dlp.utils import (
from ..base import BaseAnimeProvider
from ..params import AnimeParams, EpisodeStreamsParams, SearchParams
from ..types import Anime, SearchResults, Server
from ..types import Anime, AnimeEpisodeInfo, SearchResult, SearchResults, Server
from ..utils.debug import debug_provider
from .constants import (
ANIMEPAHE_BASE,
@@ -21,7 +22,7 @@ from .constants import (
SERVER_HEADERS,
)
from .extractors import process_animepahe_embed_page
from .parser import map_to_anime_result, map_to_server, map_to_search_results
from .parser import map_to_anime_result, map_to_search_results, map_to_server
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimePaheSearchResult
logger = logging.getLogger(__name__)
@@ -32,62 +33,53 @@ class AnimePahe(BaseAnimeProvider):
@debug_provider
def search(self, params: SearchParams) -> SearchResults | None:
response = self.client.get(
ANIMEPAHE_ENDPOINT, params={"m": "search", "q": params.query}
)
return self._search(params)
@lru_cache()
def _search(self, params: SearchParams) -> SearchResults | None:
url_params = {"m": "search", "q": params.query}
response = self.client.get(ANIMEPAHE_ENDPOINT, params=url_params)
response.raise_for_status()
data: AnimePaheSearchPage = response.json()
if not data.get("data"):
return
return map_to_search_results(data)
@debug_provider
def get(self, params: AnimeParams) -> Anime | None:
return self._get_anime(params)
@lru_cache()
def _get_anime(self, params: AnimeParams) -> Anime | None:
page = 1
standardized_episode_number = 0
anime_result: AnimePaheSearchResult = self.search(SearchParams(query=params.id)).results[0]
data: AnimePaheAnimePage = {} # pyright:ignore
def _pages_loader(
self,
data,
session_id,
params,
page,
standardized_episode_number,
):
response = self.client.get(ANIMEPAHE_ENDPOINT, params=params)
response.raise_for_status()
if not data:
data.update(response.json())
elif ep_data := response.json().get("data"):
data["data"].extend(ep_data)
if response.json()["next_page_url"]:
# TODO: Refine this
time.sleep(
random.choice(
[
0.25,
0.1,
0.5,
0.75,
1,
]
)
)
page += 1
self._pages_loader(
data,
session_id,
params={
"m": "release",
"page": page,
"id": session_id,
"sort": "episode_asc",
},
search_result = self._get_search_result(params)
if not search_result:
logger.error(f"No search result found for ID {params.id}")
return None
anime: Optional[AnimePaheAnimePage] = None
has_next_page = True
while has_next_page:
logger.debug(f"Loading page: {page}")
_anime_page = self._anime_page_loader(
m="release",
id=params.id,
sort="episode_asc",
page=page,
standardized_episode_number=standardized_episode_number,
)
else:
for episode in data.get("data", []):
has_next_page = True if _anime_page["next_page_url"] else False
page += 1
if not anime:
anime = _anime_page
else:
anime["data"].extend(_anime_page["data"])
if anime:
for episode in anime.get("data", []):
if episode["episode"] % 1 == 0:
standardized_episode_number += 1
episode.update({"episode": standardized_episode_number})
@@ -95,103 +87,52 @@ class AnimePahe(BaseAnimeProvider):
standardized_episode_number += episode["episode"] % 1
episode.update({"episode": standardized_episode_number})
standardized_episode_number = int(standardized_episode_number)
return data
@debug_provider
def get(self, params: AnimeParams) -> Anime | None:
page = 1
standardized_episode_number = 0
search_results = self.search(SearchParams(query=params.id))
return map_to_anime_result(search_result, anime)
@lru_cache()
def _get_search_result(self, params: AnimeParams) -> Optional[SearchResult]:
search_results = self._search(SearchParams(query=params.query))
if not search_results or not search_results.results:
logger.error(f"[ANIMEPAHE-ERROR]: No search results found for ID {params.id}")
logger.error(f"No search results found for ID {params.id}")
return None
anime_result: AnimePaheSearchResult = search_results.results[0]
for search_result in search_results.results:
if search_result.id == params.id:
return search_result
data: AnimePaheAnimePage = {} # pyright:ignore
data = self._pages_loader(
data,
params.id,
params={
"m": "release",
"id": params.id,
"sort": "episode_asc",
"page": page,
},
page=page,
standardized_episode_number=standardized_episode_number,
)
if not data:
return None
# Construct AnimePaheAnime TypedDict for mapping
anime_pahe_anime_data = {
"id": params.id,
"title": anime_result.title,
"year": anime_result.year,
"season": anime_result.season,
"poster": anime_result.poster,
"score": anime_result.score,
"availableEpisodesDetail": {
"sub": list(map(str, [episode["episode"] for episode in data["data"]])),
"dub": list(map(str, [episode["episode"] for episode in data["data"]])),
"raw": list(map(str, [episode["episode"] for episode in data["data"]])),
},
"episodesInfo": [
{
"title": episode["title"],
"episode": episode["episode"],
"id": episode["session"],
"translation_type": episode["audio"],
"duration": episode["duration"],
"poster": episode["snapshot"],
}
for episode in data["data"]
],
@lru_cache()
def _anime_page_loader(self, m, id, sort, page) -> AnimePaheAnimePage:
url_params = {
"m": m,
"id": id,
"sort": sort,
"page": page,
}
return map_to_anime_result(anime_pahe_anime_data)
response = self.client.get(ANIMEPAHE_ENDPOINT, params=url_params)
response.raise_for_status()
return response.json()
@debug_provider
def episode_streams(self, params: EpisodeStreamsParams) -> "Iterator[Server] | None":
anime_info = self.get(AnimeParams(id=params.anime_id))
if not anime_info:
logger.error(
f"[ANIMEPAHE-ERROR]: Anime with ID {params.anime_id} not found"
)
return
episode = next(
(
ep
for ep in anime_info.episodes_info
if float(ep.episode) == float(params.episode)
),
None,
)
def episode_streams(self, params: EpisodeStreamsParams) -> Iterator[Server] | None:
episode = self._get_episode_info(params)
if not episode:
logger.error(
f"[ANIMEPAHE-ERROR]: Episode {params.episode} doesn't exist for anime {anime_info.title}"
f"Episode {params.episode} doesn't exist for anime {params.anime_id}"
)
return
url = f"{ANIMEPAHE_BASE}/play/{params.anime_id}/{episode.id}"
response = self.client.get(url)
url = f"{ANIMEPAHE_BASE}/play/{params.anime_id}/{episode.session_id}"
response = self.client.get(url, follow_redirects=True)
response.raise_for_status()
c = get_element_by_id("resolutionMenu", response.text)
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
quality = None
translation_type = None
stream_link = None
streams = {
"server": "kwik",
"links": [],
"episode_title": f"{episode.title or anime_info.title}; Episode {episode.episode}",
"subtitles": [],
"headers": {},
}
# TODO: better document the scraping process
for res_dict in res_dicts:
embed_url = res_dict["data-src"]
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
@@ -200,40 +141,54 @@ class AnimePahe(BaseAnimeProvider):
continue
if not embed_url:
logger.warning(
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
)
logger.warning("embed url not found please report to the developers")
continue
embed_response = self.client.get(
embed_url, headers={"User-Agent": self.client.headers["User-Agent"], **SERVER_HEADERS}
embed_url,
headers={
"User-Agent": self.client.headers["User-Agent"],
**SERVER_HEADERS,
},
)
embed_response.raise_for_status()
embed_page = embed_response.text
decoded_js = process_animepahe_embed_page(embed_page)
if not decoded_js:
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
logger.error("failed to decode embed page")
continue
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
if not juicy_stream:
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
logger.error("failed to find juicy stream")
continue
juicy_stream = juicy_stream.group(1)
quality = res_dict["data-resolution"]
translation_type = data_audio
stream_link = juicy_stream
streams["links"].append(
{
"quality": res_dict["data-resolution"],
"translation_type": data_audio,
"link": juicy_stream,
}
)
if streams["links"]:
yield map_to_server(streams)
if translation_type and quality and stream_link:
yield map_to_server(episode, translation_type, quality, stream_link)
@lru_cache()
def _get_episode_info(
self, params: EpisodeStreamsParams
) -> Optional[AnimeEpisodeInfo]:
anime_info = self._get_anime(
AnimeParams(id=params.anime_id, query=params.query)
)
if not anime_info:
logger.error(f"No anime info for {params.anime_id}")
return
if not anime_info.episodes_info:
logger.error(f"No episodes info for {params.anime_id}")
return
for episode in anime_info.episodes_info:
if episode.episode == params.episode:
return episode
if __name__ == "__main__":
from httpx import Client
from ..utils.debug import test_anime_provider
test_anime_provider(AnimePahe, Client())
test_anime_provider(AnimePahe)

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from typing import Literal
@dataclass
@dataclass(frozen=True)
class SearchParams:
"""Parameters for searching anime."""
@@ -24,10 +24,11 @@ class SearchParams:
country_of_origin: str | None = None
@dataclass
@dataclass(frozen=True)
class EpisodeStreamsParams:
"""Parameters for fetching episode streams."""
query: str
anime_id: str
episode: str
translation_type: Literal["sub", "dub"] = "sub"
@@ -36,8 +37,10 @@ class EpisodeStreamsParams:
subtitles: bool = True
@dataclass
@dataclass(frozen=True)
class AnimeParams:
"""Parameters for fetching anime details."""
id: str
# HACK: for the sake of providers which require previous data
query: str

View File

@@ -1,4 +1,4 @@
from typing import Literal
from typing import Literal, Optional
from pydantic import BaseModel
@@ -25,20 +25,23 @@ class SearchResult(BaseAnimeProviderModel):
episodes: AnimeEpisodes
other_titles: list[str] = []
media_type: str | None = None
score: int | None = None
score: float | None = None
status: str | None = None
season: str | None = None
poster: str | None = None
year: str | None = None
class SearchResults(BaseAnimeProviderModel):
page_info: PageInfo
results: list[SearchResult]
model_config = {"frozen": True}
class AnimeEpisodeInfo(BaseAnimeProviderModel):
id: str
episode: str
session_id: Optional[str] = None
title: str | None = None
poster: str | None = None
duration: str | None = None

View File

@@ -46,7 +46,7 @@ def test_anime_provider(AnimeProvider: Type[BaseAnimeProvider]):
result = search_results.results[
int(input(f"Select result (1-{len(search_results.results)}): ")) - 1
]
anime = anime_provider.get(AnimeParams(id=result.id))
anime = anime_provider.get(AnimeParams(id=result.id, query=query))
if not anime:
return
@@ -56,6 +56,7 @@ def test_anime_provider(AnimeProvider: Type[BaseAnimeProvider]):
episode_number = input("What episode do you wish to watch: ")
episode_streams = anime_provider.episode_streams(
EpisodeStreamsParams(
query=query,
anime_id=anime.id,
episode=episode_number,
translation_type=translation_type, # type:ignore