Clean up how we scrobble videos

This commit is contained in:
2023-03-13 03:22:44 -04:00
parent f504d9f2a1
commit 52fc67803a
8 changed files with 271 additions and 110 deletions

View File

@ -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)

View File

@ -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"))

View File

@ -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",)

View File

@ -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 = ""
imdb_id = int(name_or_id)
except ValueError:
pass
# 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}")
if imdb_id:
imdb_result = imdb_client.get_movie(name_or_id)
video_dict = imdb_result
return data_dict
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

View File

@ -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),
),
]

View File

@ -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,
}
from videos.utils import (
get_or_create_video,
get_or_create_video_from_jellyfin,
)
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
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
return
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 get_or_create_video_from_jellyfin(data_dict)

View File

@ -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

View File

@ -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",