Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compuglobal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
__title__ = "compuglobal"
__author__ = "MitchellAW"
__license__ = "MIT"
__version__ = "0.4.0"
__version__ = "0.4.1"

__all__ = [
"APIPageStatusError",
Expand Down
53 changes: 43 additions & 10 deletions compuglobal/aio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Used for interacting with and building CGHMC APIs."""

import dataclasses
import json
import logging
from typing import overload
Expand Down Expand Up @@ -32,17 +33,19 @@ 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
----------
BASE_URL : str
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
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -523,15 +558,14 @@ class CapitalBeatUs(AsyncCompuGlobalAPI):

BASE_URL = "https://capitalbeat.us"
TITLE = "West Wing"
DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.IMPACT)


class Frinkiac(AsyncCompuGlobalAPI):
"""An API Wrapper for accessing Frinkiac API endpoints (The Simpsons)."""

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")
Expand All @@ -540,12 +574,11 @@ class MasterOfAllScience(AsyncCompuGlobalAPI):

BASE_URL = "https://masterofallscience.com"
TITLE = "Rick and Morty"
DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.IMPACT)


class Morbotron(AsyncCompuGlobalAPI):
"""An API Wrapper for accessing Morbotron API endpoints (Futurama)."""

BASE_URL = "https://morbotron.com"
TITLE = "Futurama"
DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.FR_BOLD)
EXTRA_FONTS = frozenset({FontFamily.FR_BOLD})
14 changes: 14 additions & 0 deletions compuglobal/api/config.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)
17 changes: 17 additions & 0 deletions compuglobal/models/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
14 changes: 14 additions & 0 deletions compuglobal/models/overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
12 changes: 12 additions & 0 deletions compuglobal/models/subtitle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
10 changes: 10 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----

Expand Down
16 changes: 8 additions & 8 deletions tests/test_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,17 @@ class CustomCompuGlobalAPI(AsyncCompuGlobalAPI):

BASE_URL = "https://example.com"
TITLE = "Testing"
DEFAULT_FORMAT = OverlayFormat(font_family=FontFamily.JOST)


class _InvalidRequestJSONError(Exception):
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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
}
Expand All @@ -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",
Expand All @@ -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,
}
Expand All @@ -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",
Expand Down