From 64e29c2d5ad9411fcde43fa641373db0c50fb07c Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Sat, 11 Jul 2026 13:29:38 +0200 Subject: [PATCH] fix(parser): handle movie results in search and rss --- backend/src/module/api/rss.py | 11 ++++++----- backend/src/module/searcher/searcher.py | 6 +++--- backend/src/test/test_api_rss.py | 16 +++++++++++++++- backend/src/test/test_searcher.py | 20 +++++++++++++++++++- backend/src/test/test_title_parser.py | 4 ++++ 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/backend/src/module/api/rss.py b/backend/src/module/api/rss.py index 5f930b3c..a115a593 100644 --- a/backend/src/module/api/rss.py +++ b/backend/src/module/api/rss.py @@ -5,7 +5,7 @@ from module.conf.search_provider import get_provider from module.database import Database, get_db from module.downloader import DownloadClient from module.manager import SeasonCollector -from module.models import APIResponse, Bangumi, RSSItem, RSSUpdate, Torrent +from module.models import APIResponse, Bangumi, Movie, RSSItem, RSSUpdate, Torrent from module.rss import RSSAnalyser, RSSEngine from module.security.api import get_current_user @@ -185,14 +185,15 @@ analyser = RSSAnalyser() @router.post( - "/analysis", response_model=Bangumi, dependencies=[Depends(get_current_user)] + "/analysis", + response_model=Bangumi | Movie, + dependencies=[Depends(get_current_user)], ) async def analysis(rss: RSSItem): data = await analyser.link_to_data(rss) - if isinstance(data, Bangumi): + if isinstance(data, (Bangumi, Movie)): return data - else: - return u_response(data) + return u_response(data) @router.post( diff --git a/backend/src/module/searcher/searcher.py b/backend/src/module/searcher/searcher.py index 4a0a267b..f642f438 100644 --- a/backend/src/module/searcher/searcher.py +++ b/backend/src/module/searcher/searcher.py @@ -4,7 +4,7 @@ from collections import OrderedDict from typing import TypeAlias from module.conf import settings -from module.models import Bangumi, RSSItem, Torrent +from module.models import Bangumi, Movie, RSSItem, Torrent from module.network import RequestContent from module.parser.analyser.tmdb_parser import tmdb_parser from module.rss import RSSAnalyser @@ -109,8 +109,8 @@ class SearchTorrent: yield json.dumps(bangumi.dict(), separators=(",", ":")) @staticmethod - def special_url(data: Bangumi, site: str) -> RSSItem: - keywords = [getattr(data, key) for key in SEARCH_KEY if getattr(data, key)] + def special_url(data: Bangumi | Movie, site: str) -> RSSItem: + keywords = [value for key in SEARCH_KEY if (value := getattr(data, key, None))] url = search_url(site, keywords) return url diff --git a/backend/src/test/test_api_rss.py b/backend/src/test/test_api_rss.py index cdf8206c..d40eb636 100644 --- a/backend/src/test/test_api_rss.py +++ b/backend/src/test/test_api_rss.py @@ -8,7 +8,7 @@ from fastapi.testclient import TestClient from module.api import v1 from module.database import get_db -from module.models import Bangumi, ResponseModel, RSSItem, RSSUpdate, Torrent +from module.models import Bangumi, Movie, ResponseModel, RSSItem, RSSUpdate, Torrent from module.security.api import get_current_user from test.factories import make_bangumi, make_rss_item, make_torrent @@ -307,6 +307,20 @@ class TestAnalysis: data = response.json() assert data["official_title"] == "Parsed Anime" + def test_analysis_returns_movie(self, authed_client): + """POST /rss/analysis preserves a parsed movie response.""" + movie = Movie(official_title="Parsed Movie", title_raw="Parsed Movie") + with patch("module.api.rss.analyser") as mock_analyser: + mock_analyser.link_to_data = AsyncMock(return_value=movie) + + response = authed_client.post( + "/api/v1/rss/analysis", + json={"url": "https://mikanani.me/RSS/link", "parser": "mikan"}, + ) + + assert response.status_code == 200 + assert response.json()["official_title"] == "Parsed Movie" + # --------------------------------------------------------------------------- # POST /rss/collect diff --git a/backend/src/test/test_searcher.py b/backend/src/test/test_searcher.py index befb0e31..6644016b 100644 --- a/backend/src/test/test_searcher.py +++ b/backend/src/test/test_searcher.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch import pytest from module.conf import settings -from module.models import Bangumi, RSSItem +from module.models import Bangumi, Movie, RSSItem from module.searcher.provider import search_url # --------------------------------------------------------------------------- @@ -165,6 +165,24 @@ class TestSpecialUrl: # Only title_raw should be in the URL assert "Test" in result.url + def test_supports_movie_without_season_fields(self): + """Movie search results do not expose Bangumi-only season fields.""" + from module.searcher.searcher import SearchTorrent + + movie = Movie( + official_title="Movie Title", + title_raw="Movie Title", + group_name="MovieGroup", + dpi="1080p", + ) + with patch( + "module.searcher.provider.SEARCH_CONFIG", + {"mikan": "https://mikanani.me/RSS/Search?searchstr=%s"}, + ): + result = SearchTorrent.special_url(movie, "mikan") + + assert "Movie" in result.url + # --------------------------------------------------------------------------- # _poster_cache: bounded LRU + reset_cache() diff --git a/backend/src/test/test_title_parser.py b/backend/src/test/test_title_parser.py index f8f5b1ff..e3f04a81 100644 --- a/backend/src/test/test_title_parser.py +++ b/backend/src/test/test_title_parser.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest from module.conf import settings +from module.models import Bangumi from module.models.bangumi import Episode from module.models.config import LLM, ExperimentalOpenAI from module.parser import title_parser as title_parser_module @@ -34,6 +35,7 @@ class TestTitleParser: async def test_parse_without_llm(self): result = await TitleParser.raw_parser(RAW_TITLE) assert result is not None + assert isinstance(result, Bangumi) assert result.group_name == "梦蓝字幕组" assert result.title_raw == "New Doraemon" assert result.dpi == "1080P" @@ -47,6 +49,7 @@ class TestTitleParser: async def test_parse_with_llm(self): result = await TitleParser.raw_parser(RAW_TITLE) assert result is not None + assert isinstance(result, Bangumi) assert result.group_name == "梦蓝字幕组" assert result.title_raw == "New Doraemon" assert result.dpi == "1080P" @@ -144,6 +147,7 @@ class TestRawParserLLMPrimaryMode: result = await TitleParser.raw_parser("[Group] LLM Title Movie [1080P]") assert result is not None + assert isinstance(result, Bangumi) assert result.episode_type == "movie" async def test_llm_failure_falls_back_to_regex(self):