[scrobbles] Use IDs not UUIDs in URLs
All checks were successful
build / test (push) Successful in 1m59s

This commit is contained in:
2026-06-18 11:25:57 -04:00
parent fcf86d5b3f
commit c6b1e42d7a
16 changed files with 74 additions and 77 deletions

View File

@ -88,7 +88,7 @@ fetching and simple saving.
*** Metadata sources *** Metadata sources
**** Scraper **** 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: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
:PROPERTIES: :PROPERTIES:
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb :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 a helper method to create board game scrobbles given a json blob. It's
independent of the email flow it was originally creatdd for 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: ** DONE [#B] Display videogame screenshots on scrobble detail if they exist :videogames:templates:
:PROPERTIES: :PROPERTIES:
:ID: 0406d082-20f6-0d12-76e2-f281c4801468 :ID: 0406d082-20f6-0d12-76e2-f281c4801468

View File

@ -519,7 +519,7 @@ def test_scrobble_detail_view_with_notes_as_flat_list(client):
"description": "Test description", "description": "Test description",
}, },
) )
url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid}) url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
assert "First note" in response.content.decode() 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", "description": "Test description",
}, },
) )
url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid}) url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
content = response.content.decode() content = response.content.decode()
@ -574,7 +574,7 @@ def test_scrobble_detail_view_with_notes_and_labels(client):
"description": "Test description", "description": "Test description",
}, },
) )
url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid}) url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
content = response.content.decode() content = response.content.decode()
@ -597,7 +597,7 @@ def test_scrobble_detail_view_post_updates_log(client):
"description": "Original description", "description": "Original description",
}, },
) )
url = reverse("scrobbles:detail", kwargs={"uuid": scrobble.uuid}) url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id})
client.force_login(user) client.force_login(user)
response = client.post( response = client.post(
@ -896,7 +896,7 @@ def test_change_visibility_owner_can_change(client):
) )
client.force_login(user) 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"}) response = client.post(url, {"visibility": "shared"})
assert response.status_code == 302 assert response.status_code == 302
@ -919,7 +919,7 @@ def test_change_visibility_non_owner_gets_404(client):
) )
client.force_login(other) 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"}) response = client.post(url, {"visibility": "shared"})
assert response.status_code == 404 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", task=task, media_type="Task", user=user, visibility="private",
timestamp=timezone.now(), 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"}) response = client.post(url, {"visibility": "shared"})
assert response.status_code == 302 assert response.status_code == 302
assert "/login/" in response.url 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) old_sqid = encode_scrobble_share(scrobble.id, scrobble.share_token_version)
client.force_login(user) 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) response = client.post(url)
assert response.status_code == 302 assert response.status_code == 302
@ -985,7 +985,7 @@ def test_share_analytics_owner_can_view(client):
) )
client.force_login(user) 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) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
@ -1005,7 +1005,7 @@ def test_share_analytics_non_owner_gets_404(client):
) )
client.force_login(other) 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) response = client.get(url)
assert response.status_code == 404 assert response.status_code == 404
@ -1027,7 +1027,7 @@ def test_share_analytics_shows_view_logs(client):
client.get(share_url) client.get(share_url)
client.force_login(user) 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) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
content = response.content.decode() content = response.content.decode()

View File

