diff --git a/twitch/feeds.py b/twitch/feeds.py index 97e4058..6ca0cdb 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -497,27 +497,16 @@ class GameFeed(Feed): return box_art return "" - def item_enclosure_length(self, item: Game) -> int: - """Returns the length of the enclosure. + def item_enclosure_length(self, item: Game) -> int: # noqa: ARG002 + """Returns the length of the enclosure.""" + # TODO(TheLovinator): Track image size for proper length # noqa: TD003 - Prefer the newly-added ``box_art_size_bytes`` field so that the RSS - feed can include an accurate ``length`` attribute. Fall back to 0 if - the value is missing or ``None``. - """ - try: - size = getattr(item, "box_art_size_bytes", None) - return int(size) if size is not None else 0 - except TypeError, ValueError: - return 0 + return 0 - def item_enclosure_mime_type(self, item: Game) -> str: - """Returns the MIME type of the enclosure. - - Use the ``box_art_mime_type`` field when available, otherwise fall back - to a generic JPEG string (as was previously hard-coded). - """ - mime: str = getattr(item, "box_art_mime_type", "") - return mime or "image/jpeg" + def item_enclosure_mime_type(self, item: Game) -> 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/campaigns/ @@ -644,26 +633,15 @@ class DropCampaignFeed(Feed): """Returns the URL of the campaign image for enclosure.""" return item.get_feed_enclosure_url() - def item_enclosure_length(self, item: DropCampaign) -> int: - """Returns the length of the enclosure. + 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 - Reads the `image_size_bytes` field added to the model. If the field is - unset it returns `0` to match previous behavior. - """ - try: - size = getattr(item, "image_size_bytes", None) - return int(size) if size is not None else 0 - except TypeError, ValueError: - return 0 - - def item_enclosure_mime_type(self, item: DropCampaign) -> str: - """Returns the MIME type of the enclosure. - - Uses `image_mime_type` on the campaign if set, falling back to the - previous hard-coded `image/jpeg`. - """ - mime: str = getattr(item, "image_mime_type", "") - return mime or "image/jpeg" + 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/games//campaigns/ @@ -823,26 +801,16 @@ class GameCampaignFeed(Feed): """Returns the URL of the campaign image for enclosure.""" return item.get_feed_enclosure_url() - def item_enclosure_length(self, item: DropCampaign) -> int: - """Returns the length of the enclosure. + 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 - Reads the ``image_size_bytes`` field added to the model when rendering a - game-specific campaign feed. - """ - try: - size = getattr(item, "image_size_bytes", None) - return int(size) if size is not None else 0 - except TypeError, ValueError: - return 0 + return 0 - def item_enclosure_mime_type(self, item: DropCampaign) -> str: - """Returns the MIME type of the enclosure. - - Prefers `image_mime_type` on the campaign object; falls back to - `image/jpeg` when not available. - """ - mime: str = getattr(item, "image_mime_type", "") - return mime or "image/jpeg" + 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/reward-campaigns/ diff --git a/twitch/management/commands/backfill_image_dimensions.py b/twitch/management/commands/backfill_image_dimensions.py index fb62032..44e7a9b 100644 --- a/twitch/management/commands/backfill_image_dimensions.py +++ b/twitch/management/commands/backfill_image_dimensions.py @@ -1,5 +1,4 @@ -import contextlib -import mimetypes +"""Management command to backfill image dimensions for existing cached images.""" from django.core.management.base import BaseCommand @@ -25,14 +24,6 @@ class Command(BaseCommand): try: # Opening the file and saving triggers dimension calculation game.box_art_file.open() - - # populate size and mime if available - with contextlib.suppress(Exception): - game.box_art_size_bytes = game.box_art_file.size - mime, _ = mimetypes.guess_type(game.box_art_file.name or "") - if mime: - game.box_art_mime_type = mime - game.save() total_updated += 1 self.stdout.write(self.style.SUCCESS(f" Updated {game}")) @@ -45,13 +36,6 @@ class Command(BaseCommand): if campaign.image_file and not campaign.image_width: try: campaign.image_file.open() - - with contextlib.suppress(Exception): - campaign.image_size_bytes = campaign.image_file.size - mime, _ = mimetypes.guess_type(campaign.image_file.name or "") - if mime: - campaign.image_mime_type = mime - campaign.save() total_updated += 1 self.stdout.write(self.style.SUCCESS(f" Updated {campaign}")) diff --git a/twitch/migrations/0013_game_box_art_mime_type_game_box_art_size_bytes.py b/twitch/migrations/0013_game_box_art_mime_type_game_box_art_size_bytes.py deleted file mode 100644 index 64b73a1..0000000 --- a/twitch/migrations/0013_game_box_art_mime_type_game_box_art_size_bytes.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 6.0.3 on 2026-03-09 18:33 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - """Add box art MIME type and size fields to Game, and backfill existing data.""" - - dependencies = [ - ("twitch", "0012_dropcampaign_operation_names_gin_index"), - ] - - operations = [ - migrations.AddField( - model_name="game", - name="box_art_mime_type", - field=models.CharField( - blank=True, - default="", - editable=False, - help_text="MIME type of the cached box art image (e.g., 'image/png').", - max_length=50, - ), - ), - migrations.AddField( - model_name="game", - name="box_art_size_bytes", - field=models.PositiveIntegerField( - blank=True, - editable=False, - help_text="File size of the cached box art image in bytes.", - null=True, - ), - ), - ] diff --git a/twitch/migrations/0014_dropcampaign_image_mime_type_and_more.py b/twitch/migrations/0014_dropcampaign_image_mime_type_and_more.py deleted file mode 100644 index 0ce0b4e..0000000 --- a/twitch/migrations/0014_dropcampaign_image_mime_type_and_more.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 6.0.3 on 2026-03-09 18:35 - -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - """Add image MIME type and size fields to DropCampaign, and backfill existing data.""" - - dependencies = [ - ("twitch", "0013_game_box_art_mime_type_game_box_art_size_bytes"), - ] - - operations = [ - migrations.AddField( - model_name="dropcampaign", - name="image_mime_type", - field=models.CharField( - blank=True, - default="", - editable=False, - help_text="MIME type of the cached campaign image (e.g., 'image/png').", - max_length=50, - ), - ), - migrations.AddField( - model_name="dropcampaign", - name="image_size_bytes", - field=models.PositiveIntegerField( - blank=True, - editable=False, - help_text="File size of the cached campaign image in bytes.", - null=True, - ), - ), - ] diff --git a/twitch/models.py b/twitch/models.py index 12f4052..5f704fd 100644 --- a/twitch/models.py +++ b/twitch/models.py @@ -115,19 +115,6 @@ class Game(auto_prefetch.Model): editable=False, help_text="Height of cached box art image in pixels.", ) - box_art_size_bytes = models.PositiveIntegerField( - null=True, - blank=True, - editable=False, - help_text="File size of the cached box art image in bytes.", - ) - box_art_mime_type = models.CharField( - max_length=50, - blank=True, - default="", - editable=False, - help_text="MIME type of the cached box art image (e.g., 'image/png').", - ) owners = models.ManyToManyField( Organization, @@ -370,19 +357,6 @@ class DropCampaign(auto_prefetch.Model): editable=False, help_text="Height of cached image in pixels.", ) - image_size_bytes = models.PositiveIntegerField( - null=True, - blank=True, - editable=False, - help_text="File size of the cached campaign image in bytes.", - ) - image_mime_type = models.CharField( - max_length=50, - blank=True, - default="", - editable=False, - help_text="MIME type of the cached campaign image (e.g., 'image/png').", - ) start_at = models.DateTimeField( null=True, blank=True, diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index 08005b6..fe12daa 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -6,15 +6,10 @@ from datetime import timedelta from typing import TYPE_CHECKING import pytest -from django.core.files.base import ContentFile -from django.core.management import call_command from django.urls import reverse from django.utils import timezone from hypothesis.extra.django import TestCase -from twitch.feeds import DropCampaignFeed -from twitch.feeds import GameCampaignFeed -from twitch.feeds import GameFeed from twitch.models import Channel from twitch.models import ChatBadge from twitch.models import ChatBadgeSet @@ -56,19 +51,6 @@ class RSSFeedTestCase(TestCase): operation_names=["DropCampaignDetails"], ) - # populate the new enclosure metadata fields so feeds can return them - self.game.box_art_size_bytes = 42 - self.game.box_art_mime_type = "image/png" - # provide a URL so that the RSS enclosure element is emitted - self.game.box_art = "https://example.com/box.png" - self.game.save() - - self.campaign.image_size_bytes = 314 - self.campaign.image_mime_type = "image/gif" - # feed will only include an enclosure if there is some image URL/field - self.campaign.image_url = "https://example.com/campaign.png" - self.campaign.save() - def test_organization_feed(self) -> None: """Test organization feed returns 200.""" url: str = reverse("twitch:organization_feed") @@ -91,16 +73,6 @@ class RSSFeedTestCase(TestCase): ) assert expected_rss_link in content - # enclosure metadata from our new fields should be present - assert 'length="42"' in content - assert 'type="image/png"' in content - - def test_game_feed_enclosure_helpers(self) -> None: - """Helper methods should return values from model fields.""" - feed = GameFeed() - assert feed.item_enclosure_length(self.game) == 42 - assert feed.item_enclosure_mime_type(self.game) == "image/png" - def test_campaign_feed(self) -> None: """Test campaign feed returns 200.""" url: str = reverse("twitch:campaign_feed") @@ -108,17 +80,6 @@ class RSSFeedTestCase(TestCase): assert response.status_code == 200 assert response["Content-Type"] == "application/rss+xml; charset=utf-8" - content: str = response.content.decode("utf-8") - # verify enclosure meta - assert 'length="314"' in content - assert 'type="image/gif"' in content - - def test_campaign_feed_enclosure_helpers(self) -> None: - """Helper methods for campaigns should respect new fields.""" - feed = DropCampaignFeed() - assert feed.item_enclosure_length(self.campaign) == 314 - assert feed.item_enclosure_mime_type(self.campaign) == "image/gif" - def test_campaign_feed_includes_badge_description(self) -> None: """Badge benefit descriptions should be visible in the RSS drop summary.""" drop: TimeBasedDrop = TimeBasedDrop.objects.create( @@ -191,60 +152,6 @@ class RSSFeedTestCase(TestCase): # Should NOT contain other campaign assert "Other Campaign" not in content - # enclosure metadata also should appear here - assert 'length="314"' in content - assert 'type="image/gif"' in content - - def test_game_campaign_feed_enclosure_helpers(self) -> None: - """GameCampaignFeed helper methods should pull from the model fields.""" - feed = GameCampaignFeed() - assert feed.item_enclosure_length(self.campaign) == 314 - assert feed.item_enclosure_mime_type(self.campaign) == "image/gif" - - def test_backfill_command_sets_metadata(self) -> None: - """Running the backfill command should populate size and mime fields. - - We create a fake file for both a game and a campaign and then execute the - management command. Afterward, the corresponding metadata fields should - be filled in. - """ - # create game with local file - game2: Game = Game.objects.create( - twitch_id="file-game", - slug="file-game", - name="File Game", - display_name="File Game", - ) - game2.box_art_file.save("sample.png", ContentFile(b"hello")) - game2.save() - - campaign2: DropCampaign = DropCampaign.objects.create( - twitch_id="file-camp", - name="File Campaign", - game=self.game, - start_at=timezone.now(), - end_at=timezone.now() + timedelta(days=1), - operation_names=["DropCampaignDetails"], - ) - campaign2.image_file.save("camp.jpg", ContentFile(b"world")) - campaign2.save() - - # ensure fields are not populated initially - assert campaign2.image_size_bytes is None - assert not campaign2.image_mime_type - assert game2.box_art_size_bytes is None - assert not game2.box_art_mime_type - - call_command("backfill_image_dimensions") - - campaign2.refresh_from_db() - game2.refresh_from_db() - - assert campaign2.image_size_bytes == len(b"world") - assert campaign2.image_mime_type == "image/jpeg" - assert game2.box_art_size_bytes == len(b"hello") - assert game2.box_art_mime_type == "image/png" - QueryAsserter = Callable[..., AbstractContextManager[object]] diff --git a/twitch/views.py b/twitch/views.py index 5a88a60..7e1560e 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -614,7 +614,7 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse: """ # TODO(TheLovinator): Instead of only using sql we should also support other formats like parquet, csv, or json. # noqa: TD003 # TODO(TheLovinator): Upload to s3 instead. # noqa: TD003 - # TODO(TheLovinator): https://developers.google.com/search/docs/appearance/structured-data/dataset#json-ld + datasets_root: Path = settings.DATA_DIR / "datasets" search_dirs: list[Path] = [datasets_root] seen_paths: set[str] = set()