Embed YouTube videos in /feed HTML. Strong code, many bananas! 🦍🦍🦍🦍

This commit is contained in:
2025-04-03 06:20:01 +02:00
parent ac63041b28
commit 97d06ddb43
4 changed files with 187 additions and 1 deletions

View File

@ -67,6 +67,10 @@ def send_entry_to_discord(entry: Entry, custom_reader: Reader | None = None) ->
logger.exception("Error getting should_send_embed tag for feed: %s", entry.feed.url) logger.exception("Error getting should_send_embed tag for feed: %s", entry.feed.url)
should_send_embed = True should_send_embed = True
# YouTube feeds should never use embeds
if is_youtube_feed(entry.feed.url):
should_send_embed = False
if should_send_embed: if should_send_embed:
webhook = create_embed_webhook(webhook_url, entry) webhook = create_embed_webhook(webhook_url, entry)
else: else:
@ -295,6 +299,18 @@ def execute_webhook(webhook: DiscordWebhook, entry: Entry) -> None:
logger.info("Sent entry to Discord: %s", entry.id) logger.info("Sent entry to Discord: %s", entry.id)
def is_youtube_feed(feed_url: str) -> bool:
"""Check if the feed is a YouTube feed.
Args:
feed_url: The feed URL to check.
Returns:
bool: True if the feed is a YouTube feed, False otherwise.
"""
return "youtube.com/feeds/videos.xml" in feed_url
def should_send_embed_check(reader: Reader, entry: Entry) -> bool: def should_send_embed_check(reader: Reader, entry: Entry) -> bool:
"""Check if we should send an embed to Discord. """Check if we should send an embed to Discord.
@ -305,6 +321,10 @@ def should_send_embed_check(reader: Reader, entry: Entry) -> bool:
Returns: Returns:
bool: True if we should send an embed, False otherwise. bool: True if we should send an embed, False otherwise.
""" """
# YouTube feeds should never use embeds - only links
if is_youtube_feed(entry.feed.url):
return False
try: try:
should_send_embed = bool(reader.get_tag(entry.feed, "should_send_embed")) should_send_embed = bool(reader.get_tag(entry.feed, "should_send_embed"))
except TagNotFoundError: except TagNotFoundError:

View File

