diff --git a/PROJECT.org b/PROJECT.org index f75d800..d11fa59 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [1/17] :vrobbler:project:personal: +* Backlog [2/17] :vrobbler:project:personal: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles: :PROPERTIES: :ID: 702462cf-d54b-48c6-8a7c-78b8de751deb @@ -544,22 +544,43 @@ log a warning and move on. We should have a global view `/favorites/` that shows the logged in users's favorited media objects. -** TODO [#A] Allow updating all a user's scrobble visibility at once :scrobbles:sharing:feature: +** DONE [#A] Allow updating all a user's scrobble visibility at once :scrobbles:sharing:feature: :PROPERTIES: :ID: 9ed2ec65-bf69-4300-965c-6a7d3ef7ea03 :END: *** Description -We now have the ability to share or unshare scrobbles and create private links. We should add a toggle -in the user's settings that will bulk make all their scrobbles public or private, so that a user -can either share everything, or lock their account down. +We now have the ability to share or unshare scrobbles and create private links. +We should add a toggle in the user's settings that will bulk make all their +scrobbles public or private, so that a user can either share everything, or lock +their account down. This should not affect scrobbles that are in the "Shared" visibility state. +And users should be able to also control whether all scrobbles of a specific +type are shared or not. Maybe this could be a JSONField in profile that contains +a media_type key with a visibility type for a value, and if it's not present, +sharing defaults to private? + Additionally, users's should have links in their settings to see what scrobbles are either public, shared or private. Probably this could be done with a ?visbility=<> filter on the /scrobbles/ page. + +*** Changes + +- Added `media_type_visibility` JSONField to UserProfile (migration 0038) +- Created `BulkVisibilityView` at `/settings/visibility/` with: + - Radio toggle to make all non-shared scrobbles Public or Private + - Per-media-type dropdown for each of the 20 media types (inherit/public/shared/private) +- Created `BulkVisibilityForm` with dynamic media_type fields +- Created `profiles/visibility_settings.html` template with visibility stats + filter links +- Added link from main settings page to visibility settings +- Added `?visibility=` filter support to `ScrobbleListView` (public/shared/private) +- Added filter indicator to `scrobble_all_list.html` +- Updated `Scrobble.create()` to check `user.profile.media_type_visibility` for media-type-specific defaults before falling back to PRIVATE + + ** DONE [#A] Replace columsn of Top Artists, Tracks and Series with Maloja widget :templates:charts: :PROPERTIES: diff --git a/vrobbler/apps/profiles/forms.py b/vrobbler/apps/profiles/forms.py index 35ba2e8..8dc2833 100644 --- a/vrobbler/apps/profiles/forms.py +++ b/vrobbler/apps/profiles/forms.py @@ -1,6 +1,8 @@ from django import forms from profiles.models import UserProfile +from scrobbles.constants import Visibility +from scrobbles.models import Scrobble class UserProfileForm(forms.ModelForm): @@ -45,3 +47,51 @@ class UserProfileForm(forms.ModelForm): "archivebox_password": forms.PasswordInput(render_value=True), "webdav_pass": forms.PasswordInput(render_value=True), } + + +MEDIA_TYPE_LABELS = { + mt.value: mt.label for mt in Scrobble.MediaType +} + +INHERIT = "" + + +class BulkVisibilityForm(forms.Form): + bulk_action = forms.ChoiceField( + choices=[ + (Visibility.PUBLIC, "Public"), + (Visibility.PRIVATE, "Private"), + ], + widget=forms.RadioSelect, + required=False, + label="Set all non-shared scrobbles to", + ) + + def __init__(self, *args, **kwargs): + self.profile = kwargs.pop("profile") + super().__init__(*args, **kwargs) + media_types = Scrobble.MediaType.values + choices = [ + (Visibility.PUBLIC, "Public"), + (Visibility.SHARED, "Shared"), + (Visibility.PRIVATE, "Private"), + ] + existing_overrides = self.profile.media_type_visibility or {} + for mt in sorted(media_types): + label = MEDIA_TYPE_LABELS.get(mt, mt) + self.fields[f"media_type_{mt}"] = forms.ChoiceField( + choices=choices, + required=False, + label=label, + initial=existing_overrides.get(mt, Visibility.PRIVATE), + ) + + def clean(self): + cleaned = super().clean() + overrides = {} + for mt in Scrobble.MediaType.values: + val = cleaned.get(f"media_type_{mt}") + if val: + overrides[mt] = val + cleaned["media_type_visibility"] = overrides + return cleaned diff --git a/vrobbler/apps/profiles/migrations/0038_userprofile_media_type_visibility.py b/vrobbler/apps/profiles/migrations/0038_userprofile_media_type_visibility.py new file mode 100644 index 0000000..951ba92 --- /dev/null +++ b/vrobbler/apps/profiles/migrations/0038_userprofile_media_type_visibility.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("profiles", "0037_alter_userprofile_default_scrobble_visibility"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="media_type_visibility", + field=models.JSONField( + blank=True, + default=dict, + help_text='Per-media-type visibility overrides, e.g. {"Video": "public", "Track": "private"}', + ), + ), + ] diff --git a/vrobbler/apps/profiles/models.py b/vrobbler/apps/profiles/models.py index 25df542..25ddb9f 100644 --- a/vrobbler/apps/profiles/models.py +++ b/vrobbler/apps/profiles/models.py @@ -90,6 +90,12 @@ class UserProfile(TimeStampedModel): default="private", ) + media_type_visibility = models.JSONField( + default=dict, + blank=True, + help_text="Per-media-type visibility overrides, e.g. {\"Video\": \"public\", \"Track\": \"private\"}", + ) + home_scrobble_limit = models.IntegerField(default=20) weigh_in_units = models.CharField( diff --git a/vrobbler/apps/profiles/urls.py b/vrobbler/apps/profiles/urls.py index ffa333a..f70778e 100644 --- a/vrobbler/apps/profiles/urls.py +++ b/vrobbler/apps/profiles/urls.py @@ -6,4 +6,9 @@ app_name = "profiles" urlpatterns = [ path("settings/", views.ProfileFormView.as_view(), name="profile_settings"), + path( + "settings/visibility/", + views.BulkVisibilityView.as_view(), + name="bulk_visibility", + ), ] diff --git a/vrobbler/apps/profiles/views.py b/vrobbler/apps/profiles/views.py index 4dfb1a4..09beb87 100644 --- a/vrobbler/apps/profiles/views.py +++ b/vrobbler/apps/profiles/views.py @@ -1,8 +1,12 @@ +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Count, Q from django.http.response import HttpResponseBadRequest from django.urls import reverse_lazy from django.views.generic import FormView -from profiles.forms import UserProfileForm +from profiles.forms import BulkVisibilityForm, UserProfileForm +from scrobbles.constants import Visibility +from scrobbles.models import Scrobble from tasks.todoist import generate_todoist_oauth_url @@ -30,3 +34,46 @@ class ProfileFormView(LoginRequiredMixin, FormView): context["profile"] = self.request.user.profile context["todoist_oauth_url"] = generate_todoist_oauth_url(self.request.user.id) return context + + +class BulkVisibilityView(LoginRequiredMixin, FormView): + template_name = "profiles/visibility_settings.html" + form_class = BulkVisibilityForm + success_url = reverse_lazy("profiles:bulk_visibility") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["profile"] = self.request.user.profile + return kwargs + + def form_valid(self, form): + request = self.request + profile = request.user.profile + + bulk_action = form.cleaned_data.get("bulk_action") + if bulk_action: + qs = Scrobble.objects.filter( + user=request.user, + ).exclude(visibility=Visibility.SHARED) + total = qs.count() + qs.update(visibility=bulk_action) + messages.success( + request, + f"Updated {total} scrobble(s) to {bulk_action}.", + ) + + profile.media_type_visibility = form.cleaned_data["media_type_visibility"] + profile.save(update_fields=["media_type_visibility"]) + messages.success(request, "Per-media-type visibility overrides saved.") + return super().form_valid(form) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + profile = self.request.user.profile + qs = Scrobble.objects.filter(user=self.request.user) + ctx["scrobble_count"] = qs.count() + ctx["visibility_counts"] = qs.values("visibility").annotate( + count=Count("id") + ) + ctx["profile"] = profile + return ctx diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 985ab5d..cb52d61 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -1613,7 +1613,17 @@ class Scrobble(TimeStampedModel): scrobble_data: dict, ) -> "Scrobble": if "visibility" not in scrobble_data: - scrobble_data["visibility"] = Visibility.PRIVATE + user = scrobble_data.get("user") + media_type = scrobble_data.get("media_type") + override = None + if user and media_type: + try: + profile = user.profile + overrides = profile.media_type_visibility or {} + override = overrides.get(media_type) + except user.__class__.profile.RelatedObjectDoesNotExist: + pass + scrobble_data["visibility"] = override or Visibility.PRIVATE scrobble = cls.objects.create(**scrobble_data) if not ( scrobble.media_type == cls.MediaType.GEO_LOCATION diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 9ffa153..464a3f3 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -371,6 +371,9 @@ class ScrobbleListView(LoginRequiredMixin, ListView): qs = qs.filter(id__in=matching_ids).distinct() else: tag_list = [] + visibility_param = self.request.GET.get("visibility", "") + if visibility_param in ("public", "shared", "private"): + qs = qs.filter(visibility=visibility_param) self.tag_list = tag_list self._full_queryset = qs return qs diff --git a/vrobbler/templates/profiles/settings_form.html b/vrobbler/templates/profiles/settings_form.html index 46c928a..e261d35 100644 --- a/vrobbler/templates/profiles/settings_form.html +++ b/vrobbler/templates/profiles/settings_form.html @@ -106,6 +106,7 @@ {% block title %}Settings{% endblock %} {% block details %}
+ +{% endblock %} diff --git a/vrobbler/templates/scrobbles/scrobble_all_list.html b/vrobbler/templates/scrobbles/scrobble_all_list.html index bc89105..6a8634e 100644 --- a/vrobbler/templates/scrobbles/scrobble_all_list.html +++ b/vrobbler/templates/scrobbles/scrobble_all_list.html @@ -13,6 +13,9 @@Total time: {{ total_time_seconds|natural_duration }}
{% endif %} {% endif %} + {% if request.GET.visibility %} +