瀏覽代碼

Add manual scrobbling by TheSportsDB ID

Colin Powell 2 年之前
父節點
當前提交
c484905d11

+ 45 - 5
poetry.lock

@@ -667,6 +667,19 @@ category = "dev"
 optional = false
 python-versions = ">=3.6"
 
+[[package]]
+name = "mock"
+version = "5.0.1"
+description = "Rolling backport of unittest.mock for all Pythons"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+build = ["blurb", "twine", "wheel"]
+docs = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
 [[package]]
 name = "musicbrainzngs"
 version = "0.7.1"
@@ -861,6 +874,18 @@ category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
+[[package]]
+name = "pysportsdb"
+version = "0.1.0"
+description = "An simple Python interface to thesportsdb.com's API"
+category = "main"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+mock = ">=5.0.1,<6.0.0"
+requests = ">=2.28.2,<3.0.0"
+
 [[package]]
 name = "pytest"
 version = "7.2.0"
@@ -1095,7 +1120,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
 
 [[package]]
 name = "requests"
-version = "2.28.1"
+version = "2.28.2"
 description = "Python HTTP for Humans."
 category = "main"
 optional = false
@@ -1103,7 +1128,7 @@ python-versions = ">=3.7, <4"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<3"
+charset-normalizer = ">=2,<4"
 idna = ">=2.5,<4"
 urllib3 = ">=1.21.1,<1.27"
 