@ -732,6 +732,27 @@ def create_html_for_feed(entries: Iterable[Entry]) -> str:
entry_id: str = urllib.parse.quote(entry.id) entry_id: str = urllib.parse.quote(entry.id)
to_discord_html: str = f"<a class='text-muted' href='/post_entry?entry_id={entry_id}'>Send to Discord</a>" to_discord_html: str = f"<a class='text-muted' href='/post_entry?entry_id={entry_id}'>Send to Discord</a>"
# Check if this is a YouTube feed entry and the entry has a link
is_youtube_feed = "youtube.com/feeds/videos.xml" in entry.feed.url
video_embed_html = ""
if is_youtube_feed and entry.link:
# Extract the video ID and create an embed if possible
video_id: str | None = extract_youtube_video_id(entry.link)
if video_id:
video_embed_html: str = f"""
<div class="ratio ratio-16x9 mt-3 mb-3">
<iframe src="https://www.youtube.com/embed/{video_id}"
title="{entry.title}"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
</div>
"""
# Don't use the first image if we have a video embed
first_image = ""
image_html: str = f"<img src='{first_image}' class='img-fluid'>" if first_image else "" image_html: str = f"<img src='{first_image}' class='img-fluid'>" if first_image else ""
html += f"""<div class="p-2 mb-2 border border-dark"> html += f"""<div class="p-2 mb-2 border border-dark">
@ -739,6 +760,7 @@ def create_html_for_feed(entries: Iterable[Entry]) -> str:
{f"By {entry.author} @" if entry.author else ""}{published} - {to_discord_html} {f"By {entry.author} @" if entry.author else ""}{published} - {to_discord_html}
{text} {text}
{video_embed_html}
{image_html} {image_html}
</div> </div>
""" """
@ -991,6 +1013,29 @@ def modify_webhook(old_hook: Annotated[str, Form()], new_hook: Annotated[str, Fo
return RedirectResponse(url="/webhooks", status_code=303) return RedirectResponse(url="/webhooks", status_code=303)
def extract_youtube_video_id(url: str) -> str | None:
"""Extract YouTube video ID from a YouTube video URL.
Args:
url: The YouTube video URL.
Returns:
The video ID if found, None otherwise.
"""
if not url:
return None
# Handle standard YouTube URLs (youtube.com/watch?v=VIDEO_ID)
if "youtube.com/watch" in url and "v=" in url:
return url.split("v=")[1].split("&")[0]
# Handle shortened YouTube URLs (youtu.be/VIDEO_ID)
if "youtu.be/" in url:
return url.split("youtu.be/")[1].split("?")[0]
return None
if __name__ == "__main__": if __name__ == "__main__":
sentry_sdk.init( sentry_sdk.init(
dsn="https://6e77a0d7acb9c7ea22e85a375e0ff1f4@o4505228040339456.ingest.us.sentry.io/4508792887967744", dsn="https://6e77a0d7acb9c7ea22e85a375e0ff1f4@o4505228040339456.ingest.us.sentry.io/4508792887967744",

View File

@ -43,6 +43,7 @@
</form> </form>
{% endif %} {% endif %}
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
{% if should_send_embed %} {% if should_send_embed %}
<form action="/use_text" method="post" class="d-inline"> <form action="/use_text" method="post" class="d-inline">
<button class="btn btn-dark btn-sm" name="feed_url" value="{{ feed.url }}"> <button class="btn btn-dark btn-sm" name="feed_url" value="{{ feed.url }}">
@ -56,6 +57,7 @@
</button> </button>
</form> </form>
{% endif %} {% endif %}
{% endif %}
</div> </div>
<!-- Additional Links --> <!-- Additional Links -->
@ -65,9 +67,11 @@
<a class="text-muted d-block" href="/custom?feed_url={{ feed.url|encode_url }}"> <a class="text-muted d-block" href="/custom?feed_url={{ feed.url|encode_url }}">
Customize message {% if not should_send_embed %}(Currently active){% endif %} Customize message {% if not should_send_embed %}(Currently active){% endif %}
</a> </a>
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
<a class="text-muted d-block" href="/embed?feed_url={{ feed.url|encode_url }}"> <a class="text-muted d-block" href="/embed?feed_url={{ feed.url|encode_url }}">
Customize embed {% if should_send_embed %}(Currently active){% endif %} Customize embed {% if should_send_embed %}(Currently active){% endif %}
</a> </a>
{% endif %}
</div> </div>
</div> </div>

View File

@ -4,11 +4,18 @@ import os
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import LiteralString from typing import LiteralString
from unittest.mock import MagicMock, patch
import pytest import pytest
from reader import Feed, Reader, make_reader from reader import Feed, Reader, make_reader
from discord_rss_bot.feeds import send_to_discord, truncate_webhook_message from discord_rss_bot.feeds import (
is_youtube_feed,
send_entry_to_discord,
send_to_discord,
should_send_embed_check,
truncate_webhook_message,
)
from discord_rss_bot.missing_tags import add_missing_tags from discord_rss_bot.missing_tags import add_missing_tags
@ -85,3 +92,113 @@ def test_truncate_webhook_message_long_message():
# Test the end of the message # Test the end of the message
assert_msg = "The end of the truncated message should be '...' to indicate truncation." assert_msg = "The end of the truncated message should be '...' to indicate truncation."
assert truncated_message[-half_length:] == "A" * half_length, assert_msg assert truncated_message[-half_length:] == "A" * half_length, assert_msg
def test_is_youtube_feed():
"""Test the is_youtube_feed function."""
# YouTube feed URLs
assert is_youtube_feed("https://www.youtube.com/feeds/videos.xml?channel_id=123456") is True
assert is_youtube_feed("https://www.youtube.com/feeds/videos.xml?user=username") is True
# Non-YouTube feed URLs
assert is_youtube_feed("https://www.example.com/feed.xml") is False
assert is_youtube_feed("https://www.youtube.com/watch?v=123456") is False
assert is_youtube_feed("https://www.reddit.com/r/Python/.rss") is False
@patch("discord_rss_bot.feeds.logger")
def test_should_send_embed_check_youtube_feeds(mock_logger: MagicMock) -> None:
"""Test should_send_embed_check returns False for YouTube feeds regardless of settings."""
# Create mocks
mock_reader = MagicMock()
mock_entry = MagicMock()
# Configure a YouTube feed
mock_entry.feed.url = "https://www.youtube.com/feeds/videos.xml?channel_id=123456"
# Set reader to return True for should_send_embed (would normally create an embed)
mock_reader.get_tag.return_value = True
# Result should be False, overriding the feed settings
result = should_send_embed_check(mock_reader, mock_entry)
assert result is False, "YouTube feeds should never use embeds"
# Function should not even call get_tag for YouTube feeds
mock_reader.get_tag.assert_not_called()
@patch("discord_rss_bot.feeds.logger")
def test_should_send_embed_check_normal_feeds(mock_logger: MagicMock) -> None:
"""Test should_send_embed_check returns feed settings for non-YouTube feeds."""
# Create mocks
mock_reader = MagicMock()
mock_entry = MagicMock()
# Configure a normal feed
mock_entry.feed.url = "https://www.example.com/feed.xml"
# Test with should_send_embed set to True
mock_reader.get_tag.return_value = True
result = should_send_embed_check(mock_reader, mock_entry)
assert result is True, "Normal feeds should use embeds when enabled"
# Test with should_send_embed set to False
mock_reader.get_tag.return_value = False
result = should_send_embed_check(mock_reader, mock_entry)
assert result is False, "Normal feeds should not use embeds when disabled"
@patch("discord_rss_bot.feeds.get_reader")
@patch("discord_rss_bot.feeds.get_custom_message")
@patch("discord_rss_bot.feeds.replace_tags_in_text_message")
@patch("discord_rss_bot.feeds.create_embed_webhook")
@patch("discord_rss_bot.feeds.DiscordWebhook")
@patch("discord_rss_bot.feeds.execute_webhook")
def test_send_entry_to_discord_youtube_feed(
mock_execute_webhook: MagicMock,
mock_discord_webhook: MagicMock,
mock_create_embed: MagicMock,
mock_replace_tags: MagicMock,
mock_get_custom_message: MagicMock,
mock_get_reader: MagicMock,
):
"""Test send_entry_to_discord function with YouTube feeds."""
# Set up mocks
mock_reader = MagicMock()
mock_get_reader.return_value = mock_reader
mock_entry = MagicMock()
mock_feed = MagicMock()
# Configure a YouTube feed
mock_entry.feed = mock_feed
mock_entry.feed.url = "https://www.youtube.com/feeds/videos.xml?channel_id=123456"
mock_entry.feed_url = "https://www.youtube.com/feeds/videos.xml?channel_id=123456"
# Mock the tags
mock_reader.get_tag.side_effect = lambda feed, tag, default=None: { # noqa: ARG005
"webhook": "https://discord.com/api/webhooks/123/abc",
"should_send_embed": True, # This should be ignored for YouTube feeds
}.get(tag, default)
# Mock custom message
mock_get_custom_message.return_value = "Custom message"
mock_replace_tags.return_value = "Formatted message with {{entry_link}}"
# Mock webhook
mock_webhook = MagicMock()
mock_discord_webhook.return_value = mock_webhook
# Call the function
send_entry_to_discord(mock_entry)
# Assertions
mock_create_embed.assert_not_called()
mock_discord_webhook.assert_called_once()
# Check webhook was created with the right message
webhook_call_kwargs = mock_discord_webhook.call_args[1]
assert "content" in webhook_call_kwargs, "Webhook should have content"
assert webhook_call_kwargs["url"] == "https://discord.com/api/webhooks/123/abc"
# Verify execute_webhook was called
mock_execute_webhook.assert_called_once_with(mock_webhook, mock_entry)