"""AnimeTosho moved: animetosho.org froze, animetosho.xyz carries the live index. Measured 2026-09-14: the newest entry on ``feed.animetosho.org`` is dated 2026-05-09, while ``feed.animetosho.xyz`` was serving releases minutes old. Anything published after May was unreachable through this provider (GH #208). The successor is not a domain swap. Three things differ: 1. The attachment URL is no longer derivable from the attachment id. ``.org`` sharded as ``/storage/attach/{id:08x}/{id}.xz``; ``.xyz`` serves ``storage.animetosho.xyz/attachments/10003/1f0e.xz`` for attachment 3586174 and ``.../1f12.xz`` for 3586175 — a step of four for an id step of one. It has to be read from the response, which now carries a ``url``. 2. The attachment ``info`` block renamed its keys: ``codec`` -> ``format``, ``lang`` -> ``language_code``, ``name`` -> ``language``. 3. The payload lives on its own host, so the P1 allowlist needs it. Both schemas are accepted — ``.org`` may come back, and the parser has no reason to be brittle about which spelling it gets. """ import os import sys from unittest.mock import MagicMock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from providers.animetosho import ATTACH_BASE, FEED_API, AnimeToshoProvider from providers.base import SubtitleFormat, VideoQuery from security_utils import validate_download_url XYZ_STORAGE_URL = "https://storage.animetosho.xyz/attachments/10003/1f0e.xz" def _xyz_attachment(): """An attachment exactly as feed.animetosho.xyz returns it.""" return { "id": 3586174, "type": "subtitle", "size": None, "url": XYZ_STORAGE_URL, "info": { "default": True, "forced": False, "format": "ASS", "language": "English", "language_code": "eng", }, } def _org_attachment(): """An attachment as the frozen feed.animetosho.org still returns it.""" return { "id": 2758355, "type": "subtitle", "size": 25534, "info": { "codec": "ASS", "lang": "eng", "name": "English", "default": 0, "enabled": 1, "forced": 0, "trackid": 4, "tracknum": 5, }, } def _provider_returning(attachment): """A provider whose detail call yields one file holding ``attachment``.""" provider = AnimeToshoProvider() provider.session = MagicMock() detail = MagicMock() detail.status_code = 200 detail.json.return_value = {"files": [{"filename": "ep01.mkv", "attachments": [attachment]}]} provider.session.get.return_value = detail return provider def _entry(): return {"id": 687355, "title": "[BlackRabbit] Frieren - S01E01 [Bluray-1080p]", "num_files": 28} def _query(): return VideoQuery(series_title="Frieren", episode=1, languages=["en"]) def test_the_search_feed_points_at_the_live_index(): """Querying the frozen index returns nothing published after May 2026.""" assert "animetosho.xyz" in FEED_API def test_an_attachment_carrying_its_own_url_is_downloaded_from_there(): """The .xyz sharding is not derivable from the id — it must be read, not built.""" provider = _provider_returning(_xyz_attachment()) results = provider._process_entry(_entry(), _query()) assert len(results) == 1 assert results[0].download_url == XYZ_STORAGE_URL def test_the_renamed_attachment_info_keys_are_understood(): """``format``/``language_code`` carry what ``codec``/``lang`` used to.""" provider = _provider_returning(_xyz_attachment()) results = provider._process_entry(_entry(), _query()) assert len(results) == 1 assert results[0].format == SubtitleFormat.ASS assert results[0].language == "en" def test_the_legacy_attachment_schema_still_resolves(): """Regression guard: .org's spelling and its derived URL keep working.""" provider = _provider_returning(_org_attachment()) results = provider._process_entry(_entry(), _query()) assert len(results) == 1 assert results[0].format == SubtitleFormat.ASS assert results[0].language == "en" assert results[0].download_url == f"{ATTACH_BASE}/002a16d3/2758355.xz" def test_the_xyz_storage_host_is_on_the_download_allowlist(): """P1 blocks any host it was not told about — the payload moved off the main domain.""" ok, err = validate_download_url(XYZ_STORAGE_URL, "animetosho") assert ok is True, err def test_the_xyz_main_domain_is_on_the_download_allowlist(): ok, err = validate_download_url("https://animetosho.xyz/view/687355", "animetosho") assert ok is True, err def test_the_net_storage_host_is_on_the_download_allowlist(): """The third spelling, found the expensive way. Prod 2026-09-27/28 rejected 81 downloads in 24 h with "Download URL domain 'storage.animetosho.net' is not in the allowlist" while .org-hosted results kept succeeding — 425 of them that month — so the provider looked healthy in every count that mattered. An index hit hands out whichever storage host it likes; the allowlist has to know all of them or the search was wasted at the last step. """ ok, err = validate_download_url( "https://storage.animetosho.net/attachments/10003/1f0e.xz", "animetosho" ) assert ok is True, err def test_an_unrelated_host_is_still_rejected(): """The allowlist is still an allowlist — three entries, not a suffix match.""" ok, _err = validate_download_url("https://animetosho.evil.com/x.xz", "animetosho") assert ok is False # --------------------------------------------------------------------------- # Repair — a stored id no longer yields the URL on its own # --------------------------------------------------------------------------- def test_a_repair_asks_the_api_where_the_attachment_lives(): """History stores ``entry_id:attach_id``; on .xyz that is not enough to rebuild the URL, so the detail call has to supply it.""" provider = _provider_returning(_xyz_attachment()) result = provider.result_for_download("687355:3586174", "en") assert result.download_url == XYZ_STORAGE_URL def test_a_repair_falls_back_to_the_derived_url_without_a_session(): """Regression guard: the .org derivation still answers when no API is reachable.""" result = AnimeToshoProvider().result_for_download("687242:2376990", "de") assert result.download_url == f"{ATTACH_BASE}/0024451e/2376990.xz" assert result.provider_data["is_xz"] is True # --------------------------------------------------------------------------- # A url that is a file name, not a storage location # --------------------------------------------------------------------------- MALFORMED_URL = ( "https://animetosho.net/%5BDelta%5D%20The%20Detective%20Is%20Already%20Dead%20S01E12" "%20%5B1080p%20CR-WEB-DL%20AAC-192%20H.264%5D_track13.ger.ass" ) def _malformed_attachment(): attachment = _xyz_attachment() attachment["url"] = MALFORMED_URL return attachment def test_an_attachment_whose_url_is_only_a_file_name_is_skipped(): """The .xyz feed hands out ``https://animetosho.net/`` for some attachments — a page that does not exist. Prod 2026-10-01..08: 11 downloads picked such a candidate and died with a 404 after the search had already ranked it. The id cannot rebuild an .xyz location either, so the candidate is unusable and must not compete.""" provider = _provider_returning(_malformed_attachment()) assert provider._process_entry(_entry(), _query()) == [] def test_a_repair_does_not_take_a_file_name_url_for_a_location(): provider = _provider_returning(_malformed_attachment()) result = provider.result_for_download("687355:3586174", "en") assert result.download_url != MALFORMED_URL