fix: allanime provider

This commit is contained in:
Benexl
2026-05-02 23:50:04 +03:00
parent 1f8c739448
commit 2a7017fb54
5 changed files with 119 additions and 15 deletions
+1
View File
@@ -9,6 +9,7 @@ dependencies = [
"click>=8.1.7",
"httpx>=0.28.1",
"inquirerpy>=0.3.4",
"pycryptodomex>=3.23.0",
"pydantic>=2.11.7",
"rich>=13.9.2",
]
Generated
+2
View File
@@ -3816,6 +3816,7 @@ dependencies = [
{ name = "click" },
{ name = "httpx" },
{ name = "inquirerpy" },
{ name = "pycryptodomex" },
{ name = "pydantic" },
{ name = "rich" },
]
@@ -3882,6 +3883,7 @@ requires-dist = [
{ name = "mpv", marker = "extra == 'mpv'", specifier = ">=1.0.7" },
{ name = "plyer", marker = "extra == 'notifications'", specifier = ">=2.1.0" },
{ name = "plyer", marker = "extra == 'standard'", specifier = ">=2.1.0" },
{ name = "pycryptodomex", specifier = ">=3.23.0" },
{ name = "pycryptodomex", marker = "extra == 'download'", specifier = ">=3.23.0" },
{ name = "pycryptodomex", marker = "extra == 'standard'", specifier = ">=3.23.0" },
{ name = "pydantic", specifier = ">=2.11.7" },
@@ -19,6 +19,15 @@ API_GRAPHQL_HEADERS= {
"Content-Type": "application/json",
"Origin": f"{API_GRAPHQL_REFERER}",
}
API_EPISODE_HEADERS = {
"Referer": "https://youtu-chan.com",
"Origin": "https://youtu-chan.com",
}
PERSISTED_QUERY_SHA256 = (
"d405d0edd690624b66baba3068e0edc3ac90f1597d898a1ec8db4e5c43c00fec"
)
TOBEPARSED_DECRYPTION_SEED = "Xot36i3lK3:v1"
# search constants
DEFAULT_COUNTRY_OF_ORIGIN = "all"
@@ -1,24 +1,30 @@
import logging
from typing import TYPE_CHECKING
from json import dumps
from typing import Any, Iterator
from .....core.utils.graphql import execute_graphql
from ..base import BaseAnimeProvider
from ..params import AnimeParams, EpisodeStreamsParams, SearchParams
from ..types import Anime, SearchResults, Server
from ..utils.debug import debug_provider
from .constants import (
ANIME_GQL,
API_EPISODE_HEADERS,
API_GRAPHQL_ENDPOINT,
API_GRAPHQL_HEADERS,
API_GRAPHQL_REFERER,
EPISODE_GQL,
PERSISTED_QUERY_SHA256,
SEARCH_GQL,
TOBEPARSED_DECRYPTION_SEED,
)
from .mappers import (
map_to_anime_result,
map_to_search_results,
)
from .types import AllAnimeEpisode
from .utils import decode_tobeparsed
if TYPE_CHECKING:
from .types import AllAnimeEpisode
logger = logging.getLogger(__name__)
@@ -26,7 +32,7 @@ class AllAnime(BaseAnimeProvider):
HEADERS = {"Referer": API_GRAPHQL_REFERER}
@debug_provider
def search(self, params):
def search(self, params: SearchParams) -> SearchResults | None:
response = execute_graphql(
API_GRAPHQL_ENDPOINT,
self.client,
@@ -39,28 +45,94 @@ class AllAnime(BaseAnimeProvider):
},
"limit": params.page_limit,
"page": params.current_page,
"translationtype": params.translation_type,
"countryorigin": params.country_of_origin,
"translationType": params.translation_type,
"countryOrigin": params.country_of_origin,
},
headers=API_GRAPHQL_HEADERS
headers=API_GRAPHQL_HEADERS,
)
return map_to_search_results(response)
@debug_provider
def get(self, params):
def get(self, params: AnimeParams) -> Anime | None:
response = execute_graphql(
API_GRAPHQL_ENDPOINT,
self.client,
ANIME_GQL,
variables={"showId": params.id},
headers=API_GRAPHQL_HEADERS
headers=API_GRAPHQL_HEADERS,
)
return map_to_anime_result(response)
@debug_provider
def episode_streams(self, params):
def episode_streams(self, params: EpisodeStreamsParams) -> Iterator[Server] | None:
from .extractors import extract_server
episode = self._get_episode_payload(params)
if not episode:
logger.error(
f"Could not fetch streams for episode {params.episode} ({params.translation_type})"
)
return
sources = episode.get("sourceUrls") or []
if not sources:
logger.error(
f"No sources found for episode {params.episode} ({params.translation_type})"
)
return
for source in sources:
if server := extract_server(self.client, params.episode, episode, source):
yield server
def _extract_episode_from_payload(self, payload: dict[str, Any]) -> AllAnimeEpisode | None:
data = payload.get("data")
if not isinstance(data, dict):
return None
episode = data.get("episode")
if isinstance(episode, dict):
return episode # type: ignore[return-value]
encoded_payload = data.get("tobeparsed")
if not isinstance(encoded_payload, str):
return None
parsed_payload = decode_tobeparsed(encoded_payload, TOBEPARSED_DECRYPTION_SEED)
parsed_episode = parsed_payload.get("episode")
if isinstance(parsed_episode, dict):
return parsed_episode # type: ignore[return-value]
return None
def _get_episode_payload(self, params: EpisodeStreamsParams) -> AllAnimeEpisode | None:
persisted_query_response = self.client.get(
API_GRAPHQL_ENDPOINT,
params={
"variables": dumps(
{
"showId": params.anime_id,
"translationType": params.translation_type,
"episodeString": params.episode,
},
separators=(",", ":"),
),
"extensions": dumps(
{
"persistedQuery": {
"version": 1,
"sha256Hash": PERSISTED_QUERY_SHA256,
}
},
separators=(",", ":"),
),
},
headers={**API_GRAPHQL_HEADERS, **API_EPISODE_HEADERS},
)
persisted_query_response.raise_for_status()
if episode := self._extract_episode_from_payload(persisted_query_response.json()):
return episode
episode_response = execute_graphql(
API_GRAPHQL_ENDPOINT,
self.client,
@@ -70,12 +142,9 @@ class AllAnime(BaseAnimeProvider):
"translationType": params.translation_type,
"episodeString": params.episode,
},
headers=API_GRAPHQL_HEADERS
headers=API_GRAPHQL_HEADERS,
)
episode: AllAnimeEpisode = episode_response.json()["data"]["episode"]
for source in episode["sourceUrls"]:
if server := extract_server(self.client, params.episode, episode, source):
yield server
return self._extract_episode_from_payload(episode_response.json())
if __name__ == "__main__":
@@ -1,8 +1,14 @@
import functools
import hashlib
import json
import logging
import os
import re
from base64 import b64decode
from itertools import cycle
from typing import Any
from Cryptodome.Cipher import AES
logger = logging.getLogger(__name__)
@@ -73,6 +79,23 @@ def one_digit_symmetric_xor(password: int, target: str):
return bytes(genexp()).decode("utf-8")
def decode_tobeparsed(payload: str, key_seed: str) -> dict[str, Any]:
base64_padding = (-len(payload)) % 4
encrypted_payload = b64decode(payload + ("=" * base64_padding))
iv = encrypted_payload[1:13]
ciphertext = encrypted_payload[13:-16]
decryption_key = hashlib.sha256(key_seed.encode("utf-8")).digest()
plain_text = AES.new(
decryption_key,
AES.MODE_CTR,
nonce=iv,
initial_value=2,
).decrypt(ciphertext)
return json.loads(plain_text.decode("utf-8"))
def decode_hex_string(hex_string):
"""some of the sources encrypt the urls into hex codes this function decrypts the urls