diff --git a/beetsplug/beets_music_videos/IMVDBApi.py b/beetsplug/beets_music_videos/IMVDBApi.py index 394329a..56fdbaf 100644 --- a/beetsplug/beets_music_videos/IMVDBApi.py +++ b/beetsplug/beets_music_videos/IMVDBApi.py @@ -1,18 +1,17 @@ -from typing import Any +from typing import Annotated, Any, Self + +from beets import logging +from httpx_auth import HeaderApiKey from lapidary.runtime import ( Body, ClientBase, Query, Response, Responses, - UnexpectedResponse, get, ) -from typing import Annotated, Any, Literal, Sequence, cast, Self -from httpx_auth import HeaderApiKey -from beetsplug.beets_music_videos.types import SearchResponseType, VideoType -from beets import logging +from beetsplug.beets_music_videos.types import SearchResponseType log = logging.getLogger("beets") @@ -46,12 +45,13 @@ class IMVDBApi(ClientBase): # type: ignore @get("/search/videos") # type: ignore[misc] def video_search( self: Self, + *, q: Annotated[str, Query()], ) -> Annotated[ SearchResponseType, Responses( responses={ - "2xx": Response(body=Body({"application/json": SearchResponseType})), + "2xx": Response(Body({"application/json": SearchResponseType})), } ), ]: @@ -60,35 +60,3 @@ class IMVDBApi(ClientBase): # type: ignore Implemented at runtime by Lapidary; this stub is never executed. """ raise NotImplementedError - - # def sync_video_search(self, query: str) -> list[VideoType]: - # """Sync wrapper used by the plugin. Runs video_search and returns .results.""" - # import asyncio - # # Reuse a single event loop to avoid "Event loop is closed" on subsequent calls. - # try: - # loop = asyncio.get_event_loop() - # if loop.is_closed(): - # loop = asyncio.new_event_loop() - # asyncio.set_event_loop(loop) - # except RuntimeError: - # loop = asyncio.new_event_loop() - # asyncio.set_event_loop(loop) - # try: - # resp = loop.run_until_complete(self.video_search(q=query)) - # return resp["results"] - # except UnexpectedResponse as e: - # # Lapidary raises UnexpectedResponse when body validation fails - # # (e.g. API shape differs from SearchResponseType). For 200, parse - # # body and return results. - # if e.response.status_code == 200: - # try: - # body = e.response.json() - # if isinstance(body, dict): - # return cast(list[VideoType], body["results"]) - # except Exception: - # pass - # log.debug("imvdb search failed for %r: %s", query[:50], e) - # return [] - # except Exception as e: - # log.debug("imvdb search failed for %r: %s", query[:50], e) - # return [] diff --git a/beetsplug/beets_music_videos/types.py b/beetsplug/beets_music_videos/types.py index 79ae3d9..ed396be 100644 --- a/beetsplug/beets_music_videos/types.py +++ b/beetsplug/beets_music_videos/types.py @@ -1,43 +1,48 @@ from __future__ import annotations +from collections.abc import Sequence +from typing import Annotated + +from lapidary.runtime import ModelBase +from pydantic import Field from typing_extensions import TypedDict class ImageSizes(TypedDict, total=False): """Image URL set; keys are size codes (o=original, l=large, b=big, t=thumb).""" - o: str - l: str - b: str - t: str + o: Annotated[str, Field(description="Original image URL")] + l: Annotated[str, Field(description="Large image URL")] + b: Annotated[str, Field(description="Big image URL")] + t: Annotated[str, Field(description="Thumbnail image URL")] -class ArtistType(TypedDict): - name: str - slug: str - url: str - discogs_id: int | None +class ArtistType(ModelBase): + name: Annotated[str, Field(description="Name")] + slug: Annotated[str, Field(description="Slug")] + url: Annotated[str, Field(description="URL")] + discogs_id: Annotated[int | None, Field(description="Discogs ID")] -class VideoType(TypedDict): - id: str - production_status: str - song_title: str - url: str - multiple_versions: bool - version_name: str | None - version_number: int - is_imvdb_pick: bool - aspect_ratio: str | None - year: int - verified_credits: bool - image: ImageSizes - artists: list[ArtistType] # API returns "artists", not "artist" +class VideoType(ModelBase): + id: Annotated[str, Field(description="Video ID")] + production_status: Annotated[str, Field(description="Production status")] + song_title: Annotated[str, Field(description="Song title")] + url: Annotated[str, Field(description="Video URL")] + multiple_versions: Annotated[bool, Field(description="Whether the video has multiple versions")] + version_name: Annotated[str | None, Field(description="Version name")] + version_number: Annotated[int, Field(description="Version number")] + is_imvdb_pick: Annotated[bool, Field(description="Whether the video is an IMVDB pick")] + aspect_ratio: Annotated[str | None, Field(description="Aspect ratio")] + year: Annotated[int, Field(description="Year")] + verified_credits: Annotated[bool, Field(description="Whether the video has verified credits")] + image: Annotated[ImageSizes, Field(description="Image sizes")] + artists: Annotated[Sequence[ArtistType], Field(description="List of artists")] # API returns "artists", not "artist" -class SearchResponseType(TypedDict): - total: int - current_page: int - total_pages: int - per_page: int - results: list[VideoType] +class SearchResponseType(ModelBase): + total: Annotated[int, Field(description="Total number of results")] + current_page: Annotated[int, Field(description="Current page number")] + total_pages: Annotated[int, Field(description="Total number of pages")] + per_page: Annotated[int, Field(description="Number of results per page")] + results: Annotated[Sequence[VideoType], Field(description="List of video results")]