diff --git a/twitch/feeds.py b/twitch/feeds.py index 5a559df..abf854e 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -58,13 +58,112 @@ def _with_campaign_related(queryset: QuerySet[DropCampaign]) -> QuerySet[DropCam ) -def insert_date_info(item: Model, parts: list[SafeText]) -> None: - """Insert start and end date information into parts list. +def genereate_details_link_html(item: DropCampaign) -> list[SafeText]: + """Helper method to append a details link to the description if available. + + Args: + item (DropCampaign): The drop campaign item to check for a details URL. + + Returns: + list[SafeText]: A list containing the formatted details link if a details URL is present, otherwise an empty list. + """ + parts: list[SafeText] = [] + details_url: str | None = getattr(item, "details_url", None) + if details_url: + parts.append(format_html('About', details_url)) + return parts + + +def generate_item_image(item: DropCampaign) -> list[SafeText]: + """Helper method to generate an image tag for the campaign image if available. + + Args: + item (DropCampaign): The drop campaign item to check for an image URL. + + Returns: + list[SafeText]: A list containing the formatted image tag if an image URL is present, otherwise an empty list. + """ + parts: list[SafeText] = [] + image_url: str | None = getattr(item, "image_url", None) + if image_url: + item_name: str = getattr(item, "name", str(object=item)) + parts.append( + format_html( + '{}', + image_url, + item_name, + ), + ) + + return parts + + +def generate_item_image_tag(item: DropCampaign) -> list[SafeString]: + """Helper method to append an image tag for the campaign image if available. + + Args: + item (DropCampaign): The drop campaign item to check for an image URL. + + Returns: + list[SafeString]: A list containing the formatted image tag if an image URL is present, otherwise an empty list. + """ + parts: list[SafeText] = [] + image_url: str | None = getattr(item, "image_url", None) + if image_url: + item_name: str = getattr(item, "name", str(object=item)) + parts.append( + format_html( + '{}', + image_url, + item_name, + ), + ) + + return parts + + +def generate_details_link(item: DropCampaign) -> list[SafeString]: + """Helper method to append a details link to the description if available. + + Args: + item (DropCampaign): The drop campaign item to check for a details URL. + + Returns: + list[SafeString]: A list containing the formatted details link if a details URL is present, otherwise an empty list. + """ + parts: list[SafeText] = [] + details_url: str | None = getattr(item, "details_url", None) + if details_url: + parts.append(format_html('Details', details_url)) + return parts + + +def generate_description_html(item: DropCampaign) -> list[SafeText]: + """Generate additional description HTML for a drop campaign item, such as the description text and details link. + + Args: + item (DropCampaign): The drop campaign item to generate description HTML for. + + Returns: + list[SafeText]: A list of SafeText elements containing the generated description HTML. + """ + parts: list[SafeText] = [] + desc_text: str | None = getattr(item, "description", None) + if desc_text: + parts.append(format_html("

{}

", desc_text)) + return parts + + +def generate_date_html(item: Model) -> list[SafeText]: + """Generate HTML snippets for the start and end dates of a campaign item, formatted with both absolute and relative times. Args: item (Model): The campaign item containing start_at and end_at. - parts (list[SafeText]): The list of HTML parts to append to. + + Returns: + list[SafeText]: A list of SafeText elements with formatted start and end date info. """ + parts: list[SafeText] = [] end_at: datetime.datetime | None = getattr(item, "end_at", None) start_at: datetime.datetime | None = getattr(item, "start_at", None) @@ -95,6 +194,157 @@ def insert_date_info(item: Model, parts: list[SafeText]) -> None: elif end_part: parts.append(format_html("

{}

", end_part)) + return parts + + +def generate_drops_summary_html(item: DropCampaign) -> list[SafeString]: + """Generate HTML summary for drops and append to parts list. + + Args: + item (DropCampaign): The drop campaign item containing the drops to summarize. + + Returns: + list[SafeString]: A list of SafeText elements summarizing the drops, or empty if no drops. + """ + parts: list[SafeText] = [] + drops_data: list[dict] = [] + + channels: list[Channel] | None = getattr(item, "channels_ordered", None) + channel_name: str | None = channels[0].name if channels else None + + drops: QuerySet[TimeBasedDrop] | None = getattr(item, "time_based_drops", None) + if drops: + drops_data = _build_drops_data(drops.all()) + + if drops_data: + parts.append( + format_html( + "

{}

", + _construct_drops_summary(drops_data, channel_name=channel_name), + ), + ) + + return parts + + +def generate_channels_html(item: Model) -> list[SafeText]: + """Generate HTML for the list of channels associated with a drop campaign, if applicable. + + Only generates channel links if the drop is not subscription-only. If it is subscription-only, it will skip channel links to avoid confusion. + + Args: + item (Model): The campaign item which may have an 'is_subscription_only' attribute. + + Returns: + list[SafeText]: A list containing the HTML for the channels section, or empty if subscription-only or no channels. + """ + max_links = 5 + parts: list[SafeText] = [] + + channels: list[Channel] | None = getattr(item, "channels_ordered", None) + if not channels: + return parts + + if getattr(item, "is_subscription_only", False): + return parts + + game: Game | None = getattr(item, "game", None) + + channels_all: list[Channel] = ( + list(channels) + if isinstance(channels, list) + else list(channels.all()) + if channels is not None + else [] + ) + total: int = len(channels_all) + + if channels_all: + create_channel_list_html(max_links, parts, channels_all, total) + return parts + + if not game: + logger.warning( + "No game associated with drop campaign for channel fallback link", + ) + parts.append( + format_html( + "{}", + "", + ), + ) + + return parts + + if not game.twitch_directory_url: + logger.warning( + "Game %s has no Twitch directory URL for channel fallback link", + game, + ) + + if "twitch-chat-badges-guide" in getattr(game, "details_url", ""): + # TODO(TheLovinator): Improve detection of global emotes # noqa: TD003 + parts.append( + format_html( + "{}", + "", + ), + ) + return parts + + parts.append( + format_html( + "{}", + "", + ), + ) + return parts + + display_name: str = getattr(game, "display_name", "this game") + + parts.append( + format_html( + '', + game.twitch_directory_url, + display_name, + display_name, + ), + ) + + return parts + + +def create_channel_list_html( + max_links: int, + parts: list[SafeText], + channels_all: list[Channel], + total: int, +) -> None: + """Helper function to create HTML for a list of channels, limited to max_links with a note if there are more. + + Args: + max_links (int): The maximum number of channel links to display. + parts (list[SafeText]): The list to append the generated HTML to. + channels_all (list[Channel]): The full list of channels to generate links for. + total (int): The total number of channels, used for the "... and X more" message if there are more than max_links. + """ + items: list[SafeString] = [ + format_html( + '
  • {}
  • ', + channel.name, + channel.display_name, + ) + for channel in channels_all[:max_links] + ] + if total > max_links: + items.append(format_html("
  • ... and {} more
  • ", total - max_links)) + + html: SafeText = format_html( + "", + format_html_join("", "{}", [(item,) for item in items]), + ) + parts.append(format_html("

    Channels with this drop:

    {}", html)) + def _build_drops_data(drops_qs: QuerySet[TimeBasedDrop]) -> list[dict]: """Build a simplified data structure for rendering drops in a template. @@ -137,78 +387,6 @@ def _build_drops_data(drops_qs: QuerySet[TimeBasedDrop]) -> list[dict]: return drops_data -def _build_channels_html( - channels: list[Channel] | 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 (list[Channel] | QuerySet[Channel]): The channels (already ordered). - game (Game | None): The game object for fallback link. - - Returns: - SafeText: HTML