From 5bdee66207362bcb1ed825019000231548351fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Mon, 16 Mar 2026 21:27:11 +0100 Subject: [PATCH] Add YouTube --- config/settings.py | 1 + config/tests/test_urls.py | 3 + config/urls.py | 2 + templates/base.html | 2 +- templates/youtube/index.html | 20 ++++++ youtube/__init__.py | 0 youtube/apps.py | 7 ++ youtube/migrations/__init__.py | 0 youtube/tests/__init__.py | 0 youtube/tests/test_youtube.py | 49 ++++++++++++++ youtube/urls.py | 16 +++++ youtube/views.py | 115 +++++++++++++++++++++++++++++++++ 12 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 templates/youtube/index.html create mode 100644 youtube/__init__.py create mode 100644 youtube/apps.py create mode 100644 youtube/migrations/__init__.py create mode 100644 youtube/tests/__init__.py create mode 100644 youtube/tests/test_youtube.py create mode 100644 youtube/urls.py create mode 100644 youtube/views.py diff --git a/config/settings.py b/config/settings.py index 1894f24..da3fb63 100644 --- a/config/settings.py +++ b/config/settings.py @@ -141,6 +141,7 @@ INSTALLED_APPS: list[str] = [ "django.contrib.postgres", "twitch.apps.TwitchConfig", "kick.apps.KickConfig", + "youtube.apps.YoutubeConfig", "core.apps.CoreConfig", ] diff --git a/config/tests/test_urls.py b/config/tests/test_urls.py index 04fd386..5e6a482 100644 --- a/config/tests/test_urls.py +++ b/config/tests/test_urls.py @@ -39,6 +39,9 @@ def test_top_level_named_routes_available() -> None: msg: str = f"Expected 'twitch:dashboard' to reverse to '/twitch/', got {reverse('twitch:dashboard')}" assert reverse("twitch:dashboard") == "/twitch/", msg + youtube_msg: str = f"Expected 'youtube:index' to reverse to '/youtube/', got {reverse('youtube:index')}" + assert reverse("youtube:index") == "/youtube/", youtube_msg + def test_debug_tools_not_present_while_testing() -> None: """`silk` and Django Debug Toolbar URL patterns are not present while running tests.""" diff --git a/config/urls.py b/config/urls.py index bf0c81c..22186a7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -19,6 +19,8 @@ urlpatterns: list[URLPattern | URLResolver] = [ path(route="twitch/", view=include("twitch.urls", namespace="twitch")), # Kick app path(route="kick/", view=include("kick.urls", namespace="kick")), + # YouTube app + path(route="youtube/", view=include("youtube.urls", namespace="youtube")), ] # Serve media in development diff --git a/templates/base.html b/templates/base.html index dcafd79..ae128cb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -276,7 +276,7 @@
Other sites Steam | - YouTube | + YouTube | TikTok | Discord diff --git a/templates/youtube/index.html b/templates/youtube/index.html new file mode 100644 index 0000000..f5dca90 --- /dev/null +++ b/templates/youtube/index.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %} + YouTube Drops Channels +{% endblock title %} +{% block content %} +
+

YouTube Drops Channels

+

Official channels from YouTube partner accounts where drops/rewards may be available.

+ {% for group in partner_groups %} +

{{ group.partner }}

+ + {% endfor %} +
+{% endblock content %} diff --git a/youtube/__init__.py b/youtube/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/youtube/apps.py b/youtube/apps.py new file mode 100644 index 0000000..b3b48ca --- /dev/null +++ b/youtube/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class YoutubeConfig(AppConfig): + """Django app configuration for the YouTube app.""" + + name = "youtube" diff --git a/youtube/migrations/__init__.py b/youtube/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/youtube/tests/__init__.py b/youtube/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/youtube/tests/test_youtube.py b/youtube/tests/test_youtube.py new file mode 100644 index 0000000..6b207ce --- /dev/null +++ b/youtube/tests/test_youtube.py @@ -0,0 +1,49 @@ +from typing import TYPE_CHECKING + +from django.test import TestCase +from django.urls import reverse + +if TYPE_CHECKING: + from django.test.client import _MonkeyPatchedWSGIResponse + + +class YouTubeIndexViewTest(TestCase): + """Tests for the YouTube drops channels index page.""" + + def test_index_returns_200(self) -> None: + """The YouTube index page should return HTTP 200.""" + response: _MonkeyPatchedWSGIResponse = self.client.get(reverse("youtube:index")) + assert response.status_code == 200 + + def test_index_displays_known_channels(self) -> None: + """The page should include key known channels from the partner list.""" + response: _MonkeyPatchedWSGIResponse = self.client.get(reverse("youtube:index")) + content: str = response.content.decode() + + assert "YouTube Drops Channels" in content + assert "Call of Duty" in content + assert "PlayOverwatch" in content + assert "Hearthstone" in content + assert "Fortnite" in content + assert "Riot Games" in content + assert "Ubisoft" in content + + def test_index_includes_partner_urls(self) -> None: + """The page should render partner channel links from the source list.""" + response: _MonkeyPatchedWSGIResponse = self.client.get(reverse("youtube:index")) + content: str = response.content.decode() + + assert "https://www.youtube.com/channel/UCbLIqv9Puhyp9_ZjVtfOy7w" in content + assert "https://www.youtube.com/user/epicfortnite" in content + assert "https://www.youtube.com/lolesports" in content + + def test_index_groups_partners_alphabetically(self) -> None: + """Partner sections should render grouped and in alphabetical order.""" + response: _MonkeyPatchedWSGIResponse = self.client.get(reverse("youtube:index")) + content: str = response.content.decode() + + assert "