@@ -1451,7 +1476,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "0d571d0abd62d2c4614bc29fb0475bd0bad0b0ef99b789c8c9d46fde87515a89"
+content-hash = "39b4ea9b1b67a317d159760bff3def91ead580997052759a3f64521044e0b5ae"
 
 [metadata.files]
 amqp = [
@@ -1946,6 +1971,10 @@ mccabe = [
     {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
     {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
 ]
+mock = [
+    {file = "mock-5.0.1-py3-none-any.whl", hash = "sha256:c41cfb1e99ba5d341fbcc5308836e7d7c9786d302f995b2c271ce2144dece9eb"},
+    {file = "mock-5.0.1.tar.gz", hash = "sha256:e3ea505c03babf7977fd21674a69ad328053d414f05e6433c30d8fa14a534a6b"},
+]
 musicbrainzngs = [
     {file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"},
     {file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"},
@@ -2000,6 +2029,13 @@ pbr = [
     {file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"},
 ]
 pillow = [
+    {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"},
+    {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"},
+    {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"},
+    {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"},
+    {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
+    {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
+    {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
     {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
     {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
     {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
@@ -2116,6 +2152,10 @@ pysocks = [
     {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
     {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
 ]
+pysportsdb = [
+    {file = "pysportsdb-0.1.0-py3-none-any.whl", hash = "sha256:9e3a6654e1270a176cdb4bfcec123240bb9061f80db5bf421e5422b547efe7aa"},
+    {file = "pysportsdb-0.1.0.tar.gz", hash = "sha256:d495ec5d1c416af8192be127ece500d4d2fd6224bb9a001044b823ac158d22b5"},
+]
 pytest = [
     {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
     {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
@@ -2226,8 +2266,8 @@ redis = [
     {file = "redis-4.4.0.tar.gz", hash = "sha256:7b8c87d19c45d3f1271b124858d2a5c13160c4e74d4835e28273400fa34d5228"},
 ]
 requests = [
-    {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
-    {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+    {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
+    {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
 ]
 requests-oauthlib = [
     {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},

+ 1 - 0
pyproject.toml

@@ -29,6 +29,7 @@ django-simple-history = "^3.1.1"
 whitenoise = "^6.3.0"
 musicbrainzngs = "^0.7.1"
 cinemagoer = "^2022.12.27"
+pysportsdb = "^0.1.0"
 
 [tool.poetry.dev-dependencies]
 Werkzeug = "2.0.3"

+ 19 - 0
vrobbler/apps/scrobbles/models.py

@@ -148,6 +148,25 @@ class Scrobble(TimeStampedModel):
             scrobble, backoff, wait_period, scrobble_data
         )
 
+    @classmethod
+    def create_or_update_for_sport_event(
+        cls, event: "SportEvent", user_id: int, jellyfin_data: dict
+    ) -> "Scrobble":
+        jellyfin_data['sport_event_id'] = event.id
+        scrobble = (
+            cls.objects.filter(sport_event=event, user_id=user_id)
+            .order_by('-modified')
+            .first()
+        )
+
+        # Backoff is how long until we consider this a new scrobble
+        backoff = timezone.now() + timedelta(minutes=VIDEO_BACKOFF)
+        wait_period = timezone.now() + timedelta(days=VIDEO_WAIT_PERIOD)
+
+        return cls.update_or_create(
+            scrobble, backoff, wait_period, jellyfin_data
+        )
+
     @classmethod
     def update_or_create(
         cls,

+ 28 - 7
vrobbler/apps/scrobbles/scrobblers.py

@@ -9,6 +9,7 @@ from podcasts.models import Episode
 from scrobbles.models import Scrobble
 from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
 from videos.models import Video
+from sports.models import SportEvent
 
 logger = logging.getLogger(__name__)
 
@@ -104,18 +105,23 @@ def create_jellyfin_scrobble_dict(data_dict: dict, user_id: int) -> dict:
     if data_dict.get("PlayedToCompletion"):
         jellyfin_status = "stopped"
 
-    playback_position_ticks = data_dict.get("PlaybackPositionTicks") // 10000
-    if playback_position_ticks <= 0:
-        playback_position_ticks = None
+    playback_position_ticks = None
+    if data_dict.get("PlaybackPositionTicks"):
+        playback_position_ticks = (
+            data_dict.get("PlaybackPositionTicks") // 10000
+        )
+        if playback_position_ticks <= 0:
+            playback_position_ticks = None
+
+    playback_position = data_dict.get("PlaybackPosition")
+    if playback_position:
+        playback_position = convert_to_seconds(playback_position)
 
-    logger.debug(playback_position_ticks)
     return {
         "user_id": user_id,
         "timestamp": parse(data_dict.get("UtcTimestamp")),
         "playback_position_ticks": playback_position_ticks,
-        "playback_position": convert_to_seconds(
-            data_dict.get("PlaybackPosition")
-        ),
+        "playback_position": playback_position,
         "source": "Jellyfin",
         "source_id": data_dict.get('MediaSourceId'),
         "jellyfin_status": jellyfin_status,
@@ -193,3 +199,18 @@ def manual_scrobble_video(data_dict: dict, user_id: Optional[int]):
     scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
 
     return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
+
+
+def manual_scrobble_event(data_dict: dict, user_id: Optional[int]):
+    if not data_dict.get("Provider_thesportsdb", None):
+        logger.error(
+            "No TheSportsDB ID received. This is likely because all metadata is bad, not scrobbling"
+        )
+        return
+    event = SportEvent.find_or_create(data_dict)
+
+    scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
+
+    return Scrobble.create_or_update_for_sport_event(
+        event, user_id, scrobble_dict
+    )

+ 44 - 0
vrobbler/apps/scrobbles/thesportsdb.py

@@ -0,0 +1,44 @@
+import logging
+
+from dateutil.parser import parse
+from django.conf import settings
+from django.utils import timezone
+from pysportsdb import TheSportsDbClient
+from sports.models import SportEvent
+
+logger = logging.getLogger(__name__)
+
+API_KEY = getattr(settings, "THESPORTSDB_API_KEY", "2")
+client = TheSportsDbClient(api_key=API_KEY)
+
+
+def lookup_event_from_thesportsdb(event_id: str) -> dict:
+
+    event = client.lookup_event(event_id)['events'][0]
+    league = {}  # client.lookup_league(league_id=event.get('idLeague'))
+    event_type = SportEvent.Type.GAME
+    run_time_seconds = 11700
+    run_time_ticks = run_time_seconds * 1000
+
+    data_dict = {
+        "ItemType": event_type,
+        "Name": event.get('strEventAlternate'),
+        "Start": parse(event.get('strTimestamp')),
+        "Provider_thesportsdb": event.get('idEvent'),
+        "RunTime": run_time_seconds,
+        "RunTimeTicks": run_time_ticks,
+        "LeagueId": event.get('idLeague'),
+        "LeagueName": event.get('strLeague'),
+        "HomeTeamId": event.get('idHomeTeam'),
+        "HomeTeamName": event.get('strHomeTeam'),
+        "AwayTeamId": event.get('idAwayTeam'),
+        "AwayTeamName": event.get('strAwayTeam'),
+        "RoundId": event.get('intRound'),
+        "PlaybackPositionTicks": None,
+        "PlaybackPosition": None,
+        "UtcTimestamp": timezone.now().strftime('%Y-%m-%d %H:%M:%S.%f%z'),
+        "IsPaused": False,
+        "PlayedToCompletion": False,
+    }
+
+    return data_dict

+ 11 - 8
vrobbler/apps/scrobbles/views.py

@@ -16,16 +16,19 @@ from scrobbles.constants import (
     JELLYFIN_AUDIO_ITEM_TYPES,
     JELLYFIN_VIDEO_ITEM_TYPES,
 )
+from scrobbles.forms import ScrobbleForm
 from scrobbles.imdb import lookup_video_from_imdb
 from scrobbles.models import Scrobble
 from scrobbles.scrobblers import (
     jellyfin_scrobble_track,
     jellyfin_scrobble_video,
+    manual_scrobble_event,
     manual_scrobble_video,
     mopidy_scrobble_podcast,
     mopidy_scrobble_track,
 )
 from scrobbles.serializers import ScrobbleSerializer
+from scrobbles.thesportsdb import lookup_event_from_thesportsdb
 
 from vrobbler.apps.music.aggregators import (
     scrobble_counts,
@@ -33,7 +36,6 @@ from vrobbler.apps.music.aggregators import (
     top_tracks,
     week_of_scrobbles,
 )
-from scrobbles.forms import ScrobbleForm
 
 logger = logging.getLogger(__name__)
 
@@ -95,7 +97,7 @@ class ManualScrobbleView(FormView):
 
     def form_valid(self, form):
 
-        item_id = form.cleaned_data.get('itme_id')
+        item_id = form.cleaned_data.get('item_id')
         data_dict = None
         if 'tt' in item_id:
             data_dict = lookup_video_from_imdb(
@@ -104,12 +106,13 @@ class ManualScrobbleView(FormView):
             if data_dict:
                 manual_scrobble_video(data_dict, self.request.user.id)
 
-        # if not data_dict:
-        #    data_dict = lookup_event_from_thesportsdb(
-        #        form.cleaned_data.get('item_id')
-        #    )
-        #    if data_dict:
-        #        manual_scrobble_event(data_dict, self.request.user.id)
+        if not data_dict:
+            logger.debug(f"Looking for sport event with ID {item_id}")
+            data_dict = lookup_event_from_thesportsdb(
+                form.cleaned_data.get('item_id')
+            )
+            if data_dict:
+                manual_scrobble_event(data_dict, self.request.user.id)
 
         return HttpResponseRedirect(reverse("home"))
 

+ 11 - 3
vrobbler/apps/sports/models.py

@@ -78,27 +78,35 @@ class SportEvent(ScrobblableMixin):
         exist.
 
         """
-        league_dict = {"name": data_dict.get("LeagueName", "")}
+        league_dict = {
+            "abbreviation_str": data_dict.get("LeagueName", ""),
+            "thesportsdb_id": data_dict.get("LeagueId", ""),
+        }
         league, _created = League.objects.get_or_create(**league_dict)
 
         home_team_dict = {
             "name": data_dict.get("HomeTeamName", ""),
+            "thesportsdb_id": data_dict.get("HomeTeamId", ""),
             "league": league,
         }
         home_team, _created = Team.objects.get_or_create(**home_team_dict)
 
         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 = {
-            "event_type": SportEvent.Type.GAME,
+            "title": data_dict.get("Name"),
+            "event_type": data_dict.get("ItemType"),
             "home_team": home_team,
             "away_team": away_team,
-            "start_utc": data_dict['SportEventStart'],
+            "start": data_dict['Start'],
             "league": league,
+            "run_time_ticks": data_dict.get("RunTimeTicks"),
+            "run_time": data_dict.get("RunTime", ""),
         }
         event, _created = cls.objects.get_or_create(**event_dict)
 

+ 0 - 1
vrobbler/apps/videos/models.py

@@ -96,7 +96,6 @@ class Video(ScrobblableMixin):
 
         video, created = cls.objects.get_or_create(**video_dict)
 
-        logger.debug(data_dict)
         run_time_ticks = data_dict.get("RunTimeTicks", None)
         if run_time_ticks:
             run_time_ticks = run_time_ticks // 10000