[videos] Fixing imdb lookups and making it more modular
This commit is contained in:
@ -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 = {
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
if not video_metadata:
|
||||||
logger.warn(f"No video found for key {name_or_id}")
|
logger.info(
|
||||||
return video_dict
|
f"[lookup_video_from_imdb] no video found on imdb",
|
||||||
imdb_client.update(video_dict)
|
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)
|
cover_url = helpers.resizeImage(cover_url, width=800)
|
||||||
return video_dict
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|||||||
@ -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():
|
video = get_or_create_video(data_dict, JELLYFIN_POST_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"):
|
if not video:
|
||||||
title = data_dict.get("Name", "")
|
|
||||||
logger.warn(
|
|
||||||
f"No IMDB ID from Jellyfin, check metadata for {title}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
return get_or_create_video_from_jellyfin(data_dict)
|
return get_or_create_video_from_jellyfin(data_dict)
|
||||||
|
|||||||
@ -7,55 +7,61 @@ 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):
|
def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False):
|
||||||
imdb_dict = lookup_video_from_imdb(name_or_id)
|
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)
|
||||||
|
|
||||||
if not imdb_dict:
|
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":
|
if video_dict.get("video_type") == Video.VideoType.TV_EPISODE:
|
||||||
series_name = imdb_dict.get("episode of").data.get("title")
|
|
||||||
|
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()
|
||||||
|
|
||||||
video.fix_metadata()
|
|
||||||
|
|
||||||
return video
|
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):
|
||||||
"""Given a Jellyfin webhook payload as a dictionary, lookup the video or
|
"""Given a Jellyfin webhook payload as a dictionary, lookup the video or
|
||||||
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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user