Browse Source

Clean up how we scrobble videos

Colin Powell 2 years ago
parent
commit
52fc67803a

+ 9 - 18
vrobbler/apps/scrobbles/scrobblers.py

@@ -158,11 +158,6 @@ def jellyfin_scrobble_track(
 
 
 def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
-    if not data_dict.get("Provider_imdb", None):
-        logger.error(
-            "No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
-        )
-        return
     video = Video.find_or_create(data_dict)
 
     scrobble_dict = build_scrobble_dict(data_dict, user_id)
@@ -170,25 +165,21 @@ def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
     return Scrobble.create_or_update(video, user_id, scrobble_dict)
 
 
-def manual_scrobble_video(data_dict: dict, user_id: Optional[int]):
-    if not data_dict.get("Provider_imdb", None):
-        logger.error(
-            "No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
-        )
-        return
-    video = Video.find_or_create(data_dict)
+def manual_scrobble_video(imdb_id: str, user_id: int):
+    video = Video.find_or_create({"imdb_id": imdb_id})
 
-    scrobble_dict = build_scrobble_dict(data_dict, user_id)
+    # TODO allow series to be marked with a source id
+    scrobble_dict = {
+        "user_id": user_id,
+        "timestamp": timezone.now(),
+        "playback_position_seconds": 0,
+        "source": "Vrobbler",
+    }
 
     return Scrobble.create_or_update(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 = build_scrobble_dict(data_dict, user_id)

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

@@ -60,7 +60,6 @@ from scrobbles.tasks import (
 )
 from sports.thesportsdb import lookup_event_from_thesportsdb
 from videogames.howlongtobeat import lookup_game_from_hltb
-from videos.imdb import lookup_video_from_imdb
 
 from books.openlibrary import lookup_book_from_openlibrary
 from scrobbles.utils import (
@@ -228,9 +227,7 @@ class ManualScrobbleView(FormView):
                 manual_scrobble_event(data_dict, self.request.user.id)
 
         if key == "-i":
-            data_dict = lookup_video_from_imdb(item_id)
-            if data_dict:
-                manual_scrobble_video(data_dict, self.request.user.id)
+            manual_scrobble_video(item_id, self.request.user.id)
 
         return HttpResponseRedirect(reverse("vrobbler-home"))
 

+ 1 - 1
vrobbler/apps/videos/admin.py

@@ -7,7 +7,7 @@ from scrobbles.admin import ScrobbleInline
 @admin.register(Series)
 class SeriesAdmin(admin.ModelAdmin):
     date_hierarchy = "created"
-    list_display = ("name", "tagline")
+    list_display = ("name", "imdb_id")
     ordering = ("-created",)
 
 

+ 33 - 45
vrobbler/apps/videos/imdb.py

@@ -1,56 +1,44 @@
 import logging
 from django.utils import timezone
 
-from imdb import Cinemagoer
+from imdb import Cinemagoer, helpers
+from imdb.Character import IMDbParserError
 
 imdb_client = Cinemagoer()
 
 logger = logging.getLogger(__name__)
 
 
-def lookup_video_from_imdb(imdb_id: str) -> dict:
-
-    if "tt" not in imdb_id:
-        logger.warning(f"IMDB ID should begin with 'tt' {imdb_id}")
-        return
-
-    lookup_id = imdb_id.strip("tt")
-    media = imdb_client.get_movie(lookup_id)
-
-    run_time_seconds = 60 * 60
-    runtimes = media.get("runtimes")
-    if runtimes:
-        run_time_seconds = int(runtimes[0]) * 60
-
-    item_type = "Movie"
-    if media.get("series title"):
-        item_type = "Episode"
+def lookup_video_from_imdb(name_or_id: str, kind: str = "movie") -> dict:
+    name_or_id = name_or_id.strip("tt")
+    video_dict = {}
+    imdb_id = None
 
     try:
-        plot = media.get("plot")[0]
-    except TypeError:
-        plot = ""
-    except IndexError:
-        plot = ""
-
-    # Build a rough approximation of a Jellyfin data response
-    data_dict = {
-        "ItemType": item_type,
-        "Name": media.get("title"),
-        "Overview": plot,
-        "Tagline": media.get("tagline"),
-        "Year": media.get("year"),
-        "Provider_imdb": imdb_id,
-        "RunTime": run_time_seconds,
-        "SeriesName": media.get("series title"),
-        "EpisodeNumber": media.get("episode"),
-        "SeasonNumber": media.get("season"),
-        "PlaybackPositionTicks": 1,
-        "PlaybackPosition": 1,
-        "UtcTimestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S.%f%z"),
-        "IsPaused": False,
-        "PlayedToCompletion": False,
-    }
-    logger.debug(f"Parsed data from IMDB data: {data_dict}")
-
-    return data_dict
+        imdb_id = int(name_or_id)
+    except ValueError:
+        pass
+
+    if imdb_id:
+        imdb_result = imdb_client.get_movie(name_or_id)
+        video_dict = imdb_result
+
+    if not video_dict:
+        imdb_results = imdb_client.search_movie(name_or_id)
+        if len(imdb_results) > 1:
+            for result in imdb_results:
+                if result["kind"] == kind:
+                    video_dict = result
+                    break
+
+        if len(imdb_results) == 1:
+            video_dict = imdb_results[0]
+
+    if not video_dict:
+        logger.warn(f"No video found for key {name_or_id}")
+        return video_dict
+
+    cover_url = video_dict.get("cover url")
+    if cover_url:
+        video_dict["cover url"] = helpers.resizeImage(cover_url, width=800)
+    return video_dict

+ 68 - 0
vrobbler/apps/videos/migrations/0009_rename_overview_series_plot_remove_series_tagline_and_more.py

@@ -0,0 +1,68 @@
+# Generated by Django 4.1.5 on 2023-03-13 04:00
+
+from django.db import migrations, models
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("taggit", "0004_alter_taggeditem_content_type_alter_taggeditem_tag"),
+        ("videos", "0008_rename_run_time_video_run_time_seconds"),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name="series",
+            old_name="overview",
+            new_name="plot",
+        ),
+        migrations.RemoveField(
+            model_name="series",
+            name="tagline",
+        ),
+        migrations.AddField(
+            model_name="series",
+            name="cover_image",
+            field=models.ImageField(
+                blank=True, null=True, upload_to="videos/series/"
+            ),
+        ),
+        migrations.AddField(
+            model_name="series",
+            name="genres",
+            field=taggit.managers.TaggableManager(
+                help_text="A comma-separated list of tags.",
+                through="taggit.TaggedItem",
+                to="taggit.Tag",
+                verbose_name="Tags",
+            ),
+        ),
+        migrations.AddField(
+            model_name="series",
+            name="imdb_id",
+            field=models.CharField(blank=True, max_length=20, null=True),
+        ),
+        migrations.AddField(
+            model_name="series",
+            name="imdb_rating",
+            field=models.FloatField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name="video",
+            name="cover_image",
+            field=models.ImageField(
+                blank=True, null=True, upload_to="videos/series/"
+            ),
+        ),
+        migrations.AddField(
+            model_name="video",
+            name="imdb_rating",
+            field=models.FloatField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name="video",
+            name="plot",
+            field=models.TextField(blank=True, null=True),
+        ),
+    ]

