Browse Source

[videos] Fixing imdb lookups and making it more modular

Colin Powell 8 months ago
parent
commit
3a50a8b015

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

@@ -19,6 +19,8 @@ JELLYFIN_POST_KEYS = {
     "ALBUM_NAME": "Album",
     "ALBUM_NAME": "Album",
     "ARTIST_NAME": "Artist",
     "ARTIST_NAME": "Artist",
     "STATUS": "Status",
     "STATUS": "Status",
+    "VIDEO_TITLE": "Name",
+    "IMDB_ID": "Provider_imdb",
 }
 }
 
 
 MOPIDY_POST_KEYS = {
 MOPIDY_POST_KEYS = {

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

@@ -769,6 +769,30 @@ class Scrobble(TimeStampedModel):
         if not padding_seconds:
         if not padding_seconds:
             return is_in_progress
             return is_in_progress
 
 
+        if not self.media_obj:
+            logger.info(
+                "[scrobbling] scrobble has no media obj",
+                extra={
+                    "media_id": self.media_obj,
+                    "scrobble_id": self.id,
+                    "media_type": self.media_type,
+                    "probably_still_in_progress": is_in_progress,
+                },
+            )
+            return is_in_progress
+
+        if not self.media_obj.run_time_seconds:
+            logger.info(
+                "[scrobbling] media has no run time seconds, cannot calculate end",
+                extra={
+                    "media_id": self.media_obj.id,
+                    "scrobble_id": self.id,
+                    "media_type": self.media_type,
+                    "probably_still_in_progress": is_in_progress,
+                },
+            )
+            return is_in_progress
+
         expected_end = self.timestamp + datetime.timedelta(
         expected_end = self.timestamp + datetime.timedelta(
             seconds=self.media_obj.run_time_seconds
             seconds=self.media_obj.run_time_seconds
         )
         )

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

@@ -65,7 +65,7 @@ def jellyfin_scrobble_media(
         media_type = Scrobble.MediaType.TRACK
         media_type = Scrobble.MediaType.TRACK
 
 
     logger.info(
     logger.info(
-        "[jellyfin_scrobble_track] called",
+        "[jellyfin_scrobble_media] called",
         extra={
         extra={
             "user_id": user_id,
             "user_id": user_id,
             "post_data": post_data,
             "post_data": post_data,
@@ -81,32 +81,45 @@ def jellyfin_scrobble_media(
     # Jellyfin has some race conditions with it's webhooks, these hacks fix some of them
     # Jellyfin has some race conditions with it's webhooks, these hacks fix some of them
     if null_position_on_progress:
     if null_position_on_progress:
         logger.info(
         logger.info(
-            "[jellyfin_scrobble_track] no playback position tick, aborting",
+            "[jellyfin_scrobble_media] no playback position tick, aborting",
             extra={"post_data": post_data},
             extra={"post_data": post_data},
         )
         )
         return
         return
 
 
+    timestamp = parse(
+        post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"))
+    ).replace(tzinfo=pytz.utc)
+
     if media_type == Scrobble.MediaType.VIDEO:
     if media_type == Scrobble.MediaType.VIDEO:
         media_obj = Video.find_or_create(post_data)
         media_obj = Video.find_or_create(post_data)
+        playback_position_seconds = (timezone.now() - timestamp).seconds
     else:
     else:
         media_obj = get_or_create_track(
         media_obj = get_or_create_track(
             post_data, post_keys=JELLYFIN_POST_KEYS
             post_data, post_keys=JELLYFIN_POST_KEYS
         )
         )
+        playback_position_seconds = 0
+
+    if not media_obj:
+        logger.info(
+            "[jellyfin_scrobble_media] no video found from POST data",
+            extra={"post_data": post_data},
+        )
+        return
 
 
-    timestamp = parse(
-        post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"))
-    ).replace(tzinfo=pytz.utc)
     playback_status = "resumed"
     playback_status = "resumed"
     if post_data.get("IsPaused"):
     if post_data.get("IsPaused"):
         playback_status = "paused"
         playback_status = "paused"
     elif post_data.get("NotificationType") == "PlaybackStop":
     elif post_data.get("NotificationType") == "PlaybackStop":
         playback_status = "stopped"
         playback_status = "stopped"
 
 
-    # TODO Add some logging here, maybe?
+    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(
     return media_obj.scrobble_for_user(
         user_id,
         user_id,
-        playback_position_seconds=(timezone.now() - timestamp).seconds,
+        playback_position_seconds=playback_position_seconds,
         status=playback_status,
         status=playback_status,
     )
     )
 
 
@@ -150,6 +163,17 @@ def manual_scrobble_video_game(hltb_id: str, user_id: int):
     game = VideoGame.objects.filter(hltb_id=hltb_id).first()
     game = VideoGame.objects.filter(hltb_id=hltb_id).first()
     if not game:
     if not game:
         data_dict = lookup_game_from_hltb(hltb_id)
         data_dict = lookup_game_from_hltb(hltb_id)
+        if not data_dict:
+            logger.info(
+                "[manual_scrobble_video_game] game not found on hltb",
+                extra={
+                    "hltb_id": hltb_id,
+                    "user_id": user_id,
+                    "media_type": Scrobble.MediaType.VIDEO_GAME,
+                },
+            )
+            return
+
         game = VideoGame.find_or_create(data_dict)
         game = VideoGame.find_or_create(data_dict)
 
 
     scrobble_dict = {
     scrobble_dict = {
@@ -295,3 +319,49 @@ def gpslogger_scrobble_location(data_dict: dict, user_id: int) -> Scrobble:
     )
     )
 
 
     return scrobble
     return scrobble
+
+
+def web_scrobbler_scrobble_video_or_song(
+    data_dict: dict, user_id: Optional[int]
+) -> Scrobble:
+    # We're not going to create music tracks, because the only time
+    # we'd hit this is if we're listening to a concert or something.
+    artist_name = data_dict.get("artist")
+    track_name = data_dict.get("track")
+    tracks = Track.objects.filter(
+        artist__name=data_dict.get("artist"), title=data_dict.get("track")
+    )
+    if tracks.count() > 1:
+        logger.warning(
+            "Multiple tracks found for Web Scrobbler",
+            extra={"artist": artist_name, "track": track_name},
+        )
+    track = tracks.first()
+
+    # No track found, create a Video
+    if not track:
+        Video.find_or_create(data_dict)
+
+    # Now we run off a scrobble
+    mopidy_data = {
+        "user_id": user_id,
+        "timestamp": timezone.now(),
+        "playback_position_seconds": data_dict.get("playback_time_ticks"),
+        "source": "Mopidy",
+        "mopidy_status": data_dict.get("status"),
+    }
+
+    logger.info(
+        "[scrobblers] webhook mopidy scrobble request received",
+        extra={
+            "episode_id": episode.id if episode else None,
+            "user_id": user_id,
+            "scrobble_dict": mopidy_data,
+            "media_type": Scrobble.MediaType.PODCAST_EPISODE,
+        },
+    )
+
+    scrobble = None
+    if episode:
+        scrobble = Scrobble.create_or_update(episode, user_id, mopidy_data)
+    return scrobble

+ 52 - 13
vrobbler/apps/videos/imdb.py

@@ -3,19 +3,21 @@ from django.utils import timezone
 
 
 from imdb import Cinemagoer, helpers
 from imdb import Cinemagoer, helpers
 from imdb.Character import IMDbParserError
 from imdb.Character import IMDbParserError
+from scrobbles.dataclasses import VideoLogData
 
 
 imdb_client = Cinemagoer()
 imdb_client = Cinemagoer()
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def lookup_video_from_imdb(name_or_id: str, kind: str = "movie") -> dict:
+def lookup_video_from_imdb(
+    name_or_id: str, kind: str = "movie"
+) -> VideoLogData:
 
 
     # Very few video titles start with tt, but IMDB IDs often come in with it
     # Very few video titles start with tt, but IMDB IDs often come in with it
     if name_or_id.startswith("tt"):
     if name_or_id.startswith("tt"):
         name_or_id = name_or_id[2:]
         name_or_id = name_or_id[2:]
 
 
-    video_dict = {}
     imdb_id = None
     imdb_id = None
 
 
     try:
     try:
@@ -23,27 +25,64 @@ def lookup_video_from_imdb(name_or_id: str, kind: str = "movie") -> dict:
     except ValueError:
     except ValueError:
         pass
         pass
 
 
+    video_metadata = None
     if imdb_id:
     if imdb_id:
         imdb_result = imdb_client.get_movie(name_or_id)
         imdb_result = imdb_client.get_movie(name_or_id)
-        video_dict = imdb_result
+        imdb_client.update(imdb_result, info=["plot", "synopsis", "taglines"])
+        video_metadata = imdb_result
 
 
-    if not video_dict:
+    if not video_metadata:
         imdb_results = imdb_client.search_movie(name_or_id)
         imdb_results = imdb_client.search_movie(name_or_id)
         if len(imdb_results) > 1:
         if len(imdb_results) > 1:
             for result in imdb_results:
             for result in imdb_results:
                 if result["kind"] == kind:
                 if result["kind"] == kind:
-                    video_dict = result
+                    video_metadata = result
                     break
                     break
 
 
         if len(imdb_results) == 1:
         if len(imdb_results) == 1:
-            video_dict = imdb_results[0]
+            video_metadata = imdb_results[0]
+        imdb_client.update(
+            video_metadata, info=["plot", "synopsis", "taglines"]
+        )
 
 
-    if not video_dict:
-        logger.warn(f"No video found for key {name_or_id}")
-        return video_dict
-    imdb_client.update(video_dict)
+    if not video_metadata:
+        logger.info(
+            f"[lookup_video_from_imdb] no video found on imdb",
+            extra={"name_or_id": name_or_id},
+        )
+        return video_metadata
 
 
-    cover_url = video_dict.get("cover url")
+    imdb_client.update(video_metadata)
+
+    cover_url = video_metadata.get("cover url")
     if cover_url:
     if cover_url:
-        video_dict["cover url"] = helpers.resizeImage(cover_url, width=800)
-    return video_dict
+        cover_url = helpers.resizeImage(cover_url, width=800)
+
+    from videos.models import Video
+
+    video_type = Video.VideoType.MOVIE
+    series_name = None
+    if video_metadata.get("kind") == "episode":
+        series_name = video_metadata.get("episode of", None).data.get(
+            "title", None
+        )
+        video_type = Video.VideoType.TV_EPISODE
+
+    run_time_seconds = 0
+    if video_metadata.get("runtimes"):
+        run_time_seconds = int(video_metadata.get("runtimes")[0]) * 60
+
+    return {
+        "title": video_metadata.get("title"),
+        "imdb_id": video_metadata.get("imdbID"),
+        "video_type": video_type,
+        "run_time_seconds": run_time_seconds,
+        "episode_number": video_metadata.get("episode", None),
+        "season_number": video_metadata.get("season", None),
+        "next_imdb_id": video_metadata.get("next episode", None),
+        "year": video_metadata.get("year", None),
+        "series_name": series_name,
+        "plot": video_metadata.get("plot outline"),
+        "imdb_rating": video_metadata.get("rating"),
+        "cover_url": cover_url,
+    }

+ 13 - 10
vrobbler/apps/videos/models.py

@@ -11,6 +11,7 @@ from django.utils.translation import gettext_lazy as _
 from django_extensions.db.models import TimeStampedModel
 from django_extensions.db.models import TimeStampedModel
 from imagekit.models import ImageSpecField
 from imagekit.models import ImageSpecField
 from imagekit.processors import ResizeToFit
 from imagekit.processors import ResizeToFit
+from music.constants import JELLYFIN_POST_KEYS
 from scrobbles.mixins import ObjectWithGenres, ScrobblableMixin
 from scrobbles.mixins import ObjectWithGenres, ScrobblableMixin
 from taggit.managers import TaggableManager
 from taggit.managers import TaggableManager
 from videos.imdb import lookup_video_from_imdb
 from videos.imdb import lookup_video_from_imdb
@@ -218,8 +219,17 @@ class Video(ScrobblableMixin):
         if genres := imdb_dict.data.get("genres"):
         if genres := imdb_dict.data.get("genres"):
             self.genre.add(*genres)
             self.genre.add(*genres)
 
 
+    def scrape_cover_from_url(
+        self, cover_url: str, force_update: bool = False
+    ):
+        if not self.cover_image or force_update:
+            r = requests.get(cover_url)
+            if r.status_code == 200:
+                fname = f"{self.title}_{self.uuid}.jpg"
+                self.cover_image.save(fname, ContentFile(r.content), save=True)
+
     @classmethod
     @classmethod
-    def find_or_create(cls, data_dict: Dict) -> "Video":
+    def find_or_create(cls, data_dict: Dict) -> Optional["Video"]:
         """Given a data dict from Jellyfin, does the heavy lifting of looking up
         """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
         the video and, if need, TV Series, creating both if they don't yet
         exist.
         exist.
@@ -230,16 +240,9 @@ class Video(ScrobblableMixin):
             get_or_create_video_from_jellyfin,
             get_or_create_video_from_jellyfin,
         )
         )
 
 
-        if "NotificationType" not in data_dict.keys():
-            name_or_id = data_dict.get("imdb_id") or data_dict.get("title")
-            video = get_or_create_video(name_or_id)
-            return video
+        video = get_or_create_video(data_dict, JELLYFIN_POST_KEYS)
 
 
-        if not data_dict.get("Provider_imdb"):
-            title = data_dict.get("Name", "")
-            logger.warn(
-                f"No IMDB ID from Jellyfin, check metadata for {title}"
-            )
+        if not video:
             return
             return
 
 
         return get_or_create_video_from_jellyfin(data_dict)
         return get_or_create_video_from_jellyfin(data_dict)

+ 31 - 25
vrobbler/apps/videos/utils.py

@@ -7,45 +7,52 @@ from scrobbles.utils import convert_to_seconds
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def get_or_create_video(name_or_id: str, force_update=False):
-    imdb_dict = lookup_video_from_imdb(name_or_id)
-
-    if not imdb_dict:
+def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False):
+    name_or_id = data_dict.get(post_keys.get("IMDB_ID"), "") or data_dict.get(
+        post_keys.get("VIDEO_TITLE"), ""
+    )
+    imdb_metadata = lookup_video_from_imdb(name_or_id)
+    # skatevideosite_metadata = lookup_video_from_skatevideosite(name_or_id)
+    # youtube_metadata = lookup_vide_from_youtube(name_or_id)
+
+    video_dict = imdb_metadata
+    # video_metadata = imdb_metadata or skatevideosite_metadata or youtube_metadata
+    if not video_dict:
+        logger.info(
+            "No video found on imdb, skatevideosite or youtube, cannot scrobble",
+            extra={"name_or_id": name_or_id},
+        )
         return
         return
 
 
     video, video_created = Video.objects.get_or_create(
     video, video_created = Video.objects.get_or_create(
-        imdb_id=imdb_dict.get("imdbID"), title=imdb_dict.get("title")
+        imdb_id=video_dict.get("imdb_id"),
+        title=video_dict.get("title"),
     )
     )
-
     if video_created or force_update:
     if video_created or force_update:
-        video_type = Video.VideoType.MOVIE
         series = None
         series = None
-        if imdb_dict.get("kind") == "episode":
-            series_name = imdb_dict.get("episode of").data.get("title")
+        if video_dict.get("video_type") == Video.VideoType.TV_EPISODE:
+
+            series_name = video_dict.pop("series_name")
             series, series_created = Series.objects.get_or_create(
             series, series_created = Series.objects.get_or_create(
                 name=series_name
                 name=series_name
             )
             )
-            video_type = Video.VideoType.TV_EPISODE
             if series_created:
             if series_created:
                 series.fix_metadata()
                 series.fix_metadata()
+            video_dict["tv_series_id"] = series.id
+
+        if genres := video_dict.pop("genres", None):
+            video.genre.add(*genres)
+
+        if cover_url := video_dict.pop("cover_url", None):
+            video.scrape_cover_from_url(cover_url)
 
 
-        run_time_seconds = 0
-        if imdb_dict.get("runtimes"):
-            run_time_seconds = int(imdb_dict.get("runtimes")[0]) * 60
-        video_dict = {
-            "video_type": video_type,
-            "run_time_seconds": run_time_seconds,
-            "episode_number": imdb_dict.get("episode", None),
-            "season_number": imdb_dict.get("season", None),
-            "next_imdb_id": imdb_dict.get("next episode", None),
-            "tv_series_id": series.id if series else None,
-        }
         Video.objects.filter(pk=video.id).update(**video_dict)
         Video.objects.filter(pk=video.id).update(**video_dict)
         video.refresh_from_db()
         video.refresh_from_db()
+    return video
 
 
-        video.fix_metadata()
 
 
-    return video
+def get_or_create_video_from_skatevideosite(title: str, force_update=True):
+    ...
 
 
 
 
 def get_or_create_video_from_jellyfin(jellyfin_data: dict, force_update=True):
 def get_or_create_video_from_jellyfin(jellyfin_data: dict, force_update=True):
@@ -53,9 +60,8 @@ def get_or_create_video_from_jellyfin(jellyfin_data: dict, force_update=True):
     create a new one.
     create a new one.
 
 
     """
     """
-
     video, video_created = Video.objects.get_or_create(
     video, video_created = Video.objects.get_or_create(
-        imdb_id=jellyfin_data.get("Provider_imdb").replace("tt", ""),
+        imdb_id=jellyfin_data.get("Provider_imdb", "").replace("tt", ""),
         title=jellyfin_data.get("Name"),
         title=jellyfin_data.get("Name"),
     )
     )