diff --git a/PROJECT.org b/PROJECT.org index ecff14a..97dcbbf 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 [0/14] :vrobbler:project:personal: +* Backlog [1/16] :vrobbler:project:personal: ** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: :PROPERTIES: :ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85 @@ -505,6 +505,21 @@ different. And the same for comments. If a comment (by timestamp key) is different in the webhook than what's in the scrobble.log, update the comment in the scrobble.log +** TODO [#A] Ignore tag 'inprogress' for Tasks :bug:tasks:tags: + +*** Description + +When scrobbling tasks from Todoist, the tag `inprogress` is always in the +payload, because that's how we parse tasks starting from the Todoist webhooks. + +But we don't really need anything tagged as `inprogress` Can we ignore this tag +when applying tags to Task scrobbles coming from Todoist?` + +** DONE [#B] Add ability to add track to current Mopidy queue :feature:mopidy:tracks: +:PROPERTIES: +:ID: 79d5b580-4ea6-461b-4c6c-2c950d8b3e4c +:END: + * Version 41.0 [5/5] ** DONE [#B] For any scrobble detail page with notes display them better :templates:notes:scrobbles: :PROPERTIES: diff --git a/vrobbler/apps/profiles/forms.py b/vrobbler/apps/profiles/forms.py index e3d0c5e..03493c8 100644 --- a/vrobbler/apps/profiles/forms.py +++ b/vrobbler/apps/profiles/forms.py @@ -31,6 +31,7 @@ class UserProfileForm(forms.ModelForm): "webdav_auto_import", "ntfy_url", "ntfy_enabled", + "mopidy_api_url", "redirect_to_webpage", "enable_public_widgets", "widget_custom_css", diff --git a/vrobbler/apps/profiles/migrations/0033_userprofile_mopidy_api_url.py b/vrobbler/apps/profiles/migrations/0033_userprofile_mopidy_api_url.py new file mode 100644 index 0000000..ca7aeae --- /dev/null +++ b/vrobbler/apps/profiles/migrations/0033_userprofile_mopidy_api_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.30 on 2026-06-04 16:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("profiles", "0032_userprofile_weigh_in_units"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="mopidy_api_url", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/vrobbler/apps/profiles/models.py b/vrobbler/apps/profiles/models.py index 670e635..1e56bac 100644 --- a/vrobbler/apps/profiles/models.py +++ b/vrobbler/apps/profiles/models.py @@ -64,6 +64,8 @@ class UserProfile(TimeStampedModel): ntfy_url = models.CharField(max_length=255, **BNULL) ntfy_enabled = models.BooleanField(default=False) + mopidy_api_url = models.CharField(max_length=255, **BNULL) + redirect_to_webpage = models.BooleanField(default=True) enable_public_widgets = models.BooleanField(default=False) diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index eba34fd..2e9adb2 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -158,6 +158,11 @@ urlpatterns = [ views.ScrobbleDetailView.as_view(), name="detail", ), + path( + "scrobbles//add-to-mopidy-queue/", + views.add_to_mopidy_queue, + name="add-to-mopidy-queue", + ), path("scrobbles//start/", views.scrobble_start, name="start"), path("scrobbles//finish/", views.scrobble_finish, name="finish"), path("scrobbles//cancel/", views.scrobble_cancel, name="cancel"), diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 82b8bbc..3b7fd89 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -4,6 +4,8 @@ import logging from datetime import datetime, timedelta from uuid import uuid4 +import requests + import pendulum import pytz from dateutil.relativedelta import relativedelta @@ -30,10 +32,11 @@ from django.http import ( HttpResponseRedirect, JsonResponse, ) -from django.shortcuts import redirect +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.utils import timezone from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST from django.views.generic import DetailView, FormView, TemplateView from django.views.generic.edit import CreateView from django.views.generic.list import ListView @@ -963,6 +966,65 @@ def scrobble_cancel(request, uuid): return HttpResponseRedirect(request.META.get("HTTP_REFERER")) +@require_POST +def add_to_mopidy_queue(request, uuid): + if not request.user.is_authenticated: + return redirect("scrobbles:detail", uuid=uuid) + + scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user) + mopidy_url = request.user.profile.mopidy_api_url + + if not mopidy_url: + messages.add_message( + request, + messages.ERROR, + "Mopidy API URL not configured in your profile settings.", + ) + 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}", + ) + + return redirect("scrobbles:detail", uuid=uuid) + + @api_view(["GET"]) @permission_classes([IsAuthenticated]) def export(request): diff --git a/vrobbler/templates/scrobbles/scrobble_detail.html b/vrobbler/templates/scrobbles/scrobble_detail.html index 0328bd5..dc5609c 100644 --- a/vrobbler/templates/scrobbles/scrobble_detail.html +++ b/vrobbler/templates/scrobbles/scrobble_detail.html @@ -35,11 +35,18 @@

{% if object.media_type == "Video" %}🎬{% elif object.media_type == "Track" %}🎵{% elif object.media_type == "PodcastEpisode" %}🎙️{% elif object.media_type == "SportEvent" %}⚽{% elif object.media_type == "Book" %}📚{% elif object.media_type == "Paper" %}📄{% elif object.media_type == "VideoGame" %}🎮{% elif object.media_type == "BoardGame" %}🎲{% elif object.media_type == "GeoLocation" %}📍{% elif object.media_type == "Trail" %}🥾{% elif object.media_type == "Beer" %}🍺{% elif object.media_type == "Puzzle" %}🧩{% elif object.media_type == "Food" %}🍔{% elif object.media_type == "Task" %}✅{% elif object.media_type == "WebPage" %}🌐{% elif object.media_type == "LifeEvent" %}🎉{% elif object.media_type == "Mood" %}😊{% elif object.media_type == "BrickSet" %}🧱{% elif object.media_type == "Channel" %}📺{% endif %} -{% if object.media_obj.get_absolute_url %}{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}{% endif %}

+{% if object.media_obj.get_absolute_url %}{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}{% endif %} + {% if object.media_type == "Task" and object.logdata.title %}

{{ 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 %} +
+ {% csrf_token %} + +
+{% endif %} {% if object.media_type == "Track" %}

Source: {{ object.source }}{% if object.log.mopidy_source %} ({{ object.log.mopidy_source|capfirst }}){% endif %}

{% endif %}