Dramatically simplify the scrobblig code
This commit is contained in:
@ -18,7 +18,7 @@ STARTING_DAY_OF_CURRENT_YEAR = NOW.date().replace(month=1, day=1)
|
||||
|
||||
|
||||
def scrobble_counts():
|
||||
finished_scrobbles_qs = Scrobble.objects.filter(in_progress=False)
|
||||
finished_scrobbles_qs = Scrobble.objects.filter(played_to_completion=True)
|
||||
data = {}
|
||||
data['today'] = finished_scrobbles_qs.filter(
|
||||
timestamp__gte=START_OF_TODAY
|
||||
@ -53,7 +53,7 @@ def week_of_scrobbles(media: str = 'tracks') -> dict[str, int]:
|
||||
.filter(
|
||||
timestamp__gte=start,
|
||||
timestamp__lte=end,
|
||||
in_progress=False,
|
||||
played_to_completion=True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
17
vrobbler/apps/music/migrations/0008_alter_track_options.py
Normal file
17
vrobbler/apps/music/migrations/0008_alter_track_options.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-13 01:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0007_alter_album_artists'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='track',
|
||||
options={},
|
||||
),
|
||||
]
|
||||
@ -48,35 +48,38 @@ class Album(TimeStampedModel):
|
||||
return self.artists.first()
|
||||
|
||||
def fix_metadata(self):
|
||||
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
|
||||
mb_data = musicbrainzngs.get_release_by_id(
|
||||
self.musicbrainz_id, includes=['artists']
|
||||
)
|
||||
if not self.musicbrainz_albumartist_id:
|
||||
self.musicbrainz_albumartist_id = mb_data['release'][
|
||||
'artist-credit'
|
||||
][0]['artist']['id']
|
||||
if not self.year:
|
||||
self.year = mb_data['release']['date'][0:4]
|
||||
self.save(update_fields=['musicbrainz_albumartist_id', 'year'])
|
||||
if not self.musicbrainz_albumartist_id or not self.year:
|
||||
musicbrainzngs.set_useragent('vrobbler', '0.3.0')
|
||||
mb_data = musicbrainzngs.get_release_by_id(
|
||||
self.musicbrainz_id, includes=['artists']
|
||||
)
|
||||
if not self.musicbrainz_albumartist_id:
|
||||
self.musicbrainz_albumartist_id = mb_data['release'][
|
||||
'artist-credit'
|
||||
][0]['artist']['id']
|
||||
if not self.year:
|
||||
self.year = mb_data['release']['date'][0:4]
|
||||
self.save(update_fields=['musicbrainz_albumartist_id', 'year'])
|
||||
|
||||
new_artist = Artist.objects.filter(
|
||||
musicbrainz_id=self.musicbrainz_albumartist_id
|
||||
).first()
|
||||
if self.musicbrainz_albumartist_id and new_artist:
|
||||
self.artists.add(new_artist)
|
||||
if not new_artist:
|
||||
for t in self.track_set.all():
|
||||
self.artists.add(t.artist)
|
||||
new_artist = Artist.objects.filter(
|
||||
musicbrainz_id=self.musicbrainz_albumartist_id
|
||||
).first()
|
||||
if self.musicbrainz_albumartist_id and new_artist:
|
||||
self.artists.add(new_artist)
|
||||
if not new_artist:
|
||||
for t in self.track_set.all():
|
||||
self.artists.add(t.artist)
|
||||
|
||||
def fetch_artwork(self):
|
||||
try:
|
||||
img_data = musicbrainzngs.get_image_front(self.musicbrainz_id)
|
||||
name = f"{self.name}_{self.uuid}.jpg"
|
||||
self.cover_image = ContentFile(img_data, name=name)
|
||||
if not self.cover_image:
|
||||
try:
|
||||
img_data = musicbrainzngs.get_image_front(self.musicbrainz_id)
|
||||
name = f"{self.name}_{self.uuid}.jpg"
|
||||
self.cover_image = ContentFile(img_data, name=name)
|
||||
except musicbrainzngs.ResponseError:
|
||||
logger.warning(f'No cover art found for {self.name}')
|
||||
self.cover_image = 'default-image-replace-me'
|
||||
self.save()
|
||||
except musicbrainzngs.ResponseError:
|
||||
logger.warning(f'No cover art found for {self.name}')
|
||||
|
||||
@property
|
||||
def mb_link(self):
|
||||
@ -132,8 +135,10 @@ class Track(ScrobblableMixin):
|
||||
logger.debug(f"Created new album {album}")
|
||||
else:
|
||||
logger.debug(f"Found album {album}")
|
||||
|
||||
album.fix_metadata()
|
||||
album.fetch_artwork()
|
||||
if not album.cover_image:
|
||||
album.fetch_artwork()
|
||||
|
||||
track_dict['album_id'] = getattr(album, "id", None)
|
||||
track_dict['artist_id'] = artist.id
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-13 01:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('podcasts', '0004_episode_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='episode',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='episode',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@ -10,10 +10,11 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
"timestamp",
|
||||
"media_name",
|
||||
"media_type",
|
||||
"playback_percent",
|
||||
"source",
|
||||
"playback_position",
|
||||
"in_progress",
|
||||
"is_paused",
|
||||
"played_to_completion",
|
||||
)
|
||||
list_filter = ("is_paused", "in_progress", "source", "track__artist")
|
||||
ordering = ("-timestamp",)
|
||||
@ -33,3 +34,6 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
||||
return "Track"
|
||||
if obj.podcast_episode:
|
||||
return "Podcast"
|
||||
|
||||
def playback_percent(self, obj):
|
||||
return obj.percent_played
|
||||
|
||||
@ -45,7 +45,7 @@ class Scrobble(TimeStampedModel):
|
||||
if (
|
||||
self.playback_position_ticks
|
||||
and self.media_obj.run_time_ticks
|
||||
and source != 'Mopidy'
|
||||
and self.source != 'Mopidy'
|
||||
):
|
||||
return int(
|
||||
(self.playback_position_ticks / self.media_obj.run_time_ticks)
|
||||
@ -61,26 +61,6 @@ class Scrobble(TimeStampedModel):
|
||||
|
||||
return 0
|
||||
|
||||
def is_stale(self, backoff, wait_period) -> bool:
|
||||
scrobble_is_stale = self.in_progress and self.modified > wait_period
|
||||
|
||||
# Check if found in progress scrobble is more than a day old
|
||||
if scrobble_is_stale:
|
||||
logger.info(
|
||||
'Found a in-progress scrobble for this item more than a day old, creating a new scrobble'
|
||||
)
|
||||
delete_stale_scrobbles = getattr(
|
||||
settings, "DELETE_STALE_SCROBBLES", True
|
||||
)
|
||||
|
||||
if delete_stale_scrobbles:
|
||||
logger.info(
|
||||
'Deleting {scrobble} that has been in-progress too long'
|
||||
)
|
||||
self.delete()
|
||||
|
||||
return scrobble_is_stale
|
||||
|
||||
@property
|
||||
def media_obj(self):
|
||||
media_obj = None
|
||||
@ -171,60 +151,31 @@ class Scrobble(TimeStampedModel):
|
||||
) -> 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
|
||||
scrobble_status = scrobble_data.pop('mopidy_status', None)
|
||||
if not scrobble_status:
|
||||
scrobble_status = scrobble_data.pop('jellyfin_status', None)
|
||||
if not scrobble_status:
|
||||
logger.warning(
|
||||
f"No status update found in message, not scrobbling"
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(f"Scrobbling to {scrobble} with status {scrobble_status}")
|
||||
if scrobble:
|
||||
logger.debug(f"Updating scrobble ticks")
|
||||
scrobble.playback_position_ticks = scrobble_data.get(
|
||||
"playback_position_ticks"
|
||||
)
|
||||
scrobble.save(update_fields=['playback_position_ticks'])
|
||||
scrobble.update_ticks(scrobble_data)
|
||||
|
||||
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
|
||||
# On stop, stop progress and send it to the check for completion
|
||||
if scrobble_status == "stopped":
|
||||
return scrobble.stop()
|
||||
|
||||
# Mopidy finished a play, scrobble away
|
||||
scrobble.in_progress = False
|
||||
scrobble.played_to_completion = True
|
||||
scrobble.save(
|
||||
update_fields=['in_progress', 'played_to_completion']
|
||||
)
|
||||
return scrobble
|
||||
# On pause, set is_paused and stop scrobbling
|
||||
if scrobble_status == "paused":
|
||||
return scrobble.pause()
|
||||
|
||||
if mopidy_status == "paused":
|
||||
logger.info(f"Mopidy sent a message to pause {scrobble}")
|
||||
if not scrobble:
|
||||
logger.info("Message to pause while not started, ignoring")
|
||||
return
|
||||
if scrobble.is_paused:
|
||||
logger.info("Message to pause while paused, ignoring")
|
||||
return
|
||||
if scrobble_status == "resumed":
|
||||
return scrobble.resume()
|
||||
|
||||
# Mopidy finished a play, scrobble away
|
||||
scrobble.is_paused = True
|
||||
scrobble.save(update_fields=["is_paused"])
|
||||
scrobble = check_scrobble_for_finish(scrobble)
|
||||
return scrobble
|
||||
|
||||
if mopidy_status == "resumed":
|
||||
logger.info(f"Mopidy sent a message to resume {scrobble}")
|
||||
if not scrobble:
|
||||
logger.info("Message to resume while not started, ignoring")
|
||||
return
|
||||
if not scrobble.is_paused:
|
||||
logger.info("Message to resume while not paused, resuming")
|
||||
# Mopidy finished a play, scrobble away
|
||||
scrobble.is_paused = False
|
||||
scrobble.save(update_fields=["is_paused"])
|
||||
return scrobble
|
||||
|
||||
if scrobble and not mopidy_status:
|
||||
# We're not changing the scrobble, but we don't want to walk over an existing one
|
||||
scrobble_is_finished = (
|
||||
not scrobble.in_progress and scrobble.modified < backoff
|
||||
)
|
||||
@ -234,9 +185,7 @@ class Scrobble(TimeStampedModel):
|
||||
)
|
||||
return
|
||||
|
||||
scrobble_is_stale = scrobble.is_stale(backoff, wait_period)
|
||||
|
||||
if (not scrobble or scrobble_is_stale) or mopidy_status:
|
||||
if not scrobble:
|
||||
# If we default this to "" we can probably remove this
|
||||
scrobble_data['scrobble_log'] = ""
|
||||
scrobble = cls.objects.create(
|
||||
@ -252,3 +201,37 @@ class Scrobble(TimeStampedModel):
|
||||
scrobble = check_scrobble_for_finish(scrobble)
|
||||
|
||||
return scrobble
|
||||
|
||||
def stop(self):
|
||||
if not self.in_progress:
|
||||
logger.warning("Scrobble already stopped")
|
||||
return
|
||||
self.in_progress = False
|
||||
self.save(update_fields=['in_progress'])
|
||||
return check_scrobble_for_finish(self)
|
||||
|
||||
def pause(self):
|
||||
if self.is_paused:
|
||||
logger.warning("Scrobble already paused")
|
||||
return
|
||||
self.is_paused = True
|
||||
self.save(update_fields=["is_paused"])
|
||||
return check_scrobble_for_finish(self)
|
||||
|
||||
def resume(self):
|
||||
if self.is_paused or not self.in_progress:
|
||||
self.is_paused = False
|
||||
self.in_progress = True
|
||||
return self.save(update_fields=["is_paused", "in_progress"])
|
||||
return self
|
||||
|
||||
def update_ticks(self, data):
|
||||
self.playback_position_ticks = data.get("playback_position_ticks")
|
||||
self.playback_position = data.get("playback_position")
|
||||
logger.debug(
|
||||
f"Updating scrobble ticks to {self.playback_position_ticks}"
|
||||
)
|
||||
self.save(
|
||||
update_fields=['playback_position_ticks', 'playback_position']
|
||||
)
|
||||
return self
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from dateutil.parser import parse
|
||||
from django.utils import timezone
|
||||
from music.constants import JELLYFIN_POST_KEYS
|
||||
from music.models import Track
|
||||
from podcasts.models import Episode
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.utils import parse_mopidy_uri
|
||||
from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
|
||||
from videos.models import Video
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -41,7 +44,7 @@ def mopidy_scrobble_podcast(
|
||||
"timestamp": timezone.now(),
|
||||
"playback_position_ticks": data_dict.get("playback_time_ticks"),
|
||||
"source": "Mopidy",
|
||||
"status": data_dict.get("status"),
|
||||
"mopidy_status": data_dict.get("status"),
|
||||
}
|
||||
|
||||
scrobble = None
|
||||
@ -79,10 +82,11 @@ def mopidy_scrobble_track(
|
||||
"timestamp": timezone.now(),
|
||||
"playback_position_ticks": data_dict.get("playback_time_ticks"),
|
||||
"source": "Mopidy",
|
||||
"status": data_dict.get("status"),
|
||||
"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")
|
||||
@ -91,3 +95,84 @@ def mopidy_scrobble_track(
|
||||
track, user_id, mopidy_data
|
||||
)
|
||||
return scrobble
|
||||
|
||||
|
||||
def create_jellyfin_scrobble_dict(data_dict: dict, user_id: int) -> dict:
|
||||
jellyfin_status = "resumed"
|
||||
if data_dict.get("IsPaused"):
|
||||
jellyfin_status = "paused"
|
||||
if data_dict.get("PlayedToCompletion"):
|
||||
jellyfin_status = "stopped"
|
||||
|
||||
return {
|
||||
"user_id": 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'),
|
||||
"jellyfin_status": jellyfin_status,
|
||||
}
|
||||
|
||||
|
||||
def jellyfin_scrobble_track(
|
||||
data_dict: dict, user_id: Optional[int]
|
||||
) -> Optional[Scrobble]:
|
||||
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
|
||||
|
||||
artist_dict = {
|
||||
'name': data_dict.get(JELLYFIN_POST_KEYS["ARTIST_NAME"], None),
|
||||
'musicbrainz_id': data_dict.get(
|
||||
JELLYFIN_POST_KEYS["ARTIST_MB_ID"], None
|
||||
),
|
||||
}
|
||||
|
||||
album_dict = {
|
||||
"name": data_dict.get(JELLYFIN_POST_KEYS["ALBUM_NAME"], None),
|
||||
"musicbrainz_id": data_dict.get(JELLYFIN_POST_KEYS['ALBUM_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(
|
||||
JELLYFIN_POST_KEYS["RUN_TIME_TICKS"], None
|
||||
)
|
||||
// 10000,
|
||||
"run_time": convert_to_seconds(
|
||||
data_dict.get(JELLYFIN_POST_KEYS["RUN_TIME"], None)
|
||||
),
|
||||
}
|
||||
track = Track.find_or_create(artist_dict, album_dict, track_dict)
|
||||
|
||||
# Prefer Mopidy MD IDs to Jellyfin, so skip if we already have one
|
||||
if not track.musicbrainz_id:
|
||||
track.musicbrainz_id = data_dict.get(
|
||||
JELLYFIN_POST_KEYS["TRACK_MB_ID"], None
|
||||
)
|
||||
track.save()
|
||||
|
||||
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
|
||||
|
||||
return Scrobble.create_or_update_for_track(track, user_id, scrobble_dict)
|
||||
|
||||
|
||||
def jellyfin_scrobble_video(data_dict: dict, user_id: Optional[int]):
|
||||
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
|
||||
video = Video.find_or_create(data_dict)
|
||||
|
||||
scrobble_dict = create_jellyfin_scrobble_dict(data_dict, user_id)
|
||||
|
||||
return Scrobble.create_or_update_for_video(video, user_id, scrobble_dict)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import unquote
|
||||
|
||||
from dateutil.parser import ParserError, parse
|
||||
@ -61,26 +61,19 @@ def parse_mopidy_uri(uri: str) -> dict:
|
||||
|
||||
|
||||
def check_scrobble_for_finish(scrobble: "Scrobble") -> None:
|
||||
completion_percent = getattr(settings, "MUSIC_COMPLETION_PERCENT", 90)
|
||||
completion_percent = getattr(settings, "MUSIC_COMPLETION_PERCENT", 95)
|
||||
if scrobble.video:
|
||||
completion_percent = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
|
||||
if scrobble.podcast_episode:
|
||||
completion_percent = getattr(
|
||||
settings, "PODCAST_COMPLETION_PERCENT", 25
|
||||
)
|
||||
logger.debug(f"Completion set to {completion_percent}")
|
||||
|
||||
if scrobble.percent_played >= completion_percent:
|
||||
logger.debug(
|
||||
f"{scrobble} meets completion goal of {completion_percent}, finishing"
|
||||
)
|
||||
scrobble.in_progress = False
|
||||
scrobble.is_paused = False
|
||||
scrobble.playback_position_ticks = scrobble.media_obj.run_time_ticks
|
||||
scrobble.played_to_completion = True
|
||||
scrobble.save(
|
||||
update_fields=[
|
||||
"in_progress",
|
||||
"is_paused",
|
||||
"playback_position_ticks",
|
||||
]
|
||||
update_fields=["in_progress", "is_paused", "played_to_completion"]
|
||||
)
|
||||
|
||||
if scrobble.percent_played % 5 == 0:
|
||||
|
||||
@ -18,16 +18,22 @@ from scrobbles.constants import (
|
||||
JELLYFIN_VIDEO_ITEM_TYPES,
|
||||
)
|
||||
from scrobbles.models import Scrobble
|
||||
from scrobbles.scrobblers import (
|
||||
jellyfin_scrobble_track,
|
||||
jellyfin_scrobble_video,
|
||||
mopidy_scrobble_podcast,
|
||||
mopidy_scrobble_track,
|
||||
)
|
||||
from scrobbles.serializers import ScrobbleSerializer
|
||||
from scrobbles.utils import convert_to_seconds
|
||||
from videos.models import Video
|
||||
|
||||
from vrobbler.apps.music.aggregators import (
|
||||
scrobble_counts,
|
||||
top_artists,
|
||||
top_tracks,
|
||||
week_of_scrobbles,
|
||||
)
|
||||
from scrobbles.scrobblers import mopidy_scrobble_podcast, mopidy_scrobble_track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -55,7 +61,6 @@ class RecentScrobbleList(ListView):
|
||||
data['now_playing_list'] = Scrobble.objects.filter(
|
||||
in_progress=True,
|
||||
is_paused=False,
|
||||
timestamp__gte=last_eight_minutes,
|
||||
timestamp__lte=now,
|
||||
)
|
||||
data['video_scrobble_list'] = Scrobble.objects.filter(
|
||||
@ -98,79 +103,14 @@ def jellyfin_websocket(request):
|
||||
json_data = json.dumps(data_dict, indent=4)
|
||||
logger.debug(f"{json_data}")
|
||||
|
||||
scrobble = None
|
||||
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)
|
||||
scrobble = jellyfin_scrobble_track(data_dict, request.user.id)
|
||||
|
||||
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
|
||||
)
|
||||
scrobble = jellyfin_scrobble_video(data_dict, request.user.id)
|
||||
|
||||
if not scrobble:
|
||||
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
18
vrobbler/apps/videos/migrations/0006_alter_video_year.py
Normal file
18
vrobbler/apps/videos/migrations/0006_alter_video_year.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-13 01:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('videos', '0005_alter_video_options_alter_video_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='video',
|
||||
name='year',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -44,7 +44,7 @@ class Video(ScrobblableMixin):
|
||||
)
|
||||
overview = models.TextField(**BNULL)
|
||||
tagline = models.TextField(**BNULL)
|
||||
year = models.IntegerField()
|
||||
year = models.IntegerField(**BNULL)
|
||||
|
||||
# TV show specific fields
|
||||
tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
|
||||
@ -80,11 +80,6 @@ class Video(ScrobblableMixin):
|
||||
"title": data_dict.get("Name", ""),
|
||||
"imdb_id": data_dict.get("Provider_imdb", None),
|
||||
"video_type": Video.VideoType.MOVIE,
|
||||
"year": data_dict.get("Year", ""),
|
||||
"overview": data_dict.get("Overview", None),
|
||||
"tagline": data_dict.get("Tagline", None),
|
||||
"run_time_ticks": data_dict.get("RunTimeTicks", 0) // 10000,
|
||||
"run_time": convert_to_seconds(data_dict.get("RunTime", "")),
|
||||
}
|
||||
|
||||
if data_dict.get("ItemType", "") == "Episode":
|
||||
@ -97,16 +92,27 @@ class Video(ScrobblableMixin):
|
||||
else:
|
||||
logger.debug(f"Found series {series}")
|
||||
video_dict['video_type'] = Video.VideoType.TV_EPISODE
|
||||
video_dict["tv_series_id"] = series.id
|
||||
video_dict["tvdb_id"] = data_dict.get("Provider_tvdb", None)
|
||||
video_dict["tvrage_id"] = data_dict.get("Provider_tvrage", None)
|
||||
video_dict["episode_number"] = data_dict.get("EpisodeNumber", "")
|
||||
video_dict["season_number"] = data_dict.get("SeasonNumber", "")
|
||||
|
||||
video, created = cls.objects.get_or_create(**video_dict)
|
||||
|
||||
video_extra_dict = {
|
||||
"year": data_dict.get("Year", ""),
|
||||
"overview": data_dict.get("Overview", None),
|
||||
"tagline": data_dict.get("Tagline", None),
|
||||
"run_time_ticks": data_dict.get("RunTimeTicks", 0) // 10000,
|
||||
"run_time": convert_to_seconds(data_dict.get("RunTime", "")),
|
||||
"tv_series_id": series.id,
|
||||
"tvdb_id": data_dict.get("Provider_tvdb", None),
|
||||
"tvrage_id": data_dict.get("Provider_tvrage", None),
|
||||
"episode_number": data_dict.get("EpisodeNumber", ""),
|
||||
"season_number": data_dict.get("SeasonNumber", ""),
|
||||
}
|
||||
|
||||
if created:
|
||||
logger.debug(f"Created new video: {video}")
|
||||
for key, value in video_extra_dict.items():
|
||||
setattr(video, key, value)
|
||||
video.save()
|
||||
else:
|
||||
logger.debug(f"Found video {video}")
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ DELETE_STALE_SCROBBLES = os.getenv("VROBBLER_DELETE_STALE_SCROBBLES", True)
|
||||
DUMP_REQUEST_DATA = os.getenv("VROBBLER_DUMP_REQUEST_DATA", False)
|
||||
|
||||
VIDEO_BACKOFF_MINUTES = os.getenv("VROBBLER_VIDEO_BACKOFF_MINUTES", 15)
|
||||
MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 5)
|
||||
MUSIC_BACKOFF_SECONDS = os.getenv("VROBBLER_VIDEO_BACKOFF_SECONDS", 1)
|
||||
|
||||
# If you stop waching or listening to a track, how long should we wait before we
|
||||
# give up on the old scrobble and start a new one? This could also be considered
|
||||
|
||||
@ -198,6 +198,7 @@
|
||||
{{scrobble.media_obj.title}}<br/>
|
||||
{% if scrobble.track %}<em>{{scrobble.track.artist}}</em><br/>{% endif %}
|
||||
{% if scrobble.podcast_episode%}<em>{{scrobble.podcast_episode.podcast}}</em><br/>{% endif %}
|
||||
{% if scrobble.video.tv_series %}<em>{{scrobble.video.tv_series }}</em><br/>{% endif %}
|
||||
<small>{{scrobble.created|naturaltime}}<br/>
|
||||
from {{scrobble.source}}</small>
|
||||
<div class="progress-bar" style="margin-right:5px;">
|
||||
|
||||
Reference in New Issue
Block a user