diff --git a/tests/remote/unit/test_handlers_serialize.py b/tests/remote/unit/test_handlers_serialize.py index 57f0f2a2..52c986be 100644 --- a/tests/remote/unit/test_handlers_serialize.py +++ b/tests/remote/unit/test_handlers_serialize.py @@ -2,12 +2,16 @@ from __future__ import annotations +import io + import pytest +import requests from langcodes import Language from unshackle.core.api.errors import APIError, APIErrorCode from unshackle.core.api.handlers import ( drm_preference_name, + listed_titles, sanitize_log, search_handler, serialize_audio_track, @@ -135,6 +139,26 @@ def test_serialize_title_episode_unnamed_falls_back_to_number() -> None: assert d["name"] == "Episode 05" +def test_no_titles_is_a_service_error() -> None: + with pytest.raises(APIError) as exc: + listed_titles(None) + assert exc.value.error_code == APIErrorCode.SERVICE_ERROR + + +def test_init_segment_reports_the_http_status() -> None: + refused = requests.Response() + refused.status_code = 403 + refused.raw = io.BytesIO() + + class FakeSession(requests.Session): + def request(self, *args: object, **kwargs: object) -> requests.Response: + return refused + + track = Video(url="https://cdn.example/v.mp4", language=Language.get("en")) + with pytest.raises(ValueError, match="HTTP 403"): + track.get_init_segment(session=FakeSession()) + + def test_serialize_video_track_basic() -> None: d = serialize_video_track(video()) assert d["id"] == "video-001" diff --git a/unshackle/core/api/handlers.py b/unshackle/core/api/handlers.py index bfdbac31..05c7fefe 100644 --- a/unshackle/core/api/handlers.py +++ b/unshackle/core/api/handlers.py @@ -1077,6 +1077,13 @@ def part_key_suffix(part: Optional[int]) -> str: return f".{part}" if part is not None else "" +def listed_titles(titles: Any) -> list[Any]: + """The result of a service's get_titles() as a list.""" + if titles is None: + raise APIError(APIErrorCode.SERVICE_ERROR, "The service returned no titles.") + return list(titles) if hasattr(titles, "__iter__") and not isinstance(titles, str) else [titles] + + def serialize_title(title: Title_T) -> Dict[str, Any]: """Convert a title object to JSON-serializable dict.""" title_language = str(title.language) if hasattr(title, "language") and title.language else None @@ -1560,10 +1567,7 @@ async def list_titles_handler(data: Dict[str, Any], request: Optional[web.Reques ) titles = await asyncio.to_thread(service_instance.get_titles) - if hasattr(titles, "__iter__") and not isinstance(titles, str): - title_list = [stamp_service_flags(serialize_title(t), service_instance) for t in titles] - else: - title_list = [stamp_service_flags(serialize_title(titles), service_instance)] + title_list = [stamp_service_flags(serialize_title(t), service_instance) for t in listed_titles(titles)] return request_cache.json_response({"titles": title_list}) @@ -3558,10 +3562,7 @@ async def session_titles_handler(session_id: str, request: Optional[web.Request] session.touch() session.titles = titles - if hasattr(titles, "__iter__") and not isinstance(titles, str): - titles_list = list(titles) - else: - titles_list = [titles] + titles_list = listed_titles(titles) serialized_titles = [] for t in titles_list: diff --git a/unshackle/core/tracks/track.py b/unshackle/core/tracks/track.py index 6aff7909..0c2f7e3b 100644 --- a/unshackle/core/tracks/track.py +++ b/unshackle/core/tracks/track.py @@ -1095,6 +1095,9 @@ class Track: else: init_data = None s = session.get(url, stream=True) + if not s.ok: + s.close() + raise ValueError(f"Track URI returned HTTP {s.status_code}") for chunk in s.iter_content(content_length): init_data = chunk break