Files
vrobbler/vrobbler/apps/sports/models.py

304 lines
10 KiB
Python

import logging
from typing import Dict
from uuid import uuid4
from django.conf import settings
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 ScrobblableConstants, ScrobblableMixin
from sports.utils import get_players_from_event, get_round_name_from_event
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
class SportEventType(models.TextChoices):
UNKNOWN = "UK", _("Event")
GAME = "GA", _("Game")
RACE = "RA", _("Race")
MATCH = "MA", _("Match")
class TheSportsDbMixin(TimeStampedModel):
name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
thesportsdb_id = models.CharField(max_length=255, **BNULL)
class Meta:
abstract = True
def __str__(self):
return self.name
class Sport(TheSportsDbMixin):
default_event_run_time_seconds = models.IntegerField(**BNULL)
default_event_type = models.CharField(
max_length=2,
choices=SportEventType.choices,
default=SportEventType.UNKNOWN,
)
@property
def default_event_run_time(self):
default_run_time = getattr(settings, "DEFAULT_EVENT_RUNTIME_SECONDS", 14400)
if self.default_event_run_time_seconds:
default_run_time = self.default_event_run_time_seconds
return default_run_time
class League(TheSportsDbMixin):
logo = models.ImageField(upload_to="sports/league-logos/", **BNULL)
abbreviation_str = models.CharField(max_length=10, **BNULL)
sport = models.ForeignKey(Sport, on_delete=models.DO_NOTHING, **BNULL)
@property
def abbreviation(self):
return self.abbreviation_str
class Season(TheSportsDbMixin):
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
def __str__(self):
return f"{self.name} season of {self.league}"
class Team(TheSportsDbMixin):
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
logo = models.ImageField(upload_to="sports/team-logos/", **BNULL)
class Player(TheSportsDbMixin):
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
team = models.ForeignKey(Team, on_delete=models.DO_NOTHING, **BNULL)
class Round(TheSportsDbMixin):
season = models.ForeignKey(Season, on_delete=models.DO_NOTHING, **BNULL)
def __str__(self):
value = f"{self.name} of {self.season}"
if not self.name:
value = f"{self.thesportsdb_id} of {self.season}"
return value
class SportEvent(ScrobblableMixin):
COMPLETION_PERCENT = getattr(settings, "SPORT_COMPLETION_PERCENT", 90)
event_type = models.CharField(
max_length=2,
choices=SportEventType.choices,
default=SportEventType.UNKNOWN,
)
league = models.ForeignKey(League, on_delete=models.DO_NOTHING, **BNULL)
@property
def logdata_cls(self):
from scrobbles.dataclasses import SportEventLogData
return SportEventLogData
teams = models.ManyToManyField(Team, blank=True)
players = models.ManyToManyField(Player, blank=True)
# Deprecated - data migrated to scrobble.log via SportEventLogData
thesportsdb_id = models.CharField(max_length=255, **BNULL)
start = models.DateTimeField(**BNULL)
round = models.ForeignKey(Round, on_delete=models.DO_NOTHING, **BNULL)
def save(self, *args, **kwargs):
old_instance = None
try:
old_instance = UserProfile.objects.get(pk=self.pk)
except:
pass
if not self.title or (old_instance and old_instance.title != self.title):
self.title = self.comp_str
super(SportEvent, self).save(*args, **kwargs)
def __str__(self) -> str:
return f"{self.title} - {self.subtitle}"
def get_absolute_url(self):
return reverse("sports:event_detail", kwargs={"slug": self.uuid})
@property
def subtitle(self) -> str:
league = self.league
if self.league and self.league.abbreviation_str:
league = self.league.abbreviation_str
return f"{league} {self.get_event_type_display()}"
@property
def comp_str(self) -> str:
if self.players.exists():
return " v ".join(str(p) for p in self.players.all())
if self.teams.exists():
return " v ".join(str(t) for t in self.teams.all())
return ""
@property
def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Watching", tags="trophy")
@property
def sportsdb_link(self):
return f"https://thesportsdb.com/event/{self.thesportsdb_id}"
@property
def info_link(self):
return self.sportsdb_link
@property
def primary_image_url(self) -> str:
if self.league and self.league.logo:
return self.league.logo.url
return ""
@classmethod
def find_or_create(cls, data_dict: Dict) -> tuple["Event", dict]:
"""Given a data dict from TheSportsDB, finds or creates a canonical
SportEvent by teams, players or title, and returns (event, logdata).
The logdata dict contains per-scrobble details (thesportsdb_id, start,
round/season names) that should be stored in the scrobble's log field.
"""
# Find or create our Sport
sid = data_dict.get("Sport")
sport, s_created = Sport.objects.get_or_create(thesportsdb_id=sid)
if s_created:
sport.name = sid
sport.save(update_fields=["name"])
# Find or create our League
lid = data_dict.get("LeagueId")
league, l_created = League.objects.get_or_create(
thesportsdb_id=lid, sport=sport
)
if l_created:
league.sport = sport
league.name = data_dict.get("LeagueName", "")
league.save(update_fields=["sport", "name"])
# Find or create our Season
seid = data_dict.get("Season")
season, se_created = Season.objects.get_or_create(
thesportsdb_id=seid, league=league
)
if se_created:
season.name = seid
season.save(update_fields=["name"])
# Find or create our Round
rid = data_dict.get("RoundId")
round_obj, r_created = Round.objects.get_or_create(
thesportsdb_id=rid,
season=season,
name=rid,
)
if r_created:
round_obj.season = season
round_obj.save(update_fields=["season"])
# Build logdata with per-scrobble details
logdata = {}
logdata["thesportsdb_id"] = data_dict.get("EventId")
start = data_dict.get("Start")
if start:
logdata["start"] = (
start.isoformat() if hasattr(start, "isoformat") else str(start)
)
if round_obj:
logdata["round_name"] = round_obj.name or str(round_obj)
if round_obj.season:
logdata["season_name"] = round_obj.season.name or str(round_obj.season)
# Look up or create teams/players
home_team = None
away_team = None
if data_dict.get("HomeTeamName"):
home_team_dict = {
"name": data_dict.get("HomeTeamName", ""),
"thesportsdb_id": data_dict.get("HomeTeamId", ""),
"league": league,
}
home_team, ht_created = Team.objects.get_or_create(**home_team_dict)
if ht_created:
from sports.thesportsdb import enrich_team_logo
enrich_team_logo(home_team)
away_team_dict = {
"name": data_dict.get("AwayTeamName", ""),
"thesportsdb_id": data_dict.get("AwayTeamId", ""),
"league": league,
}
away_team, at_created = Team.objects.get_or_create(**away_team_dict)
if at_created:
from sports.thesportsdb import enrich_team_logo
enrich_team_logo(away_team)
players_list = []
if data_dict.get("Sport") == "Tennis":
event_name = data_dict.get("Name", "")
if not round_obj.name:
round_obj.name = get_round_name_from_event(event_name)
round_obj.save(update_fields=["name"])
players_list = get_players_from_event(event_name)
# Find existing canonical event by teams, players, or title
event = None
if home_team and away_team:
event = (
cls.objects.filter(league=league, teams=home_team)
.filter(teams=away_team)
.first()
)
if not event and players_list:
player_objs = []
for player_name in players_list:
player = Player.objects.filter(name__icontains=player_name).first()
if not player:
player = Player.objects.create(name=player_name)
player_objs.append(player)
qs = cls.objects.filter(league=league, players=player_objs[0])
for player in player_objs[1:]:
qs = qs.filter(players=player)
event = qs.first()
if not event:
title = data_dict.get("Name", "").strip()
if title:
event = cls.objects.filter(league=league, title=title).first()
if not event:
event = cls.objects.create(
title=data_dict.get("Name"),
event_type=sport.default_event_type,
league=league,
base_run_time_seconds=data_dict.get("RunTime"),
)
# Ensure M2M is populated on the canonical event
if home_team and not event.teams.filter(id=home_team.id).exists():
event.teams.add(home_team)
if away_team and not event.teams.filter(id=away_team.id).exists():
event.teams.add(away_team)
for player_name in players_list:
player = Player.objects.filter(name__icontains=player_name).first()
if player and not event.players.filter(id=player.id).exists():
event.players.add(player)
return event, logdata