Add rudimentary support for mopidy-webhooks

This commit is contained in:
2023-01-08 00:26:24 -05:00
parent 638be0b56a
commit 07ad6005c8
5 changed files with 147 additions and 56 deletions

View File

@ -10,7 +10,7 @@ from django_extensions.db.models import TimeStampedModel
from music.models import Track
from videos.models import Video
logger = logging.getLogger('__name__')
logger = logging.getLogger(__name__)
User = get_user_model()
BNULL = {"blank": True, "null": True}
VIDEO_BACKOFF = getattr(settings, 'VIDEO_BACKOFF_MINUTES')
@ -37,9 +37,12 @@ class Scrobble(TimeStampedModel):
@property
def percent_played(self) -> int:
return int(
(self.playback_position_ticks / self.media_run_time_ticks) * 100
)
if self.playback_position_ticks and self.media_run_time_ticks:
return int(
(self.playback_position_ticks / self.media_run_time_ticks)
* 100
)
return 0
@property
def media_run_time_ticks(self) -> int:
@ -105,24 +108,24 @@ class Scrobble(TimeStampedModel):
@classmethod
def create_or_update_for_track(
cls, track: "Track", user_id: int, jellyfin_data: dict
cls, track: "Track", user_id: int, scrobble_data: dict
) -> "Scrobble":
jellyfin_data['track_id'] = track.id
logger.debug(
f"Creating or updating scrobble for track {track}",
{"jellyfin_data": jellyfin_data},
)
scrobble_data['track_id'] = track.id
scrobble = (
Scrobble.objects.filter(track=track, user_id=user_id)
.order_by('-modified')
.first()
)
logger.debug(
f"Found existing scrobble for track {track}, updating",
{"scrobble_data": scrobble_data},
)
backoff = timezone.now() + timedelta(minutes=TRACK_BACKOFF)
backoff = timezone.now() + timedelta(seconds=TRACK_BACKOFF)
wait_period = timezone.now() + timedelta(minutes=TRACK_WAIT_PERIOD)
return cls.update_or_create(
scrobble, backoff, wait_period, jellyfin_data
scrobble, backoff, wait_period, scrobble_data
)
@classmethod
@ -131,11 +134,27 @@ class Scrobble(TimeStampedModel):
scrobble: Optional["Scrobble"],
backoff,
wait_period,
jellyfin_data: dict,
scrobble_data: dict,
) -> Optional["Scrobble"]:
# Status is a field we get from Mopidy, which refuses to poll us
mopidy_status = scrobble_data.pop('status', None)
scrobble_is_stale = False
if scrobble:
if mopidy_status == "stopped":
logger.info(f"Mopidy sent a message to stop {scrobble}")
if not scrobble:
logger.warning(
'Mopidy sent us a stopped message, without ever starting'
)
return
# Mopidy finished a play, scrobble away
scrobble.in_progress = False
scrobble.save(update_fields=['in_progress'])
return scrobble
if scrobble and not mopidy_status:
scrobble_is_finished = (
not scrobble.in_progress and scrobble.modified < backoff
)
@ -147,14 +166,14 @@ class Scrobble(TimeStampedModel):
scrobble_is_stale = scrobble.is_stale(backoff, wait_period)
if not scrobble or scrobble_is_stale:
if (not scrobble or scrobble_is_stale) or mopidy_status:
# If we default this to "" we can probably remove this
jellyfin_data['scrobble_log'] = ""
scrobble_data['scrobble_log'] = ""
scrobble = cls.objects.create(
**jellyfin_data,
**scrobble_data,
)
else:
for key, value in jellyfin_data.items():
for key, value in scrobble_data.items():
setattr(scrobble, key, value)
scrobble.save()

View File

@ -6,4 +6,5 @@ app_name = 'scrobbles'
urlpatterns = [
path('', views.scrobble_endpoint, name='scrobble-list'),
path('jellyfin/', views.jellyfin_websocket, name='jellyfin-websocket'),
path('mopidy/', views.mopidy_websocket, name='mopidy-websocket'),
]

