Remove concurrent-reading trend, replace with concurrent-media-listening

This commit is contained in:
2026-07-30 12:07:55 -04:00
parent b437e2a970
commit 124eb86b74
8 changed files with 106 additions and 104 deletions

View File

@ -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

View File

@ -0,0 +1,47 @@
<div class="row">
{% for category, info in data.items %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">
{% 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 }}
</h5>
<p class="text-muted small">{{ info.total_anchor_sessions }} session{{ info.total_anchor_sessions|pluralize }}</p>
{% if info.artists %}
<table class="table table-sm table-borderless mb-0">
<thead>
<tr>
<th>Artist</th>
<th class="text-end">Plays</th>
</tr>
</thead>
<tbody>
{% for artist in info.artists %}
<tr>
<td>{{ artist.artist_name }}</td>
<td class="text-end">{{ artist.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted mb-0">No concurrent listening data.</p>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<p class="text-muted">No concurrent listening data found.</p>
</div>
{% endfor %}
</div>

View File

@ -1,42 +0,0 @@
<div class="row">
{% if data.books %}
{% for book in data.books %}
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body py-2">
<h6 class="card-title mb-1">
{% if book.book_uuid %}
<a href="{% url 'books:book_detail' book.book_uuid %}">{{ book.book_title }}</a>
{% else %}
{{ book.book_title }}
{% endif %}
<small class="text-muted">({{ book.total_sessions }} listening sessions)</small>
</h6>
{% if book.tracks %}
<table class="table table-sm table-borderless mb-0">
<thead>
<tr>
<th>Track</th>
<th>Artist</th>
<th class="text-end">Plays</th>
</tr>
</thead>
<tbody>
{% for t in book.tracks %}
<tr>
<td>{% if t.track_uuid %}<a href="{% url 'music:track_detail' t.track_uuid %}">{{ t.track_name }}</a>{% else %}{{ t.track_name }}{% endif %}</td>
<td>{{ t.artist_name }}</td>
<td class="text-end">{{ t.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No concurrent reading data found.</p>
{% endif %}
</div>

View File

@ -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" %}

View File

@ -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)

View File

@ -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

View File

@ -18,7 +18,6 @@ PERIOD_LABELS = dict(PERIOD_CHOICES)
TIME_BOUND_TRENDS = {
"activity-distribution",
"concurrent-reading",
"concurrent-listening",
"fasting",
"mood-by-time",

View File

@ -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",