Browse Source

Add sports as a preliminary scobble type

Colin Powell 2 năm trước cách đây
mục cha
commit
bd3a381346

+ 5 - 1
vrobbler/apps/scrobbles/admin.py

@@ -23,7 +23,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
         "is_paused",
         "played_to_completion",
     )
-    raw_id_fields = ('video', 'podcast_episode', 'track')
+    raw_id_fields = ('video', 'podcast_episode', 'track', 'sport_event')
     list_filter = ("is_paused", "in_progress", "source", "track__artist")
     ordering = ("-timestamp",)
 
@@ -34,6 +34,8 @@ class ScrobbleAdmin(admin.ModelAdmin):
             return obj.track
         if obj.podcast_episode:
             return obj.podcast_episode
+        if obj.sport_event:
+            return obj.sport_event
 
     def media_type(self, obj):
         if obj.video:
@@ -42,6 +44,8 @@ class ScrobbleAdmin(admin.ModelAdmin):
             return "Track"
         if obj.podcast_episode:
             return "Podcast"
+        if obj.sport_event:
+            return "Sport Event"
 
     def playback_percent(self, obj):
         return obj.percent_played

+ 2 - 2
vrobbler/apps/scrobbles/forms.py

@@ -1,8 +1,8 @@
 from django import forms
 
 
