From 124eb86b74788102c611cbb33a1c01c46aa6c620 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Thu, 30 Jul 2026 12:07:55 -0400 Subject: [PATCH] Remove concurrent-reading trend, replace with concurrent-media-listening --- tests/scrobbles_tests/test_views.py | 3 + .../trends/_concurrent_media_listening.html | 47 +++++++++ .../templates/trends/_concurrent_reading.html | 42 -------- .../trends/templates/trends/trend_detail.html | 4 +- vrobbler/apps/trends/trends/__init__.py | 6 +- vrobbler/apps/trends/trends/concurrent.py | 99 +++++++++---------- vrobbler/apps/trends/utils.py | 1 - vrobbler/apps/trends/views.py | 8 +- 8 files changed, 106 insertions(+), 104 deletions(-) create mode 100644 vrobbler/apps/trends/templates/trends/_concurrent_media_listening.html delete mode 100644 vrobbler/apps/trends/templates/trends/_concurrent_reading.html diff --git a/tests/scrobbles_tests/test_views.py b/tests/scrobbles_tests/test_views.py index 69299f9..c190f04 100644 --- a/tests/scrobbles_tests/test_views.py +++ b/tests/scrobbles_tests/test_views.py @@ -561,11 +561,13 @@ def test_scrobble_detail_view_with_notes_and_labels(client): username="testuser", email="test@example.com", password="testpass" ) task = Task.objects.create(title="Test Task", description="Test description") + from django.utils import timezone scrobble = Scrobble.objects.create( task=task, media_type="Task", user=user, visibility="public", + timestamp=timezone.now(), log={ "notes": [ {"2024-01-01 10:00:00": "Note with label"}, @@ -574,6 +576,7 @@ def test_scrobble_detail_view_with_notes_and_labels(client): "description": "Test description", }, ) + client.login(username="testuser", password="testpass") url = reverse("scrobbles:detail", kwargs={"pk": scrobble.id}) response = client.get(url) assert response.status_code == 200 diff --git a/vrobbler/apps/trends/templates/trends/_concurrent_media_listening.html b/vrobbler/apps/trends/templates/trends/_concurrent_media_listening.html new file mode 100644 index 0000000..c2f6ad0 --- /dev/null +++ b/vrobbler/apps/trends/templates/trends/_concurrent_media_listening.html @@ -0,0 +1,47 @@ +
+ {% for category, info in data.items %} +
+
+
+
+ {% if category == "Trails" %}🥾 + {% elif category == "Books" %}📖 + {% elif category == "Foods" %}🍔 + {% elif category == "Drinks" %}🍺 + {% elif category == "Tasks" %}✅ + {% elif category == "Board Games" %}🎲 + {% elif category == "Video Games" %}🎮 + {% else %}🎧 + {% endif %} + {{ category }} +
+

{{ info.total_anchor_sessions }} session{{ info.total_anchor_sessions|pluralize }}

+ {% if info.artists %} + + + + + + + + + {% for artist in info.artists %} + + + + + {% endfor %} + +
ArtistPlays
{{ artist.artist_name }}{{ artist.count }}
+ {% else %} +

No concurrent listening data.

+ {% endif %} +
+
+
+ {% empty %} +
+

No concurrent listening data found.

+
+ {% endfor %} +
diff --git a/vrobbler/apps/trends/templates/trends/_concurrent_reading.html b/vrobbler/apps/trends/templates/trends/_concurrent_reading.html deleted file mode 100644 index 0adc7a6..0000000 --- a/vrobbler/apps/trends/templates/trends/_concurrent_reading.html +++ /dev/null @@ -1,42 +0,0 @@ -
- {% if data.books %} - {% for book in data.books %} -
-
-
-
- {% if book.book_uuid %} - {{ book.book_title }} - {% else %} - {{ book.book_title }} - {% endif %} - ({{ book.total_sessions }} listening sessions) -
- {% if book.tracks %} - - - - - - - - - - {% for t in book.tracks %} - - - - - - {% endfor %} - -
TrackArtistPlays
{% if t.track_uuid %}{{ t.track_name }}{% else %}{{ t.track_name }}{% endif %}{{ t.artist_name }}{{ t.count }}
- {% endif %} -
-
-
- {% endfor %} - {% else %} -

No concurrent reading data found.

- {% endif %} -
diff --git a/vrobbler/apps/trends/templates/trends/trend_detail.html b/vrobbler/apps/trends/templates/trends/trend_detail.html index 5f3ce9c..2b75a77 100644 --- a/vrobbler/apps/trends/templates/trends/trend_detail.html +++ b/vrobbler/apps/trends/templates/trends/trend_detail.html @@ -61,8 +61,8 @@ {% elif trend.slug == "concurrent-listening" %} {% include "trends/_concurrent_listening.html" %} -{% elif trend.slug == "concurrent-reading" %} - {% include "trends/_concurrent_reading.html" %} +{% elif trend.slug == "concurrent-media-listening" %} + {% include "trends/_concurrent_media_listening.html" %} {% elif trend.slug == "reading-pace-vs-activity" %} {% include "trends/_reading_pace.html" %} diff --git a/vrobbler/apps/trends/trends/__init__.py b/vrobbler/apps/trends/trends/__init__.py index 403d759..fcb30fc 100644 --- a/vrobbler/apps/trends/trends/__init__.py +++ b/vrobbler/apps/trends/trends/__init__.py @@ -5,7 +5,7 @@ from trends.trends.activity import ( ) from trends.trends.concurrent import ( compute_concurrent_listening, - compute_concurrent_reading, + compute_concurrent_media_listening, ) from trends.trends.fasting import compute_fasting from trends.trends.mood import ( @@ -36,7 +36,9 @@ compute_activity_distribution = register("activity-distribution")( # compute_concurrent_listening = register("concurrent-listening")( # compute_concurrent_listening # ) -compute_concurrent_reading = register("concurrent-reading")(compute_concurrent_reading) +compute_concurrent_media_listening = register("concurrent-media-listening")( + compute_concurrent_media_listening +) compute_mood_by_time = register("mood-by-time")(compute_mood_by_time) compute_mood_distribution = register("mood-distribution")(compute_mood_distribution) compute_mood_streaks = register("mood-streaks")(compute_mood_streaks) diff --git a/vrobbler/apps/trends/trends/concurrent.py b/vrobbler/apps/trends/trends/concurrent.py index d970537..c3216d7 100644 --- a/vrobbler/apps/trends/trends/concurrent.py +++ b/vrobbler/apps/trends/trends/concurrent.py @@ -35,6 +35,20 @@ def _find_concurrent(anchor_scrobbles, paired_scrobbles): return anchor_to_paired +CONCURRENT_MEDIA_TYPES = { + "Trail": "Trails", + "Book": "Books", + "Food": "Foods", + "Beer": "Drinks", + "Wine": "Drinks", + "Coffee": "Drinks", + "Drink": "Drinks", + "Task": "Tasks", + "BoardGame": "Board Games", + "VideoGame": "Video Games", +} + + def _get_media_name(scrobble): """Return the name of the media object associated with a scrobble.""" for attr in [ @@ -173,11 +187,11 @@ def compute_concurrent_listening(user, period="all_time"): } -def compute_concurrent_reading(user, period="all_time"): - """Find what music was listened to while reading books. +def compute_concurrent_media_listening(user, period="last_30"): + """Find what music (by artist) you listen to while doing other activities. - Returns a dict with key 'books' containing a list of entries with the - book title and the tracks listened to while reading. + Groups concurrent listening by media type (Trails, Books, Foods, Drinks, + Tasks, Board Games, Video Games) and shows the top 3 artists per type. """ from trends.utils import get_date_range @@ -188,15 +202,15 @@ def compute_concurrent_reading(user, period="all_time"): if end: base_filters &= Q(timestamp__lte=end) + anchor_media_types = list(CONCURRENT_MEDIA_TYPES.keys()) + anchor_scrobbles = list( Scrobble.objects.filter( base_filters, - media_type="Book", + media_type__in=anchor_media_types, stop_timestamp__isnull=False, played_to_completion=True, - ) - .select_related("book") - .order_by("-timestamp") + ).order_by("-timestamp") ) paired_scrobbles = list( @@ -206,70 +220,49 @@ def compute_concurrent_reading(user, period="all_time"): stop_timestamp__isnull=False, played_to_completion=True, ) - .select_related("track") + .select_related("track", "track__artist_fk") .order_by("-timestamp") ) if not anchor_scrobbles or not paired_scrobbles: - return {"books": []} + return {slug: [] for slug in set(CONCURRENT_MEDIA_TYPES.values())} anchor_to_paired = _find_concurrent(anchor_scrobbles, paired_scrobbles) paired_by_pk = {s.pk: s for s in paired_scrobbles} - books_by_uuid = {} + media_type_artists = defaultdict(lambda: defaultdict(int)) for anchor in anchor_scrobbles: paired_pks = anchor_to_paired.get(anchor.pk, []) if not paired_pks: continue - book = anchor.book - book_uuid = str(book.uuid) if book and book.uuid else "" - book_key = book_uuid or str(book) if book else "Unknown" - - if book_key not in books_by_uuid: - books_by_uuid[book_key] = { - "book_title": str(book) if book else "Unknown", - "book_uuid": book_uuid, - "total_sessions": 0, - "tracks_by_name": defaultdict(int), - "track_details": {}, - } - - books_by_uuid[book_key]["total_sessions"] += len(paired_pks) - for p_pk in paired_pks: ps = paired_by_pk[p_pk] track = ps.track if track is None: continue - name = str(track) - books_by_uuid[book_key]["tracks_by_name"][name] += 1 - if name not in books_by_uuid[book_key]["track_details"]: - books_by_uuid[book_key]["track_details"][name] = { - "track_name": name, - "track_uuid": str(track.uuid) if track.uuid else "", - "artist_name": str(track.artist) if track.artist else "", - } + artist = track.artist_fk + artist_name = str(artist) if artist else "Unknown" + category = CONCURRENT_MEDIA_TYPES.get(anchor.media_type, anchor.media_type) + media_type_artists[category][artist_name] += 1 - books = [] - for bd in books_by_uuid.values(): - books.append( - { - "book_title": bd["book_title"], - "book_uuid": bd["book_uuid"], - "total_sessions": bd["total_sessions"], - "tracks": sorted( - [ - {**bd["track_details"][name], "count": count} - for name, count in bd["tracks_by_name"].items() - ], - key=lambda x: x["count"], - reverse=True, - )[:5], - } + result = {} + for category, artists in sorted(media_type_artists.items()): + sorted_artists = sorted( + [{"artist_name": name, "count": count} for name, count in artists.items()], + key=lambda x: x["count"], + reverse=True, + )[:3] + + anchor_count = sum( + 1 + for a in anchor_scrobbles + if CONCURRENT_MEDIA_TYPES.get(a.media_type, a.media_type) == category ) + result[category] = { + "artists": sorted_artists, + "total_anchor_sessions": anchor_count, + } - return { - "books": sorted(books, key=lambda x: x["total_sessions"], reverse=True)[:20], - } + return result diff --git a/vrobbler/apps/trends/utils.py b/vrobbler/apps/trends/utils.py index f68d4ea..00ba5ac 100644 --- a/vrobbler/apps/trends/utils.py +++ b/vrobbler/apps/trends/utils.py @@ -18,7 +18,6 @@ PERIOD_LABELS = dict(PERIOD_CHOICES) TIME_BOUND_TRENDS = { "activity-distribution", - "concurrent-reading", "concurrent-listening", "fasting", "mood-by-time", diff --git a/vrobbler/apps/trends/views.py b/vrobbler/apps/trends/views.py index 96a8a9c..ba30d79 100644 --- a/vrobbler/apps/trends/views.py +++ b/vrobbler/apps/trends/views.py @@ -18,10 +18,10 @@ TREND_METADATA = { "description": "What music were you listening to while on trails or at locations?", "icon": "🎧", }, - "concurrent-reading": { - "title": "Concurrent Reading", - "description": "What music did you listen to while reading books?", - "icon": "📖", + "concurrent-media-listening": { + "title": "Concurrent Media Listening", + "description": "What are you listening to while doing other things?", + "icon": "🎧", }, "mood-trajectory": { "title": "Mood Trajectory",