From ec219ef3eaf8c7a06432879fd396195ea2fcae2b Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Fri, 5 Jun 2026 14:41:37 -0400 Subject: [PATCH] [tracks] Fix adding tracks without mopidy_uri --- PROJECT.org | 16 +++- vrobbler/apps/scrobbles/tasks.py | 88 +++++++++++++++++++ vrobbler/apps/scrobbles/utils.py | 43 ++++----- vrobbler/apps/scrobbles/views.py | 60 ++++--------- .../templates/scrobbles/scrobble_detail.html | 2 +- 5 files changed, 141 insertions(+), 68 deletions(-) diff --git a/PROJECT.org b/PROJECT.org index 94d4902..a42bf1c 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -93,7 +93,7 @@ fetching and simple saving. :LOGBOOK: CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 :END: -* Backlog [-1/13] :vrobbler:project:personal: +* Backlog [1/14] :vrobbler:project:personal: ** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: :PROPERTIES: :ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85 @@ -509,6 +509,20 @@ needed import celery task. This is how the WebDAV celery task currently works. This would also be an opporunity to clean up the code around WebDAV imports and make them more re-usable for other import services. +** DONE [#B] Mopidy favorites or monthly playlist adds should look at all scrobbles :bug:mopidy:favorites:tracks: +:PROPERTIES: +:ID: 0be7d11e-e268-2fd5-836a-e5b4d210e0fa +:END: + +*** Description + +When favoriting a track and trying to add it to the Moidy favorite playlist, it +sometimes happens that one scrobble did not come from Mopidy, but an earlier or +later one did. + +Can we scan all the scrobbles of the track for a given user to see if any have +`mopidy_uri` in the log and if so, use that to send along to Mopidy? + * Version 45.0 [1/1] ** DONE [#B] Add ability to add mopidy tracks to Monthly playlists :feature:favorites:tracks: :PROPERTIES: diff --git a/vrobbler/apps/scrobbles/tasks.py b/vrobbler/apps/scrobbles/tasks.py index 775afa2..37abca3 100644 --- a/vrobbler/apps/scrobbles/tasks.py +++ b/vrobbler/apps/scrobbles/tasks.py @@ -576,3 +576,91 @@ def remove_favorite_from_mopidy_playlist(user_id, track_id): track=track, ) remove_track_from_mopidy_favorites_playlist(proxy) + + +@shared_task +def add_scrobble_to_mopidy_queue(scrobble_id): + from scrobbles.models import Scrobble + + scrobble = Scrobble.objects.filter(id=scrobble_id).first() + if not scrobble: + return + + profile = scrobble.user.profile + mopidy_url = profile.mopidy_api_url + if not mopidy_url: + return + + mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri") + track = scrobble.track if scrobble.media_type == "Track" else None + if not mopidy_uri and track: + sibling = ( + Scrobble.objects.filter(track=track, user=scrobble.user) + .order_by("-timestamp") + .iterator() + ) + for s in sibling: + uri = (s.log or {}).get("raw_data", {}).get("mopidy_uri") + if uri: + mopidy_uri = uri + break + + if not mopidy_uri: + logger.warning( + "No Mopidy URI found for scrobble", + extra={"scrobble_id": scrobble_id, "user_id": scrobble.user_id}, + ) + return + + import requests + + rpc_url = mopidy_url.rstrip("/") + "/rpc" + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "core.tracklist.add", + "params": {"uris": [mopidy_uri]}, + } + try: + resp = requests.post(rpc_url, json=payload, timeout=10) + resp.raise_for_status() + rpc_result = resp.json() + if rpc_result.get("error"): + logger.error( + "Mopidy error adding to queue", + extra={"error": rpc_result["error"], "scrobble_id": scrobble_id}, + ) + else: + logger.info( + "Added track to Mopidy queue", + extra={"scrobble_id": scrobble_id, "mopidy_uri": mopidy_uri}, + ) + except requests.RequestException as e: + logger.error( + "Failed to add track to Mopidy queue", + extra={"scrobble_id": scrobble_id, "error": str(e)}, + ) + + +@shared_task +def add_scrobble_to_mopidy_monthly_playlist(scrobble_id): + from scrobbles.models import Scrobble + from scrobbles.utils import add_track_to_mopidy_monthly_playlist + + scrobble = Scrobble.objects.filter(id=scrobble_id).first() + if not scrobble: + return + + track = scrobble.track if scrobble.media_type == "Track" else None + if track: + sibling = ( + Scrobble.objects.filter(track=track, user=scrobble.user) + .order_by("-timestamp") + .iterator() + ) + for s in sibling: + if (s.log or {}).get("raw_data", {}).get("mopidy_uri"): + scrobble = s + break + + add_track_to_mopidy_monthly_playlist(scrobble) diff --git a/vrobbler/apps/scrobbles/utils.py b/vrobbler/apps/scrobbles/utils.py index 2f23bba..5b7a949 100644 --- a/vrobbler/apps/scrobbles/utils.py +++ b/vrobbler/apps/scrobbles/utils.py @@ -468,9 +468,22 @@ def _ensure_mopidy_playlist(profile): return result -def add_track_to_mopidy_favorites_playlist(favorite): +def _scrobble_with_mopidy_uri(track, user): + """Find a scrobble for this track+user that has a mopidy_uri in its log.""" from scrobbles.models import Scrobble + for scrobble in ( + Scrobble.objects.filter(track=track, user=user) + .order_by("-timestamp") + .iterator() + ): + raw_data = scrobble.log.get("raw_data") or {} + if raw_data.get("mopidy_uri"): + return scrobble + return None + + +def add_track_to_mopidy_favorites_playlist(favorite): if favorite.media_type != "Track" or not favorite.track: return @@ -479,15 +492,7 @@ def add_track_to_mopidy_favorites_playlist(favorite): return track = favorite.track - scrobble = ( - Scrobble.objects.filter( - track=track, - user=favorite.user, - log__raw_data__mopidy_uri__isnull=False, - ) - .order_by("-timestamp") - .first() - ) + scrobble = _scrobble_with_mopidy_uri(track, favorite.user) if not scrobble: logger.warning( @@ -496,7 +501,7 @@ def add_track_to_mopidy_favorites_playlist(favorite): ) return - mopidy_uri = scrobble.log["raw_data"]["mopidy_uri"] + mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri") try: playlist = _ensure_mopidy_playlist(profile) @@ -557,8 +562,6 @@ def resubmit_favorites_to_mopidy(user): def remove_track_from_mopidy_favorites_playlist(favorite): - from scrobbles.models import Scrobble - if favorite.media_type != "Track" or not favorite.track: return @@ -567,15 +570,7 @@ def remove_track_from_mopidy_favorites_playlist(favorite): return track = favorite.track - scrobble = ( - Scrobble.objects.filter( - track=track, - user=favorite.user, - log__raw_data__mopidy_uri__isnull=False, - ) - .order_by("-timestamp") - .first() - ) + scrobble = _scrobble_with_mopidy_uri(track, favorite.user) if not scrobble: logger.warning( @@ -584,7 +579,7 @@ def remove_track_from_mopidy_favorites_playlist(favorite): ) return - mopidy_uri = scrobble.log["raw_data"]["mopidy_uri"] + mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri") try: playlist = _ensure_mopidy_playlist(profile) @@ -655,7 +650,7 @@ def add_track_to_mopidy_monthly_playlist(scrobble): if not pattern or not profile.mopidy_api_url: return - mopidy_uri = scrobble.log.get("raw_data", {}).get("mopidy_uri") + mopidy_uri = (scrobble.log or {}).get("raw_data", {}).get("mopidy_uri") if not mopidy_uri: return diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 8310f2e..420e650 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -992,46 +992,11 @@ def add_to_mopidy_queue(request, uuid): ) return redirect("scrobbles:detail", uuid=uuid) - raw_data = scrobble.log.get("raw_data", {}) - mopidy_uri = raw_data.get("mopidy_uri") - logger.debug(mopidy_uri) - - if not mopidy_uri: - messages.add_message( - request, - messages.ERROR, - "No Mopidy URI found for this scrobble.", - ) - return redirect("scrobbles:detail", uuid=uuid) - - rpc_url = mopidy_url.rstrip("/") + "/rpc" - payload = { - "jsonrpc": "2.0", - "id": 1, - "method": "core.tracklist.add", - "params": {"uris": [mopidy_uri]}, - } - - try: - resp = requests.post(rpc_url, json=payload, timeout=10) - resp.raise_for_status() - rpc_result = resp.json() - if rpc_result.get("error"): - messages.add_message( - request, - messages.ERROR, - f'Mopidy error: {rpc_result["error"]}', - ) - else: - msg = f'Added "{scrobble.media_obj}" to Mopidy queue.' - messages.add_message(request, messages.SUCCESS, msg) - except requests.RequestException as e: - messages.add_message( - request, - messages.ERROR, - f"Failed to contact Mopidy: {e}", - ) + from scrobbles.tasks import add_scrobble_to_mopidy_queue as task + task.delay(scrobble.id) + msg = f'Adding "{scrobble.media_obj}" to Mopidy queue.' + messages.add_message(request, messages.SUCCESS, msg) return redirect("scrobbles:detail", uuid=uuid) @@ -1055,13 +1020,13 @@ def add_to_mopidy_monthly_playlist(request, uuid): now = now_user_timezone(profile) playlist_name = DateFormat(now).format(pattern) - from scrobbles.utils import add_track_to_mopidy_monthly_playlist + from scrobbles.tasks import add_scrobble_to_mopidy_monthly_playlist as task - add_track_to_mopidy_monthly_playlist(scrobble) + task.delay(scrobble.id) messages.add_message( request, messages.SUCCESS, - f'Added "{scrobble.media_obj}" to monthly playlist "{playlist_name}".', + f'Adding "{scrobble.media_obj}" to monthly playlist "{playlist_name}".', ) return redirect("scrobbles:detail", uuid=uuid) @@ -1275,6 +1240,17 @@ class ScrobbleDetailView(DetailView): user=self.request.user, **{fk_field: media_obj} ).exists() + if media_type == "Track" and media_obj: + scrobbles = Scrobble.objects.filter( + track=media_obj, user=self.object.user + ).order_by("-timestamp")[:20] + context["has_mopidy_uri"] = any( + (s.log or {}).get("raw_data", {}).get("mopidy_uri") + for s in scrobbles + ) + else: + context["has_mopidy_uri"] = False + return context diff --git a/vrobbler/templates/scrobbles/scrobble_detail.html b/vrobbler/templates/scrobbles/scrobble_detail.html index cdf75f3..3a821f1 100644 --- a/vrobbler/templates/scrobbles/scrobble_detail.html +++ b/vrobbler/templates/scrobbles/scrobble_detail.html @@ -66,7 +66,7 @@

{{ object.logdata.title }}

{% endif %}

{{ object.local_timestamp }}

-{% if object.media_type == "Track" and object.log.raw_data.mopidy_uri and user.profile.mopidy_api_url %} +{% if object.media_type == "Track" and has_mopidy_uri and user.profile.mopidy_api_url %}
{% csrf_token %}