+ 65 - 46
vrobbler/apps/videos/models.py

@@ -2,13 +2,18 @@ import logging
 from typing import Dict
 from uuid import uuid4
 
+import requests
 from django.conf import settings
+from django.core.files.base import ContentFile
 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.utils import convert_to_seconds
 from scrobbles.mixins import ScrobblableMixin
+from scrobbles.utils import convert_to_seconds
+from taggit.managers import TaggableManager
+
+from videos.imdb import lookup_video_from_imdb
 
 logger = logging.getLogger(__name__)
 BNULL = {"blank": True, "null": True}
@@ -17,10 +22,12 @@ BNULL = {"blank": True, "null": True}
 class Series(TimeStampedModel):
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
     name = models.CharField(max_length=255)
-    overview = models.TextField(**BNULL)
-    tagline = models.TextField(**BNULL)
-    # tvdb_id = models.CharField(max_length=20, **BNULL)
-    # imdb_id = models.CharField(max_length=20, **BNULL)
+    plot = models.TextField(**BNULL)
+    imdb_id = models.CharField(max_length=20, **BNULL)
+    imdb_rating = models.FloatField(**BNULL)
+    cover_image = models.ImageField(upload_to="videos/series/", **BNULL)
+
+    genres = TaggableManager()
 
     def __str__(self):
         return self.name