@ -225,7 +225,7 @@ class Book(LongPlayScrobblableMixin):
@property @property
def resume_start_url(self): 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 @classmethod
def get_from_comicvine( def get_from_comicvine(

View File

@ -114,7 +114,7 @@ class ScrobblableMixin(TimeStampedModel):
@property @property
def start_url(self): def start_url(self):
return reverse("scrobbles:start", kwargs={"uuid": self.uuid}) return reverse("scrobbles:start", kwargs={"media_uuid": self.uuid})
@property @property
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:
@ -162,7 +162,7 @@ class LongPlayScrobblableMixin(ScrobblableMixin):
return False return False
def get_longplay_finish_url(self): 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"]: def first_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]:
last = self.last_long_play_scrobble_for_user(user) last = self.last_long_play_scrobble_for_user(user)

View File

@ -906,7 +906,7 @@ class Scrobble(TimeStampedModel):
@property @property
def finish_url(self) -> str: 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): def save(self, *args, **kwargs):
class_name = self.media_obj.__class__.__name__ class_name = self.media_obj.__class__.__name__
@ -948,10 +948,7 @@ class Scrobble(TimeStampedModel):
return super(Scrobble, self).save(*args, **kwargs) return super(Scrobble, self).save(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):
if not self.uuid: return reverse("scrobbles:detail", kwargs={"pk": self.pk})
self.uuid = uuid4()
self.save(update_fields=["uuid"])
return reverse("scrobbles:detail", kwargs={"uuid": self.uuid})
def get_share_url(self): def get_share_url(self):
if self.visibility == Visibility.PRIVATE: if self.visibility == Visibility.PRIVATE:

View File

@ -44,7 +44,7 @@ urlpatterns = [
name="lookup-manual-scrobble", name="lookup-manual-scrobble",
), ),
path( path(
"long-play-finish/<slug:uuid>/", "long-play-finish/<slug:media_uuid>/",
views.scrobble_longplay_finish, views.scrobble_longplay_finish,
name="longplay-finish", name="longplay-finish",
), ),
@ -160,38 +160,38 @@ urlpatterns = [
name="shared-detail", name="shared-detail",
), ),
path( path(
"scrobbles/<slug:uuid>/", "scrobbles/<int:pk>/",
views.ScrobbleDetailView.as_view(), views.ScrobbleDetailView.as_view(),
name="detail", name="detail",
), ),
path( path(
"scrobbles/<slug:uuid>/regenerate-share-token/", "scrobbles/<int:pk>/regenerate-share-token/",
views.RegenerateShareTokenView.as_view(), views.RegenerateShareTokenView.as_view(),
name="regenerate-share-token", name="regenerate-share-token",
), ),
path( path(
"scrobbles/<slug:uuid>/change-visibility/", "scrobbles/<int:pk>/change-visibility/",
views.ChangeVisibilityView.as_view(), views.ChangeVisibilityView.as_view(),
name="change-visibility", name="change-visibility",
), ),
path( path(
"scrobbles/<slug:uuid>/share-analytics/", "scrobbles/<int:pk>/share-analytics/",
views.ScrobbleShareAnalyticsView.as_view(), views.ScrobbleShareAnalyticsView.as_view(),
name="share-analytics", name="share-analytics",
), ),
path( path(
"scrobbles/<slug:uuid>/add-to-mopidy-queue/", "scrobbles/<int:pk>/add-to-mopidy-queue/",
views.add_to_mopidy_queue, views.add_to_mopidy_queue,
name="add-to-mopidy-queue", name="add-to-mopidy-queue",
), ),
path( path(
"scrobbles/<slug:uuid>/add-to-mopidy-monthly-playlist/", "scrobbles/<int:pk>/add-to-mopidy-monthly-playlist/",
views.add_to_mopidy_monthly_playlist, views.add_to_mopidy_monthly_playlist,
name="add-to-mopidy-monthly-playlist", name="add-to-mopidy-monthly-playlist",
), ),
path("scrobbles/<slug:uuid>/start/", views.scrobble_start, name="start"), path("scrobbles/<slug:media_uuid>/start/", views.scrobble_start, name="start"),
path("scrobbles/<slug:uuid>/finish/", views.scrobble_finish, name="finish"), path("scrobbles/<int:pk>/finish/", views.scrobble_finish, name="finish"),
path("scrobbles/<slug:uuid>/cancel/", views.scrobble_cancel, name="cancel"), path("scrobbles/<int:pk>/cancel/", views.scrobble_cancel, name="cancel"),
path( path(
"favorite/<str:media_type>/<int:object_id>/toggle/", "favorite/<str:media_type>/<int:object_id>/toggle/",
views.toggle_favorite, views.toggle_favorite,

View File

@ -839,10 +839,10 @@ def import_audioscrobbler_file(request):
@api_view(["GET"]) @api_view(["GET"])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def scrobble_start(request, uuid): def scrobble_start(request, media_uuid):
logger.info( logger.info(
"[scrobble_start] called", "[scrobble_start] called",
extra={"request": request, "uuid": uuid}, extra={"request": request, "media_uuid": media_uuid},
) )
user = request.user user = request.user
success_url = request.META.get("HTTP_REFERER") success_url = request.META.get("HTTP_REFERER")
@ -853,14 +853,14 @@ def scrobble_start(request, uuid):
media_obj = None media_obj = None
for app, model in PLAY_AGAIN_MEDIA.items(): for app, model in PLAY_AGAIN_MEDIA.items():
media_model = apps.get_model(app_label=app, model_name=model) 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: if media_obj:
break break
if not media_obj: if not media_obj:
logger.info( logger.info(
"[scrobble_start] media object not found", "[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") raise Exception("No media object provided to scrobble")
@ -897,7 +897,7 @@ def scrobble_start(request, uuid):
) )
else: else:
messages.add_message( 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 ( if (
@ -915,7 +915,7 @@ def scrobble_start(request, uuid):
@api_view(["GET"]) @api_view(["GET"])
def scrobble_longplay_finish(request, uuid): def scrobble_longplay_finish(request, media_uuid):
user = request.user user = request.user
success_url = request.META.get("HTTP_REFERER") success_url = request.META.get("HTTP_REFERER")
@ -923,7 +923,7 @@ def scrobble_longplay_finish(request, uuid):
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
# Try scrobble UUID first # 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:
if scrobble.long_play_complete == True: if scrobble.long_play_complete == True:
scrobble.long_play_complete = None scrobble.long_play_complete = None
@ -947,13 +947,13 @@ def scrobble_longplay_finish(request, uuid):
media_obj = None media_obj = None
for app, model in LONG_PLAY_MEDIA.items(): for app, model in LONG_PLAY_MEDIA.items():
media_model = apps.get_model(app_label=app, model_name=model) 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: if media_obj:
break break
if not media_obj: if not media_obj:
messages.add_message( 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) return HttpResponseRedirect(success_url)
@ -976,14 +976,14 @@ def scrobble_longplay_finish(request, uuid):
) )
else: else:
messages.add_message( 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) return HttpResponseRedirect(success_url)
@api_view(["GET"]) @api_view(["GET"])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def scrobble_finish(request, uuid): def scrobble_finish(request, pk):
user = request.user user = request.user
success_url = request.META.get("HTTP_REFERER") success_url = request.META.get("HTTP_REFERER")
if not success_url: if not success_url:
@ -992,7 +992,7 @@ def scrobble_finish(request, uuid):
if not user.is_authenticated: if not user.is_authenticated:
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first() scrobble = Scrobble.objects.filter(user=user, pk=pk).first()
if scrobble: if scrobble:
scrobble.stop(force_finish=True) scrobble.stop(force_finish=True)
messages.add_message( messages.add_message(
@ -1007,14 +1007,14 @@ def scrobble_finish(request, uuid):
@api_view(["GET"]) @api_view(["GET"])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def scrobble_cancel(request, uuid): def scrobble_cancel(request, pk):
user = request.user user = request.user
success_url = reverse_lazy("vrobbler-home") success_url = reverse_lazy("vrobbler-home")
if not user.is_authenticated: if not user.is_authenticated:
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
scrobble = Scrobble.objects.filter(user=user, uuid=uuid).first() scrobble = Scrobble.objects.filter(user=user, pk=pk).first()
if scrobble: if scrobble:
scrobble.cancel() scrobble.cancel()
messages.add_message( messages.add_message(
@ -1028,11 +1028,11 @@ def scrobble_cancel(request, uuid):
@require_POST @require_POST
def add_to_mopidy_queue(request, uuid): def add_to_mopidy_queue(request, pk):
if not request.user.is_authenticated: 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 mopidy_url = request.user.profile.mopidy_api_url
if not mopidy_url: if not mopidy_url:
@ -1041,22 +1041,22 @@ def add_to_mopidy_queue(request, uuid):
messages.ERROR, messages.ERROR,
"Mopidy API URL not configured in your profile settings.", "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 from scrobbles.tasks import add_scrobble_to_mopidy_queue as task
task.delay(scrobble.id) task.delay(scrobble.id)
msg = f'Adding "{scrobble.media_obj}" to Mopidy queue.' msg = f'Adding "{scrobble.media_obj}" to Mopidy queue.'
messages.add_message(request, messages.SUCCESS, msg) messages.add_message(request, messages.SUCCESS, msg)
return redirect("scrobbles:detail", uuid=uuid) return redirect("scrobbles:detail", pk=pk)
@require_POST @require_POST
def add_to_mopidy_monthly_playlist(request, uuid): def add_to_mopidy_monthly_playlist(request, pk):
if not request.user.is_authenticated: 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 profile = request.user.profile
pattern = profile.monthly_mopidy_playlist_pattern pattern = profile.monthly_mopidy_playlist_pattern
@ -1066,7 +1066,7 @@ def add_to_mopidy_monthly_playlist(request, uuid):
messages.ERROR, messages.ERROR,
"Monthly playlist pattern or Mopidy API URL not configured in your profile.", "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) now = now_user_timezone(profile)
playlist_name = DateFormat(now).format(pattern) playlist_name = DateFormat(now).format(pattern)
@ -1079,7 +1079,7 @@ def add_to_mopidy_monthly_playlist(request, uuid):
messages.SUCCESS, messages.SUCCESS,
f'Adding "{scrobble.media_obj}" to monthly playlist "{playlist_name}".', f'Adding "{scrobble.media_obj}" to monthly playlist "{playlist_name}".',
) )
return redirect("scrobbles:detail", uuid=uuid) return redirect("scrobbles:detail", pk=pk)
@require_POST @require_POST
@ -1184,8 +1184,6 @@ class ScrobbleStatusView(LoginRequiredMixin, TemplateView):
class ScrobbleDetailView(DetailView): class ScrobbleDetailView(DetailView):
model = Scrobble model = Scrobble
slug_field = "uuid"
slug_url_kwarg = "uuid"
paginate_by = 100 paginate_by = 100
def get_object(self, queryset=None): def get_object(self, queryset=None):
@ -1385,15 +1383,15 @@ class ScrobbleExploreView(ListView):
class RegenerateShareTokenView(LoginRequiredMixin, View): class RegenerateShareTokenView(LoginRequiredMixin, View):
def post(self, request, uuid): def post(self, request, pk):
scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user) scrobble = get_object_or_404(Scrobble, pk=pk, user=request.user)
scrobble.regenerate_share_token() scrobble.regenerate_share_token()
return redirect(scrobble.get_absolute_url()) return redirect(scrobble.get_absolute_url())
class ChangeVisibilityView(LoginRequiredMixin, View): class ChangeVisibilityView(LoginRequiredMixin, View):
def post(self, request, uuid): def post(self, request, pk):
scrobble = get_object_or_404(Scrobble, uuid=uuid, user=request.user) scrobble = get_object_or_404(Scrobble, pk=pk, user=request.user)
visibility = request.POST.get("visibility") visibility = request.POST.get("visibility")
if visibility not in (Visibility.PUBLIC, Visibility.SHARED, Visibility.PRIVATE): if visibility not in (Visibility.PUBLIC, Visibility.SHARED, Visibility.PRIVATE):
return redirect(scrobble.get_absolute_url()) return redirect(scrobble.get_absolute_url())
@ -1404,8 +1402,6 @@ class ChangeVisibilityView(LoginRequiredMixin, View):
class ScrobbleShareAnalyticsView(LoginRequiredMixin, DetailView): class ScrobbleShareAnalyticsView(LoginRequiredMixin, DetailView):
model = Scrobble model = Scrobble
slug_field = "uuid"
slug_url_kwarg = "uuid"
template_name = "scrobbles/scrobble_share_analytics.html" template_name = "scrobbles/scrobble_share_analytics.html"
def get_queryset(self): def get_queryset(self):

View File

@ -322,8 +322,8 @@
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span> <span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div> </div>
<p class="action-buttons"> <p class="action-buttons">
<a href="{% url "scrobbles:cancel" scrobble.uuid %}">Cancel</a> <a href="{% url "scrobbles:cancel" scrobble.id %}">Cancel</a>
<a class="right" href="{% url "scrobbles:finish" scrobble.uuid %}">Finish</a> <a class="right" href="{% url "scrobbles:finish" scrobble.id %}">Finish</a>
</p> </p>
{% if not forloop.last %}<hr/>{% endif %} {% if not forloop.last %}<hr/>{% endif %}
</div> </div>

View File

@ -65,7 +65,7 @@
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td> <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>{% 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> <td>{% for author in scrobble.book.authors.all %}<a href="{{author.get_absolute_url}}">{{author}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
</tr> </tr>

View File

@ -48,7 +48,7 @@
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %} {% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr> <tr>
<td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td> <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>{% if scrobble.in_progress %}Now reading{% else %}{{scrobble.session_pages_read}}{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -1,7 +1,7 @@
{% load humanize %} {% load humanize %}
{% load naturalduration %} {% load naturalduration %}
<tr {% if scrobble.in_progress %}class="in-progress"{% endif %}> <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> <td>
{% if scrobble.media_type in "Task" %} {% 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> <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>

View File

@ -205,7 +205,7 @@ header.navbar { display: none !important; }
style="background:{% if not cd.is_today %}{{ cd.color }}{% endif %};"> 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> <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 %} {% for s in cd.scrobbles %}
<a href="{% url 'scrobbles:detail' uuid=s.uuid %}" <a href="{% url 'scrobbles:detail' pk=s.id %}"
class="event-card" class="event-card"
title="{{ s.title }} — {{ s.media_type }}">{{ s.emoji }} {{ s.title }}</a> title="{{ s.title }} — {{ s.media_type }}">{{ s.emoji }} {{ s.title }}</a>
{% endfor %} {% endfor %}

View File

@ -88,7 +88,7 @@
{% endif %}"> {% endif %}">
{{ object.get_visibility_display }} {{ object.get_visibility_display }}
</span> </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 %} {% csrf_token %}
<select name="visibility" class="form-select form-select-sm" style="width:auto;" onchange="this.form.submit()"> <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> <option value="private" {% if object.visibility == 'private' %}selected{% endif %}>Private</option>
@ -100,25 +100,25 @@
<span class="small text-muted">Share link:</span> <span class="small text-muted">Share link:</span>
<code class="small" id="share-link">{{ request.scheme }}://{{ request.get_host }}{{ object.get_share_url }}</code> <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> <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 %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-warning">Regenerate</button> <button type="submit" class="btn btn-sm btn-outline-warning">Regenerate</button>
</form> </form>
{% if object.share_view_count %} {% if object.share_view_count %}
<span class="text-muted small ms-2">{{ object.share_view_count }} view{{ object.share_view_count|pluralize }}</span> <span class="text-muted small ms-2">{{ object.share_view_count }} view{{ object.share_view_count|pluralize }}</span>
{% endif %} {% 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 %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% if object.media_type == "Track" and has_mopidy_uri and user.profile.mopidy_api_url %} {% 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 %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button> <button type="submit" class="btn btn-sm btn-outline-secondary">add to mopidy queue</button>
</form> </form>
{% if user.profile.monthly_mopidy_playlist_pattern %} {% 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 %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-secondary">add to monthly playlist</button> <button type="submit" class="btn btn-sm btn-outline-secondary">add to monthly playlist</button>
</form> </form>
@ -288,7 +288,7 @@
<tbody> <tbody>
{% for scrobble in related_scrobbles %} {% for scrobble in related_scrobbles %}
<tr{% if scrobble.id == object.id %} class="table-active fw-bold"{% endif %}> <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> <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 %} {% 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> </td>

View File

@ -103,7 +103,7 @@
{% for scrobble in scrobbles %} {% for scrobble in scrobbles %}
<div class="result-item"> <div class="result-item">
<div class="result-title"> <div class="result-title">
<a href="{% url 'scrobbles:detail' scrobble.uuid %}"> <a href="{% url 'scrobbles:detail' scrobble.id %}">
{{ scrobble|truncatechars:100 }} {{ scrobble|truncatechars:100 }}
</a> </a>
</div> </div>

View File

@ -81,7 +81,7 @@
<td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.title}}</a></td> <td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.title}}</a></td>
<td>{{scrobble.logdata.notes_as_str}}</td> <td>{{scrobble.logdata.notes_as_str}}</td>
<td>{{scrobble.source}}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -87,7 +87,7 @@
<tr> <tr>
<td><a href="{{scrobble.get_absolute_url}}">{{scrobble.local_timestamp}}</a></td> <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>{% 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>{% 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> <td>{% if scrobble.videogame_save_data %}<a href="{{scrobble.videogame_save_data.url}}">Save data</a>{% else %}Not yet{% endif %}</td>