Embed YouTube videos in /feed HTML. Strong code, many bananas! 🦍🦍🦍🦍
This commit is contained in:
@ -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:
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user