@@ -31,6 +38,22 @@ class Series(TimeStampedModel):
     class Meta:
         verbose_name_plural = "series"
 
+    def fix_metadata(self, force_update=False):
+        imdb_dict = lookup_video_from_imdb(self.name, kind="tv series")
+        logger.info(imdb_dict)
+        self.imdb_id = imdb_dict.get("movieID")
+        self.imdb_rating = imdb_dict.get("rating")
+        self.plot = imdb_dict.get("plot outline")
+        self.save(update_fields=["imdb_id", "imdb_rating", "plot"])
+
+        cover_url = imdb_dict.get("cover url")
+
+        if (not self.cover_image or force_update) and cover_url:
+            r = requests.get(cover_url)
+            if r.status_code == 200:
+                fname = f"{self.name}_{self.uuid}.jpg"
+                self.cover_image.save(fname, ContentFile(r.content), save=True)
+
 
 class Video(ScrobblableMixin):
     COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
@@ -54,9 +77,13 @@ class Video(ScrobblableMixin):
     tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
     season_number = models.IntegerField(**BNULL)
     episode_number = models.IntegerField(**BNULL)
-    tvdb_id = models.CharField(max_length=20, **BNULL)
     imdb_id = models.CharField(max_length=20, **BNULL)
+    imdb_rating = models.FloatField(**BNULL)
+    cover_image = models.ImageField(upload_to="videos/video/", **BNULL)
     tvrage_id = models.CharField(max_length=20, **BNULL)
+    tvdb_id = models.CharField(max_length=20, **BNULL)
+    plot = models.TextField(**BNULL)
+    year = models.IntegerField(**BNULL)
 
     class Meta:
         unique_together = [["title", "imdb_id"]]
@@ -87,6 +114,21 @@ class Video(ScrobblableMixin):
     def link(self):
         return self.imdb_link
 
+    def fix_metadata(self, force_update=False):
+        imdb_dict = lookup_video_from_imdb(self.imdb_id)
+        self.imdb_rating = imdb_dict.get("rating")
+        self.plot = imdb_dict.get("plot outline")
+        self.year = imdb_dict.get("year")
+        self.save(update_fields=["imdb_rating", "plot", "year"])
+
+        cover_url = imdb_dict.get("cover url")
+
+        if (not self.cover_image or force_update) and cover_url:
+            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
     def find_or_create(cls, data_dict: Dict) -> "Video":
         """Given a data dict from Jellyfin, does the heavy lifting of looking up
@@ -94,44 +136,21 @@ class Video(ScrobblableMixin):
         exist.
 
         """
