From b36e0970b68650088916e6ea9eab3a10e9c777b0 Mon Sep 17 00:00:00 2001 From: mzmonkey <75185931+mzmonkey@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:27:06 +0200 Subject: [PATCH] fix: solve the s.to captcha modal when a mirror answers the redirect --- src/mediaforge/models/s_to/episode.py | 8 +++-- tests/test_sto_provider_redirect.py | 46 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/test_sto_provider_redirect.py diff --git a/src/mediaforge/models/s_to/episode.py b/src/mediaforge/models/s_to/episode.py index 1db2f33..6c6e25b 100644 --- a/src/mediaforge/models/s_to/episode.py +++ b/src/mediaforge/models/s_to/episode.py @@ -303,7 +303,7 @@ def redirect_url(self): @property def provider_url(self): if self.__provider_url is None: - from urllib.parse import urlparse + from ...mirrors import site_for_url try: from ...playwright.captcha import solve_sto_modal except ImportError: @@ -311,7 +311,11 @@ def provider_url(self): # Try plain HTTP first — works when no modal is shown resp = GLOBAL_SESSION.get(self.redirect_url) - if urlparse(resp.url).netloc != urlparse(self.redirect_url).netloc: + # Compare against every s.to mirror, not the redirect's own host: + # the mirror failover may answer from another one (serienstream.to + # for a dead s.to, or the bare-IP mirror), and that is still the + # s.to page with its captcha modal, not the provider. + if site_for_url(resp.url) != "sto": # Redirect left s.to — no modal, plain session worked self.__provider_url = resp.url else: diff --git a/tests/test_sto_provider_redirect.py b/tests/test_sto_provider_redirect.py new file mode 100644 index 0000000..a02fe99 --- /dev/null +++ b/tests/test_sto_provider_redirect.py @@ -0,0 +1,46 @@ +"""SerienstreamEpisode.provider_url must only skip the captcha modal once the +redirect has actually left s.to -- not when the mirror failover merely answered +from another s.to mirror, whose page still carries the modal and no provider. +""" + +from types import SimpleNamespace + +import pytest + +from mediaforge.models.s_to import episode as episode_module +from mediaforge.playwright import captcha + +EPISODE = "https://serienstream.to/serie/dragonball-z/staffel-7/episode-19" +PROVIDER = "https://voe.sx/e/abc123" + + +@pytest.fixture +def resolve(monkeypatch): + """provider_url for a redirect built on *redirect_host* that the session + answered from *final_url*; returns (provider_url, solver was called).""" + def run(redirect_host, final_url): + solved = [] + monkeypatch.setattr(episode_module, "GLOBAL_SESSION", + SimpleNamespace(get=lambda url, **kw: SimpleNamespace(url=final_url))) + monkeypatch.setattr(captcha, "solve_sto_modal", lambda *a, **kw: solved.append(a) or PROVIDER) + ep = episode_module.SerienstreamEpisode(url=EPISODE, selected_language="German Dub", + selected_provider="VOE") + ep._SerienstreamEpisode__redirect_url = f"https://{redirect_host}/r?t=token" + return ep.provider_url, bool(solved) + return run + + +def test_a_redirect_that_reached_the_provider_is_taken_as_is(resolve): + assert resolve("serienstream.to", PROVIDER) == (PROVIDER, False) + + +def test_the_bare_ip_mirror_is_still_sto_and_gets_the_modal_solved(resolve): + assert resolve("serienstream.to", "https://186.2.175.5/r?t=token") == (PROVIDER, True) + + +def test_a_dead_primary_failing_over_to_serienstream_is_still_sto(resolve): + assert resolve("s.to", "https://serienstream.to/r?t=token") == (PROVIDER, True) + + +def test_staying_on_the_same_mirror_still_gets_the_modal_solved(resolve): + assert resolve("serienstream.to", "https://serienstream.to/r?t=token") == (PROVIDER, True)