This commit has no recorded session.
diff
commit c6b1e42d7af76a5e2c7a37a137f2cc252198b757Author: Colin Powell <colin@unbl.ink>Date: Thu Jun 18 11:25:57 2026 -0400 [scrobbles] Use IDs not UUIDs in URLs@@ -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-f281c4801468@@ -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()@@ -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(@@ -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)@@ -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:@@ -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,@@ -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):@@ -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>@@ -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>@@ -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 %}@@ -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>@@ -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 %}@@ -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>@@ -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>@@ -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>@@ -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>