View File

@ -20,6 +20,8 @@ from scrobbles.models import Scrobble
from scrobbles.serializers import ScrobbleSerializer
from videos.models import Video
from vrobbler.apps.music.constants import JELLYFIN_POST_KEYS as KEYS
logger = logging.getLogger(__name__)
TRUTHY_VALUES = [
@ -79,14 +81,35 @@ def jellyfin_websocket(request):
track = None
video = None
existing_scrobble = False
if media_type in JELLYFIN_AUDIO_ITEM_TYPES:
if not data_dict.get("Provider_musicbrainztrack", None):
logger.error(
"No MBrainz Track ID received. This is likely because all metadata is bad, not scrobbling"
)
return Response({}, status=status.HTTP_400_BAD_REQUEST)
track = Track.find_or_create(data_dict)
artist_dict = {
'name': data_dict.get(KEYS["ARTIST_NAME"], None),
'musicbrainz_id': data_dict.get(KEYS["ARTIST_MB_ID"], None),
}
album_dict = {
"name": data_dict.get(KEYS["ALBUM_NAME"], None),
"year": data_dict.get(KEYS["YEAR"], ""),
"musicbrainz_id": data_dict.get(KEYS['ALBUM_MB_ID']),
"musicbrainz_releasegroup_id": data_dict.get(
KEYS["RELEASEGROUP_MB_ID"]
),
"musicbrainz_albumartist_id": data_dict.get(KEYS["ARTIST_MB_ID"]),
}
track_dict = {
"title": data_dict.get("Name", ""),
"musicbrainz_id": data_dict.get(KEYS["TRACK_MB_ID"], None),
"run_time_ticks": data_dict.get(KEYS["RUN_TIME_TICKS"], None),
"run_time": data_dict.get(KEYS["RUN_TIME"], None),
}
track = Track.find_or_create(artist_dict, album_dict, track_dict)
if media_type in JELLYFIN_VIDEO_ITEM_TYPES:
if not data_dict.get("Provider_imdb", None):
@ -102,7 +125,7 @@ def jellyfin_websocket(request):
"timestamp": parse(data_dict.get("UtcTimestamp")),
"playback_position_ticks": data_dict.get("PlaybackPositionTicks"),
"playback_position": data_dict.get("PlaybackPosition"),
"source": data_dict.get('ClientName'),
"source": "Jellyfin",
"source_id": data_dict.get('MediaSourceId'),
"is_paused": data_dict.get("IsPaused") in TRUTHY_VALUES,
}
@ -123,3 +146,53 @@ def jellyfin_websocket(request):
return Response(
{'scrobble_id': scrobble.id}, status=status.HTTP_201_CREATED
)
@csrf_exempt
@api_view(['POST'])
def mopidy_websocket(request):
data_dict = request.data
# For making things easier to build new input processors
if getattr(settings, "DUMP_REQUEST_DATA", False):
json_data = json.dumps(data_dict, indent=4)
artist_dict = {
"name": data_dict.get("artist", None),
"musicbrainz_id": data_dict.get("musicbrainz_artist_id", None),
}
album_dict = {
"name": data_dict.get("album"),
"musicbrainz_id": data_dict.get("musicbrainz_album_id"),
}
track_dict = {
"title": data_dict.get("name"),
"musicbrainz_id": data_dict.get("musicbrainz_track_id"),
"run_time_ticks": data_dict.get("run_time_ticks"),
"run_time": data_dict.get("run_time"),
}
track = Track.find_or_create(artist_dict, album_dict, track_dict)
# Now we run off a scrobble
mopidy_data = {
"user_id": request.user.id,
"timestamp": timezone.now(),
"source": "Mopidy",
"status": data_dict.get("status"),
}
scrobble = None
if track:
scrobble = Scrobble.create_or_update_for_track(
track, request.user.id, mopidy_data
)
if not scrobble:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{'scrobble_id': scrobble.id}, status=status.HTTP_201_CREATED
)