diff --git a/vrobbler/apps/charts/urls.py b/vrobbler/apps/charts/urls.py index 7dd919b..15eafb2 100644 --- a/vrobbler/apps/charts/urls.py +++ b/vrobbler/apps/charts/urls.py @@ -1,4 +1,4 @@ -from charts.views import ChartRecordView, SpotifyTracksView +from charts.views import BirdsChartView, ChartRecordView, SpotifyTracksView from django.urls import path app_name = "charts" @@ -6,4 +6,5 @@ app_name = "charts" urlpatterns = [ path("charts/", ChartRecordView.as_view(), name="charts-home"), path("charts/spotify/", SpotifyTracksView.as_view(), name="spotify-tracks"), + path("charts/birds/", BirdsChartView.as_view(), name="birds-chart"), ] diff --git a/vrobbler/apps/charts/views.py b/vrobbler/apps/charts/views.py index f9948e2..fab2392 100644 --- a/vrobbler/apps/charts/views.py +++ b/vrobbler/apps/charts/views.py @@ -415,6 +415,18 @@ class ChartRecordView(TemplateView): ), } + bird_data = self.get_bird_chart_data( + user, + year=context.get("year"), + month=context.get("month"), + ) + context["birds_chart"] = bird_data["top_birds"] + context["bird_stats"] = { + "total_species": bird_data["total_species"], + "total_outings": bird_data["total_outings"], + "total_individuals": bird_data["total_individuals"], + } + context["chart_years"] = self.get_available_years(user) context["period_type"] = self.get_period_type() context["prev_period"] = self.get_prev_period_url(user) @@ -558,6 +570,52 @@ class ChartRecordView(TemplateView): return f"/charts/?date={year + 1}" return None + def get_bird_chart_data(self, user, year=None, month=None, limit=20): + from birds.models import Bird + + filters = { + "user": user, + "media_type": Scrobble.MediaType.BIRDING_LOCATION, + } + if year: + filters["timestamp__year"] = year + if month: + filters["timestamp__month"] = month + + scrobbles = Scrobble.objects.filter(**filters) + + bird_counts = {} + outings = 0 + for scrobble in scrobbles.iterator(): + outings += 1 + birds_data = scrobble.log.get("birds", []) if scrobble.log else [] + for entry in birds_data: + bird_id = entry.get("bird_id") + quantity = entry.get("quantity", 1) + if bird_id: + bird_counts[bird_id] = bird_counts.get(bird_id, 0) + quantity + + sorted_birds = sorted( + bird_counts.items(), key=lambda x: x[1], reverse=True + )[:limit] + + bird_ids = [bid for bid, _ in sorted_birds] + birds = Bird.objects.filter(id__in=bird_ids) + bird_map = {b.id: b for b in birds} + + top_birds = [ + {"bird": bird_map[bid], "count": count} + for bid, count in sorted_birds + if bid in bird_map + ] + + return { + "top_birds": top_birds, + "total_outings": outings, + "total_species": len(bird_counts), + "total_individuals": sum(bird_counts.values()), + } + class SpotifyTracksView(TemplateView): template_name = "charts/spotify_tracks.html" @@ -594,3 +652,59 @@ class SpotifyTracksView(TemplateView): user = self.request.user context["spotify_tracks"] = self.get_spotify_tracks(user) return context + + +class BirdsChartView(TemplateView): + template_name = "charts/birds_chart.html" + + def get_bird_data(self, user, limit=50): + from birds.models import Bird + + scrobbles = Scrobble.objects.filter( + user=user, + media_type=Scrobble.MediaType.BIRDING_LOCATION, + ) + + bird_counts = {} + outings = 0 + for scrobble in scrobbles.iterator(): + outings += 1 + birds_data = scrobble.log.get("birds", []) if scrobble.log else [] + for entry in birds_data: + bird_id = entry.get("bird_id") + quantity = entry.get("quantity", 1) + if bird_id: + bird_counts[bird_id] = bird_counts.get(bird_id, 0) + quantity + + sorted_birds = sorted( + bird_counts.items(), key=lambda x: x[1], reverse=True + )[:limit] + + bird_ids = [bid for bid, _ in sorted_birds] + birds = Bird.objects.filter(id__in=bird_ids) + bird_map = {b.id: b for b in birds} + + top_birds = [ + {"bird": bird_map[bid], "count": count} + for bid, count in sorted_birds + if bid in bird_map + ] + + return { + "top_birds": top_birds, + "total_outings": outings, + "total_species": len(bird_counts), + "total_individuals": sum(bird_counts.values()), + } + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + user = self.request.user + bird_data = self.get_bird_data(user) + context["birds_chart"] = bird_data["top_birds"] + context["bird_stats"] = { + "total_species": bird_data["total_species"], + "total_outings": bird_data["total_outings"], + "total_individuals": bird_data["total_individuals"], + } + return context diff --git a/vrobbler/templates/charts/birds_chart.html b/vrobbler/templates/charts/birds_chart.html new file mode 100644 index 0000000..16cc801 --- /dev/null +++ b/vrobbler/templates/charts/birds_chart.html @@ -0,0 +1,63 @@ +{% extends "base_list.html" %} + +{% block title %}Top Birds{% endblock %} + +{% block head_extra %} + +{% endblock %} + +{% block lists %} +
+
+

🐦 Birding Charts

+

All-time bird sightings based on scrobble log data.

+
+
+ +{% if bird_stats %} +
+
+
+
+
{{ bird_stats.total_species }}
+

Species seen

+
+
+
+
+
+
+
{{ bird_stats.total_outings }}
+

Outings

+
+
+
+
+
+
+
{{ bird_stats.total_individuals }}
+

Total individuals

+
+
+
+
+{% endif %} + +
+
+ +
+
+{% endblock %} diff --git a/vrobbler/templates/charts/chart_index.html b/vrobbler/templates/charts/chart_index.html index 2efb8da..a451091 100644 --- a/vrobbler/templates/charts/chart_index.html +++ b/vrobbler/templates/charts/chart_index.html @@ -215,6 +215,26 @@ {% endif %} + + {% if birds_chart %} +
+

🐦 Top Birds

+ +
+ {% endif %} {% if not charts %}