304 lines
10 KiB
Python
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
|