diff --git a/vrobbler/apps/scrobbles/admin.py b/vrobbler/apps/scrobbles/admin.py index 646d577..962349f 100644 --- a/vrobbler/apps/scrobbles/admin.py +++ b/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 diff --git a/vrobbler/apps/scrobbles/forms.py b/vrobbler/apps/scrobbles/forms.py index f088285..34bc6ce 100644 --- a/vrobbler/apps/scrobbles/forms.py +++ b/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={ diff --git a/vrobbler/apps/scrobbles/migrations/0008_scrobble_sport_event.py b/vrobbler/apps/scrobbles/migrations/0008_scrobble_sport_event.py new file mode 100644 index 0000000..f41a346 --- /dev/null +++ b/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', + ), + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index d53d7c8..d2e49d4 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/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 diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 497d879..7d0ad31 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/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): diff --git a/vrobbler/apps/sports/__init__.py b/vrobbler/apps/sports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vrobbler/apps/sports/admin.py b/vrobbler/apps/sports/admin.py new file mode 100644 index 0000000..6f6d8ef --- /dev/null +++ b/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, + ] diff --git a/vrobbler/apps/sports/apps.py b/vrobbler/apps/sports/apps.py new file mode 100644 index 0000000..fec4c46 --- /dev/null +++ b/vrobbler/apps/sports/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SportsConfig(AppConfig): + name = "sports" diff --git a/vrobbler/apps/sports/migrations/0001_initial.py b/vrobbler/apps/sports/migrations/0001_initial.py new file mode 100644 index 0000000..5102f0a --- /dev/null +++ b/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, + }, + ), + ] diff --git a/vrobbler/apps/sports/migrations/0002_rename_start_utc_sportevent_start.py b/vrobbler/apps/sports/migrations/0002_rename_start_utc_sportevent_start.py new file mode 100644 index 0000000..9f7e59e --- /dev/null +++ b/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', + ), + ] diff --git a/vrobbler/apps/sports/migrations/__init__.py b/vrobbler/apps/sports/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vrobbler/apps/sports/models.py b/vrobbler/apps/sports/models.py new file mode 100644 index 0000000..9467d7c --- /dev/null +++ b/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 diff --git a/vrobbler/settings.py b/vrobbler/settings.py index 2e9cec7..f1b1fd7 100644 --- a/vrobbler/settings.py +++ b/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", diff --git a/vrobbler/urls.py b/vrobbler/urls.py index b152ea1..156a375 100644 --- a/vrobbler/urls.py +++ b/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")),