This commit has no recorded session.
diff
commit ea1b43d1b81c280c279b55e39c6c0ac16585ddd3Author: Colin Powell <colin@unbl.ink>Date: Sat Jun 6 23:32:21 2026 -0400 [sports] Big sports structure revamp This should make scrobbling sports more like tasks. The root scrobbled items are a little more generic, but it's easier to see viewing patterns.@@ -2,6 +2,7 @@ We should convert this PROJECT file to put tickets in a subdirectory, tickets, with each ticket having it's own shortid_title.org * Overview+ Vrobbler began humbly enough as a way to use Jellyfin's webhook to keep track of the shows and movies I was watching. More specifically, I broke my ankle a few days after Christmas in 2022 and spent the next four months very slowly@@ -85,15 +86,7 @@ fetching and simple saving. **** Bookmarklet *** Metadata sources **** Scraper-* Chores-** DONE Document various vrobbler features :chore:personal:project:vrobbler:documentation:-:PROPERTIES:-:ID: 514e9285-96f1-265f-56df-118c12f60918-:END:-:LOGBOOK:-CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20-:END:-* Backlog [0/12] :vrobbler:project:personal:+* Backlog [1/13] :vrobbler:project:personal: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles: :PROPERTIES: :ID: 702462cf-d54b-48c6-8a7c-78b8de751deb@@ -505,6 +498,62 @@ needed import celery task. This is how the WebDAV celery task currently works. This would also be an opporunity to clean up the code around WebDAV imports and make them more re-usable for other import services. +** DONE [#B] Change sports scrobbling a bit :feature:sports:scrobbles:+:PROPERTIES:+:ID: cd27d683-c847-4251-b3d1-8243f45c01ca+:END:+ +*** Description++Currently, the way we scrobble sports means that basically the same event will+never be scrobbled again. I will likely never watch the 2025 Monaco Grand Prix+again, but I will watch the Monaco Grand Prix again. But I also wont watch one+specific game between Arsenal and Man City twice, but I may watch those two+teams play multiple times.++What if instead of scrobbling a specific sports event on a specific date, we+make the unique Scrobblable item the players or teams in the event?++That would not work for races where the unique item would have to be the name of+the event.++Maybe that means SportEvent is too generic, and we'd need the event type to be+scrobble items.++A race, the Indy 500 or Coke 600, or Boston Marathon would be scrobblable, while+for games, the teams would be unique, so a game between Arsenal and Man City+would be unique (with extra logdata context for who's home and who's away, and+the location, could even have the weather per scrobble).++And finally, for Tennis, the title would be the round of the event, Roland+Garros Women's Semifinal, US Open Men's Final, Miami Invitational Round of 32,+with two players, or two teams and a start datetime, which is similar to what we+have now. The round becomes not a foreign key, but just a string, and we'd need+a FK to an organizer field which would replace league, and would be like "ATP+Tour" or "PGA Tour". Season would also need to be a string, and would be+something like: 2026 or 2024-2025.++Examples:+ - Super Bowl+ - Sochaux v Concarneau+ - French Open Final+ - Carlos Alcaraz v Jannik Sinner++We'd also want a script to reorganize existing sports events and move scrobbles+to the right place as best as we're able, and to flag sportsevents and scrobbles+that could not automatically be migrated with a unique tag like+"migration-failed"++Ultimately I think what we need is to greatly simplify the SportEvent to be just a placeholder+for a sport event type for a given league, then each scrobble holds the details of teams, players+start, thesportsdb_id, round and season.++Thus, I've already simplified that model, but what we need is a migration script that will move+existing complex SportEvent instances into very basic ones, and updating any scrobbles for those+events with a new SportEventLogData structure with all the specific details in it. We also need to+move the obj.round.season.league into the FK for the given event.++ * Version 46.0 [1/1] ** DONE [#C] Add sentiment parsing for Scrobbles with notes :scrobbles:sentiment: :PROPERTIES:@@ -112,9 +112,7 @@ class BaseLogData(JSONDataclass): continue if dt is None:- m = re.match(- r"(\d{4}-\d{2}-\d{2})\s+\w{3}\s+(\d{2}:\d{2})", cleaned- )+ m = re.match(r"(\d{4}-\d{2}-\d{2})\s+\w{3}\s+(\d{2}:\d{2})", cleaned) if m: try: dt = datetime.strptime(@@ -143,9 +141,25 @@ class BaseLogData(JSONDataclass): md = markdown.Markdown(extensions=["extra"]) allowed_tags = [- "p", "br", "strong", "em", "a", "ul", "ol", "li",- "code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6",- "hr", "img",+ "p",+ "br",+ "strong",+ "em",+ "a",+ "ul",+ "ol",+ "li",+ "code",+ "pre",+ "blockquote",+ "h1",+ "h2",+ "h3",+ "h4",+ "h5",+ "h6",+ "hr",+ "img", ] note_items = []@@ -174,7 +188,9 @@ class BaseLogData(JSONDataclass): ts_html = "" if ts:- ts_html = f'<h5 class="note-timestamp">{self._format_timestamp(ts)}</h5>'+ ts_html = (+ f'<h5 class="note-timestamp">{self._format_timestamp(ts)}</h5>'+ ) content_html = bleach.clean( md.convert(text),@@ -194,6 +210,14 @@ class LongPlayLogData(JSONDataclass): long_play_complete: bool = False +@dataclass+class SportEventLogData(BaseLogData):+ thesportsdb_id: Optional[str] = None+ start: Optional[str] = None+ round_name: Optional[str] = None+ season_name: Optional[str] = None++ @dataclass class WithPeopleLogData(JSONDataclass): with_people_ids: Optional[list[int]] = None@@ -884,8 +884,7 @@ class Scrobble(TimeStampedModel): # Strip log-only keys (stored in JSONField but not part of LogData dataclass) logdata_kwargs = {- k: v for k, v in log_dict.items()- if k in logdata_cls.__dataclass_fields__+ k: v for k, v in log_dict.items() if k in logdata_cls().__dataclass_fields__ } try:@@ -242,12 +242,13 @@ def manual_scrobble_event( ): data_dict = lookup_event_from_thesportsdb(thesportsdb_id) - event = SportEvent.find_or_create(data_dict)+ event, logdata = SportEvent.find_or_create(data_dict) scrobble_dict = { "user_id": user_id, "timestamp": timezone.now(), "playback_position_seconds": 0, "source": "TheSportsDB",+ "log": logdata, } return Scrobble.create_or_update(event, user_id, scrobble_dict) @@ -59,19 +59,27 @@ class SportEventAdmin(admin.ModelAdmin): date_hierarchy = "created" list_display = ( "title",+ "league", "event_type",- "start", "comp_str",- "round", )- list_filter = ("round__season", "home_team", "away_team")+ list_filter = ("league", "event_type") ordering = ("-created",) inlines = [ ScrobbleInline, ] def comp_str(self, obj):- if obj.home_team:- return f"{obj.away_team} @ {obj.home_team}"- if obj.player_one:- return f"{obj.player_one} v {obj.player_two}"+ teams = list(obj.teams.all())+ if len(teams) >= 2:+ return f"{teams[1]} v {teams[0]}"++ players = list(obj.players.all())+ if len(players) >= 2:+ return f"{players[0]} v {players[1]}"++ if len(players) == 1:+ return str(players[0])++ if len(teams) == 1:+ return str(teams[0])@@ -0,0 +1,36 @@+from django.core.management.base import BaseCommand++from sports.models import League, Team+from sports.thesportsdb import enrich_league_logo, enrich_team_logo, has_logo+++class Command(BaseCommand):+ help = "Fetch missing league and team logos from TheSportsDB"++ def add_arguments(self, parser):+ parser.add_argument(+ "--dry-run",+ action="store_true",+ help="Show what would be done without making changes",+ )++ def handle(self, *args, **options):+ dry_run = options["dry_run"]++ leagues = League.objects.filter(thesportsdb_id__isnull=False)+ for league in leagues:+ if has_logo(league):+ continue+ if dry_run:+ self.stdout.write(f"Would enrich logo for league: {league.name} ({league.thesportsdb_id})")+ else:+ enrich_league_logo(league)++ teams = Team.objects.filter(thesportsdb_id__isnull=False)+ for team in teams:+ if has_logo(team):+ continue+ if dry_run:+ self.stdout.write(f"Would enrich logo for team: {team.name} ({team.thesportsdb_id})")+ else:+ enrich_team_logo(team)@@ -0,0 +1,24 @@+# Generated by Django 4.2.30 on 2026-06-06 15:29++from django.db import migrations, models+import django.db.models.deletion+++class Migration(migrations.Migration):++ dependencies = [+ ("sports", "0018_alter_sportevent_genre"),+ ]++ operations = [+ migrations.AddField(+ model_name="sportevent",+ name="league",+ field=models.ForeignKey(+ blank=True,+ null=True,+ on_delete=django.db.models.deletion.DO_NOTHING,+ to="sports.league",+ ),+ ),+ ]@@ -0,0 +1,127 @@+import json++from django.db import migrations, models+import django.db.models.deletion+++def canonical_key(event):+ if event.home_team_id and event.away_team_id:+ return ("teams", event.league_id, event.home_team_id, event.away_team_id)+ if event.player_one_id and event.player_two_id:+ return ("players", event.league_id, event.player_one_id, event.player_two_id)+ return ("title", event.league_id, event.event_type, (event.title or "").strip())+++def build_logdata(event):+ logdata = {}+ if event.thesportsdb_id:+ logdata["thesportsdb_id"] = event.thesportsdb_id+ if event.start:+ logdata["start"] = (+ event.start.isoformat()+ if hasattr(event.start, "isoformat")+ else str(event.start)+ )+ if event.round:+ logdata["round_name"] = event.round.name or str(event.round)+ if event.round.season:+ logdata["season_name"] = event.round.season.name or str(event.round.season)+ return logdata+++def merge_scrobble_logs(scrobble, logdata):+ existing_log = scrobble.log or {}+ if isinstance(existing_log, str):+ existing_log = json.loads(existing_log)+ existing_log.update(logdata)+ scrobble.log = existing_log+ scrobble.save(update_fields=["log"])+++def populate_league(event):+ if event.league:+ return+ if event.round and event.round.season and event.round.season.league:+ event.league = event.round.season.league+ event.save(update_fields=["league"])+++def populate_m2m(event):+ if event.home_team_id:+ event.teams.add(event.home_team_id)+ if event.away_team_id:+ event.teams.add(event.away_team_id)+ if event.player_one_id:+ event.players.add(event.player_one_id)+ if event.player_two_id:+ event.players.add(event.player_two_id)+++def migrate_sport_event_data(apps, schema_editor):+ SportEvent = apps.get_model("sports", "SportEvent")+ Scrobble = apps.get_model("scrobbles", "Scrobble")+ db_alias = schema_editor.connection.alias++ canonical_events = {}++ for event in SportEvent.objects.using(db_alias).iterator():+ populate_league(event)+ key = canonical_key(event)++ canonical = canonical_events.get(key)+ if not canonical:+ canonical_events[key] = event+ populate_m2m(event)+ logdata = build_logdata(event)+ for scrobble in (+ Scrobble.objects.using(db_alias).filter(sport_event=event).iterator()+ ):+ merge_scrobble_logs(scrobble, logdata)+ else:+ logdata = build_logdata(event)+ for scrobble in (+ Scrobble.objects.using(db_alias).filter(sport_event=event).iterator()+ ):+ scrobble.sport_event = canonical+ merge_scrobble_logs(scrobble, logdata)+ scrobble.save(update_fields=["sport_event", "log"])+ event.delete()+++class Migration(migrations.Migration):++ dependencies = [+ ("sports", "0019_sportevent_league_alter_sportevent_away_team_and_more"),+ ]++ operations = [+ migrations.AddField(+ model_name="sportevent",+ name="teams",+ field=models.ManyToManyField(blank=True, to="sports.team"),+ ),+ migrations.AddField(+ model_name="sportevent",+ name="players",+ field=models.ManyToManyField(blank=True, to="sports.player"),+ ),+ migrations.RunPython(+ migrate_sport_event_data, reverse_code=migrations.RunPython.noop+ ),+ migrations.RemoveField(+ model_name="sportevent",+ name="home_team",+ ),+ migrations.RemoveField(+ model_name="sportevent",+ name="away_team",+ ),+ migrations.RemoveField(+ model_name="sportevent",+ name="player_one",+ ),+ migrations.RemoveField(+ model_name="sportevent",+ name="player_two",+ ),+ ]@@ -0,0 +1,20 @@+# Generated by Django 4.2.29 on 2026-06-07 03:24++from django.db import migrations, models+++class Migration(migrations.Migration):++ dependencies = [+ ("sports", "0020_migrate_sport_event_data_to_logdata"),+ ]++ operations = [+ migrations.AddField(+ model_name="team",+ name="logo",+ field=models.ImageField(+ blank=True, null=True, upload_to="sports/team-logos/"+ ),+ ),+ ]@@ -68,6 +68,7 @@ class Season(TheSportsDbMixin): 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):@@ -88,50 +89,61 @@ class Round(TheSportsDbMixin): class SportEvent(ScrobblableMixin): COMPLETION_PERCENT = getattr(settings, "SPORT_COMPLETION_PERCENT", 90) - thesportsdb_id = models.CharField(max_length=255, **BNULL) event_type = models.CharField( max_length=2, choices=SportEventType.choices, default=SportEventType.UNKNOWN, )- round = models.ForeignKey(Round, on_delete=models.DO_NOTHING, **BNULL)+ 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)- 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,- )- player_one = models.ForeignKey(- Player,- on_delete=models.DO_NOTHING,- related_name="player_one_set",- **BNULL,- )- player_two = models.ForeignKey(- Player,- on_delete=models.DO_NOTHING,- related_name="player_two_set",- **BNULL,- )+ round = models.ForeignKey(Round, on_delete=models.DO_NOTHING, **BNULL) - def __str__(self):- return (- f"{self.start.date()} - {self.round} - {self.home_team} v {self.away_team}"- )+ 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:+ league = self.league+ if self.league and self.league.abbreviation_str:+ league = self.league.abbreviation_str+ return f"{self.title} - {league}" def get_absolute_url(self): return reverse("sports:event_detail", kwargs={"slug": self.uuid}) @property- def subtitle(self):- return self.round.season.league+ def subtitle(self) -> str:+ return self.comp_str++ @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:@@ -147,16 +159,17 @@ class SportEvent(ScrobblableMixin): @property def primary_image_url(self) -> str:- url = ""- if self.round.season.league.logo:- url = self.round.season.league.logo.url- return url+ if self.league and self.league.logo:+ return self.league.logo.url+ return "" @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.+ 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@@ -187,32 +200,29 @@ class SportEvent(ScrobblableMixin): # Find or create our Round rid = data_dict.get("RoundId")- round, r_created = Round.objects.get_or_create(+ round_obj, r_created = Round.objects.get_or_create( thesportsdb_id=rid, season=season, name=rid, ) if r_created:- round.season = season- round.save(update_fields=["season"])-- # Set some special data for Tennis- player_one = None- player_two = None- if data_dict.get("Sport") == "Tennis":- event_name = data_dict.get("Name", "")- if not round.name:- round.name = get_round_name_from_event(event_name)- round.save(update_fields=["name"])-- players_list = get_players_from_event(event_name)- player_one = Player.objects.filter(name__icontains=players_list[0]).first()- if not player_one:- player_one = Player.objects.create(name=players_list[0])- player_two = Player.objects.filter(name__icontains=players_list[1]).first()- if not player_two:- player_two = Player.objects.create(name=players_list[1])-+ 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"):@@ -221,27 +231,73 @@ class SportEvent(ScrobblableMixin): "thesportsdb_id": data_dict.get("HomeTeamId", ""), "league": league, }- home_team, _created = Team.objects.get_or_create(**home_team_dict)+ 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, _created = Team.objects.get_or_create(**away_team_dict)-- event_dict = {- "thesportsdb_id": data_dict.get("EventId"),- "title": data_dict.get("Name"),- "event_type": sport.default_event_type,- "home_team": home_team,- "away_team": away_team,- "player_one": player_one,- "player_two": player_two,- "start": data_dict.get("Start"),- "round": round,- "base_run_time_seconds": data_dict.get("RunTime"),- }- event, _created = cls.objects.get_or_create(**event_dict)-- return event+ 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@@ -1,10 +1,12 @@ import logging +import requests from dateutil.parser import parse from django.conf import settings+from django.core.files.base import ContentFile from django.utils import timezone from pysportsdb import TheSportsDbClient-from sports.models import Sport+from sports.models import League, Sport, Team logger = logging.getLogger(__name__) @@ -12,6 +14,84 @@ API_KEY = getattr(settings, "THESPORTSDB_API_KEY", "2") client = TheSportsDbClient(api_key=API_KEY) +def has_logo(league_or_team) -> bool:+ """Check if a model instance has a logo (handles both NULL and empty string)."""+ return bool(league_or_team.logo and league_or_team.logo.name)+++def enrich_league_logo(league: League) -> None:+ """Fetch the league badge from TheSportsDB and save it as the league logo."""+ if not league.thesportsdb_id or has_logo(league):+ return++ url = (+ f"https://www.thesportsdb.com/api/v1/json/{API_KEY}"+ f"/lookupleague.php?id={league.thesportsdb_id}"+ )+ try:+ resp = requests.get(url, timeout=10)+ data = resp.json()+ leagues = data.get("leagues", [])+ if not leagues:+ return+ badge_url = leagues[0].get("strBadge")+ if badge_url:+ r = requests.get(badge_url, timeout=10)+ if r.status_code == 200:+ fname = f"{league.uuid or league.thesportsdb_id}.png"+ league.logo.save(fname, ContentFile(r.content), save=True)+ logger.info(+ "Saved league logo from TheSportsDB",+ extra={"league_id": league.id, "league_name": league.name},+ )+ except Exception as e:+ logger.warning(+ "Failed to fetch league logo from TheSportsDB",+ extra={"league_id": league.id, "error": str(e)},+ )+++def enrich_team_logo(team: Team) -> None:+ """Fetch the team badge from TheSportsDB and save it as the team logo."""+ if not team.thesportsdb_id or has_logo(team):+ return++ try:+ badge_url = None++ # Try direct lookup by thesportsdb_id first (more reliable)+ url = (+ f"https://www.thesportsdb.com/api/v1/json/{API_KEY}"+ f"/lookupteam.php?id={team.thesportsdb_id}"+ )+ resp = requests.get(url, timeout=10)+ data = resp.json()+ api_teams = data.get("teams", [])+ if api_teams:+ badge_url = api_teams[0].get("strBadge")+ else:+ # Fall back to name search+ result = client.search_teams(team.name) or {}+ api_teams = result.get("teams", [])+ if api_teams:+ badge_url = api_teams[0].get("strBadge")++ if badge_url:+ r = requests.get(badge_url, timeout=10)+ if r.status_code == 200:+ fname = f"{team.uuid or team.thesportsdb_id}.png"+ team.logo.save(fname, ContentFile(r.content), save=True)+ logger.info(+ "Saved team logo from TheSportsDB",+ extra={"team_id": team.id, "team_name": team.name},+ )+ except Exception as e:+ logger.warning(+ "Failed to fetch team logo from TheSportsDB",+ extra={"team_id": team.id, "error": str(e)},+ )++ def lookup_event_from_thesportsdb(event_id: str) -> dict: try:@@ -23,6 +103,18 @@ def lookup_event_from_thesportsdb(event_id: str) -> dict: return {} sport, _created = Sport.objects.get_or_create(thesportsdb_id=event.get("strSport")) + # Find or create the league and optionally enrich its logo+ lid = event.get("idLeague")+ league, l_created = League.objects.get_or_create(+ thesportsdb_id=lid,+ defaults={"name": event.get("strLeague", "")},+ )+ if l_created:+ league.name = event.get("strLeague", "")+ league.sport = sport+ league.save(update_fields=["name", "sport"])+ enrich_league_logo(league)+ try: start = parse(event.get("strTimestamp")) except:@@ -38,7 +130,7 @@ def lookup_event_from_thesportsdb(event_id: str) -> dict: "RunTime": sport.default_event_run_time_seconds, "Sport": event.get("strSport"), "Season": event.get("strSeason"),- "LeagueId": event.get("idLeague"),+ "LeagueId": lid, "LeagueName": event.get("strLeague"), "HomeTeamId": event.get("idHomeTeam"), "HomeTeamName": event.get("strHomeTeam"),@@ -1,12 +1,12 @@-from django.views import generic from sports.models import SportEvent+from vrobbler.apps.scrobbles.views import ScrobbleableDetailView, ScrobbleableListView -class SportEventListView(generic.ListView):+class SportEventListView(ScrobbleableListView): model = SportEvent paginate_by = 50 -class SportEventDetailView(generic.DetailView):+class SportEventDetailView(ScrobbleableDetailView): model = SportEvent slug_field = "uuid"@@ -315,8 +315,7 @@ {% for scrobble in now_playing_list %} <div class="now-playing"> {% if scrobble.media_obj.primary_image_url %}<div style="float:left;padding-right:10px;padding-bottom:10px;"><img src="{{scrobble.media_obj.primary_image_url}}" /></div>{% endif %}- <p><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title}}</a></p>- {% if scrobble.media_obj.subtitle %}<p><em><a href="{{scrobble.media_obj.subtitle.get_absolute_url}}">{{scrobble.media_obj.subtitle}}</a></em></p>{% endif %}+ <p><a href="{{scrobble.get_absolute_url}}">{{scrobble.media_obj}}</a></p> {% if scrobble.logdata %}{% if scrobble.logdata.title%}<p><em>{{scrobble.logdata.title}}</em></p>{% endif %}{% endif %} <p><small>{{scrobble.local_timestamp|naturaltime}} from {{scrobble.source}}</small></p> <div class="progress-bar" style="margin-right:5px;">@@ -110,7 +110,7 @@ {% if sporting %} <div class="titles"> <p><a href="{{sporting.media_obj.get_absolute_url}}">{{sporting.media_obj}}</a></p>- {% if sporting.media_obj.subtitle %}<p><em><a href="{{sporting.media_obj.subtitle.get_absolute_url}}">{{sporting.media_obj.subtitle}}</a></em></p>{% endif %}+ {% if sporting.media_obj.subtitle %}<p><em><a href="{{sporting.get_absolute_url}}">{{sporting.media_obj.subtitle}}</a></em></p>{% endif %} </div> <p><small>{{sporting.timestamp|naturaltime}} from {{sporting.source}}</small></p> {% else %}@@ -1,8 +1,9 @@ {% extends "base_detail.html" %} -{% block title %}{{object.title}} - {{object.round.season.league}}{% endblock %}+{% block title %}{{object}}{% endblock %} {% block details %}+<h2>{{object.subtitle}}</h2> <div class="row"> <h2>{{object.tv_series}}</h2>