From fd856d839b5ea000eed5aff919e06b87588b9aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Fri, 9 Jan 2026 04:59:35 +0100 Subject: [PATCH] Update feeds --- twitch/feeds.py | 788 +++++++++++++++++++++++++++++++----------------- 1 file changed, 515 insertions(+), 273 deletions(-) diff --git a/twitch/feeds.py b/twitch/feeds.py index 2378a3d..b1c80c2 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -32,6 +32,233 @@ if TYPE_CHECKING: logger: logging.Logger = logging.getLogger("ttvdrops") +def insert_date_info(item: Model, parts: list[SafeText]) -> None: + """Insert start and end date information into parts list. + + Args: + item (Model): The campaign item containing start_at and end_at. + parts (list[SafeText]): The list of HTML parts to append to. + """ + end_at: datetime.datetime | None = getattr(item, "end_at", None) + start_at: datetime.datetime | None = getattr(item, "start_at", None) + + if start_at or end_at: + start_part: SafeString = ( + format_html("Starts: {} ({})", start_at.strftime("%Y-%m-%d %H:%M %Z"), naturaltime(start_at)) + if start_at + else SafeText("") + ) + end_part: SafeString = ( + format_html("Ends: {} ({})", end_at.strftime("%Y-%m-%d %H:%M %Z"), naturaltime(end_at)) + if end_at + else SafeText("") + ) + # Start date and end date separated by a line break if both present + if start_part and end_part: + parts.append(format_html("

{}
{}

", start_part, end_part)) + elif start_part: + parts.append(format_html("

{}

", start_part)) + elif end_part: + parts.append(format_html("

{}

", end_part)) + + +def _build_drops_data(drops_qs: QuerySet[TimeBasedDrop]) -> list[dict]: + """Build a simplified data structure for rendering drops in a template. + + Returns: + list[dict]: A list of dictionaries each containing `name`, `benefits`, + `requirements`, and `period` for a drop, suitable for template rendering. + """ + drops_data: list[dict] = [] + for drop in drops_qs: + requirements: str = "" + required_minutes: int | None = getattr(drop, "required_minutes_watched", None) + required_subs: int = getattr(drop, "required_subs", 0) or 0 + if required_minutes: + requirements = f"{required_minutes} minutes watched" + if required_subs > 0: + sub_word: Literal["subs", "sub"] = "subs" if required_subs > 1 else "sub" + if requirements: + requirements += f" and {required_subs} {sub_word} required" + else: + requirements = f"{required_subs} {sub_word} required" + + period: str = "" + drop_start: datetime.datetime | None = getattr(drop, "start_at", None) + drop_end: datetime.datetime | None = getattr(drop, "end_at", None) + if drop_start is not None: + period += drop_start.strftime("%Y-%m-%d %H:%M %Z") + if drop_end is not None: + if period: + period += " - " + drop_end.strftime("%Y-%m-%d %H:%M %Z") + else: + period = drop_end.strftime("%Y-%m-%d %H:%M %Z") + + drops_data.append({ + "name": getattr(drop, "name", str(drop)), + "benefits": list(drop.benefits.all()), + "requirements": requirements, + "period": period, + }) + return drops_data + + +def _build_channels_html(channels: QuerySet[Channel], game: Game | None) -> SafeText: + """Render up to max_links channel links as
  • , then a count of additional channels, or fallback to game category link. + + If only one channel and drop_requirements is '1 subscriptions required', + merge the Twitch link with the '1 subs' row. + + Args: + channels (QuerySet[Channel]): The queryset of channels. + game (Game | None): The game object for fallback link. + + Returns: + SafeText: HTML