diff --git a/core/urls.py b/core/urls.py index 6966557..51bcf42 100644 --- a/core/urls.py +++ b/core/urls.py @@ -2,9 +2,27 @@ from typing import TYPE_CHECKING from django.urls import path -from core import views +from core.views import dashboard +from core.views import dataset_backup_download_view +from core.views import dataset_backups_view +from core.views import debug_view +from core.views import docs_rss_view +from core.views import search_view +from twitch.feeds import DropCampaignAtomFeed +from twitch.feeds import DropCampaignDiscordFeed from twitch.feeds import DropCampaignFeed +from twitch.feeds import GameAtomFeed +from twitch.feeds import GameCampaignAtomFeed +from twitch.feeds import GameCampaignDiscordFeed +from twitch.feeds import GameCampaignFeed +from twitch.feeds import GameDiscordFeed from twitch.feeds import GameFeed +from twitch.feeds import OrganizationAtomFeed +from twitch.feeds import OrganizationDiscordFeed +from twitch.feeds import OrganizationRSSFeed +from twitch.feeds import RewardCampaignAtomFeed +from twitch.feeds import RewardCampaignDiscordFeed +from twitch.feeds import RewardCampaignFeed if TYPE_CHECKING: from django.urls.resolvers import URLPattern @@ -15,21 +33,21 @@ app_name = "core" urlpatterns: list[URLPattern | URLResolver] = [ # / - path("", views.dashboard, name="dashboard"), + path("", dashboard, name="dashboard"), # /search/ - path("search/", views.search_view, name="search"), + path("search/", search_view, name="search"), # /debug/ - path("debug/", views.debug_view, name="debug"), + path("debug/", debug_view, name="debug"), # /datasets/ - path("datasets/", views.dataset_backups_view, name="dataset_backups"), + path("datasets/", dataset_backups_view, name="dataset_backups"), # /datasets/download// path( "datasets/download//", - views.dataset_backup_download_view, + dataset_backup_download_view, name="dataset_backup_download", ), # /docs/rss/ - path("docs/rss/", views.docs_rss_view, name="docs_rss"), + path("docs/rss/", docs_rss_view, name="docs_rss"), # RSS feeds # /rss/campaigns/ - all active campaigns path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"), @@ -38,59 +56,59 @@ urlpatterns: list[URLPattern | URLResolver] = [ # /rss/games//campaigns/ - active campaigns for a specific game path( "rss/games//campaigns/", - views.GameCampaignFeed(), + GameCampaignFeed(), name="game_campaign_feed", ), # /rss/organizations/ - newly added organizations path( "rss/organizations/", - views.OrganizationRSSFeed(), + OrganizationRSSFeed(), name="organization_feed", ), # /rss/reward-campaigns/ - all active reward campaigns path( "rss/reward-campaigns/", - views.RewardCampaignFeed(), + RewardCampaignFeed(), name="reward_campaign_feed", ), # Atom feeds (added alongside RSS to preserve backward compatibility) - path("atom/campaigns/", views.DropCampaignAtomFeed(), name="campaign_feed_atom"), - path("atom/games/", views.GameAtomFeed(), name="game_feed_atom"), + path("atom/campaigns/", DropCampaignAtomFeed(), name="campaign_feed_atom"), + path("atom/games/", GameAtomFeed(), name="game_feed_atom"), path( "atom/games//campaigns/", - views.GameCampaignAtomFeed(), + view=GameCampaignAtomFeed(), name="game_campaign_feed_atom", ), path( "atom/organizations/", - views.OrganizationAtomFeed(), + OrganizationAtomFeed(), name="organization_feed_atom", ), path( "atom/reward-campaigns/", - views.RewardCampaignAtomFeed(), + RewardCampaignAtomFeed(), name="reward_campaign_feed_atom", ), # Discord feeds (Atom feeds with Discord relative timestamps) path( "discord/campaigns/", - views.DropCampaignDiscordFeed(), + DropCampaignDiscordFeed(), name="campaign_feed_discord", ), - path("discord/games/", views.GameDiscordFeed(), name="game_feed_discord"), + path("discord/games/", GameDiscordFeed(), name="game_feed_discord"), path( "discord/games//campaigns/", - views.GameCampaignDiscordFeed(), + GameCampaignDiscordFeed(), name="game_campaign_feed_discord", ), path( "discord/organizations/", - views.OrganizationDiscordFeed(), + OrganizationDiscordFeed(), name="organization_feed_discord", ), path( "discord/reward-campaigns/", - views.RewardCampaignDiscordFeed(), + RewardCampaignDiscordFeed(), name="reward_campaign_feed_discord", ), ] diff --git a/core/views.py b/core/views.py index 797a0a5..f59c9ca 100644 --- a/core/views.py +++ b/core/views.py @@ -3,7 +3,6 @@ import json import logging import operator from collections import OrderedDict -from copy import copy from typing import TYPE_CHECKING from typing import Any @@ -27,21 +26,6 @@ from django.utils import timezone from kick.models import KickChannel from kick.models import KickDropCampaign -from twitch.feeds import DropCampaignAtomFeed -from twitch.feeds import DropCampaignDiscordFeed -from twitch.feeds import DropCampaignFeed -from twitch.feeds import GameAtomFeed -from twitch.feeds import GameCampaignAtomFeed -from twitch.feeds import GameCampaignDiscordFeed -from twitch.feeds import GameCampaignFeed -from twitch.feeds import GameDiscordFeed -from twitch.feeds import GameFeed -from twitch.feeds import OrganizationAtomFeed -from twitch.feeds import OrganizationDiscordFeed -from twitch.feeds import OrganizationRSSFeed -from twitch.feeds import RewardCampaignAtomFeed -from twitch.feeds import RewardCampaignDiscordFeed -from twitch.feeds import RewardCampaignFeed from twitch.models import Channel from twitch.models import ChatBadge from twitch.models import ChatBadgeSet @@ -53,13 +37,11 @@ from twitch.models import RewardCampaign from twitch.models import TimeBasedDrop if TYPE_CHECKING: - from collections.abc import Callable from os import stat_result from pathlib import Path from django.db.models import QuerySet from django.http import HttpRequest - from django.http.request import QueryDict logger: logging.Logger = logging.getLogger("ttvdrops.views") @@ -279,178 +261,33 @@ def sitemap_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0915 # MARK: /docs/rss/ def docs_rss_view(request: HttpRequest) -> HttpResponse: - """View for /docs/rss that lists all available RSS feeds. + """View for /docs/rss that lists all available feeds and explains how to use them. Args: request: The HTTP request object. Returns: - Rendered HTML response with list of RSS feeds. + HttpResponse: The rendered documentation page. """ - - def absolute(path: str) -> str: - try: - return request.build_absolute_uri(path) - except Exception: - logger.exception("Failed to build absolute URL for %s", path) - return path - - def _pretty_example(xml_str: str, max_items: int = 1) -> str: - try: - trimmed: str = xml_str.strip() - first_item: int = trimmed.find("", second_item) - if end_channel != -1: - trimmed = trimmed[:second_item] + trimmed[end_channel:] - formatted: str = trimmed.replace("><", ">\n<") - return "\n".join(line for line in formatted.splitlines() if line.strip()) - except Exception: - logger.exception("Failed to pretty-print RSS example") - return xml_str - - def render_feed(feed_view: Callable[..., HttpResponse], *args: object) -> str: - try: - limited_request: HttpRequest = copy(request) - # Add limit=1 to GET parameters - get_data: QueryDict = request.GET.copy() - get_data["limit"] = "1" - limited_request.GET = get_data - - response: HttpResponse = feed_view(limited_request, *args) - return _pretty_example(response.content.decode("utf-8")) - except Exception: - logger.exception( - "Failed to render %s for RSS docs", - feed_view.__class__.__name__, - ) - return "" - - show_atom: bool = bool(request.GET.get("show_atom")) - - feeds: list[dict[str, str]] = [ - { - "title": "All Organizations", - "description": "Latest organizations added to TTVDrops", - "url": absolute(reverse("core:organization_feed")), - "atom_url": absolute(reverse("core:organization_feed_atom")), - "discord_url": absolute(reverse("core:organization_feed_discord")), - "example_xml": render_feed(OrganizationRSSFeed()), - "example_xml_atom": render_feed(OrganizationAtomFeed()) - if show_atom - else "", - "example_xml_discord": render_feed(OrganizationDiscordFeed()) - if show_atom - else "", - }, - { - "title": "All Games", - "description": "Latest games added to TTVDrops", - "url": absolute(reverse("core:game_feed")), - "atom_url": absolute(reverse("core:game_feed_atom")), - "discord_url": absolute(reverse("core:game_feed_discord")), - "example_xml": render_feed(GameFeed()), - "example_xml_atom": render_feed(GameAtomFeed()) if show_atom else "", - "example_xml_discord": render_feed(GameDiscordFeed()) if show_atom else "", - }, - { - "title": "All Drop Campaigns", - "description": "Latest drop campaigns across all games", - "url": absolute(reverse("core:campaign_feed")), - "atom_url": absolute(reverse("core:campaign_feed_atom")), - "discord_url": absolute(reverse("core:campaign_feed_discord")), - "example_xml": render_feed(DropCampaignFeed()), - "example_xml_atom": render_feed(DropCampaignAtomFeed()) - if show_atom - else "", - "example_xml_discord": render_feed(DropCampaignDiscordFeed()) - if show_atom - else "", - }, - { - "title": "All Reward Campaigns", - "description": "Latest reward campaigns (Quest rewards) on Twitch", - "url": absolute(reverse("core:reward_campaign_feed")), - "atom_url": absolute(reverse("core:reward_campaign_feed_atom")), - "discord_url": absolute(reverse("core:reward_campaign_feed_discord")), - "example_xml": render_feed(RewardCampaignFeed()), - "example_xml_atom": render_feed(RewardCampaignAtomFeed()) - if show_atom - else "", - "example_xml_discord": render_feed(RewardCampaignDiscordFeed()) - if show_atom - else "", - }, - ] - - sample_game: Game | None = Game.objects.order_by("-added_at").first() - sample_org: Organization | None = Organization.objects.order_by("-added_at").first() - if sample_org is None and sample_game is not None: - sample_org = sample_game.owners.order_by("-pk").first() - - filtered_feeds: list[dict[str, str | bool]] = [ - { - "title": "Campaigns for a Single Game", - "description": "Latest drop campaigns for one game.", - "url": ( - absolute( - reverse("core:game_campaign_feed", args=[sample_game.twitch_id]), - ) - if sample_game - else absolute("/rss/games//campaigns/") - ), - "atom_url": ( - absolute( - reverse( - "core:game_campaign_feed_atom", - args=[sample_game.twitch_id], - ), - ) - if sample_game - else absolute("/atom/games//campaigns/") - ), - "discord_url": ( - absolute( - reverse( - "core:game_campaign_feed_discord", - args=[sample_game.twitch_id], - ), - ) - if sample_game - else absolute("/discord/games//campaigns/") - ), - "has_sample": bool(sample_game), - "example_xml": render_feed(GameCampaignFeed(), sample_game.twitch_id) - if sample_game - else "", - "example_xml_atom": ( - render_feed(GameCampaignAtomFeed(), sample_game.twitch_id) - if sample_game and show_atom - else "" - ), - "example_xml_discord": ( - render_feed(GameCampaignDiscordFeed(), sample_game.twitch_id) - if sample_game and show_atom - else "" - ), - }, - ] + now: datetime.datetime = timezone.now() + sample_game: Game | None = ( + Game.objects + .filter(drop_campaigns__start_at__lte=now, drop_campaigns__end_at__gte=now) + .distinct() + .first() + ) seo_context: dict[str, Any] = _build_seo_context( - page_title="Twitch RSS Feeds", - page_description="RSS feeds for Twitch drops.", + page_title="Feed Documentation", + page_description="Documentation for the RSS feeds available on ttvdrops.lovinator.space, including how to use them and what data they contain.", page_url=request.build_absolute_uri(reverse("core:docs_rss")), ) + return render( request, "twitch/docs_rss.html", { - "feeds": feeds, - "filtered_feeds": filtered_feeds, - "sample_game": sample_game, - "sample_org": sample_org, + "game": sample_game, **seo_context, }, ) diff --git a/templates/core/dashboard.html b/templates/core/dashboard.html index d6b1bdf..44155b8 100644 --- a/templates/core/dashboard.html +++ b/templates/core/dashboard.html @@ -48,6 +48,7 @@ title="Atom feed for Twitch campaigns">[atom] [discord] + [explain] {% if campaigns_by_game %} @@ -223,6 +224,7 @@ title="Atom feed for all Kick campaigns">[atom] [discord] + [explain] {% if kick_campaigns_by_game %} diff --git a/templates/kick/campaign_detail.html b/templates/kick/campaign_detail.html index 30b03dc..376d25f 100644 --- a/templates/kick/campaign_detail.html +++ b/templates/kick/campaign_detail.html @@ -59,6 +59,7 @@ title="Atom feed for {{ campaign.category.name }} campaigns">[atom] [discord] + [explain] {% endif %}

diff --git a/templates/kick/campaign_list.html b/templates/kick/campaign_list.html index 7e6d149..8b58cc8 100644 --- a/templates/kick/campaign_list.html +++ b/templates/kick/campaign_list.html @@ -26,6 +26,7 @@ title="Atom feed for all campaigns">[atom] [discord] + [explain]

{% if category.kick_url %}

diff --git a/templates/kick/category_list.html b/templates/kick/category_list.html index 9efd2a8..b09c3bf 100644 --- a/templates/kick/category_list.html +++ b/templates/kick/category_list.html @@ -25,6 +25,7 @@ title="Atom feed for all games">[atom] [discord] + [explain] {% if categories %}