From f7cacf828d6e8d7f90bd9c8cdae34e1bbf8d3120 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Mon, 10 Aug 2026 22:24:40 +0300 Subject: [PATCH 1/5] add OMDB api --- backend/backlog_app/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/backlog_app/config.py b/backend/backlog_app/config.py index ec7fa08..9b32605 100644 --- a/backend/backlog_app/config.py +++ b/backend/backlog_app/config.py @@ -83,6 +83,11 @@ class AIAgentConfig(BaseModel): timeout: int = 10 +class OmdbConfig(BaseModel): + base_url: str + api_key: str + + class Settings(BaseSettings): model_config = SettingsConfigDict( case_sensitive=False, @@ -137,7 +142,7 @@ def settings_customise_sources( smtp: SMTPConfig ai_agent: AIAgentConfig cors_origins: list[str] = ["http://localhost:5173"] - imdb_url: str = "https://api.imdbapi.dev" + omdb: OmdbConfig settings = Settings() From 6f1ae47196649f7696a0f95d3956159a2d021c02 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Mon, 10 Aug 2026 22:25:07 +0300 Subject: [PATCH 2/5] change provider --- .../servicies/imdb_api/provider.py | 140 ++++++++---------- backend/backlog_app/tasks/movie_task.py | 2 +- 2 files changed, 59 insertions(+), 83 deletions(-) diff --git a/backend/backlog_app/servicies/imdb_api/provider.py b/backend/backlog_app/servicies/imdb_api/provider.py index b109144..b81a188 100644 --- a/backend/backlog_app/servicies/imdb_api/provider.py +++ b/backend/backlog_app/servicies/imdb_api/provider.py @@ -1,8 +1,8 @@ import logging -from http import HTTPMethod from typing import Any import httpx +from config import settings logger = logging.getLogger(__name__) @@ -18,119 +18,95 @@ class TitleNotFoundError(IMDBProviderError): class IMDBProvider: def __init__( self, - base_url: str, + api_key: str, client: httpx.AsyncClient | None = None, ) -> None: - self.base_url = base_url.rstrip("/") + self.api_key = api_key self.client = client or httpx.AsyncClient() - async def _request( - self, - method: HTTPMethod, - endpoint: str, - *, - params: dict[str, Any] | None = None, - data: dict[str, Any] | list[Any] | None = None, - ) -> dict[str, Any]: + async def _request(self, params: dict[str, Any]) -> dict[str, Any]: try: - response = await self.client.request( - method=method, - url=f"{self.base_url}/{endpoint}", - params=params, - json=data, + response = await self.client.get( + settings.omdb.base_url, + params={"apikey": self.api_key, **params}, ) - response.raise_for_status() + data = response.json() - return response.json() + if data.get("Response") == "False": + raise TitleNotFoundError(data.get("Error", "Not found")) + + return data + + except TitleNotFoundError: + raise except httpx.HTTPStatusError as e: - logger.exception( - "IMDB API returned HTTP %s", - e.response.status_code, - ) + logger.exception("OMDb API returned HTTP %s", e.response.status_code) raise IMDBProviderError( - f"IMDB API returned {e.response.status_code}" + f"OMDb API returned {e.response.status_code}" ) from e except httpx.HTTPError as e: - logger.exception("IMDB API request failed") - raise IMDBProviderError("IMDB API request failed") from e + logger.exception("OMDb API request failed") + raise IMDBProviderError("OMDb API request failed") from e async def search_title( self, title: str, - limit: int = 10, + year: int | None = None, ) -> list[dict[str, Any]]: - response = await self._request( - HTTPMethod.GET, - endpoint="search/titles", - params={ - "query": title, - "limit": limit, - }, - ) - - return response.get("titles", []) + params: dict[str, Any] = {"s": title} + if year is not None: + params["y"] = year + response = await self._request(params) + return response.get("Search", []) async def get_title_id( self, title: str, year: int | None = None, ) -> str: - titles = await self.search_title(title) + results = await self.search_title(title, year) - if not titles: + if not results and year is not None: + logger.warning( + "No results for '%s' (%s), retrying without year filter", + title, + year, + ) + results = await self.search_title(title) + + if not results: raise TitleNotFoundError(f"Title '{title}' not found") - if year is not None: - for item in titles: - if item.get("startYear") == year: - logger.debug( - "Found title '%s' by year %s", - title, - year, - ) - return item["id"] + return results[0]["imdbID"] - def popularity_score(item: dict[str, Any]) -> float: - rating = item.get("rating", {}).get("aggregateRating", 0) + async def get_title( + self, + title: str, + year: int | None = None, + ) -> dict[str, Any]: + title_id = await self.get_title_id(title=title, year=year) - votes = item.get("rating", {}).get("voteCount", 0) + logger.debug("Fetching OMDb title %s for '%s'", title_id, title) - return rating * votes + raw = await self._request({"i": title_id, "plot": "full"}) + return self._normalize(raw) - best_match = max( - titles, - key=popularity_score, - ) + def _normalize(self, raw: dict[str, Any]) -> dict[str, Any]: + result: dict[str, Any] = {} - logger.warning( - "No exact year match for '%s' (%s), " "using most popular result '%s'", - title, - year, - best_match.get("primaryTitle"), - ) + imdb_rating = raw.get("imdbRating") + if imdb_rating and imdb_rating != "N/A": + result["rating"] = {"aggregateRating": float(imdb_rating)} - return best_match["id"] + metascore = raw.get("Metascore") + if metascore and metascore != "N/A": + result["metacritic"] = {"score": int(metascore)} - async def get_title( - self, - title: str, - year: int | None = None, - ) -> dict[str, Any]: - title_id = await self.get_title_id( - title=title, - year=year, - ) - - logger.debug( - "Fetching imdb title %s for '%s'", - title_id, - title, - ) - - return await self._request( - HTTPMethod.GET, - endpoint=f"titles/{title_id}", - ) + plot = raw.get("Plot") + if plot and plot != "N/A": + result["plot"] = plot + + return result diff --git a/backend/backlog_app/tasks/movie_task.py b/backend/backlog_app/tasks/movie_task.py index eb876e5..2d11731 100644 --- a/backend/backlog_app/tasks/movie_task.py +++ b/backend/backlog_app/tasks/movie_task.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) -provider = IMDBProvider(base_url=settings.imdb_url) +provider = IMDBProvider(api_key=settings.omdb.api_key) translator = TranslationService() From 1b2ce335506bd1d9e80a1e9b12bf2a002e3f73b3 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Mon, 10 Aug 2026 22:25:16 +0300 Subject: [PATCH 3/5] edit tests --- .../test_imdb_api/test_provider.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/backend/tests/test_servicies/test_imdb_api/test_provider.py b/backend/tests/test_servicies/test_imdb_api/test_provider.py index f1e846a..ac49d2f 100644 --- a/backend/tests/test_servicies/test_imdb_api/test_provider.py +++ b/backend/tests/test_servicies/test_imdb_api/test_provider.py @@ -1,4 +1,3 @@ -from http import HTTPMethod from unittest.mock import AsyncMock import pytest @@ -16,7 +15,7 @@ ) @pytest.mark.asyncio async def test_get_title_id(title, year): - imdb = IMDBProvider(base_url=settings.imdb_url) + imdb = IMDBProvider(api_key=settings.omdb.api_key) title_id = await imdb.get_title_id(title, year) assert title_id is not None @@ -25,10 +24,10 @@ async def test_get_title_id(title, year): @pytest.mark.asyncio async def test_get_title_success(monkeypatch): - imdb = IMDBProvider(base_url="https://mocked-api.com") + imdb = IMDBProvider(api_key="test-key") mock_get_id = AsyncMock(return_value="tt0816692") - mock_request = AsyncMock(return_value={"id": "tt0816692"}) + mock_request = AsyncMock(return_value={"imdbID": "tt0816692", "Response": "True"}) monkeypatch.setattr(imdb, "get_title_id", mock_get_id) monkeypatch.setattr(imdb, "_request", mock_request) @@ -36,21 +35,19 @@ async def test_get_title_success(monkeypatch): result = await imdb.get_title("Interstellar", 2014) mock_get_id.assert_awaited_once_with(title="Interstellar", year=2014) - mock_request.assert_awaited_once_with( - HTTPMethod.GET, - endpoint="titles/tt0816692", - ) + mock_request.assert_awaited_once_with({"i": "tt0816692", "plot": "full"}) - assert result == {"id": "tt0816692"} + assert isinstance(result, dict) @pytest.mark.asyncio async def test_get_title_with_rating_and_metacritic(monkeypatch): - imdb = IMDBProvider(base_url="https://mocked-api.com") + imdb = IMDBProvider(api_key="test-key") mock_title_data = { - "rating": {"aggregateRating": 8.6}, - "metacritic": {"score": 74}, + "imdbRating": "8.6", + "Metascore": "74", + "Response": "True", } monkeypatch.setattr(imdb, "get_title_id", AsyncMock(return_value="tt0816692")) @@ -64,9 +61,13 @@ async def test_get_title_with_rating_and_metacritic(monkeypatch): @pytest.mark.asyncio async def test_get_title_without_metacritic(monkeypatch): - imdb = IMDBProvider(base_url="https://mocked-api.com") + imdb = IMDBProvider(api_key="test-key") - mock_title_data = {"rating": {"aggregateRating": 7.1}} + mock_title_data = { + "imdbRating": "7.1", + "Metascore": "N/A", + "Response": "True", + } monkeypatch.setattr(imdb, "get_title_id", AsyncMock(return_value="tt0317219")) monkeypatch.setattr(imdb, "_request", AsyncMock(return_value=mock_title_data)) @@ -79,10 +80,10 @@ async def test_get_title_without_metacritic(monkeypatch): @pytest.mark.asyncio async def test_get_title_empty_data(monkeypatch): - imdb = IMDBProvider(base_url="https://mocked-api.com") + imdb = IMDBProvider(api_key="test-key") monkeypatch.setattr(imdb, "get_title_id", AsyncMock(return_value="tt0000000")) - monkeypatch.setattr(imdb, "_request", AsyncMock(return_value={})) + monkeypatch.setattr(imdb, "_request", AsyncMock(return_value={"Response": "True"})) result = await imdb.get_title("Unknown", 0) From 58779a045e62032f514dd579f81fcf58e42f38ea Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Mon, 10 Aug 2026 22:29:12 +0300 Subject: [PATCH 4/5] fix CI --- .github/workflows/python-check.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-check.yml b/.github/workflows/python-check.yml index 70202f2..eb02603 100644 --- a/.github/workflows/python-check.yml +++ b/.github/workflows/python-check.yml @@ -75,7 +75,8 @@ jobs: BACKLOG__AI_AGENT__BASE_URL: https://example.com BACKLOG__AI_AGENT__ACCESS_ID: 1234 BACKLOG__AI_AGENT__TOKEN: secret3 - + BACKLOG__OMDB__BASE_URL: "https://www.omdbapi.com" + BACKLOG__OMDB__API_KEY: ${{ secrets.OMDB_API_KEY }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: From 2698317cd813f73566ac7bd4e614aedcb1c747b1 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Mon, 10 Aug 2026 22:31:34 +0300 Subject: [PATCH 5/5] fix import --- backend/backlog_app/servicies/imdb_api/provider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/backlog_app/servicies/imdb_api/provider.py b/backend/backlog_app/servicies/imdb_api/provider.py index b81a188..b0ef42a 100644 --- a/backend/backlog_app/servicies/imdb_api/provider.py +++ b/backend/backlog_app/servicies/imdb_api/provider.py @@ -2,7 +2,8 @@ from typing import Any import httpx -from config import settings + +from backlog_app.config import settings logger = logging.getLogger(__name__)