Source code for grobber.anime.sources.masteranime

# Masteranime got taken down RIP

import json
import logging
from typing import Any, AsyncIterator, Dict, List, Optional, Union

from grobber import utils
from grobber.decorators import cached_property, retry_with_proxy
from grobber.languages import Language
from grobber.request import DefaultUrlFormatter, Request
from grobber.url_pool import UrlPool
from . import register_source
from ..models import SearchResult, SourceAnime, SourceEpisode

log = logging.getLogger(__name__)

BASE_URL = "{MASTERANIME_URL}"
SEARCH_URL = BASE_URL + "/api/anime/filter"
ANIME_URL = BASE_URL + "/api/anime/{anime_id}/detailed"
EPISODE_URL = BASE_URL + "/anime/watch/{anime_slug}/{episode}"

masteranime_pool = UrlPool("MasterAnime", ["https://www.masterani.me"])
DefaultUrlFormatter.add_field("MASTERANIME_URL", lambda: masteranime_pool.url)

masteranime_cdn_pool = UrlPool("MasterAnime CDN", ["https://cdn.masterani.me"])


async def get_poster_url(poster_data: Union[Dict[str, Any], str]) -> str:
    base = await masteranime_cdn_pool.url

    if isinstance(poster_data, str):
        path = f"poster/{poster_data}"
    else:
        path = poster_data["path"] + poster_data["file"]

    return f"{base}/{path}"


class MasterEpisode(SourceEpisode):
    ATTRS = ("mirror_data",)

    @cached_property
    async def mirror_data(self) -> List[Dict[str, Any]]:
        bs = await self._req.bs
        element = bs.select_one("video-mirrors")

        if not element:
            return []

        return json.loads(element[":mirrors"])

    @cached_property
    async def raw_streams(self) -> List[str]:
        links = []
        for mirror in await self.mirror_data:
            host_data = mirror["host"]
            prefix = host_data["embed_prefix"]
            suffix = host_data["embed_suffix"] or ""
            embed_id = mirror["embed_id"]
            links.append(f"{prefix}{embed_id}{suffix}")

        return links


[docs]class MasterAnime(SourceAnime): ATTRS = ("anime_id", "anime_slug") EPISODE_CLS = MasterEpisode @cached_property @retry_with_proxy(KeyError, TypeError) async def info_data(self) -> Dict[str, Any]: return (await self._req.json)["info"] @cached_property @retry_with_proxy(KeyError, TypeError) async def episode_data(self) -> List[Dict[str, Any]]: return (await self._req.json)["episodes"] @cached_property async def anime_id(self) -> int: return (await self.info_data)["id"] @cached_property async def anime_slug(self) -> str: return (await self.info_data)["slug"] @cached_property async def title(self) -> str: return (await self.info_data)["title"] @cached_property async def thumbnail(self) -> Optional[str]: try: poster = (await self._req.json)["poster"] except KeyError: return None else: return await get_poster_url(poster) @cached_property async def is_dub(self) -> bool: return False @cached_property async def language(self) -> Language: return Language.ENGLISH @cached_property async def episode_count(self) -> int: return len(await self.episode_data)
[docs] @classmethod async def search(cls, query: str, *, language=Language.ENGLISH, dubbed=False) -> AsyncIterator[SearchResult]: if dubbed or language != Language.ENGLISH: return # Query limit is 45 characters!! req = Request(SEARCH_URL, {"search": query[:45], "order": "relevance_desc"}) json_data = await req.json if not json_data: logging.warning("couldn't get json from masteranime") return for raw_anime in json_data["data"]: anime_id = raw_anime["id"] title = raw_anime["title"] ep_count = raw_anime["episode_count"] thumbnail = await get_poster_url(raw_anime["poster"]) req = Request(utils.format_available(ANIME_URL, anime_id=anime_id)) data = dict(anime_id=anime_id, anime_slug=raw_anime["slug"], title=title, thumbail=thumbnail) # ep_count can be None which severely breaks Grobber... So let's just not, kay? if ep_count is not None: data["episode_count"] = ep_count anime = cls(req, data=data) yield SearchResult(anime, utils.get_certainty(title, query))
@cached_property async def raw_eps(self) -> List[SourceEpisode]: episodes = [] slug = await self.anime_slug for ep_data in await self.episode_data: ep_id = ep_data["info"]["episode"] req = Request(utils.format_available(EPISODE_URL, anime_slug=slug, episode=ep_id)) episodes.append(self.EPISODE_CLS(req)) return episodes
[docs] async def get_episode(self, index: int) -> Optional[SourceEpisode]: return (await self.raw_eps)[index]
[docs] async def get_episodes(self) -> List[SourceEpisode]: return await self.raw_eps
register_source(MasterAnime)