Activision (Call of Duty)

" in content + assert "

Battle.net / Blizzard

" in content + assert content.index("

Activision (Call of Duty)

") < content.index( + "

Battle.net / Blizzard

", + ) diff --git a/youtube/urls.py b/youtube/urls.py new file mode 100644 index 0000000..046b2c8 --- /dev/null +++ b/youtube/urls.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING + +from django.urls import path + +from youtube import views + +if TYPE_CHECKING: + from django.urls.resolvers import URLPattern + from django.urls.resolvers import URLResolver + +app_name = "youtube" + + +urlpatterns: list[URLPattern | URLResolver] = [ + path(route="", view=views.index, name="index"), +] diff --git a/youtube/views.py b/youtube/views.py new file mode 100644 index 0000000..28b2e27 --- /dev/null +++ b/youtube/views.py @@ -0,0 +1,115 @@ +from collections import defaultdict +from typing import TYPE_CHECKING + +from django.shortcuts import render + +if TYPE_CHECKING: + from django.http import HttpRequest + from django.http import HttpResponse + + +def index(request: HttpRequest) -> HttpResponse: + """Render a minimal list of YouTube channels with known drops-enabled partners. + + Returns: + HttpResponse: Rendered index page for YouTube drops channels. + """ + channels: list[dict[str, str]] = [ + { + "partner": "Activision (Call of Duty)", + "channel": "Call of Duty", + "url": "https://www.youtube.com/channel/UCbLIqv9Puhyp9_ZjVtfOy7w", + }, + { + "partner": "Battle.net / Blizzard", + "channel": "PlayOverwatch", + "url": "https://www.youtube.com/c/playoverwatch/featured", + }, + { + "partner": "Battle.net / Blizzard", + "channel": "Hearthstone", + "url": "https://www.youtube.com/c/Hearthstone/featured", + }, + { + "partner": "Electronic Arts", + "channel": "FIFA", + "url": "https://www.youtube.com/channel/UCFA6YGp5lvgayO20lk7_Ung", + }, + { + "partner": "Electronic Arts", + "channel": "EA Madden NFL", + "url": "https://www.youtube.com/@EAMaddenNFL", + }, + { + "partner": "Epic Games", + "channel": "Fortnite", + "url": "https://www.youtube.com/user/epicfortnite", + }, + { + "partner": "Garena", + "channel": "Free Fire", + "url": "https://www.youtube.com/channel/UC_vVy4OI86F0amXqFN_zTMg", + }, + { + "partner": "Krafton (PUBG)", + "channel": "PUBG: BATTLEGROUNDS", + "url": "https://www.youtube.com/channel/UCTDO0RgowRyaAEUrPnBAg4g", + }, + { + "partner": "MLBB", + "channel": "Mobile Legends: Bang Bang", + "url": "https://www.youtube.com/channel/UCqmld-BIYME2i_ooRTo1EOg", + }, + { + "partner": "NBA", + "channel": "NBA", + "url": "https://www.youtube.com/user/NBA", + }, + { + "partner": "NFL", + "channel": "NFL", + "url": "https://www.youtube.com/@NFL", + }, + { + "partner": "PUBG Mobile", + "channel": "PUBG MOBILE", + "url": "https://www.youtube.com/channel/UCTDO0RgowRyaAEUrPnBAg4g", + }, + { + "partner": "Riot Games", + "channel": "Riot Games", + "url": "https://www.youtube.com/user/RiotGamesInc", + }, + { + "partner": "Riot Games", + "channel": "LoL Esports", + "url": "https://www.youtube.com/lolesports", + }, + { + "partner": "Supercell", + "channel": "Clash Royale", + "url": "https://www.youtube.com/channel/UC_F8DoJf9MZogEOU51TpTbQ", + }, + { + "partner": "Ubisoft", + "channel": "Ubisoft", + "url": "https://www.youtube.com/user/ubisoft", + }, + ] + + grouped_channels: dict[str, list[dict[str, str]]] = defaultdict(list) + for channel in channels: + grouped_channels[channel["partner"]].append(channel) + + partner_groups: list[dict[str, str | list[dict[str, str]]]] = [] + for partner in sorted(grouped_channels.keys(), key=str.lower): + sorted_items: list[dict[str, str]] = sorted( + grouped_channels[partner], + key=lambda item: item["channel"].lower(), + ) + partner_groups.append({"partner": partner, "channels": sorted_items}) + + context: dict[str, list[dict[str, str | list[dict[str, str]]]]] = { + "partner_groups": partner_groups, + } + return render(request=request, template_name="youtube/index.html", context=context)