-class ImdbScrobbleForm(forms.Form):
-    imdb_id = forms.CharField(
+class ScrobbleForm(forms.Form):
+    item_id = forms.CharField(
         label="",
         widget=forms.TextInput(
             attrs={

+ 25 - 0
vrobbler/apps/scrobbles/migrations/0008_scrobble_sport_event.py

@@ -0,0 +1,25 @@
+# Generated by Django 4.1.5 on 2023-01-14 21:23
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sports', '0002_rename_start_utc_sportevent_start'),
+        ('scrobbles', '0007_scrobble_podcast_episode'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='scrobble',
+            name='sport_event',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.DO_NOTHING,
+                to='sports.sportevent',
+            ),
+        ),
+    ]

+ 14 - 8
vrobbler/apps/scrobbles/models.py

@@ -11,6 +11,7 @@ from music.models import Track
 from podcasts.models import Episode
 from scrobbles.utils import check_scrobble_for_finish
 from videos.models import Video
+from sports.models import SportEvent
 
 logger = logging.getLogger(__name__)
 User = get_user_model()
@@ -27,6 +28,9 @@ class Scrobble(TimeStampedModel):
     podcast_episode = models.ForeignKey(
         Episode, on_delete=models.DO_NOTHING, **BNULL
     )
+    sport_event = models.ForeignKey(
+        SportEvent, on_delete=models.DO_NOTHING, **BNULL
+    )
     user = models.ForeignKey(
         User, blank=True, null=True, on_delete=models.DO_NOTHING
     )
@@ -73,6 +77,8 @@ class Scrobble(TimeStampedModel):
             media_obj = self.track
         if self.podcast_episode:
             media_obj = self.podcast_episode
+        if self.sport_event:
+            media_obj = self.sport_event
         return media_obj
 
     def __str__(self):
@@ -179,14 +185,14 @@ class Scrobble(TimeStampedModel):
                 return scrobble.resume()
 
             # We're not changing the scrobble, but we don't want to walk over an existing one
-            scrobble_is_finished = (
-                not scrobble.in_progress and scrobble.modified < backoff
-            )
-            if scrobble_is_finished:
-                logger.info(
-                    'Found a very recent scrobble for this item, holding off scrobbling again'
-                )
-                return
+            # scrobble_is_finished = (
+            #    not scrobble.in_progress and scrobble.modified < backoff
+            # )
+            # if scrobble_is_finished:
+            #    logger.info(
+            #        'Found a very recent scrobble for this item, holding off scrobbling again'
+            #    )
+            #    return
 
         if not scrobble:
             # If we default this to "" we can probably remove this

+ 2 - 2
vrobbler/apps/scrobbles/views.py

@@ -33,7 +33,7 @@ from vrobbler.apps.music.aggregators import (
     top_tracks,
     week_of_scrobbles,
 )
-from scrobbles.forms import ImdbScrobbleForm
+from scrobbles.forms import ScrobbleForm
 
 logger = logging.getLogger(__name__)
 
@@ -77,7 +77,7 @@ class RecentScrobbleList(ListView):
 
         data["weekly_data"] = week_of_scrobbles()
         data['counts'] = scrobble_counts()
-        data['imdb_form'] = ImdbScrobbleForm
+        data['imdb_form'] = ScrobbleForm
         return data
 
     def get_queryset(self):

+ 0 - 0
vrobbler/apps/sports/__init__.py


+ 37 - 0
vrobbler/apps/sports/admin.py

@@ -0,0 +1,37 @@
+from django.contrib import admin
+
+from sports.models import League, SportEvent, Team
+
+from scrobbles.admin import ScrobbleInline
+
+
+@admin.register(League)
+class LeagueAdmin(admin.ModelAdmin):
+    date_hierarchy = "created"
+    list_display = ("name", "abbreviation_str")
+    ordering = ("name",)
+
+
+@admin.register(Team)
+class TeamAdmin(admin.ModelAdmin):
+    date_hierarchy = "created"
+    list_display = ("name", "league")
+    ordering = ("name",)
+
+
+@admin.register(SportEvent)
+class SportEventAdmin(admin.ModelAdmin):
+    date_hierarchy = "created"
+    list_display = (
+        "title",
+        "event_type",
+        "start",
+        "home_team",
+        "away_team",
+        "season",
+    )
+    list_filter = ("season", "home_team", "away_team")
+    ordering = ("-created",)
+    inlines = [
+        ScrobbleInline,
+    ]

+ 5 - 0
vrobbler/apps/sports/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class SportsConfig(AppConfig):
+    name = "sports"

+ 214 - 0
vrobbler/apps/sports/migrations/0001_initial.py

@@ -0,0 +1,214 @@
+# Generated by Django 4.1.5 on 2023-01-14 21:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django_extensions.db.fields
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = []
+
+    operations = [
+        migrations.CreateModel(
+            name='League',
+            fields=[
+                (
+                    'id',
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                ('name', models.CharField(max_length=255)),
+                (
+                    'uuid',
+                    models.UUIDField(
+                        blank=True,
+                        default=uuid.uuid4,
+                        editable=False,
+                        null=True,
+                    ),
+                ),
+                (
+                    'logo',
+                    models.ImageField(
+                        blank=True, null=True, upload_to='sports/league-logos/'
+                    ),
+                ),
+                (
+                    'abbreviation_str',
+                    models.CharField(blank=True, max_length=10, null=True),
+                ),
+                ('thesportsdb_id', models.IntegerField(blank=True, null=True)),
+            ],
+            options={
+                'get_latest_by': 'modified',
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='Team',
+            fields=[
+                (
+                    'id',
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                ('name', models.CharField(max_length=255)),
+                (
+                    'uuid',
+                    models.UUIDField(
+                        blank=True,
+                        default=uuid.uuid4,
+                        editable=False,
+                        null=True,
+                    ),
+                ),
+                ('thesportsdb_id', models.IntegerField(blank=True, null=True)),
+                (
+                    'league',
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        to='sports.league',
+                    ),
+                ),
+            ],
+            options={
+                'get_latest_by': 'modified',
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='SportEvent',
+            fields=[
+                (
+                    'id',
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                (
+                    'uuid',
+                    models.UUIDField(
+                        blank=True,
+                        default=uuid.uuid4,
+                        editable=False,
+                        null=True,
+                    ),
+                ),
+                (
+                    'title',
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    'run_time',
+                    models.CharField(blank=True, max_length=8, null=True),
+                ),
+                (
+                    'run_time_ticks',
+                    models.PositiveBigIntegerField(blank=True, null=True),
+                ),
+                (
+                    'event_type',
+                    models.CharField(
+                        choices=[
+                            ('UK', 'Unknown'),
+                            ('GA', 'Game'),
+                            ('MA', 'Match'),
+                            ('ME', 'Meet'),
+                        ],
+                        default='UK',
+                        max_length=2,
+                    ),
+                ),
+                ('start_utc', models.DateTimeField(blank=True, null=True)),
+                (
+                    'season',
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    'away_team',
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        related_name='away_event_set',
+                        to='sports.team',
+                    ),
+                ),
+                (
+                    'home_team',
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        related_name='home_event_set',
+                        to='sports.team',
+                    ),
+                ),
+                (
+                    'league',
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        to='sports.league',
+                    ),
+                ),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]

+ 18 - 0
vrobbler/apps/sports/migrations/0002_rename_start_utc_sportevent_start.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.1.5 on 2023-01-14 21:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sports', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='sportevent',
+            old_name='start_utc',
+            new_name='start',
+        ),
+    ]

+ 0 - 0
vrobbler/apps/sports/migrations/__init__.py


+ 105 - 0
vrobbler/apps/sports/models.py

@@ -0,0 +1,105 @@
+import logging
+from typing import Dict
+from uuid import uuid4
+
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+from django_extensions.db.models import TimeStampedModel
+from scrobbles.mixins import ScrobblableMixin
+
+logger = logging.getLogger(__name__)
+BNULL = {"blank": True, "null": True}
+
+
+class League(TimeStampedModel):
+    name = models.CharField(max_length=255)
+    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
+    logo = models.ImageField(upload_to="sports/league-logos/", **BNULL)
+    abbreviation_str = models.CharField(max_length=10, **BNULL)
+    thesportsdb_id = models.IntegerField(**BNULL)
+
+    def __str__(self):
+        return self.name
+
+    @property
+    def abbreviation(self):
+        return self.abbreviation_str
+
+
+class Team(TimeStampedModel):
+    name = models.CharField(max_length=255)
+    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
+    league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
+    thesportsdb_id = models.IntegerField(**BNULL)
+
+    def __str__(self):
+        return self.name
+
+
+class SportEvent(ScrobblableMixin):
+    class Type(models.TextChoices):
+        UNKNOWN = 'UK', _('Unknown')
+        GAME = 'GA', _('Game')
+        MATCH = 'MA', _('Match')
+        MEET = 'ME', _('Meet')
+
+    event_type = models.CharField(
+        max_length=2,
+        choices=Type.choices,
+        default=Type.UNKNOWN,
+    )
+    league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
+    start = models.DateTimeField(**BNULL)
+    home_team = models.ForeignKey(
+        Team,
+        on_delete=models.DO_NOTHING,
+        related_name='home_event_set',
+        **BNULL,
+    )
+    away_team = models.ForeignKey(
+        Team,
+        on_delete=models.DO_NOTHING,
+        related_name='away_event_set',
+        **BNULL,
+    )
+    season = models.CharField(max_length=255, **BNULL)
+
+    def __str__(self):
+        return f"{self.start.date()} - {self.league.abbreviation} - {self.home_team} v {self.away_team}"
+
+    def get_absolute_url(self):
+        return reverse("sports:event_detail", kwargs={'slug': self.uuid})
+
+    @classmethod
+    def find_or_create(cls, data_dict: Dict) -> "Event":
+        """Given a data dict from Jellyfin, does the heavy lifting of looking up
+        the video and, if need, TV Series, creating both if they don't yet
+        exist.
+
+        """
+        league_dict = {"name": data_dict.get("LeagueName", "")}
+        league, _created = League.objects.get_or_create(**league_dict)
+
+        home_team_dict = {
+            "name": data_dict.get("HomeTeamName", ""),
+            "league": league,
+        }
+        home_team, _created = Team.objects.get_or_create(**home_team_dict)
+
+        away_team_dict = {
+            "name": data_dict.get("AwayTeamName", ""),
+            "league": league,
+        }
+        away_team, _created = Team.objects.get_or_create(**away_team_dict)
+
+        event_dict = {
+            "event_type": SportEvent.Type.GAME,
+            "home_team": home_team,
+            "away_team": away_team,
+            "start_utc": data_dict['SportEventStart'],
+            "league": league,
+        }
+        event, _created = cls.objects.get_or_create(**event_dict)
+
+        return event

+ 5 - 0
vrobbler/settings.py

@@ -58,6 +58,10 @@ MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 1)
 VIDEO_WAIT_PERIOD_DAYS = os.getenv("VROBBLER_VIDEO_WAIT_PERIOD_DAYS", 1)
 MUSIC_WAIT_PERIOD_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 1)
 
+THESPORTSDB_API_KEY = os.getenv("VROBBLER_THESPORTSDB_API_KEY", "2")
+THESPORTSDB_BASE_URL = os.getenv(
+    "VROBBLER_THESPORTSDB_BASE_URL", "https://www.thesportsdb.com/api/v1/json/"
+)
 TMDB_API_KEY = os.getenv("VROBBLER_TMDB_API_KEY", "")
 
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@@ -94,6 +98,7 @@ INSTALLED_APPS = [
     "videos",
     "music",
     "podcasts",
+    "sports",
     "rest_framework",
     "allauth",
     "allauth.account",

+ 1 - 1
vrobbler/urls.py

@@ -17,7 +17,7 @@ urlpatterns = [
     path("api/v1/scrobbles/", include(scrobble_urls, namespace="scrobbles")),
     path(
         'manual/imdb/',
-        scrobbles_views.ManualImdbScrobbleView.as_view(),
+        scrobbles_views.ManualScrobbleView.as_view(),
         name='imdb-manual-scrobble',
     ),
     path("", include(video_urls, namespace="videos")),