-        video_dict = {
-            "title": data_dict.get("Name", ""),
-            "imdb_id": data_dict.get("Provider_imdb", None),
-            "video_type": Video.VideoType.MOVIE,
-        }
-
-        series = None
-        if data_dict.get("ItemType", "") == "Episode":
-            series_name = data_dict.get("SeriesName", "")
-            series, series_created = Series.objects.get_or_create(
-                name=series_name
+        from videos.utils import (
+            get_or_create_video,
+            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
+
+        if not data_dict.get("Provider_imdb"):
+            title = data_dict.get("Name", "")
+            logger.warn(
+                f"No IMDB ID from Jellyfin, check metadata for {title}"
             )
-            video_dict["video_type"] = Video.VideoType.TV_EPISODE
-
-        video, created = cls.objects.get_or_create(**video_dict)
-
-        run_time_ticks = data_dict.get("RunTimeTicks", None)
-        if run_time_ticks:
-            run_time_ticks = run_time_ticks // 10000
-
-        video_extra_dict = {
-            "year": data_dict.get("Year", ""),
-            "overview": data_dict.get("Overview", None),
-            "tagline": data_dict.get("Tagline", None),
-            "run_time_ticks": run_time_ticks,
-            "run_time": convert_to_seconds(data_dict.get("RunTime", "")),
-            "tvdb_id": data_dict.get("Provider_tvdb", None),
-            "tvrage_id": data_dict.get("Provider_tvrage", None),
-            "episode_number": data_dict.get("EpisodeNumber", None),
-            "season_number": data_dict.get("SeasonNumber", None),
-        }
-
-        if series:
-            video_extra_dict["tv_series_id"] = series.id
-
-        if not video.run_time_ticks:
-            for key, value in video_extra_dict.items():
-                setattr(video, key, value)
-            video.save()
-
-        return video
+            return
+
+        return get_or_create_video_from_jellyfin(data_dict)

+ 95 - 0
vrobbler/apps/videos/utils.py

@@ -0,0 +1,95 @@
+import logging
+
+from videos.imdb import lookup_video_from_imdb
+from videos.models import Series, Video
+from scrobbles.utils import convert_to_seconds
+
+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:
+        return
+
+    video, video_created = Video.objects.get_or_create(
+        imdb_id=imdb_dict.get("imdbID"), title=imdb_dict.get("title")
+    )
+
+    if video_created or force_update:
+        video_type = Video.VideoType.MOVIE
+        series = None
+        if imdb_dict.get("kind") == "episode":
+            series_name = imdb_dict.get("episode of").data.get("title")
+            series, series_created = Series.objects.get_or_create(
+                name=series_name
+            )
+            video_type = Video.VideoType.TV_EPISODE
+            if series_created:
+                series.fix_metadata()
+
+        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),
+            "tv_series_id": series.id if series else None,
+        }
+        Video.objects.filter(pk=video.id).update(**video_dict)
+        video.refresh_from_db()
+
+        video.fix_metadata()
+
+    return video
+
+
+def get_or_create_video_from_jellyfin(jellyfin_data: dict, force_update=True):
+    """Given a Jellyfin webhook payload as a dictionary, lookup the video or
+    create a new one.
+
+    """
+
+    video, video_created = Video.objects.get_or_create(
+        imdb_id=jellyfin_data.get("Provider_imdb").replace("tt", ""),
+        title=jellyfin_data.get("Name"),
+    )
+
+    if video_created:
+        video_type = Video.VideoType.MOVIE
+        series = None
+        if jellyfin_data.get("ItemType", "") == "Episode":
+            series_name = jellyfin_data.get("SeriesName", "")
+            series, series_created = Series.objects.get_or_create(
+                name=series_name
+            )
+            if series_created:
+                series.fix_metadata()
+            video_type = Video.VideoType.TV_EPISODE
+
+        video_dict = {
+            "video_type": video_type,
+            "year": jellyfin_data.get("Year", ""),
+            "overview": jellyfin_data.get("Overview", None),
+            "tagline": jellyfin_data.get("Tagline", None),
+            "run_time_seconds": convert_to_seconds(
+                jellyfin_data.get("RunTime", 0)
+            ),
+            "tvdb_id": jellyfin_data.get("Provider_tvdb", None),
+            "tvrage_id": jellyfin_data.get("Provider_tvrage", None),
+            "episode_number": jellyfin_data.get("EpisodeNumber", None),
+            "season_number": jellyfin_data.get("SeasonNumber", None),
+        }
+
+        if series:
+            video_dict["tv_series_id"] = series.id
+
+        Video.objects.filter(pk=video.id).update(**video_dict)
+        video.refresh_from_db()
+
+        video.fix_metadata()
+
+    return video

+ 3 - 0
vrobbler/settings.py

@@ -33,6 +33,8 @@ DEBUG = os.getenv("VROBBLER_DEBUG", False)
 
 TESTING = len(sys.argv) > 1 and sys.argv[1] == "test"
 
+TAGGIT_CASE_INSENSITIVE = True
+
 KEEP_DETAILED_SCROBBLE_LOGS = os.getenv(
     "VROBBLER_KEEP_DETAILED_SCROBBLE_LOGS", False
 )
@@ -91,6 +93,7 @@ INSTALLED_APPS = [
     "django.contrib.humanize",
     "django_filters",
     "django_extensions",
+    "taggit",
     "rest_framework.authtoken",
     "encrypted_field",
     "profiles",