sessions / 7d708ad8a6c0dbc59190526b9497ee854ccc4f8f

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

ignore whitespace

commit 7d708ad8a6c0dbc59190526b9497ee854ccc4f8fAuthor: Colin Powell <colin@unbl.ink>Date:   Sun Jun 21 22:39:46 2026 -0400    [templates] Add polling live page for all scrobblesdiff --git a/PROJECT.org b/PROJECT.orgindex 6c6f1f3..ad34b6a 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [0/23] :vrobbler:project:personal:+* Backlog [3/26] :vrobbler:project:personal: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles: :PROPERTIES: :ID:       702462cf-d54b-48c6-8a7c-78b8de751deb@@ -604,7 +604,22 @@ independent of the email flow it was originally creatdd for  ** TODO [#B] Is there way to create unique slugs for media instances :media_types: -** TODO [#C] What would it look like to add an MCP server to expose scrobbles and media items? :mcpserver:feature:+** STRT [#B] Use HTMx to update the Now Playing widget :feature:templates:+:PROPERTIES:+:ID:       5f5631fc-9ee1-d5a5-d0f8-94fea6fbbfa4+:END:+** DONE [#B] Add a live page that updates the scrobble list via JS polling :feature:templates:+:PROPERTIES:+:ID:       58790d76-dc6e-8aa5-2dc0-e64fe786fbf1+:END:+** DONE [#A] Turns out we cant cache the now playing widget :bug:templates:+:PROPERTIES:+:ID:       9ce669ea-c000-cdfe-a634-ad5cdaeae81c+:END:+** DONE [#C] What would it look like to add an MCP server to expose scrobbles and media items? :mcpserver:feature:+:PROPERTIES:+:ID:       c5fca159-c7e0-5795-7c05-bbc48f539650+:END: * Version 56.4 [3/3] ** DONE [#B] Add ability to do reverse address lookup on lat-long pairs :geolocations:feature: :PROPERTIES:diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.pyindex 18ce5ba..7b2c589 100644--- a/vrobbler/apps/scrobbles/views.py+++ b/vrobbler/apps/scrobbles/views.py@@ -366,6 +366,11 @@ class ScrobbleListView(LoginRequiredMixin, ListView):     paginate_by = 100     template_name = "scrobbles/scrobble_all_list.html" +    def get_template_names(self):+        if self.request.headers.get("HX-Request"):+            return ["scrobbles/_scrobble_all_content.html"]+        return ["scrobbles/scrobble_all_list.html"]+     def get_queryset(self):         qs = Scrobble.objects.filter(user=self.request.user).order_by("-timestamp")         tags_param = self.request.GET.get("tags", "")diff --git a/vrobbler/templates/base.html b/vrobbler/templates/base.htmlindex af964f5..450ee9a 100644--- a/vrobbler/templates/base.html+++ b/vrobbler/templates/base.html@@ -13,6 +13,7 @@         <script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" crossorigin="anonymous"></script>         <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>         <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>+        <script src="https://unpkg.com/htmx.org@2.0.4"></script>         <style type="text/css">         dl {             display: flex;diff --git a/vrobbler/templates/scrobbles/_scrobble_all_content.html b/vrobbler/templates/scrobbles/_scrobble_all_content.htmlnew file mode 100644index 0000000..9bf9a36--- /dev/null+++ b/vrobbler/templates/scrobbles/_scrobble_all_content.html@@ -0,0 +1,146 @@+{% load humanize %}+{% load naturalduration %}+<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" hx-get="{% url 'scrobbles:scrobble-list' %}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}" hx-trigger="every 7s" hx-swap="outerHTML">+    <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">+        <div>+            <h1 class="h2">All Scrobbles</h1>+            {% if tag_list %}+            <h6 class="text-muted">Tagged {{ tag_list|join:", " }}</h6>+            {% if total_time_seconds %}+            <p class="text-muted small mb-0">Total time: {{ total_time_seconds|natural_duration }}</p>+            {% endif %}+            {% endif %}+            {% if request.GET.visibility %}+            <h6 class="text-muted">Filter: {{ request.GET.visibility|title }} scrobbles only</h6>+            {% endif %}+        </div>+    </div>++    <div class="table-responsive">+        <table class="table table-striped table-sm">+            <thead>+                <tr>+                    <th scope="col">Date</th>+                    <th scope="col">Type</th>+                    <th scope="col">Title</th>+                    <th scope="col">Time</th>+                </tr>+            </thead>+            <tbody>+                {% for scrobble in object_list %}+                <tr class="{% if scrobble.id in overlap_map %}{{ overlap_map.scrobble.id }}{% endif %}">+                    <td>+                        {% if scrobble.id in overlap_map %}โฑ {% endif %}+                        <a href="{{scrobble.get_absolute_url}}">{{ scrobble.timestamp|naturaltime }}</a>+                    </td>+                    <td>+                        {% if scrobble.video %}+                        ๐ŸŽฌ Video+                        {% elif scrobble.track %}+                        ๐ŸŽต Track+                        {% elif scrobble.podcast_episode %}+                        ๐ŸŽ™๏ธ Podcast episode+                        {% elif scrobble.sport_event %}+                        โšฝ Sport event+                        {% elif scrobble.book %}+                        ๐Ÿ“– Book+                        {% elif scrobble.paper %}+                        ๐Ÿ“„ Paper+                        {% elif scrobble.video_game %}+                        ๐ŸŽฎ Video game+                        {% elif scrobble.board_game %}+                        ๐ŸŽฒ Board game+                        {% elif scrobble.geo_location %}+                        ๐Ÿ“ GeoLocation+                        {% elif scrobble.trail %}+                        ๐Ÿฅพ Trail+                        {% elif scrobble.beer %}+                        ๐Ÿบ Beer+                        {% elif scrobble.puzzle %}+                        ๐Ÿงฉ Puzzle+                        {% elif scrobble.food %}+                        ๐Ÿ” Food+                        {% elif scrobble.task %}+                        โœ… Task+                        {% elif scrobble.web_page %}+                        ๐ŸŒ Web Page+                        {% elif scrobble.life_event %}+                        ๐ŸŽ‰ Life event+                        {% elif scrobble.mood %}+                        ๐Ÿ˜Š Mood+                        {% elif scrobble.brick_set %}+                        ๐Ÿงฑ Brick set+                        {% elif scrobble.channel %}+                        ๐Ÿ“บ Channel+                        {% else %}+                        Unknown+                        {% endif %}+                    </td>+                    <td>+                        {% if scrobble.video %}+                        <a href="{% url 'videos:video_detail' scrobble.video.uuid %}">{{ scrobble.video.title }}</a>+                        {% elif scrobble.track %}+                        <a href="{% url 'music:track_detail' scrobble.track.uuid %}">{{ scrobble.track.title }}</a>+                        {% elif scrobble.video_game %}+                        <a href="{% url 'videogames:videogame_detail' scrobble.video_game.uuid %}">{{ scrobble.video_game.title }}</a>+                        {% elif scrobble.book %}+                        <a href="{% url 'books:book_detail' scrobble.book.uuid %}">{{ scrobble.book.title }}</a>+                        {% elif scrobble.food %}+                        <a href="{% url 'foods:food_detail' scrobble.food.uuid %}">{{ scrobble.food.title }}</a>+                        {% elif scrobble.beer %}+                        <a href="{% url 'beers:beer_detail' scrobble.beer.uuid %}">{{ scrobble.beer.title }}</a>+                        {% elif scrobble.web_page %}+                        <a href="{% url 'webpages:webpage_detail' scrobble.web_page.uuid %}">{{ scrobble.web_page.title }}</a>+                        {% elif scrobble.podcast_episode %}+                        <a href="{% url 'podcasts:podcast_detail' scrobble.podcast_episode.podcast.id %}">{{ scrobble.podcast_episode.title }}</a>+                        {% elif scrobble.board_game %}+                        <a href="{% url 'boardgames:boardgame_detail' scrobble.board_game.uuid %}">{{ scrobble.board_game.title }}</a>+                        {% elif scrobble.trail %}+                        <a href="{% url 'trails:trail_detail' scrobble.trail.uuid %}">{{ scrobble.trail.title }}</a>+                        {% elif scrobble.puzzle %}+                        <a href="{% url 'puzzles:puzzle_detail' scrobble.puzzle.uuid %}">{{ scrobble.puzzle.title }}</a>+                        {% elif scrobble.brick_set %}+                        <a href="{% url 'bricksets:brickset_detail' scrobble.brick_set.uuid %}">{{ scrobble.brick_set.title }}</a>+                        {% elif scrobble.task %}+                        <a href="{% url 'tasks:task_detail' scrobble.task.uuid %}">{{scrobble.media_obj}}{% if scrobble.log.title  %} - {{ scrobble.log.title }}{% endif %}</a>+                        {% elif scrobble.life_event %}+                        <a href="{% url 'lifeevents:lifeevent_detail' scrobble.life_event.uuid %}">{{ scrobble.life_event.title }}</a>+                        {% elif scrobble.mood %}+                        <a href="{% url 'moods:mood_detail' scrobble.mood.uuid %}">{{ scrobble.mood.title}}</a>+                        {% elif scrobble.geo_location %}+                        <a href="{% url 'locations:geolocation_detail' scrobble.geo_location.uuid %}">{{ scrobble.geo_location.title }}</a>+                        {% else %}+                        Unknown+                        {% endif %}+                    </td>+                    <td>+                        {% if scrobble.in_progress and not scrobble.played_to_completion %}+                        In progress ...+                        {% elif scrobble.playback_position_seconds %}+                        {{ scrobble.playback_position_seconds|natural_duration }}+                        {% endif %}+                    </td>+                </tr>+                {% empty %}+                <tr>+                    <td colspan="4">No scrobbles found.</td>+                </tr>+                {% endfor %}+            </tbody>+        </table>+    </div>++    {% if page_obj.has_previous or page_obj.has_next %}+    <nav>+        <ul class="pagination">+            {% if page_obj.has_previous %}+            <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if tags_param %}&tags={{ tags_param }}{% endif %}">Previous</a></li>+            {% endif %}+            <li class="page-item"><span class="page-link">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>+            {% if page_obj.has_next %}+            <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if tags_param %}&tags={{ tags_param }}{% endif %}">Next</a></li>+            {% endif %}+        </ul>+    </nav>+    {% endif %}+</main>diff --git a/vrobbler/templates/scrobbles/scrobble_all_list.html b/vrobbler/templates/scrobbles/scrobble_all_list.htmlindex 6a8634e..c68fd22 100644--- a/vrobbler/templates/scrobbles/scrobble_all_list.html+++ b/vrobbler/templates/scrobbles/scrobble_all_list.html@@ -1,148 +1,4 @@ {% extends "base.html" %}-{% load humanize %}-{% load naturalduration %}- {% block content %}-<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">-    <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">-        <div>-            <h1 class="h2">All Scrobbles</h1>-            {% if tag_list %}-            <h6 class="text-muted">Tagged {{ tag_list|join:", " }}</h6>-            {% if total_time_seconds %}-            <p class="text-muted small mb-0">Total time: {{ total_time_seconds|natural_duration }}</p>-            {% endif %}-            {% endif %}-            {% if request.GET.visibility %}-            <h6 class="text-muted">Filter: {{ request.GET.visibility|title }} scrobbles only</h6>-            {% endif %}-        </div>-    </div>--    <div class="table-responsive">-        <table class="table table-striped table-sm">-            <thead>-                <tr>-                    <th scope="col">Date</th>-                    <th scope="col">Type</th>-                    <th scope="col">Title</th>-                    <th scope="col">Time</th>-                </tr>-            </thead>-            <tbody>-                {% for scrobble in object_list %}-                <tr class="{% if scrobble.id in overlap_map %}{{ overlap_map.scrobble.id }}{% endif %}">-                    <td>-                        {% if scrobble.id in overlap_map %}โฑ {% endif %}-                        <a href="{{scrobble.get_absolute_url}}">{{ scrobble.timestamp|naturaltime }}</a>-                    </td>-                    <td>-                        {% if scrobble.video %}-                        ๐ŸŽฌ Video-                        {% elif scrobble.track %}-                        ๐ŸŽต Track-                        {% elif scrobble.podcast_episode %}-                        ๐ŸŽ™๏ธ Podcast episode-                        {% elif scrobble.sport_event %}-                        โšฝ Sport event-                        {% elif scrobble.book %}-                        ๐Ÿ“– Book-                        {% elif scrobble.paper %}-                        ๐Ÿ“„ Paper-                        {% elif scrobble.video_game %}-                        ๐ŸŽฎ Video game-                        {% elif scrobble.board_game %}-                        ๐ŸŽฒ Board game-                        {% elif scrobble.geo_location %}-                        ๐Ÿ“ GeoLocation-                        {% elif scrobble.trail %}-                        ๐Ÿฅพ Trail-                        {% elif scrobble.beer %}-                        ๐Ÿบ Beer-                        {% elif scrobble.puzzle %}-                        ๐Ÿงฉ Puzzle-                        {% elif scrobble.food %}-                        ๐Ÿ” Food-                        {% elif scrobble.task %}-                        โœ… Task-                        {% elif scrobble.web_page %}-                        ๐ŸŒ Web Page-                        {% elif scrobble.life_event %}-                        ๐ŸŽ‰ Life event-                        {% elif scrobble.mood %}-                        ๐Ÿ˜Š Mood-                        {% elif scrobble.brick_set %}-                        ๐Ÿงฑ Brick set-                        {% elif scrobble.channel %}-                        ๐Ÿ“บ Channel-                        {% else %}-                        Unknown-                        {% endif %}-                    </td>-                    <td>-                        {% if scrobble.video %}-                        <a href="{% url 'videos:video_detail' scrobble.video.uuid %}">{{ scrobble.video.title }}</a>-                        {% elif scrobble.track %}-                        <a href="{% url 'music:track_detail' scrobble.track.uuid %}">{{ scrobble.track.title }}</a>-                        {% elif scrobble.video_game %}-                        <a href="{% url 'videogames:videogame_detail' scrobble.video_game.uuid %}">{{ scrobble.video_game.title }}</a>-                        {% elif scrobble.book %}-                        <a href="{% url 'books:book_detail' scrobble.book.uuid %}">{{ scrobble.book.title }}</a>-                        {% elif scrobble.food %}-                        <a href="{% url 'foods:food_detail' scrobble.food.uuid %}">{{ scrobble.food.title }}</a>-                        {% elif scrobble.beer %}-                        <a href="{% url 'beers:beer_detail' scrobble.beer.uuid %}">{{ scrobble.beer.title }}</a>-                        {% elif scrobble.web_page %}-                        <a href="{% url 'webpages:webpage_detail' scrobble.web_page.uuid %}">{{ scrobble.web_page.title }}</a>-                        {% elif scrobble.podcast_episode %}-                        <a href="{% url 'podcasts:podcast_detail' scrobble.podcast_episode.podcast.id %}">{{ scrobble.podcast_episode.title }}</a>-                        {% elif scrobble.board_game %}-                        <a href="{% url 'boardgames:boardgame_detail' scrobble.board_game.uuid %}">{{ scrobble.board_game.title }}</a>-                        {% elif scrobble.trail %}-                        <a href="{% url 'trails:trail_detail' scrobble.trail.uuid %}">{{ scrobble.trail.title }}</a>-                        {% elif scrobble.puzzle %}-                        <a href="{% url 'puzzles:puzzle_detail' scrobble.puzzle.uuid %}">{{ scrobble.puzzle.title }}</a>-                        {% elif scrobble.brick_set %}-                        <a href="{% url 'bricksets:brickset_detail' scrobble.brick_set.uuid %}">{{ scrobble.brick_set.title }}</a>-                        {% elif scrobble.task %}-                        <a href="{% url 'tasks:task_detail' scrobble.task.uuid %}">{{scrobble.media_obj}}{% if scrobble.log.title  %} - {{ scrobble.log.title }}{% endif %}</a>-                        {% elif scrobble.life_event %}-                        <a href="{% url 'lifeevents:lifeevent_detail' scrobble.life_event.uuid %}">{{ scrobble.life_event.title }}</a>-                        {% elif scrobble.mood %}-                        <a href="{% url 'moods:mood_detail' scrobble.mood.uuid %}">{{ scrobble.mood.title}}</a>-                        {% elif scrobble.geo_location %}-                        <a href="{% url 'locations:geolocation_detail' scrobble.geo_location.uuid %}">{{ scrobble.geo_location.title }}</a>-                        {% else %}-                        Unknown-                        {% endif %}-                    </td>-                    <td>-                        {% if scrobble.playback_position_seconds %}-                        {{ scrobble.playback_position_seconds|natural_duration }}-                        {% endif %}-                    </td>-                </tr>-                {% empty %}-                <tr>-                    <td colspan="4">No scrobbles found.</td>-                </tr>-                {% endfor %}-            </tbody>-        </table>-    </div>--    {% if page_obj.has_previous or page_obj.has_next %}-    <nav>-        <ul class="pagination">-            {% if page_obj.has_previous %}-            <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if tags_param %}&tags={{ tags_param }}{% endif %}">Previous</a></li>-            {% endif %}-            <li class="page-item"><span class="page-link">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>-            {% if page_obj.has_next %}-            <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if tags_param %}&tags={{ tags_param }}{% endif %}">Next</a></li>-            {% endif %}-        </ul>-    </nav>-    {% endif %}-</main>+{% include "scrobbles/_scrobble_all_content.html" %} {% endblock %}diff --git a/vrobbler/templates/scrobbles/scrobble_list.html b/vrobbler/templates/scrobbles/scrobble_list.htmlindex fee53da..80fe0f2 100644--- a/vrobbler/templates/scrobbles/scrobble_list.html+++ b/vrobbler/templates/scrobbles/scrobble_list.html@@ -73,24 +73,19 @@             <div class="btn-group me-2">               <a href="{% url 'charts:charts-home' %}" class="btn btn-sm btn-outline-secondary">Charts</a>             </div>+            <div class="btn-group me-2">+              <a href="{% url 'scrobbles:scrobble-list' %}" class="btn btn-sm btn-outline-secondary">Live</a>+            </div>+            <div class="btn-group me-2">+              <a href="{% url 'trends:trends-home' %}" class="btn btn-sm btn-outline-secondary">Trends</a>+            </div>             <div class="btn-group me-2">                 {% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}                 <form action="{% url 'scrobbles:lastfm-import' %}" method="get">                     <button type="submit" class="btn btn-sm btn-outline-secondary">Last.fm Sync</button>                 </form>                 {% endif %}-                {% if prev_link %}-                <a type="button" class="btn btn-sm btn-outline-secondary" href="{{prev_link}}"-                    data-bs-target="#">Previous</a>-                {% endif %}-                {% if today_link %}-                <a type="button" class="btn btn-sm btn-outline-secondary" href="{{today_link}}"-                    data-bs-target="#">Today</a>-                {% endif %}-                {% if next_link %}-                <a type="button" class="btn btn-sm btn-outline-secondary" href="{{next_link}}"-                    data-bs-target="#">Next</a>-                {% endif %}+             </div>             <div class="btn-group me-2">                 {% if user.profile.lastfm_username and not user.profile.lastfm_auto_import %}@@ -105,19 +100,6 @@                     data-bs-target="#exportModal">Export</button>             </div>             {% endif %}-            <div class="dropdown">-                <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" id="graphDateButton"-                    data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">-                    <div data-feather="calendar"></div>-                    {{title}}-                </button>-                <div class="dropdown-menu" data-bs-toggle="#graphDataChange" aria-labelledby="graphDateButton">-                    <a class="dropdown-item" href="?date=today">Today</a>-                    <a class="dropdown-item" href="?date=this_week">This week</a>-                    <a class="dropdown-item" href="?date=this_month">This month</a>-                    <a class="dropdown-item" href="?date=this_year">This year</a>-                </div>-            </div>         </div>     </div>