|
@@ -1,25 +1,21 @@
|
|
|
+import json
|
|
|
import logging
|
|
|
from typing import Optional
|
|
|
|
|
|
import pendulum
|
|
|
-from boardgames.bgg import lookup_boardgame_from_bgg
|
|
|
from boardgames.models import BoardGame
|
|
|
from books.models import Book
|
|
|
-from books.openlibrary import lookup_book_from_openlibrary
|
|
|
from dateutil.parser import parse
|
|
|
from django.utils import timezone
|
|
|
from locations.constants import LOCATION_PROVIDERS
|
|
|
from locations.models import GeoLocation
|
|
|
-from music.constants import JELLYFIN_POST_KEYS
|
|
|
+from music.constants import JELLYFIN_POST_KEYS, MOPIDY_POST_KEYS
|
|
|
from music.models import Track
|
|
|
-from music.utils import (
|
|
|
- get_or_create_album,
|
|
|
- get_or_create_artist,
|
|
|
- get_or_create_track,
|
|
|
-)
|
|
|
-from podcasts.models import PodcastEpisode
|
|
|
+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, parse_mopidy_uri
|
|
|
+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
|
|
@@ -30,181 +26,89 @@ from webpages.models import WebPage
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
-def mopidy_scrobble_podcast(
|
|
|
- data_dict: dict, user_id: Optional[int]
|
|
|
-) -> Scrobble:
|
|
|
- mopidy_uri = data_dict.get("mopidy_uri", "")
|
|
|
- parsed_data = parse_mopidy_uri(mopidy_uri)
|
|
|
-
|
|
|
- producer_dict = {"name": data_dict.get("artist")}
|
|
|
-
|
|
|
- podcast_name = data_dict.get("album")
|
|
|
- if not podcast_name:
|
|
|
- podcast_name = parsed_data.get("podcast_name")
|
|
|
- podcast_dict = {"name": podcast_name}
|
|
|
-
|
|
|
- episode_name = parsed_data.get("episode_filename")
|
|
|
- episode_dict = {
|
|
|
- "title": episode_name,
|
|
|
- "run_time_seconds": data_dict.get("run_time"),
|
|
|
- "number": parsed_data.get("episode_num"),
|
|
|
- "pub_date": parsed_data.get("pub_date"),
|
|
|
- "mopidy_uri": mopidy_uri,
|
|
|
- }
|
|
|
-
|
|
|
- episode = PodcastEpisode.find_or_create(
|
|
|
- podcast_dict, producer_dict, episode_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"),
|
|
|
- }
|
|
|
+def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
|
|
|
+ media_type = Scrobble.MediaType.TRACK
|
|
|
+ if "podcast" in post_data.get("mopidy_uri", ""):
|
|
|
+ media_type = Scrobble.MediaType.PODCAST_EPISODE
|
|
|
|
|
|
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,
|
|
|
+ "post_data": post_data,
|
|
|
+ "media_type": media_type,
|
|
|
},
|
|
|
)
|
|
|
|
|
|
- scrobble = None
|
|
|
- if episode:
|
|
|
- scrobble = Scrobble.create_or_update(episode, user_id, mopidy_data)
|
|
|
- return scrobble
|
|
|
+ if media_type == Scrobble.MediaType.PODCAST_EPISODE:
|
|
|
+ media_obj = get_or_create_podcast(post_data)
|
|
|
+ else:
|
|
|
+ media_obj = get_or_create_track(post_data, MOPIDY_POST_KEYS)
|
|
|
|
|
|
+ # Now we run off a scrobble
|
|
|
+ playback_seconds = post_data.get("playback_time_ticks", 1) / 1000
|
|
|
+ playback_status = post_data.get(MOPIDY_POST_KEYS.get("STATUS"), "")
|
|
|
|
|
|
-def mopidy_scrobble_track(
|
|
|
- data_dict: dict, user_id: Optional[int]
|
|
|
-) -> Optional[Scrobble]:
|
|
|
- artist = get_or_create_artist(
|
|
|
- data_dict.get("artist"),
|
|
|
- mbid=data_dict.get("musicbrainz_artist_id", None),
|
|
|
- )
|
|
|
- album = get_or_create_album(
|
|
|
- data_dict.get("album"),
|
|
|
- artist=artist,
|
|
|
- mbid=data_dict.get("musicbrainz_album_id"),
|
|
|
- )
|
|
|
- track = get_or_create_track(
|
|
|
- title=data_dict.get("name"),
|
|
|
- mbid=data_dict.get("musicbrainz_track_id"),
|
|
|
- artist=artist,
|
|
|
- album=album,
|
|
|
- run_time_seconds=data_dict.get("run_time"),
|
|
|
+ return media_obj.scrobble_for_user(
|
|
|
+ user_id,
|
|
|
+ source="Mopidy",
|
|
|
+ playback_position_seconds=playback_seconds,
|
|
|
+ status=playback_status,
|
|
|
)
|
|
|
|
|
|
- # Now we run off a scrobble
|
|
|
- playback_seconds = data_dict.get("playback_time_ticks") / 1000
|
|
|
- mopidy_data = {
|
|
|
- "user_id": user_id,
|
|
|
- "timestamp": timezone.now(),
|
|
|
- "playback_position_seconds": playback_seconds,
|
|
|
- "source": "Mopidy",
|
|
|
- "mopidy_status": data_dict.get("status"),
|
|
|
- }
|
|
|
+
|
|
|
+def jellyfin_scrobble_media(
|
|
|
+ post_data: dict, user_id: int
|
|
|
+) -> Optional[Scrobble]:
|
|
|
+ media_type = Scrobble.MediaType.VIDEO
|
|
|
+ if post_data.pop("ItemType", "") in JELLYFIN_AUDIO_ITEM_TYPES:
|
|
|
+ media_type = Scrobble.MediaType.TRACK
|
|
|
|
|
|
logger.info(
|
|
|
- "[scrobblers] webhook mopidy scrobble request received",
|
|
|
+ "[jellyfin_scrobble_track] called",
|
|
|
extra={
|
|
|
- "track_id": track.id,
|
|
|
"user_id": user_id,
|
|
|
- "scrobble_dict": mopidy_data,
|
|
|
- "media_type": Scrobble.MediaType.TRACK,
|
|
|
+ "post_data": post_data,
|
|
|
+ "media_type": media_type,
|
|
|
},
|
|
|
)
|
|
|
|
|
|
- scrobble = Scrobble.create_or_update(track, user_id, mopidy_data)
|
|
|
-
|
|
|
- return scrobble
|
|
|
-
|
|
|
-
|
|
|
-def build_scrobble_dict(data_dict: dict, user_id: int) -> dict:
|
|
|
- jellyfin_status = "resumed"
|
|
|
- if data_dict.get("IsPaused"):
|
|
|
- jellyfin_status = "paused"
|
|
|
- elif data_dict.get("NotificationType") == "PlaybackStop":
|
|
|
- jellyfin_status = "stopped"
|
|
|
-
|
|
|
- playback_seconds = convert_to_seconds(
|
|
|
- data_dict.get("PlaybackPosition", "")
|
|
|
- )
|
|
|
- return {
|
|
|
- "user_id": user_id,
|
|
|
- "timestamp": parse(data_dict.get("UtcTimestamp")),
|
|
|
- "playback_position_seconds": playback_seconds,
|
|
|
- "source": data_dict.get("ClientName", "Vrobbler"),
|
|
|
- "log": {"media_source_id": data_dict.get("MediaSourceId")},
|
|
|
- "jellyfin_status": jellyfin_status,
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-def jellyfin_scrobble_track(
|
|
|
- data_dict: dict, user_id: Optional[int]
|
|
|
-) -> Optional[Scrobble]:
|
|
|
-
|
|
|
null_position_on_progress = (
|
|
|
- data_dict.get("PlaybackPosition") == "00:00:00"
|
|
|
- and data_dict.get("NotificationType") == "PlaybackProgress"
|
|
|
+ post_data.get("PlaybackPosition") == "00:00:00"
|
|
|
+ and post_data.get("NotificationType") == "PlaybackProgress"
|
|
|
)
|
|
|
|
|
|
+ playback_status = "resumed"
|
|
|
+ if post_data.get("IsPaused"):
|
|
|
+ playback_status = "paused"
|
|
|
+ elif post_data.get("NotificationType") == "PlaybackStop":
|
|
|
+ playback_status = "stopped"
|
|
|
+
|
|
|
# Jellyfin has some race conditions with it's webhooks, these hacks fix some of them
|
|
|
if null_position_on_progress:
|
|
|
- logger.error("No playback position tick from Jellyfin, aborting")
|
|
|
+ logger.info(
|
|
|
+ "[jellyfin_scrobble_track] no playback position tick, aborting",
|
|
|
+ extra={"post_data": post_data},
|
|
|
+ )
|
|
|
return
|
|
|
|
|
|
- artist = get_or_create_artist(
|
|
|
- data_dict.get(JELLYFIN_POST_KEYS["ARTIST_NAME"]),
|
|
|
- mbid=data_dict.get(JELLYFIN_POST_KEYS["ARTIST_MB_ID"]),
|
|
|
+ playback_position_seconds = convert_to_seconds(
|
|
|
+ post_data.get(JELLYFIN_POST_KEYS.get("RUN_TIME"), 0)
|
|
|
)
|
|
|
- album = get_or_create_album(
|
|
|
- data_dict.get(JELLYFIN_POST_KEYS["ALBUM_NAME"]),
|
|
|
- artist=artist,
|
|
|
- mbid=data_dict.get(JELLYFIN_POST_KEYS["ALBUM_MB_ID"]),
|
|
|
- )
|
|
|
-
|
|
|
- run_time = convert_to_seconds(
|
|
|
- data_dict.get(JELLYFIN_POST_KEYS["RUN_TIME"])
|
|
|
- )
|
|
|
- track = get_or_create_track(
|
|
|
- title=data_dict.get("Name"),
|
|
|
- artist=artist,
|
|
|
- album=album,
|
|
|
- run_time_seconds=run_time,
|
|
|
- )
|
|
|
-
|
|
|
- scrobble_dict = build_scrobble_dict(data_dict, user_id)
|
|
|
-
|
|
|
- # A hack to make Jellyfin work more like Mopidy for music tracks
|
|
|
- scrobble_dict["playback_position_seconds"] = 0
|
|
|
|
|
|
- return Scrobble.create_or_update(track, user_id, scrobble_dict)
|
|
|
+ if media_type == Scrobble.MediaType.VIDEO:
|
|
|
+ media_obj = Video.find_or_create(post_data)
|
|
|
+ else:
|
|
|
+ media_obj = get_or_create_track(
|
|
|
+ post_data, post_keys=JELLYFIN_POST_KEYS
|
|
|
+ )
|
|
|
|
|
|
-
|
|
|
-def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
|
|
|
- video = Video.find_or_create(data_dict)
|
|
|
-
|
|
|
- scrobble_dict = build_scrobble_dict(data_dict, user_id)
|
|
|
-
|
|
|
- logger.info(
|
|
|
- "[scrobblers] webhook video scrobble request received",
|
|
|
- extra={
|
|
|
- "video_id": video.id,
|
|
|
- "user_id": user_id,
|
|
|
- "scrobble_dict": scrobble_dict,
|
|
|
- "media_type": Scrobble.MediaType.VIDEO,
|
|
|
- },
|
|
|
+ return media_obj.scrobble_for_user_id(
|
|
|
+ user_id,
|
|
|
+ playback_position_seconds=playback_position_seconds,
|
|
|
+ status=playback_status,
|
|
|
)
|
|
|
|
|
|
- return Scrobble.create_or_update(video, user_id, scrobble_dict)
|
|
|
-
|
|
|
|
|
|
def manual_scrobble_video(imdb_id: str, user_id: int):
|
|
|
video = Video.find_or_create({"imdb_id": imdb_id})
|