Clean up how we scrobble videos
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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"))
|
||||
|
||||
|
||||
@ -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",)
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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)
|
||||
|
||||
95
vrobbler/apps/videos/utils.py
Normal file
95
vrobbler/apps/videos/utils.py
Normal 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
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user