From c8292d1c069db38aec5f9a71fc5947dd731fbb6d Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Tue, 9 Jun 2026 13:15:57 -0400 Subject: [PATCH] [scrobbles] Back fill visibility field --- PROJECT.org | 67 ++++++++++++++++++- vrobbler/apps/scrobbles/importers/lastfm.py | 1 + vrobbler/apps/scrobbles/importers/scale.py | 1 + .../apps/scrobbles/importers/trail_gpx.py | 1 + vrobbler/apps/scrobbles/importers/tsv.py | 1 + .../0095_backfill_null_visibility.py | 20 ++++++ vrobbler/apps/scrobbles/models.py | 2 + 7 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 vrobbler/apps/scrobbles/migrations/0095_backfill_null_visibility.py diff --git a/PROJECT.org b/PROJECT.org index 8fbf326..3ed7036 100644 --- a/PROJECT.org +++ b/PROJECT.org @@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [0/15] :vrobbler:project:personal: +* Backlog [1/16] :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 @@ -551,6 +551,71 @@ 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. +** DONE [#A] Fix bug with missing default visbility for scrobbles :bug:scrobbles:sharing: +:PROPERTIES: +:ID: 20843992-6453-9a9a-cde6-2c2b6677db23 +:END: + +*** Description + +We can't scrobble anything now because visbility is not null, but has no default +value. + +*** Notes +- Note taken on [2026-06-09 Tue 13:14] + The full stack trace: + #+begin_src sh + File "/usr/local/lib/python3.11/site-packages/vrobbler/apps/scrobbles/models.py", line 1430, in create_or_update + elif "log" in scrobble_data.keys() and scrobble.log: + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/vrobbler/apps/scrobbles/models.py", line 1583, in create + ) + + File "/usr/local/lib/python3.11/site-packages/django/db/models/manager.py", line 87, in manager_method + return getattr(self.get_queryset(), name)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/models/query.py", line 658, in create + obj.save(force_insert=True, using=self.db) + File "/usr/local/lib/python3.11/site-packages/vrobbler/apps/scrobbles/models.py", line 870, in save + if self.media_obj: + ^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django_extensions/db/models.py", line 22, in save + super().save(**kwargs) + File "/usr/local/lib/python3.11/site-packages/django/db/models/base.py", line 814, in save + self.save_base( + File "/usr/local/lib/python3.11/site-packages/django/db/models/base.py", line 877, in save_base + updated = self._save_table( + ^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/models/base.py", line 1020, in _save_table + results = self._do_insert( + ^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/models/base.py", line 1061, in _do_insert + return manager._insert( + ^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/models/manager.py", line 87, in manager_method + return getattr(self.get_queryset(), name)(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/models/query.py", line 1805, in _insert + return query.get_compiler(using=using).execute_sql(returning_fields) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/models/sql/compiler.py", line 1822, in execute_sql + cursor.execute(sql, params) + File "/usr/local/lib/python3.11/site-packages/django/db/backends/utils.py", line 67, in execute + return self._execute_with_wrappers( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers + return executor(sql, params, many, context) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/local/lib/python3.11/site-packages/django/db/backends/utils.py", line 84, in _execute + with self.db.wrap_database_errors: + File "/usr/local/lib/python3.11/site-packages/django/db/utils.py", line 91, in __exit__ + raise dj_exc_value.with_traceback(traceback) from exc_value + File "/usr/local/lib/python3.11/site-packages/django/db/backends/utils.py", line 89, in _execute + return self.cursor.execute(sql, params) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + django.db.utils.IntegrityError: null value in column "visibility" of relation "scrobbles_scrobble" violates not-null constraint + DETAIL: Failing row contains (373813, 2026-06-09 17:13:38.11355+00, 2026-06-09 17:13:38.113566+00, 2026-06-09 17:13:36+00, 0, f, f, Todoist, 1, null, t, {"title": "Animal chores", "labels": ["chore", "farm"], "todoist..., null, null, null, 68680dbf-f9a9-476c-b1c7-adbd231bbab6, null, null, null, null, , null, Task, null, null, null, America/New_York, null, null, , null, null, , 72, null, null, null, null, null, null, null, null, null, null). + #+end_src * Version 49.0 [1/1] ** DONE [#A] Fix broken tests with new sharing and add tests :scrobbles:sharing:tests: :PROPERTIES: diff --git a/vrobbler/apps/scrobbles/importers/lastfm.py b/vrobbler/apps/scrobbles/importers/lastfm.py index 842bba3..70c08b5 100644 --- a/vrobbler/apps/scrobbles/importers/lastfm.py +++ b/vrobbler/apps/scrobbles/importers/lastfm.py @@ -76,6 +76,7 @@ class LastFM: in_progress=False, media_type=Scrobble.MediaType.TRACK, timezone=tz_timestamp.tzinfo.name, + visibility="private", ) # Vrobbler scrobbles on finish, LastFM scrobbles on start seconds_eariler = timestamp - timedelta(seconds=20) diff --git a/vrobbler/apps/scrobbles/importers/scale.py b/vrobbler/apps/scrobbles/importers/scale.py index c8d3aa5..5f4f284 100644 --- a/vrobbler/apps/scrobbles/importers/scale.py +++ b/vrobbler/apps/scrobbles/importers/scale.py @@ -111,6 +111,7 @@ def import_scale_csv(file_path, user_id): played_to_completion=True, in_progress=False, media_type=Scrobble.MediaType.TASK, + visibility="private", ) new_scrobbles.append(new_scrobble) diff --git a/vrobbler/apps/scrobbles/importers/trail_gpx.py b/vrobbler/apps/scrobbles/importers/trail_gpx.py index 56c7197..e1f1408 100644 --- a/vrobbler/apps/scrobbles/importers/trail_gpx.py +++ b/vrobbler/apps/scrobbles/importers/trail_gpx.py @@ -305,6 +305,7 @@ def import_trail_gpx(file_path, user_id, original_filename=None): played_to_completion=True, in_progress=False, media_type=Scrobble.MediaType.TRAIL, + visibility="private", ) _, ext = os.path.splitext(file_path) diff --git a/vrobbler/apps/scrobbles/importers/tsv.py b/vrobbler/apps/scrobbles/importers/tsv.py index 2a722c4..978f52c 100644 --- a/vrobbler/apps/scrobbles/importers/tsv.py +++ b/vrobbler/apps/scrobbles/importers/tsv.py @@ -80,6 +80,7 @@ def import_audioscrobbler_tsv_file(file_path, user_id): in_progress=False, media_type=Scrobble.MediaType.TRACK, timezone=timestamp.tzinfo.name, + visibility="private", ) existing = Scrobble.objects.filter( timestamp=timestamp, track=track, user=user diff --git a/vrobbler/apps/scrobbles/migrations/0095_backfill_null_visibility.py b/vrobbler/apps/scrobbles/migrations/0095_backfill_null_visibility.py new file mode 100644 index 0000000..165291d --- /dev/null +++ b/vrobbler/apps/scrobbles/migrations/0095_backfill_null_visibility.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +def backfill_null_visibility(apps, schema_editor): + Scrobble = apps.get_model("scrobbles", "Scrobble") + Scrobble.objects.filter(visibility__isnull=True).update(visibility="private") + + +class Migration(migrations.Migration): + + dependencies = [ + ("scrobbles", "0094_scrobble_share_view_count_alter_scrobble_visibility_and_more"), + ] + + operations = [ + migrations.RunPython( + backfill_null_visibility, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index fd65440..985ab5d 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -1612,6 +1612,8 @@ class Scrobble(TimeStampedModel): cls, scrobble_data: dict, ) -> "Scrobble": + if "visibility" not in scrobble_data: + scrobble_data["visibility"] = Visibility.PRIVATE scrobble = cls.objects.create(**scrobble_data) if not ( scrobble.media_type == cls.MediaType.GEO_LOCATION