Skip to content

Conversation

@danielfariati
Copy link

Fixes #85

What was happening

In certain cases, Jellyfin fires PlaybackStopped or UserDataSaved more than once for the same listening session. Because the plugin triggers a scrobble during those events, two identical scrobbles could be submitted to Last.fm within a very short time window.
This results in duplicate plays of the same track on the user’s Last.fm profile.

What this PR changes

A lightweight safeguard has been added to LastfmApiClient to ignore scrobbles that meet both conditions:

  1. The previously scrobbled track for this user is the same track;
  2. The last scrobble occurred less than 15 seconds ago;

This is consistent with Last.fm’s guidelines, which state that clients should not submit multiple scrobbles within such a short interval.
The check ensures correct behavior across both scrobbling paths (default and alternative).

Summary

This PR prevents accidental duplicate scrobbles without affecting normal scrobbling behavior. No configuration changes are required from the user.

@danielfariati
Copy link
Author

If #72 is also merged, would have to change the code to not use the Helpers.CurrentTimestamp() method , as that PR removes it.
I've used it as I don't know if that other PR is going to be merged or not.

Copy link
Owner

@jesseward jesseward left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for these changes @danielfariati

I left a comment about the possibility of using an IMemoryCache that would allow us to expire records after a timeframe. Preventing growth that could lead to complaints from very heavy JF users relating to memory utilization..

This pattern is used elsewhere in JF plugins as well.


// if the last scrobble for the user is of the same song and happened within 15 seconds, do not scrobble.
private const long minimumTimeBetweenDuplicateScrobblesInSeconds = 15;
private readonly Dictionary<string, (string TrackId, long Timestamp)> _lastScrobble = new();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should look at using a Microsoft.Extensions.Caching.Memory here instead. Then Set the key with a Timespan in which it will expire.

The reason is: This plugin sees little updates and maintenance. I want to ensure that little is left behind with its usage. If there are heavy JF (music library) users that don't restart often, they may start to report (very) slow memory growth due to this object filling (unbound) with scrobble records.

Expiring the records after N hours would keep the object trim.

Copy link
Author

@danielfariati danielfariati Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's my first time using C# / .NET, but will take a look at it.

Since we only care about scrobbles in the last 15 seconds, instead of using the "minimumTimeBetweenScrobbles" logic, we could just use the cache with 15 seconds TTL...
If I use the userId / trackId as the key, that would allow me to remove most of the checks in the current implementation.
Will try to make that change, test it and commit it today.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done @jesseward ... Tested with the new logic and it's working as expected.
Also, now I'm not using the timestamp helper method as it is not needed anymore... so this PR not conflict with the other one that is open.
Can you please review once again?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, to explain why I use _scrobbleLock even though MemoryCache is already thread-safe... it's to ensure the check-and-set operation for duplicate scrobbles is atomic.
Without the lock, two concurrent requests could both see a cache miss and both set the value, resulting in duplicate scrobbles.
The lock guarantees that only one thread can perform the check-and-set at a time, fully preventing race conditions.

I know this may be overly cautious, but I prefer to be safe rather than sorry (and the operation is very lightweight, so there’s no practical downside, in my opinion).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple scobbles for the same listen.

2 participants