From 39de4bc796ea1a85f79433366335c3cad685ae21 Mon Sep 17 00:00:00 2001 From: Mitch Woollatt Date: Tue, 16 Jun 2026 14:35:22 +0930 Subject: [PATCH 1/6] Allow overriding default format in API constructor --- compuglobal/aio.py | 53 +++++++++++++++++++++++++++++++------- compuglobal/api/config.py | 14 ++++++++++ compuglobal/models/font.py | 17 ++++++++++++ 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/compuglobal/aio.py b/compuglobal/aio.py index 303ca41..14f813b 100644 --- a/compuglobal/aio.py +++ b/compuglobal/aio.py @@ -1,5 +1,6 @@ """Used for interacting with and building CGHMC APIs.""" +import dataclasses import json import logging from typing import overload @@ -32,8 +33,10 @@ class AsyncCompuGlobalAPI: Parameters ---------- - session : aiohttp.ClientSession, optional + session : aiohttp.ClientSession The client session to use for all API calls + default_format : OverlayFormat | None, optional + The default overlay format to use for all overlays/subtitles Attributes ---------- @@ -41,8 +44,8 @@ class AsyncCompuGlobalAPI: The base url of the API TITLE : str The title of the API - DEFAULT_FORMAT : OverlayFormat - The default formatting to use in all comic/gif overlays + EXTRA_FONTS : frozenset[FontFamily] + A frozenset of any extra fonts permitted by the API discovery : DiscoveryAPI The discovery API with all discovery endpoints media : MediaAPI @@ -54,16 +57,27 @@ class AsyncCompuGlobalAPI: BASE_URL: str TITLE: str - DEFAULT_FORMAT: OverlayFormat + EXTRA_FONTS: frozenset[FontFamily] = frozenset() _MAX_ALLOWED_SUBTITLES = 4 discovery: DiscoveryAPI = DiscoveryAPI() media: MediaAPI = MediaAPI() metadata: MetadataAPI = MetadataAPI() - def __init__(self, session: aiohttp.ClientSession) -> None: + def __init__(self, session: aiohttp.ClientSession, default_format: OverlayFormat | None = None) -> None: + extra_fonts = list(self.EXTRA_FONTS) + if default_format is None: + chosen_font = extra_fonts[0] if len(extra_fonts) > 0 else FontFamily.IMPACT + default_format = OverlayFormat(font_family=chosen_font) + + allowed_fonts = FontFamily.universal_fonts() + extra_fonts + + self.config = CompuGlobalAPIConfig( + title=self.TITLE, + allowed_fonts=allowed_fonts, + default_format=default_format, + ) self.client = CompuGlobalAPIClient(base_url=self.BASE_URL, session=session) - self.config = CompuGlobalAPIConfig(title=self.TITLE, default_format=self.DEFAULT_FORMAT) async def get_screencap( self, @@ -476,6 +490,21 @@ async def get_gif_maker_url( query={"b64": stream.encoded}, ) + def _resolve_font(self, overlay_format: OverlayFormat) -> OverlayFormat: + if overlay_format.font_family in self.config.allowed_fonts: + return overlay_format + + log.warning( + "Font family %s is not allowed for %s, using IMPACT font instead | overlay_format=%s", + overlay_format.font_family, + self.config.title, + overlay_format, + ) + return dataclasses.replace(overlay_format, font_family=FontFamily.IMPACT) + + def _resolve_fonts(self, overlay_formats: list[OverlayFormat]) -> list[OverlayFormat]: + return [self._resolve_font(overlay_format) for overlay_format in overlay_formats] + @overload def _resolve_overlay_inputs( self, @@ -504,6 +533,12 @@ def _resolve_overlay_inputs( if overlay_format != self.config.default_format: log.debug("Using custom overlay format | screencap=%s | overlay_format=%s", screencap, overlay_format) + # Resolve any disallowed fonts in the format(s) + if isinstance(overlay_format, list): + overlay_format = self._resolve_fonts(overlay_format) + else: + overlay_format = self._resolve_font(overlay_format) + # Use screencap subtitles if not given subtitles = subtitles or screencap.subtitles if subtitles != screencap.subtitles: @@ -523,7 +558,6 @@ class CapitalBeatUs(AsyncCompuGlobalAPI): BASE_URL = "https://capitalbeat.us" TITLE = "West Wing" - DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.IMPACT) class Frinkiac(AsyncCompuGlobalAPI): @@ -531,7 +565,7 @@ class Frinkiac(AsyncCompuGlobalAPI): BASE_URL = "https://frinkiac.com" TITLE = "Simpsons" - DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.AKBAR) + EXTRA_FONTS = frozenset({FontFamily.AKBAR}) @deprecated("The MasterOfAllScience API is deprecated, and currently redirects to Frinkiac") @@ -540,7 +574,6 @@ class MasterOfAllScience(AsyncCompuGlobalAPI): BASE_URL = "https://masterofallscience.com" TITLE = "Rick and Morty" - DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.IMPACT) class Morbotron(AsyncCompuGlobalAPI): @@ -548,4 +581,4 @@ class Morbotron(AsyncCompuGlobalAPI): BASE_URL = "https://morbotron.com" TITLE = "Futurama" - DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.FR_BOLD) + EXTRA_FONTS = frozenset({FontFamily.FR_BOLD}) diff --git a/compuglobal/api/config.py b/compuglobal/api/config.py index 718ab93..8b16fd1 100644 --- a/compuglobal/api/config.py +++ b/compuglobal/api/config.py @@ -1,9 +1,14 @@ """The configuration for a CGHMC API (title + font).""" +import dataclasses +import logging from dataclasses import dataclass, field +from compuglobal.models.font import FontFamily from compuglobal.models.overlay import OverlayFormat +log = logging.getLogger(__name__) + @dataclass class CompuGlobalAPIConfig: @@ -12,5 +17,14 @@ class CompuGlobalAPIConfig: #: The title of the API title: str + #: The allowed fonts for this API + allowed_fonts: list[FontFamily] = field(default_factory=FontFamily.universal_fonts) + #: The default formatting to use in all stream/comic overlays (subtitles) default_format: OverlayFormat = field(default_factory=OverlayFormat) + + def __post_init__(self) -> None: + """Validate font family is allowed in API. If not allowed, use IMPACT font.""" + if self.default_format.font_family not in self.allowed_fonts: + log.warning("Chosen font is not allowed for this API. Using IMPACT font instead.") + self.default_format = dataclasses.replace(self.default_format, font_family=FontFamily.IMPACT) diff --git a/compuglobal/models/font.py b/compuglobal/models/font.py index 29a962e..ef0bcc9 100644 --- a/compuglobal/models/font.py +++ b/compuglobal/models/font.py @@ -28,6 +28,23 @@ class FontFamily(StrEnum): #: The Fr Bold font family (Morbotron default) FR_BOLD = "fr" + @staticmethod + def universal_fonts() -> list["FontFamily"]: + """Get a list of universal fonts that work across all APIs. + + Returns + ------- + list[FontFamily] + List of universal fonts + + """ + return [ + FontFamily.IMPACT, + FontFamily.COMIC_NEUE, + FontFamily.JOST, + FontFamily.PACIFICO, + ] + class FontAlignment(StrEnum): """An enumeration of font alignments.""" From edbc1dae05600bd36ca8554f9073475c5801ae0c Mon Sep 17 00:00:00 2001 From: Mitch Woollatt Date: Tue, 16 Jun 2026 14:36:00 +0930 Subject: [PATCH 2/6] Validate font size is correct in overlay format --- compuglobal/models/overlay.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/compuglobal/models/overlay.py b/compuglobal/models/overlay.py index 355f9c2..15bc9d0 100644 --- a/compuglobal/models/overlay.py +++ b/compuglobal/models/overlay.py @@ -37,6 +37,20 @@ class OverlayFormat: text_alignment: FontAlignment = FontAlignment.ALIGN_CENTER all_caps: bool = True + def __post_init__(self) -> None: + """Validate font size. + + Raises + ------ + ValueError + Font size must be between 0-120 + + """ + max_font_size = 120 + if self.font_size < 0 or self.font_size > max_font_size: + msg = f"Font size must be between 0 and {max_font_size}, but got {self.font_size}" + 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} From 309f5846682354391d7cd73b77dbe9f63e3270bf Mon Sep 17 00:00:00 2001 From: Mitch Woollatt Date: Tue, 16 Jun 2026 14:36:39 +0930 Subject: [PATCH 3/6] Add timecode property to Subtitle model --- compuglobal/models/subtitle.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compuglobal/models/subtitle.py b/compuglobal/models/subtitle.py index 0db4f3e..d29088c 100644 --- a/compuglobal/models/subtitle.py +++ b/compuglobal/models/subtitle.py @@ -47,3 +47,15 @@ def duration(self) -> int: """ return Timestamp.get_duration(start_timestamp=self.start_timestamp, end_timestamp=self.end_timestamp) + + @property + def timecode(self) -> str: + """A readable timecode for the subtitle's representative timestamp in format ``mm:ss``. + + Returns + ------- + str + A readable timecode in format ``mm:ss`` + + """ + return Timestamp.get_timecode(self.representative_timestamp) From 419c926f97eabd82412aafe30d8ebb0257ed3c58 Mon Sep 17 00:00:00 2001 From: Mitch Woollatt Date: Tue, 16 Jun 2026 15:06:46 +0930 Subject: [PATCH 4/6] Apply default format in api tests --- tests/test_aio.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_aio.py b/tests/test_aio.py index 042786e..0844ba8 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -24,7 +24,6 @@ class CustomCompuGlobalAPI(AsyncCompuGlobalAPI): BASE_URL = "https://example.com" TITLE = "Testing" - DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.JOST) class _InvalidRequestJSONError(Exception): @@ -32,10 +31,10 @@ def __init__(self) -> None: super().__init__("Failed to validate request json.") -@pytest_asyncio.fixture() +@pytest_asyncio.fixture async def api() -> AsyncGenerator[CustomCompuGlobalAPI]: async with aiohttp.ClientSession() as session: - yield CustomCompuGlobalAPI(session=session) + yield CustomCompuGlobalAPI(session=session, default_format=OverlayFormat(font_family=FontFamily.JOST)) @pytest.fixture @@ -64,9 +63,10 @@ def random_frame_results(quantity: int) -> list[dict[str, Any]]: @pytest.mark.asyncio async def test_api_defaults() -> None: async with aiohttp.ClientSession() as session: - api = CustomCompuGlobalAPI(session=session) + api = CustomCompuGlobalAPI(session=session, default_format=OverlayFormat(font_family=FontFamily.JOST)) assert api.config == CompuGlobalAPIConfig( title="Testing", + allowed_fonts=FontFamily.universal_fonts(), default_format=OverlayFormat(font_family=FontFamily.JOST), ) assert api.client.base_url == "https://example.com" @@ -311,7 +311,7 @@ async def test_api_get_comic_strip_url_custom_subtitles_truncated_subtitles( @pytest.mark.asyncio async def test_api_get_gif_url(api: CustomCompuGlobalAPI, mock_http: aiointercept, screencap: Screencap) -> None: url = api.media.RENDER_GIF.build_encoded_url(api.BASE_URL) - stream = Stream.from_screencap(screencap=screencap, overlay_format=api.DEFAULT_FORMAT) + stream = Stream.from_screencap(screencap=screencap, overlay_format=api.config.default_format) payload = { "url": "/video/S02E01/CoCGOx7cJdlKOKjrZoE7S5_mXqw=.gif", } @@ -333,7 +333,7 @@ async def test_api_get_gif_url_custom_subtitles( url = api.media.RENDER_GIF.build_encoded_url(api.BASE_URL) stream_screencap = screencap.model_copy(update={"subtitles": subtitles}) - stream = Stream.from_screencap(screencap=stream_screencap, overlay_format=api.DEFAULT_FORMAT) + stream = Stream.from_screencap(screencap=stream_screencap, overlay_format=api.config.default_format) payload = { "url": "/video/S02E01/CoCGOx7cJdlKOKjrZoE7S5_mXqw=.gif", @@ -353,7 +353,7 @@ async def test_api_get_gif_url_fallback_comic( screencap: Screencap, ) -> None: url = api.media.RENDER_GIF.build_encoded_url(api.BASE_URL) - stream = Stream.from_screencap(screencap=screencap, overlay_format=api.DEFAULT_FORMAT) + stream = Stream.from_screencap(screencap=screencap, overlay_format=api.config.default_format) payload = { "progress": 0.044500000000000005, } @@ -378,7 +378,7 @@ async def test_api_get_gif_url_truncated_subtitles( url = api.media.RENDER_GIF.build_encoded_url(api.BASE_URL) stream_screencap = screencap.model_copy(update={"subtitles": subtitles[:4]}) - stream = Stream.from_screencap(screencap=stream_screencap, overlay_format=api.DEFAULT_FORMAT) + stream = Stream.from_screencap(screencap=stream_screencap, overlay_format=api.config.default_format) payload = { "url": "/video/S02E01/CoCGOx7cJdlKOKjrZoE7S5_mXqw=.gif", From 656a53edd83b6c98250ebe2674d6e4bd03729c41 Mon Sep 17 00:00:00 2001 From: Mitch Woollatt Date: Tue, 16 Jun 2026 15:08:16 +0930 Subject: [PATCH 5/6] Document changes for 0.4.1 in changelog --- docs/changelog.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 52e087b..e459884 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,16 @@ Changelog ========= +0.4.1 +----- + +Added +~~~~~ +- Optional default format to :class:`AsyncCompuGlobalAPI` constructor, for easier configuration of default preferences +- Timecode property for Subtitle model :attr:`Subtitle.timecode` +- Validation for allowed fonts in each API, if an invalid font is used, its replaced by :attr:`FontFamily.IMPACT` +- Validation for font_size in :class:`OverlayFormat` ensuring its 0-120 + 0.4.0 ----- From 27ed798f7ac4ba6ca8c1ba9092d7a77762c1df62 Mon Sep 17 00:00:00 2001 From: Mitch Woollatt Date: Tue, 16 Jun 2026 15:08:45 +0930 Subject: [PATCH 6/6] Bump package version to 0.4.1 --- compuglobal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compuglobal/__init__.py b/compuglobal/__init__.py index eb64922..ef7e0c7 100644 --- a/compuglobal/__init__.py +++ b/compuglobal/__init__.py @@ -22,7 +22,7 @@ __title__ = "compuglobal" __author__ = "MitchellAW" __license__ = "MIT" -__version__ = "0.4.0" +__version__ = "0.4.1" __all__ = [ "APIPageStatusError",