Improve RSS feed functionality
This commit is contained in:
parent
282f728870
commit
443bd88cb8
1 changed files with 136 additions and 14 deletions
150
twitch/feeds.py
150
twitch/feeds.py
|
|
@ -148,6 +148,10 @@ def _build_channels_html(channels: QuerySet[Channel], game: Game | None) -> Safe
|
||||||
|
|
||||||
if not game.twitch_directory_url:
|
if not game.twitch_directory_url:
|
||||||
logger.warning("Game %s has no Twitch directory URL for channel fallback link", game)
|
logger.warning("Game %s has no Twitch directory URL for channel fallback link", game)
|
||||||
|
if getattr(game, "details_url", "") == "https://help.twitch.tv/s/article/twitch-chat-badges-guide ":
|
||||||
|
# TODO(TheLovinator): Improve detection of global emotes # noqa: TD003
|
||||||
|
return format_html("{}", "<ul><li>Global Twitch Emote?</li></ul>")
|
||||||
|
|
||||||
return format_html("{}", "<ul><li>Failed to get Twitch category URL :(</li></ul>")
|
return format_html("{}", "<ul><li>Failed to get Twitch category URL :(</li></ul>")
|
||||||
|
|
||||||
# If no channel is associated, the drop is category-wide; link to the game's Twitch directory
|
# If no channel is associated, the drop is category-wide; link to the game's Twitch directory
|
||||||
|
|
@ -270,8 +274,8 @@ class OrganizationFeed(Feed):
|
||||||
feed_copyright: str = "Information wants to be free."
|
feed_copyright: str = "Information wants to be free."
|
||||||
|
|
||||||
def items(self) -> list[Organization]:
|
def items(self) -> list[Organization]:
|
||||||
"""Return the latest 100 organizations."""
|
"""Return the latest 200 organizations."""
|
||||||
return list(Organization.objects.order_by("-added_at")[:100])
|
return list(Organization.objects.order_by("-added_at")[:200])
|
||||||
|
|
||||||
def item_title(self, item: Model) -> SafeText:
|
def item_title(self, item: Model) -> SafeText:
|
||||||
"""Return the organization name as the item title."""
|
"""Return the organization name as the item title."""
|
||||||
|
|
@ -350,8 +354,8 @@ class GameFeed(Feed):
|
||||||
feed_copyright: str = "Information wants to be free."
|
feed_copyright: str = "Information wants to be free."
|
||||||
|
|
||||||
def items(self) -> list[Game]:
|
def items(self) -> list[Game]:
|
||||||
"""Return the latest 100 games."""
|
"""Return the latest 200 games."""
|
||||||
return list(Game.objects.order_by("-added_at")[:100])
|
return list(Game.objects.order_by("-added_at")[:200])
|
||||||
|
|
||||||
def item_title(self, item: Model) -> SafeText:
|
def item_title(self, item: Model) -> SafeText:
|
||||||
"""Return the game name as the item title (SafeText for RSS)."""
|
"""Return the game name as the item title (SafeText for RSS)."""
|
||||||
|
|
@ -467,9 +471,9 @@ class DropCampaignFeed(Feed):
|
||||||
feed_copyright: str = "Information wants to be free."
|
feed_copyright: str = "Information wants to be free."
|
||||||
|
|
||||||
def items(self) -> list[DropCampaign]:
|
def items(self) -> list[DropCampaign]:
|
||||||
"""Return the latest 100 drop campaigns."""
|
"""Return the latest 200 drop campaigns ordered by most recent start date."""
|
||||||
return list(
|
return list(
|
||||||
DropCampaign.objects.select_related("game").order_by("-added_at")[:100],
|
DropCampaign.objects.select_related("game").order_by("-start_at")[:200],
|
||||||
)
|
)
|
||||||
|
|
||||||
def item_title(self, item: Model) -> SafeText:
|
def item_title(self, item: Model) -> SafeText:
|
||||||
|
|
@ -586,6 +590,8 @@ class DropCampaignFeed(Feed):
|
||||||
class GameCampaignFeed(Feed):
|
class GameCampaignFeed(Feed):
|
||||||
"""RSS feed for the latest drop campaigns of a specific game."""
|
"""RSS feed for the latest drop campaigns of a specific game."""
|
||||||
|
|
||||||
|
feed_copyright: str = "Information wants to be free."
|
||||||
|
|
||||||
def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # noqa: ARG002
|
def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # noqa: ARG002
|
||||||
"""Retrieve the Game instance for the given Twitch ID.
|
"""Retrieve the Game instance for the given Twitch ID.
|
||||||
|
|
||||||
|
|
@ -610,12 +616,122 @@ class GameCampaignFeed(Feed):
|
||||||
"""Return a description for the feed."""
|
"""Return a description for the feed."""
|
||||||
return f"Latest drop campaigns for {obj.display_name}"
|
return f"Latest drop campaigns for {obj.display_name}"
|
||||||
|
|
||||||
|
def feed_url(self, obj: Game) -> str:
|
||||||
|
"""Return the URL to the RSS feed itself."""
|
||||||
|
return reverse("twitch:game_campaign_feed", args=[obj.twitch_id])
|
||||||
|
|
||||||
def items(self, obj: Game) -> list[DropCampaign]:
|
def items(self, obj: Game) -> list[DropCampaign]:
|
||||||
"""Return the latest 100 drop campaigns for this game, ordered by most recently added."""
|
"""Return the latest 200 drop campaigns for this game, ordered by most recent start date."""
|
||||||
return list(
|
return list(
|
||||||
DropCampaign.objects.filter(game=obj).select_related("game").order_by("-added_at")[:100],
|
DropCampaign.objects.filter(game=obj).select_related("game").order_by("-start_at")[:200],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def item_title(self, item: Model) -> SafeText:
|
||||||
|
"""Return the campaign name as the item title (SafeText for RSS)."""
|
||||||
|
clean_name: str = getattr(item, "clean_name", str(item))
|
||||||
|
return SafeText(clean_name)
|
||||||
|
|
||||||
|
def item_description(self, item: Model) -> SafeText:
|
||||||
|
"""Return a description of the campaign."""
|
||||||
|
drops_data: list[dict] = []
|
||||||
|
|
||||||
|
drops: QuerySet[TimeBasedDrop] | None = getattr(item, "time_based_drops", None)
|
||||||
|
if drops:
|
||||||
|
drops_data = _build_drops_data(drops.select_related().prefetch_related("benefits").all())
|
||||||
|
|
||||||
|
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('<img src="{}" alt="{}" width="160" height="160" />', image_url, item_name),
|
||||||
|
)
|
||||||
|
|
||||||
|
desc_text: str | None = getattr(item, "description", None)
|
||||||
|
if desc_text:
|
||||||
|
parts.append(format_html("<p>{}</p>", desc_text))
|
||||||
|
|
||||||
|
# Insert start and end date info
|
||||||
|
insert_date_info(item, parts)
|
||||||
|
|
||||||
|
if drops_data:
|
||||||
|
parts.append(format_html("<p>{}</p>", _construct_drops_summary(drops_data)))
|
||||||
|
|
||||||
|
# Only show channels if drop is not subscription only
|
||||||
|
if not getattr(item, "is_subscription_only", False):
|
||||||
|
channels: QuerySet[Channel] | None = getattr(item, "allow_channels", None)
|
||||||
|
if channels is not None:
|
||||||
|
game: Game | None = getattr(item, "game", None)
|
||||||
|
parts.append(_build_channels_html(channels, game=game))
|
||||||
|
|
||||||
|
details_url: str | None = getattr(item, "details_url", None)
|
||||||
|
if details_url:
|
||||||
|
parts.append(format_html('<a href="{}">About</a>', details_url))
|
||||||
|
|
||||||
|
account_link_url: str | None = getattr(item, "account_link_url", None)
|
||||||
|
if account_link_url:
|
||||||
|
parts.append(format_html(' | <a href="{}">Link Account</a>', account_link_url))
|
||||||
|
|
||||||
|
return SafeText("".join(str(p) for p in parts))
|
||||||
|
|
||||||
|
def item_pubdate(self, item: Model) -> datetime.datetime:
|
||||||
|
"""Returns the publication date to the feed item.
|
||||||
|
|
||||||
|
Uses start_at (when the drop starts). Fallback to added_at or now if missing.
|
||||||
|
"""
|
||||||
|
start_at: datetime.datetime | None = getattr(item, "start_at", None)
|
||||||
|
if start_at:
|
||||||
|
return start_at
|
||||||
|
added_at: datetime.datetime | None = getattr(item, "added_at", None)
|
||||||
|
if added_at:
|
||||||
|
return added_at
|
||||||
|
return timezone.now()
|
||||||
|
|
||||||
|
def item_updateddate(self, item: DropCampaign) -> datetime.datetime:
|
||||||
|
"""Returns the campaign's last update time."""
|
||||||
|
return item.updated_at
|
||||||
|
|
||||||
|
def item_categories(self, item: DropCampaign) -> tuple[str, ...]:
|
||||||
|
"""Returns the associated game's name as a category."""
|
||||||
|
categories: list[str] = ["twitch", "drops"]
|
||||||
|
|
||||||
|
item_game: Game | None = getattr(item, "game", None)
|
||||||
|
if item_game:
|
||||||
|
categories.append(item_game.get_game_name)
|
||||||
|
item_game_owner: Organization | None = getattr(item_game, "owner", None)
|
||||||
|
if item_game_owner:
|
||||||
|
categories.extend((str(item_game_owner.name), str(item_game_owner.twitch_id)))
|
||||||
|
|
||||||
|
return tuple(categories)
|
||||||
|
|
||||||
|
def item_guid(self, item: DropCampaign) -> str:
|
||||||
|
"""Return a unique identifier for each campaign."""
|
||||||
|
return item.twitch_id + "@ttvdrops.com"
|
||||||
|
|
||||||
|
def item_author_name(self, item: DropCampaign) -> str:
|
||||||
|
"""Return the author name for the campaign, typically the game name."""
|
||||||
|
item_game: Game | None = getattr(item, "game", None)
|
||||||
|
if item_game and item_game.display_name:
|
||||||
|
return item_game.display_name
|
||||||
|
|
||||||
|
return "Twitch"
|
||||||
|
|
||||||
|
def item_enclosure_url(self, item: DropCampaign) -> str:
|
||||||
|
"""Returns the URL of the campaign image for enclosure."""
|
||||||
|
return item.image_url
|
||||||
|
|
||||||
|
def item_enclosure_length(self, item: DropCampaign) -> int: # noqa: ARG002
|
||||||
|
"""Returns the length of the enclosure."""
|
||||||
|
# TODO(TheLovinator): Track image size for proper length # noqa: TD003
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def item_enclosure_mime_type(self, item: DropCampaign) -> str: # noqa: ARG002
|
||||||
|
"""Returns the MIME type of the enclosure."""
|
||||||
|
# TODO(TheLovinator): Determine actual MIME type if needed # noqa: TD003
|
||||||
|
return "image/jpeg"
|
||||||
|
|
||||||
|
|
||||||
# MARK: /rss/organizations/<twitch_id>/campaigns/
|
# MARK: /rss/organizations/<twitch_id>/campaigns/
|
||||||
class OrganizationCampaignFeed(Feed):
|
class OrganizationCampaignFeed(Feed):
|
||||||
|
|
@ -646,9 +762,9 @@ class OrganizationCampaignFeed(Feed):
|
||||||
return f"Latest drop campaigns for organization {obj.name}"
|
return f"Latest drop campaigns for organization {obj.name}"
|
||||||
|
|
||||||
def items(self, obj: Organization) -> list[DropCampaign]:
|
def items(self, obj: Organization) -> list[DropCampaign]:
|
||||||
"""Return the latest 100 drop campaigns for this organization, ordered by most recently added."""
|
"""Return the latest 200 drop campaigns for this organization, ordered by most recent start date."""
|
||||||
return list(
|
return list(
|
||||||
DropCampaign.objects.filter(game__owners=obj).select_related("game").order_by("-added_at")[:100],
|
DropCampaign.objects.filter(game__owners=obj).select_related("game").order_by("-start_at")[:200],
|
||||||
)
|
)
|
||||||
|
|
||||||
def item_author_name(self, item: DropCampaign) -> str:
|
def item_author_name(self, item: DropCampaign) -> str:
|
||||||
|
|
@ -698,8 +814,11 @@ class OrganizationCampaignFeed(Feed):
|
||||||
def item_pubdate(self, item: Model) -> datetime.datetime:
|
def item_pubdate(self, item: Model) -> datetime.datetime:
|
||||||
"""Returns the publication date to the feed item.
|
"""Returns the publication date to the feed item.
|
||||||
|
|
||||||
Fallback to updated_at or now if missing.
|
Uses start_at (when the drop starts). Fallback to added_at or now if missing.
|
||||||
"""
|
"""
|
||||||
|
start_at: datetime.datetime | None = getattr(item, "start_at", None)
|
||||||
|
if start_at:
|
||||||
|
return start_at
|
||||||
added_at: datetime.datetime | None = getattr(item, "added_at", None)
|
added_at: datetime.datetime | None = getattr(item, "added_at", None)
|
||||||
if added_at:
|
if added_at:
|
||||||
return added_at
|
return added_at
|
||||||
|
|
@ -757,9 +876,9 @@ class RewardCampaignFeed(Feed):
|
||||||
feed_copyright: str = "Information wants to be free."
|
feed_copyright: str = "Information wants to be free."
|
||||||
|
|
||||||
def items(self) -> list[RewardCampaign]:
|
def items(self) -> list[RewardCampaign]:
|
||||||
"""Return the latest 100 reward campaigns."""
|
"""Return the latest 200 reward campaigns."""
|
||||||
return list(
|
return list(
|
||||||
RewardCampaign.objects.select_related("game").order_by("-added_at")[:100],
|
RewardCampaign.objects.select_related("game").order_by("-added_at")[:200],
|
||||||
)
|
)
|
||||||
|
|
||||||
def item_title(self, item: Model) -> SafeText:
|
def item_title(self, item: Model) -> SafeText:
|
||||||
|
|
@ -828,8 +947,11 @@ class RewardCampaignFeed(Feed):
|
||||||
def item_pubdate(self, item: Model) -> datetime.datetime:
|
def item_pubdate(self, item: Model) -> datetime.datetime:
|
||||||
"""Returns the publication date to the feed item.
|
"""Returns the publication date to the feed item.
|
||||||
|
|
||||||
Fallback to added_at or now if missing.
|
Uses starts_at (when the reward starts). Fallback to added_at or now if missing.
|
||||||
"""
|
"""
|
||||||
|
starts_at: datetime.datetime | None = getattr(item, "starts_at", None)
|
||||||
|
if starts_at:
|
||||||
|
return starts_at
|
||||||
added_at: datetime.datetime | None = getattr(item, "added_at", None)
|
added_at: datetime.datetime | None = getattr(item, "added_at", None)
|
||||||
if added_at:
|
if added_at:
|
||||||
return added_at
|
return added_at
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue