[scrobbles] Add sharing of scrobbles
Some checks failed
build / test (push) Has been cancelled

This commit is contained in:
2026-06-09 12:33:25 -04:00
parent 568772a0e6
commit 7f3076608f
20 changed files with 891 additions and 3 deletions

View File

@ -14,7 +14,7 @@ from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Count, Max, Q, Sum
from django.db.models import Count, F, Max, Q, Sum
from django.db.models.query import QuerySet
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
@ -38,7 +38,7 @@ from django.utils import timezone
from django.utils.dateformat import DateFormat
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, View
from django.views.generic.edit import CreateView
from django.views.generic.list import ListView
from moods.models import Mood
@ -72,6 +72,8 @@ from scrobbles.constants import (
)
from scrobbles.export import export_scrobbles
from scrobbles.forms import ExportScrobbleForm, ScrobbleForm
from scrobbles.constants import Visibility
from scrobbles.sqids import decode_scrobble_share
from scrobbles.models import (
AudioScrobblerTSVImport,
BGStatsImport,
@ -83,6 +85,7 @@ from scrobbles.models import (
ScaleCSVImport,
Scrobble,
ScrobbleQuerySet,
ShareViewLog,
TrailGPXImport,
)
from scrobbles.scrobblers import *
@ -1136,6 +1139,15 @@ class ScrobbleDetailView(DetailView):
slug_url_kwarg = "uuid"
paginate_by = 100
def get_object(self, queryset=None):
scrobble = super().get_object(queryset=queryset)
user = self.request.user
if scrobble.visibility == Visibility.PUBLIC:
return scrobble
if user.is_authenticated and scrobble.user == user:
return scrobble
raise Http404
def get_form_class(self):
return self.object.media_obj.logdata_cls().form()
@ -1252,6 +1264,93 @@ class ScrobbleDetailView(DetailView):
return context
class ScrobbleShareView(TemplateView):
template_name = "scrobbles/scrobble_share.html"
def get_object(self):
sqid = self.kwargs.get("sqid")
decoded = decode_scrobble_share(sqid)
if not decoded or len(decoded) != 2:
raise Http404
scrobble_id, version = decoded
scrobble = get_object_or_404(Scrobble, id=scrobble_id)
if scrobble.share_token_version != version:
raise Http404
if scrobble.visibility not in (Visibility.PUBLIC, Visibility.SHARED):
raise Http404
Scrobble.objects.filter(id=scrobble.id).update(
share_view_count=F("share_view_count") + 1
)
scrobble.refresh_from_db(fields=["share_view_count"])
ShareViewLog.objects.create(
scrobble=scrobble,
ip_address=self.request.META.get("REMOTE_ADDR"),
user_agent=self.request.META.get("HTTP_USER_AGENT", "")[:500],
referrer=self.request.META.get("HTTP_REFERER", ""),
)
return scrobble
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
scrobble = self.get_object()
context["object"] = scrobble
context["log_form"] = None
context["related_scrobbles"] = Scrobble.objects.none()
context["has_mopidy_uri"] = False
if self.request.user.is_authenticated:
media_type = scrobble.media_type
fk_field = ScrobbleDetailView.MEDIA_FK_MAP.get(media_type)
media_obj = scrobble.media_obj
if fk_field and media_obj:
context["is_favorited"] = FavoriteMedia.objects.filter(
user=self.request.user, **{fk_field: media_obj}
).exists()
return context
class ScrobbleExploreView(ListView):
model = Scrobble
paginate_by = 100
template_name = "scrobbles/scrobble_explore.html"
queryset = Scrobble.objects.filter(visibility=Visibility.PUBLIC).order_by(
"-timestamp"
)
class RegenerateShareTokenView(LoginRequiredMixin, View):
def post(self, request, uuid):
scrobble = get_object_or_404(Scrobble, uuid=uuid, 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)
visibility = request.POST.get("visibility")
if visibility not in (Visibility.PUBLIC, Visibility.SHARED, Visibility.PRIVATE):
return redirect(scrobble.get_absolute_url())
scrobble.visibility = visibility
scrobble.save(update_fields=["visibility"])
return redirect(scrobble.get_absolute_url())
class ScrobbleShareAnalyticsView(LoginRequiredMixin, DetailView):
model = Scrobble
slug_field = "uuid"
slug_url_kwarg = "uuid"
template_name = "scrobbles/scrobble_share_analytics.html"
def get_queryset(self):
return Scrobble.objects.filter(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
scrobble = self.object
context["share_views"] = scrobble.share_views.order_by("-created")[:50]
return context
class BaseEmbeddableWidget(TemplateView):
template_name = "scrobbles/embeddable_top_media.html"