diff --git a/vrobbler/apps/music/constants.py b/vrobbler/apps/music/constants.py index d1b5951..ea470fb 100644 --- a/vrobbler/apps/music/constants.py +++ b/vrobbler/apps/music/constants.py @@ -90,6 +90,7 @@ JELLYFIN_POST_KEYS = { "TAGLINE": "TAGLINE", "PLAYBACK_POSITION_TICKS": "PlaybackPositionTicks", "PLAYBACK_POSITION": "PlaybackPosition", + "MEDIA_SOURCE_ID": "MediaSourceId", "ARTIST_MB_ID": "Provider_musicbrainzartist", "ALBUM_MB_ID": "Provider_musicbrainzalbum", "RELEASEGROUP_MB_ID": "Provider_musicbrainzreleasegroup", diff --git a/vrobbler/apps/scrobbles/migrations/0071_add_scrobble_source_id.py b/vrobbler/apps/scrobbles/migrations/0071_add_scrobble_source_id.py new file mode 100644 index 0000000..7994378 --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0071_add_scrobble_source_id.py @@ -0,0 +1,18 @@ +# Generated manually for deduplication fix + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0070_rename_brickset_scrobble_brick_set"), + ] + + operations = [ + migrations.AddField( + model_name="scrobble", + name="source_id", + field=models.CharField(max_length=255, null=True, blank=True), + ), + ] diff --git a/vrobbler/apps/scrobbles/mixins.py b/vrobbler/apps/scrobbles/mixins.py index 2e94793..8c5e933 100644 --- a/vrobbler/apps/scrobbles/mixins.py +++ b/vrobbler/apps/scrobbles/mixins.py @@ -79,6 +79,7 @@ class ScrobblableMixin(TimeStampedModel): self, user_id, source: str = "Vrobbler", + source_id: Optional[str] = None, playback_position_seconds: int = 0, status: str = "started", log: Optional[dict] = None, @@ -88,6 +89,7 @@ class ScrobblableMixin(TimeStampedModel): "user_id": user_id, "timestamp": timezone.now(), "source": source, + "source_id": source_id, "status": status, "playback_position_seconds": playback_position_seconds, } diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 82decb4..4cc8be5 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -582,6 +582,7 @@ class Scrobble(TimeStampedModel): # Metadata source = models.CharField(max_length=255, **BNULL) + source_id = models.CharField(max_length=255, **BNULL) log = models.JSONField( **BNULL, default=dict, @@ -1136,6 +1137,23 @@ class Scrobble(TimeStampedModel): skip_in_progress_check = kwargs.get("skip_in_progress_check", False) read_log_page = kwargs.get("read_log_page", None) + source_id = scrobble_data.get("source_id") + + # If source_id is provided (e.g., MediaSourceId from Jellyfin), check for existing scrobble first + # This prevents duplicates when webhooks arrive faster than scrobble creation + if source_id: + existing_by_source_id = cls.objects.filter( + media_query, + user_id=user_id, + source_id=source_id, + ).first() + if existing_by_source_id: + logger.info( + "[create_or_update] found existing scrobble by source_id, updating", + extra={"scrobble_id": existing_by_source_id.id, "source_id": source_id}, + ) + scrobble_data["playback_status"] = scrobble_data.pop("status", None) + return existing_by_source_id.update(scrobble_data) # Find our last scrobble of this media item (track, video, etc) scrobble = ( diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index 3e2df53..66d2a0c 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -153,6 +153,7 @@ def jellyfin_scrobble_media( return media_obj.scrobble_for_user( user_id, source=post_data.get(JELLYFIN_POST_KEYS.get("SOURCE")), + source_id=post_data.get(JELLYFIN_POST_KEYS.get("MEDIA_SOURCE_ID")), playback_position_seconds=playback_position_seconds, status=playback_status, )