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 from twitch.models import TimeBasedDrop if TYPE_CHECKING: import datetime from django.db.models import Model from django.db.models import QuerySet from django.http import HttpRequest logger: logging.Logger = logging.getLogger("ttvdrops") # MARK: /rss/organizations/ class OrganizationFeed(Feed): """RSS feed for latest organizations.""" title: str = "TTVDrops Organizations" link: str = "/organizations/" description: str = "Latest organizations on TTVDrops" def items(self) -> list[Organization]: """Return the latest 100 organizations.""" return list(Organization.objects.order_by("-updated_at")[:100]) def item_title(self, item: Model) -> SafeText: """Return the organization name as the item title.""" return SafeText(getattr(item, "name", str(item))) def item_description(self, item: Model) -> SafeText: """Return a description of the organization.""" return SafeText(f"Organization {getattr(item, 'name', str(item))}") def item_link(self, item: Model) -> str: """Return the link to the organization detail.""" return reverse("twitch:organization_detail", args=[item.pk]) # MARK: /rss/games/ class GameFeed(Feed): """RSS feed for latest games.""" title: str = "TTVDrops Games" link: str = "/games/" description: str = "Latest games on TTVDrops" def items(self) -> list[Game]: """Return the latest 100 games.""" return list(Game.objects.order_by("-id")[:100]) def item_title(self, item: Model) -> SafeText: """Return the game name as the item title (SafeText for RSS).""" return SafeText(str(item)) def item_description(self, item: Model) -> SafeText: """Return a description of the game.""" return SafeText(f"Game {getattr(item, 'display_name', str(item))}") def item_link(self, item: Model) -> str: """Return the link to the game detail.""" return reverse("twitch:game_detail", args=[item.pk]) # MARK: /rss/campaigns/ 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