From cce2db0ea1ec3a238350e4b4b2150a9c992f3924 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Tue, 24 Mar 2026 14:23:24 -0400 Subject: [PATCH] [search] Add scrobble searching --- vrobbler/apps/scrobbles/urls.py | 1 + vrobbler/apps/scrobbles/views.py | 117 +++++++++++++++ .../templates/scrobbles/scrobble_list.html | 7 + .../templates/scrobbles/scrobble_search.html | 134 ++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 vrobbler/templates/scrobbles/scrobble_search.html diff --git a/vrobbler/apps/scrobbles/urls.py b/vrobbler/apps/scrobbles/urls.py index 782d57f..5484644 100644 --- a/vrobbler/apps/scrobbles/urls.py +++ b/vrobbler/apps/scrobbles/urls.py @@ -5,6 +5,7 @@ from tasks.webhooks import emacs_webhook, todoist_webhook app_name = "scrobbles" urlpatterns = [ + path("search/", views.ScrobbleSearchView.as_view(), name="search"), path("status/", views.ScrobbleStatusView.as_view(), name="status"), path( "widget/top-artists///", diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 255da76..d3df06c 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -968,6 +968,123 @@ class EmbeddableTopArtistsWidget(TemplateView): return context +class ScrobbleSearchView(LoginRequiredMixin, TemplateView): + template_name = "scrobbles/scrobble_search.html" + + MEDIA_FIELDS = { + "Video": ("video__title", "video__overview"), + "Track": ("track__title", None), + "PodcastEpisode": ("podcast_episode__title", None), + "Book": ("book__title", "book__description"), + "Paper": ("paper__title", None), + "VideoGame": ("video_game__title", "video_game__description"), + "BoardGame": ("board_game__title", "board_game__description"), + "Beer": ("beer__name", "beer__style"), + "Food": ("food__name", "food__description"), + "Puzzle": ("puzzle__name", "puzzle__description"), + "Trail": ("trail__name", "trail__location"), + "GeoLocation": ("geo_location__name", "geo_location__description"), + "Task": ("task__title", "task__description"), + "WebPage": ("web_page__title", None), + "LifeEvent": ("life_event__title", "life_event__description"), + "Mood": ("mood__title", "mood__description"), + "BrickSet": ("brick_set__title", None), + } + + def get(self, request, *args, **kwargs): + if not request.GET.get("q") and not request.GET.get("media_type"): + return super().get(request, *args, **kwargs) + return self.search(request) + + def search(self, request): + query = request.GET.get("q", "").strip() + media_type = request.GET.get("media_type", "") + user = request.user + + scrobbles = Scrobble.objects.filter(user=user) + + if media_type: + scrobbles = scrobbles.filter(media_type=media_type) + + if query: + from django.contrib.contenttypes.models import ContentType + from taggit.models import TaggedItem + + scrobble_ct = ContentType.objects.get_for_model(Scrobble) + + tag_match_ids = list( + TaggedItem.objects.filter( + content_type=scrobble_ct, + tag__name__icontains=query, + ).values_list("object_id", flat=True) + ) + + q = Q() + q |= Q(log__notes__icontains=query) + q |= Q(log__title__icontains=query) + q |= Q(log__description__icontains=query) + q |= Q(source__icontains=query) + + if media_type and media_type in self.MEDIA_FIELDS: + title_field, desc_field = self.MEDIA_FIELDS[media_type] + if title_field: + q |= Q(**{f"{title_field}__icontains": query}) + if desc_field: + q |= Q(**{f"{desc_field}__icontains": query}) + + scrobbles = scrobbles.filter(q).distinct() + else: + all_matching_ids = set() + + base_matches = scrobbles.filter(q).values_list("id", flat=True) + all_matching_ids.update(base_matches) + + for mt, (title_field, desc_field) in self.MEDIA_FIELDS.items(): + mt_queries = [] + if title_field: + mt_queries.append(Q(**{f"{title_field}__icontains": query})) + if desc_field: + mt_queries.append(Q(**{f"{desc_field}__icontains": query})) + + if mt_queries: + mt_q = mt_queries[0] + for mq in mt_queries[1:]: + mt_q |= mq + try: + mt_matches = scrobbles.filter(mt_q).values_list( + "id", flat=True + ) + all_matching_ids.update(mt_matches) + except Exception: + pass + + all_matching_ids.update(tag_match_ids) + + scrobbles = Scrobble.objects.filter(id__in=all_matching_ids, user=user) + + scrobbles = scrobbles.order_by("-timestamp")[:100] + + context = self.get_context_data( + scrobbles=scrobbles, + query=query, + media_type=media_type, + media_types=Scrobble.MediaType.choices, + ) + return self.render_to_response(context) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + if "scrobbles" not in ctx: + ctx["scrobbles"] = [] + if "query" not in ctx: + ctx["query"] = "" + if "media_type" not in ctx: + ctx["media_type"] = "" + if "media_types" not in ctx: + ctx["media_types"] = Scrobble.MediaType.choices + return ctx + + class EmbeddableTopBoardGamesWidget(BaseEmbeddableWidget): media_type = "Board Games" count_label = "plays" diff --git a/vrobbler/templates/scrobbles/scrobble_list.html b/vrobbler/templates/scrobbles/scrobble_list.html index 7195955..468a4cd 100644 --- a/vrobbler/templates/scrobbles/scrobble_list.html +++ b/vrobbler/templates/scrobbles/scrobble_list.html @@ -57,6 +57,13 @@

{% if date %}{{date|naturaltime}}{% else %}{{title}}{% endif %}

{% if user.is_authenticated %} +
+ +
diff --git a/vrobbler/templates/scrobbles/scrobble_search.html b/vrobbler/templates/scrobbles/scrobble_search.html new file mode 100644 index 0000000..c09996f --- /dev/null +++ b/vrobbler/templates/scrobbles/scrobble_search.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} +{% load static %} +{% load humanize %} +{% load naturalduration %} + +{% block head_extra %} + +{% endblock %} + +{% block content %} +
+
+

Search Scrobbles

+
+ {% if user.is_authenticated %} +
+ Admin +
+
+ Charts +
+ {% endif %} +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+ + All + + {% for value, label in media_types %} + + {{ label }} + + {% endfor %} +
+
+ + {% if query or media_type %} +
+ {% if scrobbles %} +

{{ scrobbles|length }} result{{ scrobbles|length|pluralize }}

+ {% for scrobble in scrobbles %} +
+ +
+ {{ scrobble.get_media_type_display }} + {{ scrobble.timestamp|date:"M d, Y" }} + {% if scrobble.source %} via {{ scrobble.source }}{% endif %} +
+ {% if scrobble.tags.all %} +
+ {% for tag in scrobble.tags.all %} + {{ tag.name }} + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + {% else %} +
+ No scrobbles found matching your search. +
+ {% endif %} +
+ {% else %} +
+

Enter a search term or select a media type to filter.

+
+ {% endif %} +
+
+{% endblock %}