Selaa lähdekoodia

[music] Fix jellyfin music scrobbling sort of

Colin Powell 8 kuukautta sitten
vanhempi
commit
b5d194e74f

+ 80 - 0
vrobbler/apps/music/constants.py

@@ -1,3 +1,78 @@
+jellyfin_keys = [
+    "ServerId",
+    "ServerName",
+    "ServerVersion",
+    "ServerUrl",
+    "NotificationType",
+    "Timestamp",
+    "UtcTimestamp",
+    "Name",
+    "Overview",
+    "Tagline",
+    "ItemId",
+    "RunTimeTicks",
+    "RunTime",
+    "Year",
+    "Provider_tmdb",
+    "Provider_imdb",
+    "Provider_tmdbcollection",
+    "Provider_tmdbset",
+    "Video_0_Title",
+    "Video_0_Type",
+    "Video_0_Codec",
+    "Video_0_Profile",
+    "Video_0_Level",
+    "Video_0_Height",
+    "Video_0_Width",
+    "Video_0_AspectRatio",
+    "Video_0_Interlaced",
+    "Video_0_FrameRate",
+    "Video_0_VideoRange",
+    "Video_0_ColorSpace",
+    "Video_0_ColorTransfer",
+    "Video_0_ColorPrimaries",
+    "Video_0_PixelFormat",
+    "Video_0_RefFrames",
+    "Audio_0_Title",
+    "Audio_0_Type",
+    "Audio_0_Language",
+    "Audio_0_Codec",
+    "Audio_0_Channels",
+    "Audio_0_Bitrate",
+    "Audio_0_SampleRate",
+    "Audio_0_Default",
+    "Subtitle_0_Title",
+    "Subtitle_0_Type",
+    "Subtitle_0_Language",
+    "Subtitle_0_Codec",
+    "Subtitle_0_Default",
+    "Subtitle_0_Forced",
+    "Subtitle_0_External",
+    "Subtitle_1_Title",
+    "Subtitle_1_Type",
+    "Subtitle_1_Language",
+    "Subtitle_1_Codec",
+    "Subtitle_1_Default",
+    "Subtitle_1_Forced",
+    "Subtitle_1_External",
+    "Subtitle_2_Title",
+    "Subtitle_2_Type",
+    "Subtitle_2_Language",
+    "Subtitle_2_Codec",
+    "Subtitle_2_Default",
+    "Subtitle_2_Forced",
+    "Subtitle_2_External",
+    "PlaybackPositionTicks",
+    "PlaybackPosition",
+    "MediaSourceId",
+    "IsPaused",
+    "IsAutomated",
+    "DeviceId",
+    "DeviceName",
+    "ClientName",
+    "NotificationUsername",
+    "UserId",
+]
 VARIOUS_ARTIST_DICT = {
     "name": "Various Artists",
     "theaudiodb_id": "113641",
@@ -6,10 +81,13 @@ VARIOUS_ARTIST_DICT = {
 
 JELLYFIN_POST_KEYS = {
     "ITEM_TYPE": "ItemType",
+    "ITEM_ID": "ItemId",
     "RUN_TIME": "RunTime",
     "TRACK_TITLE": "Name",
     "TIMESTAMP": "UtcTimestamp",
     "YEAR": "Year",
+    "OVERVIEW": "Overview",
+    "TAGLINE": "TAGLINE",
     "PLAYBACK_POSITION_TICKS": "PlaybackPositionTicks",
     "PLAYBACK_POSITION": "PlaybackPosition",
     "ARTIST_MB_ID": "Provider_musicbrainzartist",
@@ -19,8 +97,10 @@ JELLYFIN_POST_KEYS = {
     "ALBUM_NAME": "Album",
     "ARTIST_NAME": "Artist",
     "STATUS": "Status",
+    "SOURCE": "ClientName",
     "VIDEO_TITLE": "Name",
     "IMDB_ID": "Provider_imdb",
+    "TMDB_ID": "Provider_tmdb",
 }
 
 MOPIDY_POST_KEYS = {

+ 5 - 2
vrobbler/apps/music/utils.py

@@ -91,8 +91,11 @@ def get_or_create_album(
 
 
 def get_or_create_track(post_data: dict, post_keys: dict) -> Track:
-    track_run_time_seconds = post_data.get(post_keys.get("RUN_TIME"), 0)
-    if post_keys.get("RUN_TIME") == "RunTime":
+    try:
+        track_run_time_seconds = int(
+            post_data.get(post_keys.get("RUN_TIME"), 0)
+        )
+    except ValueError:  # Sometimes we get run time as a string like "01:35"
         track_run_time_seconds = convert_to_seconds(
             post_data.get(post_keys.get("RUN_TIME"), 0)
         )

+ 4 - 4
vrobbler/apps/scrobbles/mixins.py

@@ -77,12 +77,12 @@ class ScrobblableMixin(TimeStampedModel):
 
     @property
     def primary_image_url(self) -> str:
-        logger.warn(f"Not implemented yet")
+        logger.warning(f"Not implemented yet")
         return ""
 
     @property
     def logdata_cls(self) -> None:
-        logger.warn("logdata_cls() not implemented yet")
+        logger.warning("logdata_cls() not implemented yet")
         return None
 
     @property
@@ -90,11 +90,11 @@ class ScrobblableMixin(TimeStampedModel):
         return ""
 
     def fix_metadata(self) -> None:
-        logger.warn("fix_metadata() not implemented yet")
+        logger.warning("fix_metadata() not implemented yet")
 
     @classmethod
     def find_or_create(cls) -> None:
-        logger.warn("find_or_create() not implemented yet")
+        logger.warning("find_or_create() not implemented yet")
 
 
 class LongPlayScrobblableMixin(ScrobblableMixin):

+ 16 - 10
vrobbler/apps/scrobbles/scrobblers.py

@@ -1,6 +1,5 @@
 import json
 import logging
-from datetime import datetime
 from typing import Optional
 
 import pendulum
@@ -8,6 +7,7 @@ import pytz
 from boardgames.models import BoardGame
 from books.models import Book
 from dateutil.parser import parse
+from django.conf import settings
 from django.utils import timezone
 from locations.constants import LOCATION_PROVIDERS
 from locations.models import GeoLocation
@@ -17,7 +17,6 @@ from music.utils import get_or_create_track
 from podcasts.utils import get_or_create_podcast
 from scrobbles.constants import JELLYFIN_AUDIO_ITEM_TYPES
 from scrobbles.models import Scrobble
-from scrobbles.utils import convert_to_seconds
 from sports.models import SportEvent
 from sports.thesportsdb import lookup_event_from_thesportsdb
 from videogames.howlongtobeat import lookup_game_from_hltb
@@ -33,6 +32,9 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
     if "podcast" in post_data.get("mopidy_uri", ""):
         media_type = Scrobble.MediaType.PODCAST_EPISODE
 
+    if settings.DUMP_REQUEST_DATA:
+        print("MOPIDY_DATA: ", post_data)
+
     logger.info(
         "[scrobblers] webhook mopidy scrobble request received",
         extra={
@@ -51,7 +53,8 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
         user_id,
         source="Mopidy",
         playback_position_seconds=int(
-            post_data.get("playback_time_ticks", 1) / 1000
+            post_data.get(MOPIDY_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1)
+            / 1000
         ),
         status=post_data.get(MOPIDY_POST_KEYS.get("STATUS"), ""),
     )
@@ -64,6 +67,9 @@ def jellyfin_scrobble_media(
     if post_data.pop("ItemType", "") in JELLYFIN_AUDIO_ITEM_TYPES:
         media_type = Scrobble.MediaType.TRACK
 
+    if settings.DUMP_REQUEST_DATA:
+        print("JELLYFIN_DATA: ", post_data)
+
     logger.info(
         "[jellyfin_scrobble_media] called",
         extra={
@@ -87,16 +93,20 @@ def jellyfin_scrobble_media(
         return
 
     timestamp = parse(
-        post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"))
+        post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"), "")
     ).replace(tzinfo=pytz.utc)
 
+    playback_position_seconds = int(
+        post_data.get(JELLYFIN_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1)
+        / 10000000
+    )
     if media_type == Scrobble.MediaType.VIDEO:
         media_obj = Video.find_or_create(post_data)
-        playback_position_seconds = (timezone.now() - timestamp).seconds
     else:
         media_obj = get_or_create_track(
             post_data, post_keys=JELLYFIN_POST_KEYS
         )
+        # A hack because we don't worry about updating music ... we either finish it or we don't
         playback_position_seconds = 0
 
     if not media_obj:
@@ -112,13 +122,9 @@ def jellyfin_scrobble_media(
     elif post_data.get("NotificationType") == "PlaybackStop":
         playback_status = "stopped"
 
-    logger.info(
-        "[jellyfin_scrobble_media] no playback position tick, aborting",
-        extra={"post_data": post_data, "playback_status": playback_status},
-    )
-
     return media_obj.scrobble_for_user(
         user_id,
+        source=post_data.get(JELLYFIN_POST_KEYS.get("SOURCE")),
         playback_position_seconds=playback_position_seconds,
         status=playback_status,
     )

+ 4 - 2
vrobbler/apps/scrobbles/views.py

@@ -29,8 +29,6 @@ from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from scrobbles.api import serializers
 from scrobbles.constants import (
-    JELLYFIN_AUDIO_ITEM_TYPES,
-    JELLYFIN_VIDEO_ITEM_TYPES,
     LONG_PLAY_MEDIA,
     MANUAL_SCROBBLE_FNS,
     PLAY_AGAIN_MEDIA,
@@ -342,6 +340,10 @@ def jellyfin_webhook(request):
     if not scrobble:
         return Response({}, status=status.HTTP_400_BAD_REQUEST)
 
+    logger.info(
+        "[jellyfin_webhook] finished",
+        extra={"scrobble_id": scrobble.id},
+    )
     return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)
 
 

+ 3 - 5
vrobbler/apps/videos/imdb.py

@@ -1,9 +1,7 @@
 import logging
-from django.utils import timezone
+from typing import Optional
 
 from imdb import Cinemagoer, helpers
-from imdb.Character import IMDbParserError
-from scrobbles.dataclasses import VideoLogData
 
 imdb_client = Cinemagoer()
 
@@ -12,7 +10,7 @@ logger = logging.getLogger(__name__)
 
 def lookup_video_from_imdb(
     name_or_id: str, kind: str = "movie"
-) -> VideoLogData:
+) -> Optional[dict]:
 
     # Very few video titles start with tt, but IMDB IDs often come in with it
     if name_or_id.startswith("tt"):
@@ -50,7 +48,7 @@ def lookup_video_from_imdb(
             f"[lookup_video_from_imdb] no video found on imdb",
             extra={"name_or_id": name_or_id},
         )
-        return video_metadata
+        return None
 
     imdb_client.update(video_metadata)
 

+ 18 - 0
vrobbler/apps/videos/migrations/0016_video_tmdb_id.py

@@ -0,0 +1,18 @@
+# Generated by Django 4.2.15 on 2024-09-06 13:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("videos", "0015_alter_video_genre"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="video",
+            name="tmdb_id",
+            field=models.CharField(blank=True, max_length=20, null=True),
+        ),
+    ]

+ 7 - 16
vrobbler/apps/videos/models.py

@@ -155,6 +155,7 @@ class Video(ScrobblableMixin):
     )
     tvrage_id = models.CharField(max_length=20, **BNULL)
     tvdb_id = models.CharField(max_length=20, **BNULL)
+    tmdb_id = models.CharField(max_length=20, **BNULL)
     plot = models.TextField(**BNULL)
     year = models.IntegerField(**BNULL)
 
@@ -229,20 +230,10 @@ class Video(ScrobblableMixin):
                 self.cover_image.save(fname, ContentFile(r.content), save=True)
 
     @classmethod
-    def find_or_create(cls, data_dict: Dict) -> Optional["Video"]:
-        """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.
-
-        """
-        from videos.utils import (
-            get_or_create_video,
-            get_or_create_video_from_jellyfin,
-        )
-
-        video = get_or_create_video(data_dict, JELLYFIN_POST_KEYS)
-
-        if not video:
-            return
+    def find_or_create(
+        cls, data_dict: dict, post_keys: dict = JELLYFIN_POST_KEYS
+    ) -> Optional["Video"]:
+        """Thes smallest of wrappers around our actual get or create utility."""
+        from videos.utils import get_or_create_video
 
-        return get_or_create_video_from_jellyfin(data_dict)
+        return get_or_create_video(data_dict, post_keys)

+ 14 - 1
vrobbler/apps/videos/utils.py

@@ -29,8 +29,21 @@ def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False):
         title=video_dict.get("title"),
     )
     if video_created or force_update:
+        if not "overview" in video_dict.keys():
+            video_dict["overview"] = data_dict.get(
+                post_keys.get("OVERVIEW"), None
+            )
+        if not "tagline" in video_dict.keys():
+            video_dict["tagline"] = data_dict.get(
+                post_keys.get("TAGLINE"), None
+            )
+        if not "tmdb_id" in video_dict.keys():
+            video_dict["tmdb_id"] = data_dict.get(
+                post_keys.get("TMDB_ID"), None
+            )
+
         series = None
-        if video_dict.get("video_type") == Video.VideoType.TV_EPISODE:
+        if video_dict.get("series_name"):
 
             series_name = video_dict.pop("series_name")
             series, series_created = Series.objects.get_or_create(