sessions / c6b1e42d7af76a5e2c7a37a137f2cc252198b757

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

ignore whitespace

commit c6b1e42d7af76a5e2c7a37a137f2cc252198b757Author: Colin Powell <colin@unbl.ink>Date:   Thu Jun 18 11:25:57 2026 -0400    [scrobbles] Use IDs not UUIDs in URLsdiff --git a/PROJECT.org b/PROJECT.orgindex cb335ae..c00b881 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [2/22] :vrobbler:project:personal:+* Backlog [3/23] :vrobbler:project:personal: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles: :PROPERTIES: :ID:       702462cf-d54b-48c6-8a7c-78b8de751deb@@ -590,6 +590,10 @@ We should rename `email_scrobble_board_game` to reflect the fact that it's just a helper method to create board game scrobbles given a json blob. It's independent of the email flow it was originally creatdd for +** DONE [#B] Use pk ID for scrobble detail view, not uuid :scrobbles:+:PROPERTIES:+:ID:       9cc3b285-e478-041e-394b-3d550aefbe1d+:END: ** DONE [#B] Display videogame screenshots on scrobble detail if they exist :videogames:templates: :PROPERTIES: :ID:       0406d082-20f6-0d12-76e2-f281c4801468diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.pyindex be26a75..69299f9 100644--- a/tests/scrobbles_tests/test_views.py+++ b/tests/scrobbles_tests/test_views.py@@ -519,7 +519,7 @@ def test_scrobble_detail_view_with_notes_as_flat_list(client):             "description": "Test description",         },     )-    url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})     response = client.get(url)     assert response.status_code == 200     assert "First note" in response.content.decode()@@ -545,7 +545,7 @@ def test_scrobble_detail_view_with_notes_as_dict_timestamps(client):             "description": "Test description",         },     )-    url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})     response = client.get(url)     assert response.status_code == 200     content = response.content.decode()@@ -574,7 +574,7 @@ def test_scrobble_detail_view_with_notes_and_labels(client):             "description": "Test description",         },     )-    url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})     response = client.get(url)     assert response.status_code == 200     content = response.content.decode()@@ -597,7 +597,7 @@ def test_scrobble_detail_view_post_updates_log(client):             "description": "Original description",         },     )-    url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})      client.force_login(user)     response = client.post(@@ -896,7 +896,7 @@ def test_change_visibility_owner_can_change(client):     )      client.force_login(user)-    url = reverse("scrobbles:change-visibility", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})     response = client.post(url, {"visibility": "shared"})     assert response.status_code == 302 @@ -919,7 +919,7 @@ def test_change_visibility_non_owner_gets_404(client):     )      client.force_login(other)-    url = reverse("scrobbles:change-visibility", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})     response = client.post(url, {"visibility": "shared"})     assert response.status_code == 404 @@ -937,7 +937,7 @@ def test_change_visibility_anonymous_redirects_to_login(client):         task=task, media_type="Task", user=user, visibility="private",         timestamp=timezone.now(),     )-    url = reverse("scrobbles:change-visibility", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:change-visibility", kwargs={"pk": scrobble.id})     response = client.post(url, {"visibility": "shared"})     assert response.status_code == 302     assert "/login/" in response.url@@ -956,7 +956,7 @@ def test_regenerate_share_token_invalidates_old_sqid(client):     old_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)      client.force_login(user)-    url = reverse("scrobbles:regenerate-share-token", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:regenerate-share-token", kwargs={"pk": scrobble.id})     response = client.post(url)     assert response.status_code == 302 @@ -985,7 +985,7 @@ def test_share_analytics_owner_can_view(client):     )      client.force_login(user)-    url = reverse("scrobbles:share-analytics", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})     response = client.get(url)     assert response.status_code == 200 @@ -1005,7 +1005,7 @@ def test_share_analytics_non_owner_gets_404(client):     )      client.force_login(other)-    url = reverse("scrobbles:share-analytics", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})     response = client.get(url)     assert response.status_code == 404 @@ -1027,7 +1027,7 @@ def test_share_analytics_shows_view_logs(client):     client.get(share_url)      client.force_login(user)-    url = reverse("scrobbles:share-analytics", kwargs={"uuid": scrobble.uuid})+    url = reverse("scrobbles:share-analytics", kwargs={"pk": scrobble.id})     response = client.get(url)     assert response.status_code == 200     content = response.content.decode()diff --git a/vrobbler/apps/books/models.py b/vrobbler/apps/books/models.pyindex 943659b..f2f13ed 100644--- a/vrobbler/apps/books/models.py+++ b/vrobbler/apps/books/models.py@@ -225,7 +225,7 @@ class Book(LongPlayScrobblableMixin):      @property     def resume_start_url(self):-        return reverse("scrobbles:start", kwargs={"uuid": self.uuid}) + "?resume=1"+        return reverse("scrobbles:start", kwargs={"media_uuid": self.uuid}) + "?resume=1"      @classmethod     def get_from_comicvine(diff --git a/vrobbler/apps/scrobbles/mixins.py b/vrobbler/apps/scrobbles/mixins.pyindex fbb909d..42afd87 100644--- a/vrobbler/apps/scrobbles/mixins.py+++ b/vrobbler/apps/scrobbles/mixins.py@@ -114,7 +114,7 @@ class ScrobblableMixin(TimeStampedModel):      @property     def start_url(self):-        return reverse("scrobbles:start", kwargs={"uuid": self.uuid})+        return reverse("scrobbles:start", kwargs={"media_uuid": self.uuid})      @property     def strings(self) -> ScrobblableConstants:@@ -162,7 +162,7 @@ class LongPlayScrobblableMixin(ScrobblableMixin):         return False      def get_longplay_finish_url(self):-        return reverse("scrobbles:longplay-finish", kwargs={"uuid": self.uuid})+        return reverse("scrobbles:longplay-finish", kwargs={"media_uuid": self.uuid})      def first_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]:         last = self.last_long_play_scrobble_for_user(user)diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.pyindex fcad691..1fe9989 100644--- a/vrobbler/apps/scrobbles/models.py+++ b/vrobbler/apps/scrobbles/models.py@@ -906,7 +906,7 @@ class Scrobble(TimeStampedModel):      @property     def finish_url(self) -> str:-        return reverse("scrobbles:finish", kwargs={"uuid": self.uuid})+        return reverse("scrobbles:finish", kwargs={"pk": self.pk})      def save(self, *args, **kwargs):         class_name = self.media_obj.__class__.__name__@@ -948,10 +948,7 @@ class Scrobble(TimeStampedModel):         return super(Scrobble, self).save(*args, **kwargs)      def get_absolute_url(self):-        if not self.uuid:-            self.uuid = uuid4()-            self.save(update_fields=["uuid"])-        return reverse("scrobbles:detail", kwargs={"uuid": self.uuid})+        return reverse("scrobbles:detail", kwargs={"pk": self.pk})      def get_share_url(self):         if self.visibility == Visibility.PRIVATE:diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.pyindex 6bc7ee3..b36ce73 100644--- a/vrobbler/apps/scrobbles/urls.py+++ b/vrobbler/apps/scrobbles/urls.py@@ -44,7 +44,7 @@ urlpatterns = [         name="lookup-manual-scrobble",     ),     path(-        "long-play-finish/<slug:uuid>/",+        "long-play-finish/<slug:media_uuid>/",         views.scrobble_longplay_finish,         name="longplay-finish",     ),@@ -160,38 +160,38 @@ urlpatterns = [         name="shared-detail",     ),     path(-        "scrobbles/<slug:uuid>/",+        "scrobbles/<int:pk>/",         views.ScrobbleDetailView.as_view(),         name="detail",     ),     path(-        "scrobbles/<slug:uuid>/regenerate-share-token/",+        "scrobbles/<int:pk>/regenerate-share-token/",         views.RegenerateShareTokenView.as_view(),         name="regenerate-share-token",     ),     path(-        "scrobbles/<slug:uuid>/change-visibility/",+        "scrobbles/<int:pk>/change-visibility/",         views.ChangeVisibilityView.as_view(),         name="change-visibility",     ),     path(-        "scrobbles/<slug:uuid>/share-analytics/",+        "scrobbles/<int:pk>/share-analytics/",         views.ScrobbleShareAnalyticsView.as_view(),         name="share-analytics",     ),     path(-        "scrobbles/<slug:uuid>/add-to-mopidy-queue/",+        "scrobbles/<int:pk>/add-to-mopidy-queue/",         views.add_to_mopidy_queue,         name="add-to-mopidy-queue",     ),     path(-        "scrobbles/<slug:uuid>/add-to-mopidy-monthly-playlist/",+        "scrobbles/<int:pk>/add-to-mopidy-monthly-playlist/",         views.add_to_mopidy_monthly_playlist,         name="add-to-mopidy-monthly-playlist",     ),-    path("scrobbles/<slug:uuid>/start/", views.scrobble_start, name="start"),-    path("scrobbles/<slug:uuid>/finish/", views.scrobble_finish, name="finish"),-    path("scrobbles/<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"),+    path("scrobbles/<slug:media_uuid>/start/", views.scrobble_start, name="start"),+    path("scrobbles/<int:pk>/finish/", views.scrobble_finish, name="finish"),+    path("scrobbles/<int:pk>/cancel/", views.scrobble_cancel, name="cancel"),     path(         "favorite/<str:media_type>/<int:object_id>/toggle/",         views.toggle_favorite,diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.pyindex 92fb3bc..394b1b6 100644--- a/vrobbler/apps/scrobbles/views.py+++ b/vrobbler/apps/scrobbles/views.py@@ -839,10 +839,10 @@ def import_audioscrobbler_file(request):  @api_view(["GET"]) @permission_classes([IsAuthenticated])-def scrobble_start(request, uuid):+def scrobble_start(request, media_uuid):     logger.info(         "[scrobble_start] called",-        extra={"request": request, "uuid": uuid},+        extra={"request": request, "media_uuid": media_uuid},     )     user = request.user     success_url = request.META.get("HTTP_REFERER")@@ -853,14 +853,14 @@ def scrobble_start(request, uuid):     media_obj = None     for app, model in PLAY_AGAIN_MEDIA.items():         media_model = apps.get_model(app_label=app, model_name=model)-        media_obj = media_model.objects.filter(uuid=uuid).first()+        media_obj = media_model.objects.filter(uuid=media_uuid).first()         if media_obj:             break      if not media_obj:         logger.info(             "[scrobble_start] media object not found",-            extra={"uuid": uuid, "user_id": user.id},+            extra={"media_uuid": media_uuid, "user_id": user.id},         )         raise Exception("No media object provided to scrobble") @@ -897,7 +897,7 @@ def scrobble_start(request, uuid):         )     else:         messages.add_message(-            request, messages.ERROR, f"Media with uuid {uuid} not found."+            request, messages.ERROR, f"Media with uuid {media_uuid} not found."         )      if (@@ -915,7 +915,7 @@ def scrobble_start(request, uuid):   @api_view(["GET"])-def scrobble_longplay_finish(request, uuid):+def scrobble_longplay_finish(request, media_uuid):     user = request.user     success_url = request.META.get("HTTP_REFERER") @@ -923,7 +923,7 @@ def scrobble_longplay_finish(request, uuid):         return HttpResponseRedirect(success_url)      # Try scrobble UUID first-    scrobble = Scrobble.objects.filter(uuid=uuid, user=user).first()+    scrobble = Scrobble.objects.filter(uuid=media_uuid, user=user).first()     if scrobble:         if scrobble.long_play_complete == True:             scrobble.long_play_complete = None@@ -947,13 +947,13 @@ def scrobble_longplay_finish(request, uuid):     media_obj = None     for app, model in LONG_PLAY_MEDIA.items():         media_model = apps.get_model(app_label=app, model_name=model)-        media_obj = media_model.objects.filter(uuid=uuid).first()+        media_obj = media_model.objects.filter(uuid=media_uuid).first()         if media_obj:             break      if not media_obj:         messages.add_message(-            request, messages.ERROR, f"Media with uuid {uuid} not found."+            request, messages.ERROR, f"Media with uuid {media_uuid} not found."         )         return HttpResponseRedirect(success_url) @@ -976,14 +976,14 @@ def scrobble_longplay_finish(request, uuid):         )     else:         messages.add_message(-            request, messages.ERROR, f"Media with uuid {uuid} not found."+            request, messages.ERROR, f"Media with uuid {media_uuid} not found."         )     return HttpResponseRedirect(success_url)   @api_view(["GET"]) @permission_classes([IsAuthenticated])-def scrobble_finish(request, uuid):+def scrobble_finish(request, pk):     user = request.user     success_url = request.META.get("HTTP_REFERER")     if not success_url:@@ -992,7 +992,7 @@ def scrobble_finish(request, uuid):     if not user.is_authenticated:         return HttpResponseRedirect(success_url) -    scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()+    scrobble = Scrobble.objects.filter(user=user, pk=pk).first()     if scrobble:         scrobble.stop(force_finish=True)         messages.add_message(@@ -1007,14 +1007,14 @@ def scrobble_finish(request, uuid):  @api_view(["GET"]) @permission_classes([IsAuthenticated])-def scrobble_cancel(request, uuid):+def scrobble_cancel(request, pk):     user = request.user     success_url = reverse_lazy("vrobbler-home")      if not user.is_authenticated:         return HttpResponseRedirect(success_url) -    scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first()+    scrobble = Scrobble.objects.filter(user=user, pk=pk).first()     if scrobble:         scrobble.cancel()         messages.add_message(@@ -1028,11 +1028,11 @@ def scrobble_cancel(request, uuid):   @require_POST-def add_to_mopidy_queue(request, uuid):+def add_to_mopidy_queue(request, pk):     if not request.user.is_authenticated:-        return redirect("scrobbles:detail", uuid=uuid)+        return redirect("scrobbles:detail", pk=pk) -    scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user)+    scrobble = get_object_or_404(Scrobble, pk=pk, user=request.user)     mopidy_url = request.user.profile.mopidy_api_url      if not mopidy_url:@@ -1041,22 +1041,22 @@ def add_to_mopidy_queue(request, uuid):             messages.ERROR,             "Mopidy API URL not configured in your profile settings.",         )-        return redirect("scrobbles:detail", uuid=uuid)+        return redirect("scrobbles:detail", pk=pk)      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)+    return redirect("scrobbles:detail", pk=pk)   @require_POST-def add_to_mopidy_monthly_playlist(request, uuid):+def add_to_mopidy_monthly_playlist(request, pk):     if not request.user.is_authenticated:-        return redirect("scrobbles:detail", uuid=uuid)+        return redirect("scrobbles:detail", pk=pk) -    scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user)+    scrobble = get_object_or_404(Scrobble, pk=pk, user=request.user)     profile = request.user.profile     pattern = profile.monthly_mopidy_playlist_pattern @@ -1066,7 +1066,7 @@ def add_to_mopidy_monthly_playlist(request, uuid):             messages.ERROR,             "Monthly playlist pattern or Mopidy API URL not configured in your profile.",         )-        return redirect("scrobbles:detail", uuid=uuid)+        return redirect("scrobbles:detail", pk=pk)      now = now_user_timezone(profile)     playlist_name = DateFormat(now).format(pattern)@@ -1079,7 +1079,7 @@ def add_to_mopidy_monthly_playlist(request, uuid):         messages.SUCCESS,         f'Adding "{scrobble.media_obj}" to monthly playlist "{playlist_name}".',     )-    return redirect("scrobbles:detail", uuid=uuid)+    return redirect("scrobbles:detail", pk=pk)   @require_POST@@ -1184,8 +1184,6 @@ class ScrobbleStatusView(LoginRequiredMixin, TemplateView):  class ScrobbleDetailView(DetailView):     model = Scrobble-    slug_field = "uuid"-    slug_url_kwarg = "uuid"     paginate_by = 100      def get_object(self, queryset=None):@@ -1385,15 +1383,15 @@ class ScrobbleExploreView(ListView):   class RegenerateShareTokenView(LoginRequiredMixin, View):-    def post(self, request, uuid):-        scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user)+    def post(self, request, pk):+        scrobble = get_object_or_404(Scrobble, pk=pk, user=request.user)         scrobble.regenerate_share_token()         return redirect(scrobble.get_absolute_url())   class ChangeVisibilityView(LoginRequiredMixin, View):-    def post(self, request, uuid):-        scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user)+    def post(self, request, pk):+        scrobble = get_object_or_404(Scrobble, pk=pk, user=request.user)         visibility = request.POST.get("visibility")         if visibility not in (Visibility.PUBLIC, Visibility.SHARED, Visibility.PRIVATE):             return redirect(scrobble.get_absolute_url())@@ -1404,8 +1402,6 @@ class ChangeVisibilityView(LoginRequiredMixin, View):  class ScrobbleShareAnalyticsView(LoginRequiredMixin, DetailView):     model = Scrobble-    slug_field = "uuid"-    slug_url_kwarg = "uuid"     template_name = "scrobbles/scrobble_share_analytics.html"      def get_queryset(self):diff --git a/vrobbler/templates/base.html b/vrobbler/templates/base.htmlindex 0d7a622..af964f5 100644--- a/vrobbler/templates/base.html+++ b/vrobbler/templates/base.html@@ -322,8 +322,8 @@                                     <span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>                                 </div>                                 <p class="action-buttons">-                                    <a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a>-                                    <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>+                                    <a href="{% url "scrobbles:cancel" scrobble.id %}">Cancel</a>+                                    <a class="right" href="{% url "scrobbles:finish" scrobble.id %}">Finish</a>                                 </p>                                 {% if not forloop.last %}<hr/>{% endif %}                             </div>diff --git a/vrobbler/templates/books/book_detail.html b/vrobbler/templates/books/book_detail.htmlindex 1dc5dd7..39b402b 100644--- a/vrobbler/templates/books/book_detail.html+++ b/vrobbler/templates/books/book_detail.html@@ -65,7 +65,7 @@                     {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}                     <tr>                         <td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td>-                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">No</a>{% endif %}</td>+                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">No</a>{% endif %}</td>                         <td>{% if scrobble.in_progress %}Now reading{% else %}{{scrobble.session_pages_read}}{% endif %}</td>                         <td>{% for author in scrobble.book.authors.all %}<a href="{{author.get_absolute_url}}">{{author}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>                     </tr>diff --git a/vrobbler/templates/bricksets/brickset_detail.html b/vrobbler/templates/bricksets/brickset_detail.htmlindex 814fe67..f463439 100644--- a/vrobbler/templates/bricksets/brickset_detail.html+++ b/vrobbler/templates/bricksets/brickset_detail.html@@ -48,7 +48,7 @@                     {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}                     <tr>                         <td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td>-                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">No</a>{% endif %}</td>+                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">No</a>{% endif %}</td>                         <td>{% if scrobble.in_progress %}Now reading{% else %}{{scrobble.session_pages_read}}{% endif %}</td>                     </tr>                     {% endfor %}diff --git a/vrobbler/templates/scrobbles/_row.html b/vrobbler/templates/scrobbles/_row.htmlindex 7d6208d..20bbe57 100644--- a/vrobbler/templates/scrobbles/_row.html+++ b/vrobbler/templates/scrobbles/_row.html@@ -1,7 +1,7 @@ {% load humanize %} {% load naturalduration %} <tr {% if scrobble.in_progress %}class="in-progress"{% endif %}>-    <td>{% if scrobble.in_progress %}<a href="{{scrobble.get_absolute_url}}">{{scrobble.media_obj.strings.verb}} now</a> | <a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a>{% else %}<a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp|naturaltime}}</a>{% endif %}</td>+    <td>{% if scrobble.in_progress %}<a href="{{scrobble.get_absolute_url}}">{{scrobble.media_obj.strings.verb}} now</a> | <a class="right" href="{% url "scrobbles:finish" scrobble.id %}">Finish</a>{% else %}<a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp|naturaltime}}</a>{% endif %}</td>     <td>         {% if scrobble.media_type in "Task" %}         <p><em><a href="{{scrobble.media_obj.get_absolute_url}}">{{scrobble.media_obj.title|truncatechars_html:45}} - {% if scrobble.logdata %}{% if scrobble.logdata.title %}{{scrobble.logdata.title}}{% endif %}{% endif %}</a></em></p>diff --git a/vrobbler/templates/scrobbles/calendar.html b/vrobbler/templates/scrobbles/calendar.htmlindex 4580651..6d4b567 100644--- a/vrobbler/templates/scrobbles/calendar.html+++ b/vrobbler/templates/scrobbles/calendar.html@@ -205,7 +205,7 @@ header.navbar { display: none !important; }              style="background:{% if not cd.is_today %}{{ cd.color }}{% endif %};">             <div class="day-number"><a href="{% url 'vrobbler-home' %}?date={{ year }}-{{ month|stringformat:'02d' }}-{{ cd.day|stringformat:'02d' }}" style="color:inherit;text-decoration:none;">{{ cd.day }}</a>{% if cd.total_count > 0 %} <span style="font-weight:400;color:#999;font-size:0.75rem;">{{ cd.total_count }}</span>{% endif %}</div>             {% for s in cd.scrobbles %}-            <a href="{% url 'scrobbles:detail' uuid=s.uuid %}"+            <a href="{% url 'scrobbles:detail' pk=s.id %}"                class="event-card"                title="{{ s.title }} — {{ s.media_type }}">{{ s.emoji }} {{ s.title }}</a>             {% endfor %}diff --git a/vrobbler/templates/scrobbles/scrobble_detail.html b/vrobbler/templates/scrobbles/scrobble_detail.htmlindex 2467a7d..b69b8af 100644--- a/vrobbler/templates/scrobbles/scrobble_detail.html+++ b/vrobbler/templates/scrobbles/scrobble_detail.html@@ -88,7 +88,7 @@         {% endif %}">         {{ object.get_visibility_display }}     </span>-    <form method="post" action="{% url 'scrobbles:change-visibility' object.uuid %}" class="d-inline-flex align-items-center gap-1">+    <form method="post" action="{% url 'scrobbles:change-visibility' object.id %}" class="d-inline-flex align-items-center gap-1">         {% csrf_token %}         <select name="visibility" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()">             <option value="private" {% if object.visibility == 'private' %}selected{% endif %}>Private</option>@@ -100,25 +100,25 @@     <span class="small text-muted">Share link:</span>     <code class="small" id="share-link">{{ request.scheme }}://{{ request.get_host }}{{ object.get_share_url }}</code>     <button class="btn btn-sm btn-outline-secondary" onclick="navigator.clipboard.writeText(document.getElementById('share-link').textContent.trim())">Copy</button>-    <form method="post" action="{% url 'scrobbles:regenerate-share-token' object.uuid %}" class="d-inline">+    <form method="post" action="{% url 'scrobbles:regenerate-share-token' object.id %}" class="d-inline">         {% csrf_token %}         <button type="submit" class="btn btn-sm btn-outline-warning">Regenerate</button>     </form>     {% if object.share_view_count %}     <span class="text-muted small ms-2">{{ object.share_view_count }} view{{ object.share_view_count|pluralize }}</span>     {% endif %}-    <a href="{% url 'scrobbles:share-analytics' object.uuid %}" class="btn btn-sm btn-outline-info ms-2">Analytics</a>+    <a href="{% url 'scrobbles:share-analytics' object.id %}" class="btn btn-sm btn-outline-info ms-2">Analytics</a>     {% endif %} </div> {% endif %}  {% if object.media_type == "Track" and has_mopidy_uri and user.profile.mopidy_api_url %}-<form method="post" action="{% url 'scrobbles:add-to-mopidy-queue' object.uuid %}" class="mb-1">+<form method="post" action="{% url 'scrobbles:add-to-mopidy-queue' object.id %}" class="mb-1">   {% csrf_token %}     <button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button> </form> {% if user.profile.monthly_mopidy_playlist_pattern %}-<form method="post" action="{% url 'scrobbles:add-to-mopidy-monthly-playlist' object.uuid %}" class="mb-1">+<form method="post" action="{% url 'scrobbles:add-to-mopidy-monthly-playlist' object.id %}" class="mb-1">   {% csrf_token %}     <button type="submit" class="btn btn-sm btn-outline-secondary">add to monthly playlist</button> </form>@@ -288,7 +288,7 @@     <tbody>         {% for scrobble in related_scrobbles %}         <tr{% if scrobble.id == object.id %} class="table-active fw-bold"{% endif %}>-            <td>{% if scrobble.id == object.id %}{{ scrobble.timestamp|date:"M d, Y" }}{% else %}<a href="{% url 'scrobbles:detail' scrobble.uuid %}">{{ scrobble.timestamp|date:"M d, Y" }}</a>{% endif %}</td>+            <td>{% if scrobble.id == object.id %}{{ scrobble.timestamp|date:"M d, Y" }}{% else %}<a href="{% url 'scrobbles:detail' scrobble.id %}">{{ scrobble.timestamp|date:"M d, Y" }}</a>{% endif %}</td>             <td>                 {% if scrobble.media_type == "Task" and scrobble.logdata.title %}{{ scrobble.media_obj.title }}: {{ scrobble.logdata.title }}{% else %}{{ scrobble.media_obj.title|default:scrobble.media_obj }}{% endif %}             </td>diff --git a/vrobbler/templates/scrobbles/scrobble_search.html b/vrobbler/templates/scrobbles/scrobble_search.htmlindex e6c807d..484c140 100644--- a/vrobbler/templates/scrobbles/scrobble_search.html+++ b/vrobbler/templates/scrobbles/scrobble_search.html@@ -103,7 +103,7 @@         {% for scrobble in scrobbles %}         <div class="result-item">             <div class="result-title">-                <a href="{% url 'scrobbles:detail' scrobble.uuid %}">+                <a href="{% url 'scrobbles:detail' scrobble.id %}">                     {{ scrobble|truncatechars:100 }}                 </a>             </div>diff --git a/vrobbler/templates/tasks/task_detail.html b/vrobbler/templates/tasks/task_detail.htmlindex f8cdc4a..d7ce7fa 100644--- a/vrobbler/templates/tasks/task_detail.html+++ b/vrobbler/templates/tasks/task_detail.html@@ -81,7 +81,7 @@                         <td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.title}}</a></td>                         <td>{{scrobble.logdata.notes_as_str}}</td>                         <td>{{scrobble.source}}</td>-                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">No</a>{% endif %}</td>+                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">No</a>{% endif %}</td>                     </tr>                     {% endfor %}                 </tbody>diff --git a/vrobbler/templates/videogames/videogame_detail.html b/vrobbler/templates/videogames/videogame_detail.htmlindex c635899..0bd7211 100644--- a/vrobbler/templates/videogames/videogame_detail.html+++ b/vrobbler/templates/videogames/videogame_detail.html@@ -87,7 +87,7 @@                     <tr>                          <td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td>-                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' uuid=scrobble.uuid %}">Not yet</a>{% endif %}</td>+                        <td>{% if scrobble.long_play_complete == True %}Yes <small><a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">(not complete?)</a></small>{% else %}<a href="{% url 'scrobbles:longplay-finish' media_uuid=scrobble.uuid %}">Not yet</a>{% endif %}</td>                         <td>{% if scrobble.in_progress %}Now playing{% else %}{{scrobble.playback_position_seconds|natural_duration}}{% endif %}</td>                         <td>{% for platform in scrobble.video_game.platforms.all %}<a href="{{platform.get_absolute_url}}">{{platform}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>                         <td>{% if scrobble.videogame_save_data %}<a href="{{scrobble.videogame_save_data.url}}">Save data</a>{% else %}Not yet{% endif %}</td>