From 4b4723c77cc4add1a659e933a7deac90bc6e1fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Thu, 8 Jan 2026 23:52:29 +0100 Subject: [PATCH] Refactor RSS stuff --- .vscode/settings.json | 2 + twitch/feeds.py | 405 ++++++++++++++++++++++++++++++------------ twitch/models.py | 9 +- twitch/views.py | 2 - 4 files changed, 305 insertions(+), 113 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6170aae..1188f98 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,8 @@ "Mailgun", "makemigrations", "McCabe", + "noopener", + "noreferrer", "platformdirs", "prefetcher", "psutil", diff --git a/twitch/feeds.py b/twitch/feeds.py index e52fa7e..2378a3d 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -1,14 +1,22 @@ from __future__ import annotations +import logging +import re from typing import TYPE_CHECKING +from typing import Literal +from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.syndication.views import Feed from django.db.models.query import QuerySet from django.urls import reverse from django.utils import timezone from django.utils.html import format_html +from django.utils.html import format_html_join +from django.utils.safestring import SafeString from django.utils.safestring import SafeText +from twitch.models import Channel +from twitch.models import DropBenefit from twitch.models import DropCampaign from twitch.models import Game from twitch.models import Organization @@ -21,6 +29,8 @@ if TYPE_CHECKING: from django.db.models import QuerySet from django.http import HttpRequest +logger: logging.Logger = logging.getLogger("ttvdrops") + # MARK: /rss/organizations/ class OrganizationFeed(Feed): @@ -76,6 +86,111 @@ class GameFeed(Feed): class DropCampaignFeed(Feed): """RSS feed for latest drop campaigns.""" + def _get_channel_name_from_drops(self, drops: QuerySet[TimeBasedDrop]) -> str | None: + for d in drops: + campaign: DropCampaign | None = getattr(d, "campaign", None) + if campaign: + allow_channels: QuerySet[Channel] | None = getattr(campaign, "allow_channels", None) + if allow_channels: + channels: QuerySet[Channel, Channel] = allow_channels.all() + if channels: + return channels[0].name + return None + + def get_channel_from_benefit(self, benefit: Model) -> str | None: + """Get the Twitch channel name associated with a drop benefit. + + Args: + benefit (Model): The drop benefit model instance. + + Returns: + str | None: The Twitch channel name if found, else None. + """ + drop_obj: QuerySet[TimeBasedDrop] | None = getattr(benefit, "drops", None) + if drop_obj and hasattr(drop_obj, "all"): + try: + return self._get_channel_name_from_drops(drop_obj.all()) + except AttributeError: + logger.exception("Exception occurred while resolving channel name for benefit") + return None + + def _resolve_channel_name(self, drop: dict) -> str | None: + """Try to resolve the Twitch channel name for a drop dict's benefits or fallback keys. + + Args: + drop (dict): The drop data dictionary. + + Returns: + str | None: The Twitch channel name if found, else None. + """ + benefits: list[Model] = drop.get("benefits", []) + benefit0: Model | None = benefits[0] if benefits else None + if benefit0 and hasattr(benefit0, "drops"): + channel_name: str | None = self.get_channel_from_benefit(benefit0) + if channel_name: + return channel_name + return None + + def _build_channels_html(self, 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