This commit has no recorded session.
diff
commit 7d708ad8a6c0dbc59190526b9497ee854ccc4f8fAuthor: Colin Powell <colin@unbl.ink>Date: Sun Jun 21 22:39:46 2026 -0400 [templates] Add polling live page for all scrobbles@@ -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:@@ -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", "")@@ -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;@@ -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>@@ -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 %}@@ -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>