diff --git a/vrobbler/apps/music/constants.py b/vrobbler/apps/music/constants.py index 35040be..d1b5951 100644 --- a/vrobbler/apps/music/constants.py +++ b/vrobbler/apps/music/constants.py @@ -1,3 +1,78 @@ +jellyfin_keys = [ + "ServerId", + "ServerName", + "ServerVersion", + "ServerUrl", + "NotificationType", + "Timestamp", + "UtcTimestamp", + "Name", + "Overview", + "Tagline", + "ItemId", + "RunTimeTicks", + "RunTime", + "Year", + "Provider_tmdb", + "Provider_imdb", + "Provider_tmdbcollection", + "Provider_tmdbset", + "Video_0_Title", + "Video_0_Type", + "Video_0_Codec", + "Video_0_Profile", + "Video_0_Level", + "Video_0_Height", + "Video_0_Width", + "Video_0_AspectRatio", + "Video_0_Interlaced", + "Video_0_FrameRate", + "Video_0_VideoRange", + "Video_0_ColorSpace", + "Video_0_ColorTransfer", + "Video_0_ColorPrimaries", + "Video_0_PixelFormat", + "Video_0_RefFrames", + "Audio_0_Title", + "Audio_0_Type", + "Audio_0_Language", + "Audio_0_Codec", + "Audio_0_Channels", + "Audio_0_Bitrate", + "Audio_0_SampleRate", + "Audio_0_Default", + "Subtitle_0_Title", + "Subtitle_0_Type", + "Subtitle_0_Language", + "Subtitle_0_Codec", + "Subtitle_0_Default", + "Subtitle_0_Forced", + "Subtitle_0_External", + "Subtitle_1_Title", + "Subtitle_1_Type", + "Subtitle_1_Language", + "Subtitle_1_Codec", + "Subtitle_1_Default", + "Subtitle_1_Forced", + "Subtitle_1_External", + "Subtitle_2_Title", + "Subtitle_2_Type", + "Subtitle_2_Language", + "Subtitle_2_Codec", + "Subtitle_2_Default", + "Subtitle_2_Forced", + "Subtitle_2_External", + "PlaybackPositionTicks", + "PlaybackPosition", + "MediaSourceId", + "IsPaused", + "IsAutomated", + "DeviceId", + "DeviceName", + "ClientName", + "NotificationUsername", + "UserId", +] VARIOUS_ARTIST_DICT = { "name": "Various Artists", "theaudiodb_id": "113641", @@ -6,10 +81,13 @@ VARIOUS_ARTIST_DICT = { JELLYFIN_POST_KEYS = { "ITEM_TYPE": "ItemType", + "ITEM_ID": "ItemId", "RUN_TIME": "RunTime", "TRACK_TITLE": "Name", "TIMESTAMP": "UtcTimestamp", "YEAR": "Year", + "OVERVIEW": "Overview", + "TAGLINE": "TAGLINE", "PLAYBACK_POSITION_TICKS": "PlaybackPositionTicks", "PLAYBACK_POSITION": "PlaybackPosition", "ARTIST_MB_ID": "Provider_musicbrainzartist", @@ -19,8 +97,10 @@ JELLYFIN_POST_KEYS = { "ALBUM_NAME": "Album", "ARTIST_NAME": "Artist", "STATUS": "Status", + "SOURCE": "ClientName", "VIDEO_TITLE": "Name", "IMDB_ID": "Provider_imdb", + "TMDB_ID": "Provider_tmdb", } MOPIDY_POST_KEYS = { diff --git a/vrobbler/apps/music/utils.py b/vrobbler/apps/music/utils.py index 3b17663..ddad175 100644 --- a/vrobbler/apps/music/utils.py +++ b/vrobbler/apps/music/utils.py @@ -91,8 +91,11 @@ def get_or_create_album( def get_or_create_track(post_data: dict, post_keys: dict) -> Track: - track_run_time_seconds = post_data.get(post_keys.get("RUN_TIME"), 0) - if post_keys.get("RUN_TIME") == "RunTime": + try: + track_run_time_seconds = int( + post_data.get(post_keys.get("RUN_TIME"), 0) + ) + except ValueError: # Sometimes we get run time as a string like "01:35" track_run_time_seconds = convert_to_seconds( post_data.get(post_keys.get("RUN_TIME"), 0) ) diff --git a/vrobbler/apps/scrobbles/mixins.py b/vrobbler/apps/scrobbles/mixins.py index 588c30a..850a44a 100644 --- a/vrobbler/apps/scrobbles/mixins.py +++ b/vrobbler/apps/scrobbles/mixins.py @@ -77,12 +77,12 @@ class ScrobblableMixin(TimeStampedModel): @property def primary_image_url(self) -> str: - logger.warn(f"Not implemented yet") + logger.warning(f"Not implemented yet") return "" @property def logdata_cls(self) -> None: - logger.warn("logdata_cls() not implemented yet") + logger.warning("logdata_cls() not implemented yet") return None @property @@ -90,11 +90,11 @@ class ScrobblableMixin(TimeStampedModel): return "" def fix_metadata(self) -> None: - logger.warn("fix_metadata() not implemented yet") + logger.warning("fix_metadata() not implemented yet") @classmethod def find_or_create(cls) -> None: - logger.warn("find_or_create() not implemented yet") + logger.warning("find_or_create() not implemented yet") class LongPlayScrobblableMixin(ScrobblableMixin): diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index d0ecc42..8e6155b 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -1,6 +1,5 @@ import json import logging -from datetime import datetime from typing import Optional import pendulum @@ -8,6 +7,7 @@ import pytz from boardgames.models import BoardGame from books.models import Book from dateutil.parser import parse +from django.conf import settings from django.utils import timezone from locations.constants import LOCATION_PROVIDERS from locations.models import GeoLocation @@ -17,7 +17,6 @@ from music.utils import get_or_create_track from podcasts.utils import get_or_create_podcast from scrobbles.constants import JELLYFIN_AUDIO_ITEM_TYPES from scrobbles.models import Scrobble -from scrobbles.utils import convert_to_seconds from sports.models import SportEvent from sports.thesportsdb import lookup_event_from_thesportsdb from videogames.howlongtobeat import lookup_game_from_hltb @@ -33,6 +32,9 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble: if "podcast" in post_data.get("mopidy_uri", ""): media_type = Scrobble.MediaType.PODCAST_EPISODE + if settings.DUMP_REQUEST_DATA: + print("MOPIDY_DATA: ", post_data) + logger.info( "[scrobblers] webhook mopidy scrobble request received", extra={ @@ -51,7 +53,8 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble: user_id, source="Mopidy", playback_position_seconds=int( - post_data.get("playback_time_ticks", 1) / 1000 + post_data.get(MOPIDY_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1) + / 1000 ), status=post_data.get(MOPIDY_POST_KEYS.get("STATUS"), ""), ) @@ -64,6 +67,9 @@ def jellyfin_scrobble_media( if post_data.pop("ItemType", "") in JELLYFIN_AUDIO_ITEM_TYPES: media_type = Scrobble.MediaType.TRACK + if settings.DUMP_REQUEST_DATA: + print("JELLYFIN_DATA: ", post_data) + logger.info( "[jellyfin_scrobble_media] called", extra={ @@ -87,16 +93,20 @@ def jellyfin_scrobble_media( return timestamp = parse( - post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP")) + post_data.get(JELLYFIN_POST_KEYS.get("TIMESTAMP"), "") ).replace(tzinfo=pytz.utc) + playback_position_seconds = int( + post_data.get(JELLYFIN_POST_KEYS.get("PLAYBACK_POSITION_TICKS"), 1) + / 10000000 + ) if media_type == Scrobble.MediaType.VIDEO: media_obj = Video.find_or_create(post_data) - playback_position_seconds = (timezone.now() - timestamp).seconds else: media_obj = get_or_create_track( post_data, post_keys=JELLYFIN_POST_KEYS ) + # A hack because we don't worry about updating music ... we either finish it or we don't playback_position_seconds = 0 if not media_obj: @@ -112,13 +122,9 @@ def jellyfin_scrobble_media( elif post_data.get("NotificationType") == "PlaybackStop": playback_status = "stopped" - 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( user_id, + source=post_data.get(JELLYFIN_POST_KEYS.get("SOURCE")), playback_position_seconds=playback_position_seconds, status=playback_status, ) diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 6c31fae..9d02091 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -29,8 +29,6 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from scrobbles.api import serializers from scrobbles.constants import ( - JELLYFIN_AUDIO_ITEM_TYPES, - JELLYFIN_VIDEO_ITEM_TYPES, LONG_PLAY_MEDIA, MANUAL_SCROBBLE_FNS, PLAY_AGAIN_MEDIA, @@ -342,6 +340,10 @@ def jellyfin_webhook(request): if not scrobble: return Response({}, status=status.HTTP_400_BAD_REQUEST) + logger.info( + "[jellyfin_webhook] finished", + extra={"scrobble_id": scrobble.id}, + ) return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK) diff --git a/vrobbler/apps/videos/imdb.py b/vrobbler/apps/videos/imdb.py index 1dc887f..de5e74d 100644 --- a/vrobbler/apps/videos/imdb.py +++ b/vrobbler/apps/videos/imdb.py @@ -1,9 +1,7 @@ import logging -from django.utils import timezone +from typing import Optional from imdb import Cinemagoer, helpers -from imdb.Character import IMDbParserError -from scrobbles.dataclasses import VideoLogData imdb_client = Cinemagoer() @@ -12,7 +10,7 @@ logger = logging.getLogger(__name__) def lookup_video_from_imdb( name_or_id: str, kind: str = "movie" -) -> VideoLogData: +) -> Optional[dict]: # Very few video titles start with tt, but IMDB IDs often come in with it if name_or_id.startswith("tt"): @@ -50,7 +48,7 @@ def lookup_video_from_imdb( f"[lookup_video_from_imdb] no video found on imdb", extra={"name_or_id": name_or_id}, ) - return video_metadata + return None imdb_client.update(video_metadata) diff --git a/vrobbler/apps/videos/migrations/0016_video_tmdb_id.py b/vrobbler/apps/videos/migrations/0016_video_tmdb_id.py new file mode 100644 index 0000000..e6b0532 --- /dev/null +++ b/vrobbler/apps/videos/migrations/0016_video_tmdb_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-09-06 13:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("videos", "0015_alter_video_genre"), + ] + + operations = [ + migrations.AddField( + model_name="video", + name="tmdb_id", + field=models.CharField(blank=True, max_length=20, null=True), + ), + ] diff --git a/vrobbler/apps/videos/models.py b/vrobbler/apps/videos/models.py index a2bdbd7..f44c8ca 100644 --- a/vrobbler/apps/videos/models.py +++ b/vrobbler/apps/videos/models.py @@ -155,6 +155,7 @@ class Video(ScrobblableMixin): ) tvrage_id = models.CharField(max_length=20, **BNULL) tvdb_id = models.CharField(max_length=20, **BNULL) + tmdb_id = models.CharField(max_length=20, **BNULL) plot = models.TextField(**BNULL) year = models.IntegerField(**BNULL) @@ -229,20 +230,10 @@ class Video(ScrobblableMixin): self.cover_image.save(fname, ContentFile(r.content), save=True) @classmethod - def find_or_create(cls, data_dict: Dict) -> Optional["Video"]: - """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 - exist. + def find_or_create( + cls, data_dict: dict, post_keys: dict = JELLYFIN_POST_KEYS + ) -> Optional["Video"]: + """Thes smallest of wrappers around our actual get or create utility.""" + from videos.utils import get_or_create_video - """ - from videos.utils import ( - get_or_create_video, - get_or_create_video_from_jellyfin, - ) - - video = get_or_create_video(data_dict, JELLYFIN_POST_KEYS) - - if not video: - return - - return get_or_create_video_from_jellyfin(data_dict) + return get_or_create_video(data_dict, post_keys) diff --git a/vrobbler/apps/videos/utils.py b/vrobbler/apps/videos/utils.py index c8990e9..73dc461 100644 --- a/vrobbler/apps/videos/utils.py +++ b/vrobbler/apps/videos/utils.py @@ -29,8 +29,21 @@ def get_or_create_video(data_dict: dict, post_keys: dict, force_update=False): title=video_dict.get("title"), ) if video_created or force_update: + if not "overview" in video_dict.keys(): + video_dict["overview"] = data_dict.get( + post_keys.get("OVERVIEW"), None + ) + if not "tagline" in video_dict.keys(): + video_dict["tagline"] = data_dict.get( + post_keys.get("TAGLINE"), None + ) + if not "tmdb_id" in video_dict.keys(): + video_dict["tmdb_id"] = data_dict.get( + post_keys.get("TMDB_ID"), None + ) + series = None - if video_dict.get("video_type") == Video.VideoType.TV_EPISODE: + if video_dict.get("series_name"): series_name = video_dict.pop("series_name") series, series_created = Series.objects.get_or_create(