[tracks] Add mopidy queue button
All checks were successful
build / test (push) Successful in 2m2s

This commit is contained in:
2026-06-04 13:11:21 -04:00
parent a70343d6f3
commit ee01e3d8df
7 changed files with 113 additions and 3 deletions

View File

@ -93,7 +93,7 @@ fetching and simple saving.
:LOGBOOK: :LOGBOOK:
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
:END: :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: ** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
:PROPERTIES: :PROPERTIES:
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85 :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 different in the webhook than what's in the scrobble.log, update the comment in
the scrobble.log 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] * Version 41.0 [5/5]
** DONE [#B] For any scrobble detail page with notes display them better :templates:notes:scrobbles: ** DONE [#B] For any scrobble detail page with notes display them better :templates:notes:scrobbles:
:PROPERTIES: :PROPERTIES:

View File

@ -31,6 +31,7 @@ class UserProfileForm(forms.ModelForm):
"webdav_auto_import", "webdav_auto_import",
"ntfy_url", "ntfy_url",
"ntfy_enabled", "ntfy_enabled",
"mopidy_api_url",
"redirect_to_webpage", "redirect_to_webpage",
"enable_public_widgets", "enable_public_widgets",
"widget_custom_css", "widget_custom_css",

View File

@ -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),
),
]

View File

@ -64,6 +64,8 @@ class UserProfile(TimeStampedModel):
ntfy_url = models.CharField(max_length=255, **BNULL) ntfy_url = models.CharField(max_length=255, **BNULL)
ntfy_enabled = models.BooleanField(default=False) ntfy_enabled = models.BooleanField(default=False)
mopidy_api_url = models.CharField(max_length=255, **BNULL)
redirect_to_webpage = models.BooleanField(default=True) redirect_to_webpage = models.BooleanField(default=True)
enable_public_widgets = models.BooleanField(default=False) enable_public_widgets = models.BooleanField(default=False)

View File

@ -158,6 +158,11 @@ urlpatterns = [
views.ScrobbleDetailView.as_view(), views.ScrobbleDetailView.as_view(),
name="detail", name="detail",
), ),
path(
"scrobbles/<slug:uuid>/add-to-mopidy-queue/",
views.add_to_mopidy_queue,
name="add-to-mopidy-queue",
),
path("scrobbles/<slug:uuid>/start/", views.scrobble_start, name="start"), path("scrobbles/<slug:uuid>/start/", views.scrobble_start, name="start"),
path("scrobbles/<slug:uuid>/finish/", views.scrobble_finish, name="finish"), path("scrobbles/<slug:uuid>/finish/", views.scrobble_finish, name="finish"),
path("scrobbles/<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"), path("scrobbles/<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"),

View File

@ -4,6 +4,8 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from uuid import uuid4 from uuid import uuid4
import requests
import pendulum import pendulum
import pytz import pytz
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@ -30,10 +32,11 @@ from django.http import (
HttpResponseRedirect, HttpResponseRedirect,
JsonResponse, JsonResponse,
) )
from django.shortcuts import redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt 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 import DetailView, FormView, TemplateView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
@ -963,6 +966,65 @@ def scrobble_cancel(request, uuid):
return HttpResponseRedirect(request.META.get("HTTP_REFERER")) 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"]) @api_view(["GET"])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def export(request): def export(request):

View File

@ -35,11 +35,18 @@
<h1> <h1>
{% 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_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 %}<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}</a>{% endif %}</h1> {% if object.media_obj.get_absolute_url %}<a href="{{ object.media_obj.get_absolute_url }}">{% endif %}{{ object.media_obj }}{% if object.media_obj.get_absolute_url %}</a>{% endif %}
</h1>
{% if object.media_type == "Task" and object.logdata.title %} {% if object.media_type == "Task" and object.logdata.title %}
<h2>{{ object.logdata.title }}</h2> <h2>{{ object.logdata.title }}</h2>
{% endif %} {% endif %}
<h3 class="text-muted">{{ object.local_timestamp }}</h3> <h3 class="text-muted">{{ object.local_timestamp }}</h3>
{% if object.media_type == "Track" and object.log.raw_data.mopidy_uri and user.profile.mopidy_api_url %}
<form method="post" action="{% url 'scrobbles:add-to-mopidy-queue' object.uuid %}" class="mb-1">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button>
</form>
{% endif %}
{% if object.media_type == "Track" %} {% if object.media_type == "Track" %}
<p class="text-muted small">Source: {{ object.source }}{% if object.log.mopidy_source %} ({{ object.log.mopidy_source|capfirst }}){% endif %}</p> <p class="text-muted small">Source: {{ object.source }}{% if object.log.mopidy_source %} ({{ object.log.mopidy_source|capfirst }}){% endif %}</p>
{% endif %} {% endif %}