diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b6f1b20..69fea58 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -104,6 +104,8 @@ "sphinx-autobuild", "docs/", "docs/_build/html", + "--watch", + "compuglobal/", "--open-browser" ], "type": "shell", diff --git a/compuglobal/__init__.py b/compuglobal/__init__.py index 37e63af..eb64922 100644 --- a/compuglobal/__init__.py +++ b/compuglobal/__init__.py @@ -11,7 +11,7 @@ from compuglobal.errors import APIPageStatusError, NoSearchResultsFoundError from compuglobal.models.comic import ComicLayout, ComicOverlay, ComicPanel, ComicStrip from compuglobal.models.episode import Episode, EpisodeMetadata, EpisodeSummary -from compuglobal.models.font import FontAlignment, FontColorRGB, FontFamily +from compuglobal.models.font import FontAlignment, FontColor, FontFamily from compuglobal.models.frame import Frame, FrameResult from compuglobal.models.overlay import OverlayFormat from compuglobal.models.screencap import Screencap, ScreencapMoment @@ -22,7 +22,7 @@ __title__ = "compuglobal" __author__ = "MitchellAW" __license__ = "MIT" -__version__ = "0.3.8" +__version__ = "0.4.0" __all__ = [ "APIPageStatusError", @@ -36,7 +36,7 @@ "EpisodeMetadata", "EpisodeSummary", "FontAlignment", - "FontColorRGB", + "FontColor", "FontFamily", "Frame", "FrameResult", diff --git a/compuglobal/aio.py b/compuglobal/aio.py index 8db766f..303ca41 100644 --- a/compuglobal/aio.py +++ b/compuglobal/aio.py @@ -67,11 +67,11 @@ def __init__(self, session: aiohttp.ClientSession) -> None: async def get_screencap( self, + *, episode: str | None = None, timestamp: int | None = None, - frame: Frame | None = None, ) -> Screencap: - """Get the screencap for the given episode & timestamp, or a screencap of the Frame object. + """Get the screencap for the given episode & timestamp. Parameters ---------- @@ -79,32 +79,14 @@ async def get_screencap( An episode key timestamp : int | None, optional A timestamp of the screencap - frame : Frame | None, optional - A Frame object Returns ------- Screencap The screencap for the given episode key and timestamp. - Raises - ------ - TypeError - Must give only episode + timestamp, or a Frame object. - """ - if isinstance(episode, str) and isinstance(timestamp, int): - params = {"e": episode, "t": timestamp, "nearby": 1} - - elif isinstance(frame, Frame): - params = {"e": frame.key, "t": frame.timestamp, "nearby": 1} - - else: - invalid_args_error = ( - f"Expected str and int or compuglobal.Frame, but received {type(episode)}," - f" {type(timestamp)} and {type(frame)} instead" - ) - raise TypeError(invalid_args_error) + params = {"e": episode, "t": timestamp, "nearby": 1} request = self.discovery.CAPTION.build_request(self.client.base_url, query=params) caption = await self.client.handle_request(request) @@ -182,7 +164,7 @@ async def search_for_screencap( """ search_results = await self.search(search_text, season_minimum=season_minimum, season_maximum=season_maximum) result = search_results[0] - return await self.get_screencap(result.key, result.timestamp) + return await self.get_screencap(episode=result.key, timestamp=result.timestamp) async def get_random_screencap( self, @@ -348,7 +330,7 @@ async def get_comic_panel_url( panel = ComicPanel.from_screencap(screencap=screencap, overlay_format=overlay_format) - params = {"b64": panel.get_encoded()} + params = {"b64": panel.encoded} return self.media.COMIC_PANEL.build_encoded_url(self.client.base_url, query=params) async def get_comic_strip_url( @@ -378,7 +360,7 @@ async def get_comic_strip_url( screencap, subtitles, overlay_format = self._resolve_overlay_inputs(screencap, subtitles, overlay_format) comic_strip = ComicStrip.from_screencap(screencap=screencap, overlay_format=overlay_format) - params = {"b64": comic_strip.get_encoded(), "layout": comic_strip.layout} + params = {"b64": comic_strip.encoded, "layout": comic_strip.layout} return self.media.COMIC_STRIP.build_encoded_url(self.client.base_url, query=params) async def get_comic_maker_url( @@ -412,7 +394,7 @@ async def get_comic_maker_url( return self.media.COMIC_MAKER.build_encoded_url( base_url=self.BASE_URL, path_params=path_params, - query={"b64": strip.get_encoded(), "layout": strip.layout}, + query={"b64": strip.encoded, "layout": strip.layout}, ) async def get_gif_url( @@ -482,8 +464,8 @@ async def get_gif_maker_url( path_params = { "key": screencap.frame.key, - "start_timestamp": screencap.get_start(), - "end_timestamp": screencap.get_end(), + "start_timestamp": screencap.start, + "end_timestamp": screencap.end, } stream = Stream.from_screencap(screencap=screencap, overlay_format=overlay_format) @@ -491,7 +473,7 @@ async def get_gif_maker_url( return self.media.GIF_MAKER.build_encoded_url( base_url=self.BASE_URL, path_params=path_params, - query={"b64": stream.get_encoded()}, + query={"b64": stream.encoded}, ) @overload diff --git a/compuglobal/models/comic.py b/compuglobal/models/comic.py index 13350a6..f1f3f30 100644 --- a/compuglobal/models/comic.py +++ b/compuglobal/models/comic.py @@ -184,7 +184,8 @@ def from_screencap( return cls(e=screencap.frame.key, ts=screencap.frame.timestamp, o=overlays) - def get_encoded(self) -> str: + @property + def encoded(self) -> str: """Get the base 64 encoded representation of this panel. Returns @@ -281,7 +282,8 @@ def build_comic_overlays( for subtitle, overlay_format in zip(subtitles, overlay_formats, strict=True) ] - def get_encoded(self) -> str: + @property + def encoded(self) -> str: """Get the base 64 encoded representation of this comic strip. Returns diff --git a/compuglobal/models/font.py b/compuglobal/models/font.py index 1516377..29a962e 100644 --- a/compuglobal/models/font.py +++ b/compuglobal/models/font.py @@ -42,7 +42,7 @@ class FontAlignment(StrEnum): ALIGN_CENTER = "c" -class FontColorRGB(BaseCompuGlobalModel): +class FontColor(BaseCompuGlobalModel): """A color for a font. Attributes @@ -53,18 +53,19 @@ class FontColorRGB(BaseCompuGlobalModel): The amount of green in the color (0-255) blue : int The amount of blue in the color (0-255) - alpha : int + alpha : int, optional The amount of alpha transparency in the color (0-255) """ - red: int = Field(alias="r", ge=0, le=255) - green: int = Field(alias="g", ge=0, le=255) - blue: int = Field(alias="b", ge=0, le=255) - alpha: int = Field(alias="a", ge=0, le=255) + red: int = Field(alias="r", ge=0, le=255, default=255) + green: int = Field(alias="g", ge=0, le=255, default=255) + blue: int = Field(alias="b", ge=0, le=255, default=255) + alpha: int = Field(alias="a", ge=0, le=255, default=255) - def get_rgba(self) -> list[int]: - """Get a list of the rgba values. + @property + def rgba(self) -> list[int]: + """The font color as a list of the rgba values. Returns ------- @@ -73,3 +74,74 @@ def get_rgba(self) -> list[int]: """ return [self.red, self.green, self.blue, self.alpha] + + @property + def hex(self) -> str: + """The font color as a hex string. + + Returns + ------- + str + The color hex code + + """ + return f"{self.red:02x}{self.green:02x}{self.blue:02x}{self.alpha:02x}" + + @classmethod + def from_rgba(cls, r: int, g: int, b: int, a: int) -> "FontColor": + """Create a FontColor from rgba. + + Parameters + ---------- + r : int + Red (0-255) + g : int + Green (0-255) + b : int + Blue (0-255) + a : int + Alpha (0-255) + + Returns + ------- + FontColor + The font color + + """ + return cls(red=r, green=g, blue=b, alpha=a) + + @classmethod + def from_hex(cls, hex_str: str) -> "FontColor": + """Create a FontColor from a hex string. + + Parameters + ---------- + hex_str : str + A hex color string, with or without a leading ``#``. + Supports 6-character (RRGGBB) or 8-character (RRGGBBAA) formats. + Alpha is 255 if not given. + + Returns + ------- + FontColor + The color represented by the hex string + + Raises + ------ + ValueError + If the hex string is not 6 or 8 characters (excluding ``#``) + + """ + hex_str = hex_str.lstrip("#") + + rgb_size = 6 + rgba_size = 8 + + if len(hex_str) not in {rgb_size, rgba_size}: + msg = f"Hex string must be 6 or 8 characters, got {len(hex_str)}" + raise ValueError(msg) + + r, g, b = (int(hex_str[i : i + 2], 16) for i in (0, 2, 4)) + a = int(hex_str[rgb_size:rgba_size], 16) if len(hex_str) == rgba_size else 255 + + return cls(red=r, green=g, blue=b, alpha=a) diff --git a/compuglobal/models/frame.py b/compuglobal/models/frame.py index 56e594c..57d91eb 100644 --- a/compuglobal/models/frame.py +++ b/compuglobal/models/frame.py @@ -24,16 +24,17 @@ class Frame(BaseCompuGlobalModel): key: str = Field(alias="Episode") timestamp: int = Field(alias="Timestamp", ge=0) - def get_real_timestamp(self) -> str: - """Get a readable timestamp for the frame in format "mm:ss". + @property + def timecode(self) -> str: + """A readable timecode for the frame's timestamp in format ``mm:ss``. Returns ------- str - A readable timestamp for the frame in format `mm:ss`. + A readable timecode in format ``mm:ss`` """ - return Timestamp.get_real_timestamp(timestamp=self.timestamp) + return Timestamp.get_timecode(timestamp=self.timestamp) def __str__(self) -> str: """Get the string representation of the Frame. @@ -44,7 +45,7 @@ def __str__(self) -> str: The frame as a string e.g. S01E01 - 00000001 (00:01) """ - return f"{self.key} - {self.timestamp} ({self.get_real_timestamp()})" + return f"{self.key} - {self.timestamp} ({self.timecode})" class FrameResult(Frame): diff --git a/compuglobal/models/overlay.py b/compuglobal/models/overlay.py index f575e00..355f9c2 100644 --- a/compuglobal/models/overlay.py +++ b/compuglobal/models/overlay.py @@ -1,9 +1,9 @@ """Helper class for formatting of StreamOverlays and ComicOverlays.""" import dataclasses -from dataclasses import dataclass +from dataclasses import dataclass, field -from compuglobal.models.font import FontAlignment, FontFamily +from compuglobal.models.font import FontAlignment, FontColor, FontFamily @dataclass(frozen=True) @@ -16,7 +16,7 @@ class OverlayFormat: The font to use for the text in the overlay font_size : int The size of the font in the overlay - font_color : tuple[int, int, int, int] + font_color : FontColor The color of the font as an RGBA tuple (0-255, 0-255, 0-255, 0-255) text_position_x : int The position of the text on the X-axis @@ -31,31 +31,12 @@ class OverlayFormat: font_family: FontFamily = FontFamily.IMPACT font_size: int = 0 - font_color: tuple[int, int, int, int] = (255, 255, 255, 255) + font_color: FontColor = field(default_factory=FontColor) text_position_x: int = 50 text_position_y: int = 97 text_alignment: FontAlignment = FontAlignment.ALIGN_CENTER all_caps: bool = True - def __post_init__(self) -> None: - """Validate font_color is correct. - - Raises - ------ - ValueError - If font_colour does not contain 4 values, or any values are not between 0 and 255. - - """ - required_rgba_values = 4 - if len(self.font_color) != required_rgba_values: - msg = f"font_color must have exactly 4 values (RGBA), got {len(self.font_color)}" - raise ValueError(msg) - - min_color, max_color = 0, 255 - if not all(min_color <= color <= max_color for color in self.font_color): - msg = f"font_color values must be between 0 and 255, got {self.font_color}" - raise ValueError(msg) - def _changed_fields(self) -> dict: return {f.name: getattr(self, f.name) for f in dataclasses.fields(self) if getattr(self, f.name) != f.default} @@ -72,8 +53,19 @@ def font_color_hex(self) -> str: The color hex code """ - r, g, b, a = self.font_color - return f"{r:02x}{g:02x}{b:02x}{a:02x}" + return self.font_color.hex + + @property + def font_color_rgba(self) -> list[int]: + """The font color as a a list of rgba values. + + Returns + ------- + list[int] + The color in rgba + + """ + return self.font_color.rgba @classmethod def normalise( diff --git a/compuglobal/models/screencap.py b/compuglobal/models/screencap.py index ee48684..cf8aa88 100644 --- a/compuglobal/models/screencap.py +++ b/compuglobal/models/screencap.py @@ -30,16 +30,29 @@ class ScreencapMoment(BaseCompuGlobalModel): content: str = Field(alias="Content") title: str = Field(alias="Title") - def get_real_timestamp(self) -> str: - """Get a readable timestamp for the moments timestamp in format `mm:ss`. + @property + def key(self) -> str: + """The episode key of the screencap (S01E01). Just an alias for episode. Returns ------- str - A readable timestamp in format `mm:ss`. + The episode key (S01E01) """ - return Timestamp.get_real_timestamp(self.timestamp) + return self.episode + + @property + def timecode(self) -> str: + """A readable timecode for the frame's timestamp in format ``mm:ss``. + + Returns + ------- + str + A readable timecode in format ``mm:ss`` + + """ + return Timestamp.get_timecode(self.timestamp) class Screencap(BaseCompuGlobalModel): @@ -56,9 +69,9 @@ class Screencap(BaseCompuGlobalModel): nearby : list[Frame] A list of nearby frames min_timestamp : int - The minimum timestamp of the screencap + The minimum timestamp of the episode of the screencap max_timestamp : int - The maximum timestamp of the screencap + The maximum timestamp of the episode of the screencap """ @@ -69,19 +82,45 @@ class Screencap(BaseCompuGlobalModel): min_timestamp: int = Field(alias="MinTimestamp", ge=0) max_timestamp: int = Field(alias="MaxTimestamp", ge=0) - def get_real_timestamp(self) -> str: - """Get a readable timestamp for the frame in format `mm:ss`. + @property + def key(self) -> str: + """The episode key of the screencap (S01E01). + + Returns + ------- + str + The episode key (S01E01) + + """ + return self.frame.key + + @property + def timestamp(self) -> int: + """The timestamp of the screencap frame. + + Returns + ------- + int + The timestamp + + """ + return self.frame.timestamp + + @property + def timecode(self) -> str: + """A readable timecode for the frame's timestamp in format ``mm:ss``. Returns ------- str - A readable timestamp for the frame in format `mm:ss`. + A readable timecode in format ``mm:ss``. """ - return Timestamp.get_real_timestamp(timestamp=self.frame.timestamp) + return Timestamp.get_timecode(timestamp=self.frame.timestamp) - def get_duration(self) -> int: - """Get duration of screencap subtitles in milliseconds. + @property + def duration(self) -> int: + """Duration of screencap subtitles in milliseconds. Returns ------- @@ -91,8 +130,9 @@ def get_duration(self) -> int: """ return Timestamp.get_subtitles_duration(self.subtitles) + @property def captions(self) -> list[str]: - """Get a list of captions for the screencap from all subtitles. + """A list of captions for the screencap from all subtitles. Returns ------- @@ -102,8 +142,9 @@ def captions(self) -> list[str]: """ return [f"{subtitle.content}" for subtitle in self.subtitles] - def get_caption(self) -> str: - """Get the entire caption for the screencap from all subtitles as a string. + @property + def caption(self) -> str: + """The entire caption for the screencap from all subtitles as a string. Returns ------- @@ -111,10 +152,11 @@ def get_caption(self) -> str: The entire caption of the screencap """ - return " ".join(self.captions()) + return " ".join(self.captions) - def get_start(self) -> int: - """Get the earliest start timestamp from the subtitles. + @property + def start(self) -> int: + """The earliest start timestamp from the subtitles. Returns ------- @@ -124,8 +166,9 @@ def get_start(self) -> int: """ return min(subtitle.start_timestamp for subtitle in self.subtitles) - def get_end(self) -> int: - """Get the latest end timestamp from the subtitles. + @property + def end(self) -> int: + """The latest end timestamp from the subtitles. Returns ------- diff --git a/compuglobal/models/stream.py b/compuglobal/models/stream.py index 32fe7ad..2c7a828 100644 --- a/compuglobal/models/stream.py +++ b/compuglobal/models/stream.py @@ -84,7 +84,7 @@ def build_with_format( start=start, end=end, font_family=overlay_format.font_family, - font_color=overlay_format.font_color, + font_color=overlay_format.font_color.rgba, font_size=overlay_format.font_size, text_position_x=overlay_format.text_position_x, text_position_y=overlay_format.text_position_y, @@ -166,8 +166,8 @@ def from_screencap( return cls( episode=screencap.episode.key, - start=screencap.get_start(), - end=screencap.get_end(), + start=screencap.start, + end=screencap.end, overlays=overlays, check_only=False, ) @@ -198,14 +198,15 @@ def build_stream_overlays( return [ StreamOverlay.build_with_format( text=subtitle.content, - start=subtitle.start_timestamp - screencap.get_start(), - end=subtitle.end_timestamp - screencap.get_start(), + start=subtitle.start_timestamp - screencap.start, + end=subtitle.end_timestamp - screencap.start, overlay_format=overlay_format, ) for subtitle, overlay_format in zip(screencap.subtitles, overlay_format, strict=True) ] - def get_caption(self) -> str: + @property + def caption(self) -> str: """Get the entire caption of the Stream (all overlays) as a string. Returns @@ -216,7 +217,8 @@ def get_caption(self) -> str: """ return " ".join(f"{overlay.text}" for overlay in self.overlays) - def get_encoded(self) -> str: + @property + def encoded(self) -> str: """Get the base 64 encoded representation of this stream's overlays. Returns diff --git a/compuglobal/models/subtitle.py b/compuglobal/models/subtitle.py index 4655b60..0db4f3e 100644 --- a/compuglobal/models/subtitle.py +++ b/compuglobal/models/subtitle.py @@ -36,7 +36,8 @@ class Subtitle(BaseCompuGlobalModel): content: str = Field(alias="Content") language: str = Field(alias="Language") - def get_duration(self) -> int: + @property + def duration(self) -> int: """Get the duration of the subtitle in milliseconds. Returns diff --git a/compuglobal/models/timestamp.py b/compuglobal/models/timestamp.py index f598922..29e6654 100644 --- a/compuglobal/models/timestamp.py +++ b/compuglobal/models/timestamp.py @@ -32,7 +32,7 @@ def get_minutes_seconds(milliseconds: int) -> tuple[int, int]: return minutes, seconds @staticmethod - def get_real_timestamp(timestamp: int) -> str: + def get_timecode(timestamp: int) -> str: """Get a readable timestamp for the frame in format `mm:ss`. Parameters diff --git a/docs/changelog.rst b/docs/changelog.rst index 89fe585..52e087b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,13 +2,57 @@ Changelog ========= +0.4.0 +----- + +Breaking Changes +~~~~~~~~~~~~~~~~ +- Many model methods have been changed to be properties, see the following section for a full list +- Comic/Gif methods now support overriding (:class:`OverlayFormat`) instead of only :class:`FontFamily`: + - :meth:`AsyncCompuGlobalAPI.get_comic_panel_url` + - :meth:`AsyncCompuGlobalAPI.get_comic_strip_url` + - :meth:`AsyncCompuGlobalAPI.get_gif_url` +- Timestamp helper method get_readable_timestamp() renamed to :meth:`Timestamp.get_timecode` +- FontColorRGB class renamed to :class:`FontColor` + +.. code-block:: py + + # Before + frinkiac.get_gif_url(screencap, font_family=FontFamily.JOST) + + # After + overlay_format = OverlayFormat(font_family=FontFamily.JOST) + frinkiac.get_gif_url(screencap, overlay_format=overlay_format) + +Added +~~~~~ +- Added some useful properties to models: + - Screencap: :attr:`Screencap.key`, :attr:`Screencap.timestamp`, :attr:`Screencap.timecode`, :attr:`Screencap.duration`, :attr:`Screencap.start`, :attr:`Screencap.end`, :attr:`Screencap.caption`, :attr:`Screencap.captions` + - ScreencapMoment: :attr:`ScreencapMoment.key`, :attr:`ScreencapMoment.timecode` + - Frame: :attr:`Frame.timecode` + - Subtitle: :attr:`Subtitle.duration` + - Stream: :attr:`Stream.caption`, :attr:`Stream.encoded` + - ComicPanel: :attr:`ComicPanel.encoded` + - ComicStrip: :attr:`ComicStrip.encoded` +- :class:`OverlayFormat` for defining format preferences to use in overlays +- Optional argument for overriding overlay formatting, overlay_formats: + - This enables overriding font, color, size, uppercase/lowercase in all overlays. + - These can override all overlays in the entire gif/comic, or you can specify different formats for each overlay by providing a list. + - See :meth:`OverlayFormat.normalise` for more details on how formats are resolved. +- Methods for gif/comic maker urls, these urls take you straight to the website to edit there: + - :meth:`AsyncCompuGlobalAPI.get_comic_maker_url` + - :meth:`AsyncCompuGlobalAPI.get_gif_maker_url` +- Logging throughout the library for: + - API requests/responses + - Endpoint validation + - Non-default behaviour (subtitles/overlay formats overrides) 0.3.8 ----- Added ~~~~~ -- Timestamp helper class for handling timestamps +- :class:`Timestamp` helper class for handling timestamps 0.3.7 ------- @@ -16,7 +60,6 @@ Added Added ~~~~~ - Optional season filters, season_minimum, and season_maximum for the following methods: - - :meth:`AsyncCompuGlobalAPI.search` - :meth:`AsyncCompuGlobalAPI.search_for_screencap` - :meth:`AsyncCompuGlobalAPI.get_random_screencap` @@ -85,18 +128,15 @@ Fixed Added ~~~~~ - Methods for missing API endpoints: - - :meth:`AsyncCompuGlobalAPI.browse_episode` - :meth:`AsyncCompuGlobalAPI.get_transcript` - :meth:`AsyncCompuGlobalAPI.navigator` - Methods for getting caption as a string: - - :meth:`StreamOverlay.get_caption` - :meth:`Screencap.get_caption` - Methods for building models from a :class:`Screencap` directly: - - :meth:`ComicPanel.from_screencap`, :meth:`ComicStrip.from_screencap`, and :meth:`Stream.from_screencap` for Fixed @@ -127,7 +167,6 @@ These changes are to accommodate the extensive update to the APIs with new featu - The Master Of All Science API appears to be unavailable at this point in time and redirects to Frinkiac, I have added a deprecation warning to this API and it will remain unless the API returns - The package now requires Python 3.13+ - Image, comic, and gif generation are all now performed using the API rather than from a Screencap: - - :meth:`AsyncCompuGlobalAPI.get_image_url` - :meth:`AsyncCompuGlobalAPI.get_gif_url` @@ -145,7 +184,6 @@ These changes are to accommodate the extensive update to the APIs with new featu Added ~~~~~ - Endpoints for comic panels/strips: - - :meth:`AsyncCompuGlobalAPI.get_comic_panel_url` - :meth:`AsyncCompuGlobalAPI.get_comic_strip_url` - Models for comics: diff --git a/docs/models.rst b/docs/models.rst index 889ed9c..ea3d312 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -67,7 +67,7 @@ Streams Fonts ~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: FontColorRGB +.. autoclass:: FontColor :members: .. autoclass:: FontAlignment @@ -76,6 +76,10 @@ Fonts .. autoclass:: FontFamily :members: +Timestamp +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: Timestamp + :members: **Base Model:** diff --git a/examples/episode_info.py b/examples/episode_info.py index 8b0c652..61f4418 100644 --- a/examples/episode_info.py +++ b/examples/episode_info.py @@ -60,8 +60,8 @@ async def example(session: aiohttp.ClientSession): print(timestamp) # Returns - 5:43 - real_timestamp = screencap.get_real_timestamp() - print(real_timestamp) + timecode = screencap.timecode + print(timecode) subtitles = screencap.subtitles print(subtitles) diff --git a/tests/api/test_config.py b/tests/api/test_config.py index 18f2116..07a73cd 100644 --- a/tests/api/test_config.py +++ b/tests/api/test_config.py @@ -2,7 +2,7 @@ from compuglobal import FontAlignment from compuglobal.api.config import CompuGlobalAPIConfig -from compuglobal.models.font import FontFamily +from compuglobal.models.font import FontColor, FontFamily from compuglobal.models.overlay import OverlayFormat @@ -11,7 +11,7 @@ def test_compuglobal_config_defaults() -> None: default_format = OverlayFormat( font_family=FontFamily.IMPACT, font_size=0, - font_color=(255, 255, 255, 255), + font_color=FontColor.from_rgba(255, 255, 255, 255), text_position_x=50, text_position_y=97, text_alignment=FontAlignment.ALIGN_CENTER, @@ -25,7 +25,7 @@ def test_compuglobal_config_overrides() -> None: custom_format = OverlayFormat( font_family=FontFamily.JOST, font_size=12, - font_color=(10, 20, 30, 40), + font_color=FontColor.from_rgba(10, 20, 30, 40), text_position_x=120, text_position_y=80, text_alignment=FontAlignment.ALIGN_LEFT, diff --git a/tests/integration/test_aio_integration.py b/tests/integration/test_aio_integration.py index 2f18d81..5d25bac 100644 --- a/tests/integration/test_aio_integration.py +++ b/tests/integration/test_aio_integration.py @@ -91,7 +91,7 @@ async def test_api_get_screencap_episode_timestamp(api: AsyncCompuGlobalAPI, ran @pytest.mark.asyncio @pytest.mark.integration async def test_api_get_screencap_frame(api: AsyncCompuGlobalAPI, random_screencap: Screencap) -> None: - screencap = await api.get_screencap(frame=random_screencap.frame) + screencap = await api.get_screencap(episode=random_screencap.key, timestamp=random_screencap.timestamp) assert random_screencap == screencap diff --git a/tests/models/test_comic.py b/tests/models/test_comic.py index ec9bb25..9ce896d 100644 --- a/tests/models/test_comic.py +++ b/tests/models/test_comic.py @@ -201,7 +201,7 @@ def test_comic_panel_from_screencap_custom_font(screencap: Screencap) -> None: ) -def test_comic_panel_get_encoded(subtitle_json: dict[str, Any]) -> None: +def test_comic_panel_encoded(subtitle_json: dict[str, Any]) -> None: subtitles = [Subtitle.model_validate(subtitle_json)] overlay = ComicOverlay.from_subtitles( subtitles=subtitles, @@ -210,7 +210,7 @@ def test_comic_panel_get_encoded(subtitle_json: dict[str, Any]) -> None: payload = {"e": "S01E01", "ts": 7777, "o": [overlay]} panel = ComicPanel.model_validate(payload) - assert panel.get_encoded() == snapshot( + assert panel.encoded == snapshot( "W3siZSI6IlMwMUUwMSIsInRzIjo3Nzc3LCJvIjpbeyJ0IjoiU3R1cGlkLCBzZXh5IEZsYW5kZXJzISIsImYiOiJha2JhciIsInMiOjAs" "ImMiOiJmZmZmZmZmZiIsIngiOjUwLCJ5Ijo5NywiYSI6ImMiLCJ1IjoxLCJiIjowLCJkIjowfV19XQ==", ) @@ -401,9 +401,9 @@ def test_comic_strip_build_comic_overlays_with_font(subtitle_json: dict[str, Any ) -def test_comic_strip_get_encoded(screencap: Screencap) -> None: +def test_comic_strip_encoded(screencap: Screencap) -> None: comic_strip = ComicStrip.from_screencap(screencap=screencap) - assert comic_strip.get_encoded() == snapshot( + assert comic_strip.encoded == snapshot( "W3siZSI6IlMxMUUxMCIsInRzIjozNDgwMTQsIm8iOlt7InQiOiJGZWVscyBsaWtlIEknbSB3ZWFyaW5nIG5vdGhpbmcgYXQgYWxsLS0iLCJmIjoiaW1wYWN0IiwicyI6MCwiYyI6ImZmZmZmZmZmIiwieCI6NTAsInkiOjk3LCJhIjoiYyIsInUiOjEsImIiOjAsImQiOjB9XX0seyJlIjoiUzExRTEwIiwidHMiOjM1MDUxNywibyI6W3sidCI6Ik5vdGhpbmcgYXQgYWxsLS0gTm90aGluZyBhdCBhbGwhXCIiLCJmIjoiaW1wYWN0IiwicyI6MCwiYyI6ImZmZmZmZmZmIiwieCI6NTAsInkiOjk3LCJhIjoiYyIsInUiOjEsImIiOjAsImQiOjB9XX0seyJlIjoiUzExRTEwIiwidHMiOjM1MzIyOCwibyI6W3sidCI6IlN0dXBpZCwgc2V4eSBGbGFuZGVycyEiLCJmIjoiaW1wYWN0IiwicyI6MCwiYyI6ImZmZmZmZmZmIiwieCI6NTAsInkiOjk3LCJhIjoiYyIsInUiOjEsImIiOjAsImQiOjB9XX1d", ) diff --git a/tests/models/test_font.py b/tests/models/test_font.py index e075104..01da98e 100644 --- a/tests/models/test_font.py +++ b/tests/models/test_font.py @@ -3,7 +3,7 @@ import pytest from pydantic import ValidationError -from compuglobal.models.font import FontColorRGB +from compuglobal.models.font import FontColor VALID_RGB_VALUES = [ (0, 0, 0, 0), @@ -26,22 +26,42 @@ (0, 0, 0, 256), ] +HEX_TO_RGBA_VALUES = [ + ("#ff0000", [255, 0, 0, 255]), + ("#00ff00", [0, 255, 0, 255]), + ("#0000ff", [0, 0, 255, 255]), + ("#ff00ff", [255, 0, 255, 255]), + ("#ffffff", [255, 255, 255, 255]), + ("#000000", [0, 0, 0, 255]), + ("#00000000", [0, 0, 0, 0]), + ("#ffffff80", [255, 255, 255, 128]), + ("#1a2b3c", [26, 43, 60, 255]), + ("#deadbe", [222, 173, 190, 255]), + ("#ff6600cc", [255, 102, 0, 204]), +] + @pytest.mark.parametrize(("red", "green", "blue", "alpha"), VALID_RGB_VALUES) def test_font_color_rgb(red: int, green: int, blue: int, alpha: int) -> None: payload = {"r": red, "g": green, "b": blue, "a": alpha} - rgba = FontColorRGB.model_validate(payload) - assert rgba.model_dump() == payload + color = FontColor.model_validate(payload) + assert color.model_dump() == payload @pytest.mark.parametrize(("red", "green", "blue", "alpha"), INVALID_RGB_VALUES) def test_font_color_invalid_invalid_range(red: int, green: int, blue: int, alpha: int) -> None: invalid_payload = {"r": red, "g": green, "b": blue, "a": alpha} with pytest.raises(ValidationError): - FontColorRGB.model_validate(invalid_payload) + FontColor.model_validate(invalid_payload) @pytest.mark.parametrize(("red", "green", "blue", "alpha"), VALID_RGB_VALUES) -def test_font_color_get_rgba(red: int, green: int, blue: int, alpha: int) -> None: - rgba = FontColorRGB(red=red, green=green, blue=blue, alpha=alpha) - assert rgba.get_rgba() == [red, green, blue, alpha] +def test_font_color_rgba(red: int, green: int, blue: int, alpha: int) -> None: + color = FontColor(red=red, green=green, blue=blue, alpha=alpha) + assert color.rgba == [red, green, blue, alpha] + + +@pytest.mark.parametrize(("hex_code", "expected"), HEX_TO_RGBA_VALUES) +def test_font_color_from_hex(hex_code: str, expected: list[int]) -> None: + color = FontColor.from_hex(hex_code) + assert color.rgba == expected diff --git a/tests/models/test_frame.py b/tests/models/test_frame.py index 43883bf..1cb2896 100644 --- a/tests/models/test_frame.py +++ b/tests/models/test_frame.py @@ -45,9 +45,9 @@ def test_frame_validate_validate_invalid_timestamp(bad_timestamp: int) -> None: @pytest.mark.parametrize(("timestamp", "expected"), TIMESTAMP_CASES) -def test_frame_get_real_timestamp(timestamp: int, expected: str) -> None: +def test_frame_timecode(timestamp: int, expected: str) -> None: frame = Frame(id=1, key="S22E22", timestamp=timestamp) - assert frame.get_real_timestamp() == expected + assert frame.timecode == expected @pytest.mark.parametrize(("timestamp", "expected"), TIMESTAMP_CASES) diff --git a/tests/models/test_overlay.py b/tests/models/test_overlay.py index 402aca2..bd391e3 100644 --- a/tests/models/test_overlay.py +++ b/tests/models/test_overlay.py @@ -4,32 +4,20 @@ from hypothesis import given from hypothesis import strategies as st -from compuglobal.models.font import FontFamily +from compuglobal.models.font import FontColor, FontFamily from compuglobal.models.overlay import OverlayFormat -def test_overlay_format_invalid_rgba_length_too_few() -> None: - with pytest.raises(ValueError, match="must have exactly 4 values"): - # pyrefly: ignore [bad-argument-type] - OverlayFormat(font_color=()) - - -def test_overlay_format_invalid_rgba_length_too_many() -> None: - with pytest.raises(ValueError, match="must have exactly 4 values"): - # pyrefly: ignore [bad-argument-type] - OverlayFormat(font_color=(255, 255, 255, 255, 255)) - - @given(st.integers(max_value=-1)) def test_overlay_format_invalid_colors_low(bad_color: int) -> None: - with pytest.raises(ValueError, match="values must be between 0 and 255"): - OverlayFormat(font_color=(1, 1, 1, bad_color)) + with pytest.raises(ValueError, match="Input should be greater than or equal to 0"): + OverlayFormat(font_color=FontColor(r=1, g=1, b=1, a=bad_color)) @given(st.integers(min_value=256)) def test_overlay_format_invalid_colors_high(bad_color: int) -> None: - with pytest.raises(ValueError, match="values must be between 0 and 255"): - OverlayFormat(font_color=(1, 1, 1, bad_color)) + with pytest.raises(ValueError, match="Input should be less than or equal to 255 "): + OverlayFormat(font_color=FontColor.from_rgba(1, 1, 1, bad_color)) def test_overlay_format_normalise_default() -> None: diff --git a/tests/models/test_screencap.py b/tests/models/test_screencap.py index 32e49e0..874bdc3 100644 --- a/tests/models/test_screencap.py +++ b/tests/models/test_screencap.py @@ -48,9 +48,9 @@ def test_screencap_moment_validate_invalid_timestamp(bad_timestamp: int) -> None ScreencapMoment.model_validate(payload) -def test_screencap_moment_get_real_timestamp(screencap_moment: dict[str, Any]) -> None: +def test_screencap_moment_timecode(screencap_moment: dict[str, Any]) -> None: moment = ScreencapMoment.model_validate(screencap_moment) - assert moment.get_real_timestamp() == snapshot("18:38") + assert moment.timecode == snapshot("18:38") def test_screencap_validate_dump(screencap: Screencap) -> None: @@ -59,34 +59,34 @@ def test_screencap_validate_dump(screencap: Screencap) -> None: assert screencap == expected -def test_screencap_get_real_timestamp(screencap: Screencap) -> None: - assert screencap.get_real_timestamp() == snapshot("5:50") +def test_screencap_timecode(screencap: Screencap) -> None: + assert screencap.timecode == snapshot("5:50") def test_screencap_captions(screencap: Screencap) -> None: - assert screencap.captions() == [ + assert screencap.captions == [ "Feels like I'm wearing nothing at all--", 'Nothing at all-- Nothing at all!"', "Stupid, sexy Flanders!", ] -def test_screencap_get_caption(screencap: Screencap) -> None: - assert screencap.get_caption() == snapshot( +def test_screencap_caption(screencap: Screencap) -> None: + assert screencap.caption == snapshot( "Feels like I'm wearing nothing at all-- Nothing at all-- Nothing at all!\" Stupid, sexy Flanders!", ) -def test_screencap_get_subtitles_duration(screencap: Screencap) -> None: - assert screencap.get_duration() == snapshot(7799) +def test_screencap_duration(screencap: Screencap) -> None: + assert screencap.duration == snapshot(7799) -def test_screencap_get_start(screencap: Screencap) -> None: - assert screencap.get_start() == 347055 +def test_screencap_start(screencap: Screencap) -> None: + assert screencap.start == 347055 -def test_screencap_get_end(screencap: Screencap) -> None: - assert screencap.get_end() == 354854 +def test_screencap_end(screencap: Screencap) -> None: + assert screencap.end == 354854 def test_screencap_str(screencap: Screencap) -> None: diff --git a/tests/models/test_stream.py b/tests/models/test_stream.py index f6fe6e1..d2ba750 100644 --- a/tests/models/test_stream.py +++ b/tests/models/test_stream.py @@ -223,7 +223,7 @@ def test_stream_build_stream_overlays_font(screencap: Screencap) -> None: ) -def test_stream_get_caption() -> None: +def test_stream_caption() -> None: overlays = [StreamOverlay(text=f"Example text {i}!", start=i - 1, end=i) for i in range(1, 3)] stream = Stream(key="S01E01", start=0, end=2, overlays=overlays, check_only=False) - assert stream.get_caption() == snapshot("Example text 1! Example text 2!") + assert stream.caption == snapshot("Example text 1! Example text 2!") diff --git a/tests/models/test_subtitle.py b/tests/models/test_subtitle.py index 8429336..0c42e6f 100644 --- a/tests/models/test_subtitle.py +++ b/tests/models/test_subtitle.py @@ -45,7 +45,7 @@ def test_subtitle_invalid_timestamp(subtitle_json: dict[str, Any], start_timesta (352143, 354854, 2711), ], ) -def test_subtitle_get_duration( +def test_subtitle_duration( subtitle_json: dict[str, Any], start_timestamp: int, end_timestamp: int, @@ -53,4 +53,4 @@ def test_subtitle_get_duration( ) -> None: subtitle = Subtitle.model_validate(subtitle_json) copy = subtitle.model_copy(update={"start_timestamp": start_timestamp, "end_timestamp": end_timestamp}) - assert copy.get_duration() == expected + assert copy.duration == expected diff --git a/tests/test_aio.py b/tests/test_aio.py index 498fa0b..042786e 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -14,7 +14,6 @@ from compuglobal.api.config import CompuGlobalAPIConfig from compuglobal.errors import NoSearchResultsFoundError from compuglobal.models.font import FontFamily -from compuglobal.models.frame import Frame from compuglobal.models.overlay import OverlayFormat from compuglobal.models.screencap import Screencap, ScreencapMoment from compuglobal.models.stream import Stream @@ -73,20 +72,6 @@ async def test_api_defaults() -> None: assert api.client.base_url == "https://example.com" -@pytest.mark.asyncio -async def test_api_get_screencap_frame( - api: CustomCompuGlobalAPI, - mock_http: aiointercept, - screencap: Screencap, -) -> None: - frame = Frame(id=9, key="S11E10", timestamp=350725) - params = {"e": "S11E10", "t": 350725, "nearby": 1} - url = api.discovery.CAPTION.build_encoded_url(api.BASE_URL, query=params) - mock_http.get(url, payload=screencap.model_dump()) - result = await api.get_screencap(frame=frame) - assert result.model_dump() == screencap.model_dump() - - @pytest.mark.asyncio async def test_api_get_screencap_episode_timestamp( api: CustomCompuGlobalAPI, @@ -103,12 +88,14 @@ async def test_api_get_screencap_episode_timestamp( @pytest.mark.asyncio async def test_api_get_screencap_no_timestamp(api: CustomCompuGlobalAPI) -> None: with pytest.raises(TypeError): + # pyrefly: ignore [missing-argument, unexpected-keyword] await api.get_screencap(episode="S01E01") @pytest.mark.asyncio async def test_api_get_screencap_no_episode(api: CustomCompuGlobalAPI) -> None: with pytest.raises(TypeError): + # pyrefly: ignore [missing-argument, unexpected-keyword] await api.get_screencap(timestamp=1000)