Turns out Jellyfin uses a really esoteric form of Musicbrainz ID for tracks. Instead of using the recording ID, it uses the specific hash for a given version of a recording. A noble effrot to be specific, but we'd much rather use Mopidy's recording ID when it's available. Thus, we'll use Jellyfin's ID if that's all we have, but if we scrobble the same track from Mopidy, overwrite the value.
212 lines
6.5 KiB
Python
212 lines
6.5 KiB
Python
import json
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
|
|
from dateutil.parser import parse
|
|
from django.conf import settings
|
|
from django.db.models.fields import timezone
|
|
from django.utils import timezone
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.generic.list import ListView
|
|
from music.constants import JELLYFIN_POST_KEYS as KEYS
|
|
from music.models import Track
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view
|
|
from rest_framework.response import Response
|
|
from scrobbles.constants import (
|
|
JELLYFIN_AUDIO_ITEM_TYPES,
|
|
JELLYFIN_VIDEO_ITEM_TYPES,
|
|
)
|
|
from scrobbles.models import Scrobble
|
|
from scrobbles.serializers import ScrobbleSerializer
|
|
from scrobbles.utils import convert_to_seconds
|
|
from videos.models import Video
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TRUTHY_VALUES = [
|
|
'true',
|
|
'1',
|
|
't',
|
|
'y',
|
|
'yes',
|
|
'yeah',
|
|
'yup',
|
|
'certainly',
|
|
'uh-huh',
|
|
]
|
|
|
|
|
|
class RecentScrobbleList(ListView):
|
|
model = Scrobble
|
|
|
|
def get_context_data(self, **kwargs):
|
|
data = super().get_context_data(**kwargs)
|
|
now = timezone.now()
|
|
last_three_minutes = timezone.now() - timedelta(minutes=3)
|
|
# Find scrobbles from the last 10 minutes
|
|
data['now_playing_list'] = Scrobble.objects.filter(
|
|
in_progress=True,
|
|
timestamp__gte=last_three_minutes,
|
|
timestamp__lte=now,
|
|
)
|
|
return data
|
|
|
|
def get_queryset(self):
|
|
return Scrobble.objects.filter(in_progress=False).order_by(
|
|
'-timestamp'
|
|
)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(['GET'])
|
|
def scrobble_endpoint(request):
|
|
"""List all Scrobbles, or create a new Scrobble"""
|
|
scrobble = Scrobble.objects.all()
|
|
serializer = ScrobbleSerializer(scrobble, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(['POST'])
|
|
def jellyfin_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)
|
|
logger.debug(f"{json_data}")
|
|
|
|
media_type = data_dict.get("ItemType", "")
|
|
|
|
track = None
|
|
video = None
|
|
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)
|
|
|
|
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"]),
|
|
}
|
|
|
|
# Convert ticks from Jellyfin from microseconds to nanoseconds
|
|
# Ain't nobody got time for nanoseconds
|
|
track_dict = {
|
|
"title": data_dict.get("Name", ""),
|
|
"run_time_ticks": data_dict.get(KEYS["RUN_TIME_TICKS"], None)
|
|
// 10000,
|
|
"run_time": convert_to_seconds(
|
|
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):
|
|
logger.error(
|
|
"No IMDB ID received. This is likely because all metadata is bad, not scrobbling"
|
|
)
|
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
|
video = Video.find_or_create(data_dict)
|
|
|
|
# Now we run off a scrobble
|
|
jellyfin_data = {
|
|
"user_id": request.user.id,
|
|
"timestamp": parse(data_dict.get("UtcTimestamp")),
|
|
"playback_position_ticks": data_dict.get("PlaybackPositionTicks")
|
|
// 10000,
|
|
"playback_position": convert_to_seconds(
|
|
data_dict.get("PlaybackPosition")
|
|
),
|
|
"source": "Jellyfin",
|
|
"source_id": data_dict.get('MediaSourceId'),
|
|
"is_paused": data_dict.get("IsPaused") in TRUTHY_VALUES,
|
|
}
|
|
|
|
scrobble = None
|
|
if video:
|
|
scrobble = Scrobble.create_or_update_for_video(
|
|
video, request.user.id, jellyfin_data
|
|
)
|
|
if track:
|
|
# Prefer Mopidy MD IDs to Jellyfin, so skip if we already have one
|
|
if not track.musicbrainz_id:
|
|
track.musicbrainz_id = data_dict.get(KEYS["TRACK_MB_ID"], None)
|
|
track.save()
|
|
scrobble = Scrobble.create_or_update_for_track(
|
|
track, request.user.id, jellyfin_data
|
|
)
|
|
|
|
if not scrobble:
|
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
return Response(
|
|
{'scrobble_id': scrobble.id}, status=status.HTTP_201_CREATED
|
|
)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(['POST'])
|
|
def mopidy_websocket(request):
|
|
data_dict = json.loads(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"),
|
|
"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:
|
|
# Jellyfin MB ids suck, so always overwrite with Mopidy if they're offering
|
|
track.musicbrainz_id = data_dict.get("musicbrainz_track_id")
|
|
track.save()
|
|
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
|
|
)
|