Compare commits

...

4 Commits

Author SHA1 Message Date
0671ab432f [release] Bump to version 57.0
Some checks failed
build / test (push) Failing after 35s
deploy / test (push) Failing after 37s
deploy / build-and-deploy (push) Has been skipped
- Scrobble button on some media list pages dont work
- Use HTMx to update the Now Playing widget
- Add a live page that updates the scrobble list via JS polling
- Turns out we cant cache the now playing widget
- What would it look like to add an MCP server to expose scrobbles and media items?
2026-06-21 23:04:23 -04:00
893867419a [templates] Shorten up naturalduration rep 2026-06-21 23:04:01 -04:00
d9dfec81aa [scrobbles] Fix bug where media list scrobble btns didnt work 2026-06-21 23:00:28 -04:00
948fbc19bf [templates] Add HTMX support to Now Playing 2026-06-21 23:00:09 -04:00
12 changed files with 126 additions and 36 deletions

View File

@ -88,7 +88,7 @@ fetching and simple saving.
*** Metadata sources
**** Scraper
* Backlog [3/26] :vrobbler:project:personal:
* Backlog [0/22] :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,12 @@ independent of the email flow it was originally creatdd for
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
** STRT [#B] Use HTMx to update the Now Playing widget :feature:templates:
* Version 57.0 [5/5]
** DONE [#A] Scrobble button on some media list pages dont work :bug:scrobbles:
:PROPERTIES:
:ID: a3a5c707-2e3d-a6b1-0f7f-4c6f7433aa1f
:END:
** DONE [#B] Use HTMx to update the Now Playing widget :feature:templates:
:PROPERTIES:
:ID: 5f5631fc-9ee1-d5a5-d0f8-94fea6fbbfa4
:END:
@ -620,6 +625,7 @@ independent of the email flow it was originally creatdd for
: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:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "56.4"
version = "57.0"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -40,6 +40,7 @@ class UserProfileForm(forms.ModelForm):
"enable_public_widgets",
"widget_custom_css",
"home_scrobble_limit",
"live_now_playing",
"weigh_in_units",
]
widgets = {

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.29 on 2026-06-22 02:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("profiles", "0038_userprofile_media_type_visibility"),
]
operations = [
migrations.AddField(
model_name="userprofile",
name="live_now_playing",
field=models.BooleanField(default=False),
),
]

View File

@ -98,6 +98,8 @@ class UserProfile(TimeStampedModel):
home_scrobble_limit = models.IntegerField(default=20)
live_now_playing = models.BooleanField(default=False)
weigh_in_units = models.CharField(
max_length=16,
choices=WeighUnit.choices,

View File

@ -25,16 +25,24 @@ AUTO_FINISH_MEDIA = {
}
PLAY_AGAIN_MEDIA = {
"videogames": "VideoGame",
"videos": "Video",
"music": "Track",
"podcasts": "PodcastEpisode",
"sports": "SportEvent",
"books": "Book",
"videogames": "VideoGame",
"boardgames": "BoardGame",
"moods": "Mood",
"bricksets": "BrickSet",
"locations": "GeoLocation",
"trails": "Trail",
"beers": "Beer",
"puzzles": "Puzzle",
"foods": "Food",
"locations": "GeoLocation",
"videos": "Video",
"tasks": "Task",
"webpages": "WebPage",
"lifeevents": "LifeEvent",
"moods": "Mood",
"bricksets": "BrickSet",
"channels": "Channel",
"birds": "BirdingLocation",
"discgolf": "DiscGolfCourse",
}

View File

@ -5,6 +5,7 @@ from tasks.webhooks import EmacsWebhookView, TodoistWebhookView
app_name = "scrobbles"
urlpatterns = [
path("now-playing/", views.NowPlayingPartialView.as_view(), name="now-playing-partial"),
path("calendar/", views.ScrobbleCalendarView.as_view(), name="calendar"),
path("search/", views.ScrobbleSearchView.as_view(), name="search"),
path("status/", views.ScrobbleStatusView.as_view(), name="status"),

View File

@ -361,6 +361,25 @@ class RecentScrobbleList(ListView):
return Scrobble.objects.all().order_by("-timestamp")
class NowPlayingPartialView(LoginRequiredMixin, TemplateView):
template_name = "scrobbles/_now_playing.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
from scrobbles.constants import EXCLUDE_FROM_NOW_PLAYING
ctx["now_playing_list"] = list(
Scrobble.objects.filter(
in_progress=True,
is_paused=False,
user=self.request.user,
)
.exclude(media_type__in=EXCLUDE_FROM_NOW_PLAYING)
.select_related("track", "video", "podcast_episode")
)
return ctx
class ScrobbleListView(LoginRequiredMixin, ListView):
model = Scrobble
paginate_by = 100
@ -899,7 +918,7 @@ def scrobble_start(request, media_uuid):
if last_scrobble and last_scrobble.logdata:
next_page = last_scrobble.logdata.page_end + 1
log_data = {"page_start": next_page}
media_obj.scrobble_for_user(user_id, log=log_data)
scrobble = media_obj.scrobble_for_user(user_id, log=log_data)
if scrobble:
messages.add_message(

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.29 on 2026-06-22 02:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"trends",
"0002_alter_trendresult_unique_together_trendresult_period_and_more",
),
]
operations = [
migrations.AlterField(
model_name="trendresult",
name="period",
field=models.CharField(
choices=[
("last_30", "Last 30 days"),
("last_90", "Last 90 days"),
("last_year", "Last year"),
],
default="last_30",
max_length=20,
),
),
]

View File

@ -15,13 +15,11 @@ def natural_duration(value):
seconds = remainder % 60
parts = []
if days:
parts.append(f"{days} day{'s' if days != 1 else ''}")
parts.append(f"{days} d")
if hours:
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
parts.append(f"{hours} hr")
if minutes:
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
parts.append(f"{minutes} min")
if seconds or not parts:
parts.append(f"{seconds} second{'s' if seconds != 1 else ''}")
if len(parts) == 1:
return parts[0]
return ", ".join(parts[:-1]) + " and " + parts[-1]
parts.append(f"{seconds} sec")
return ", ".join(parts)

View File

@ -311,26 +311,13 @@
{% endblock %}
{% if now_playing_list and user.is_authenticated %}
<ul>
<b>Now playing</b>
{% for scrobble in now_playing_list %}
<div class="now-playing">
{% if scrobble.media_obj.primary_image_url %}<div style="float:left;padding-right:10px;padding-bottom:10px;"><img src="{{scrobble.media_obj.primary_image_url}}" /></div>{% endif %}
<p><a href="{{scrobble.get_absolute_url}}">{{scrobble.media_obj}}</a></p>
{% if scrobble.logdata %}{% if scrobble.logdata.title%}<p><em>{{scrobble.logdata.title}}</em></p>{% endif %}{% endif %}
<p><small>{{scrobble.local_timestamp|naturaltime}} from {{scrobble.source}}</small></p>
<div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
<p class="action-buttons">
<a href="{% url "scrobbles:cancel" scrobble.id %}">Cancel</a>
<a class="right" href="{% url "scrobbles:finish" scrobble.id %}">Finish</a>
</p>
{% if not forloop.last %}<hr/>{% endif %}
</div>
{% endfor %}
</ul>
{% if now_playing_list|length > 1 %}<hr/>{% endif %}
{% if user.profile.live_now_playing %}
<div id="now-playing-container" hx-get="{% url 'scrobbles:now-playing-partial' %}" hx-trigger="every 7s" hx-swap="innerHTML">
{% include "scrobbles/_now_playing.html" %}
</div>
{% else %}
{% include "scrobbles/_now_playing.html" %}
{% endif %}
{% endif %}
{% if active_imports %}

View File

@ -0,0 +1,21 @@
{% load humanize %}
<ul>
<b>Now playing</b>
{% for scrobble in now_playing_list %}
<div class="now-playing">
{% if scrobble.media_obj.primary_image_url %}<div style="float:left;padding-right:10px;padding-bottom:10px;"><img src="{{scrobble.media_obj.primary_image_url}}" /></div>{% endif %}
<p><a href="{{scrobble.get_absolute_url}}">{{scrobble.media_obj}}</a></p>
{% if scrobble.logdata %}{% if scrobble.logdata.title%}<p><em>{{scrobble.logdata.title}}</em></p>{% endif %}{% endif %}
<p><small>{{scrobble.local_timestamp|naturaltime}} from {{scrobble.source}}</small></p>
<div class="progress-bar" style="margin-right:5px;">
<span class="progress-bar-fill" style="width: {{scrobble.percent_played}}%;"></span>
</div>
<p class="action-buttons">
<a href="{% url "scrobbles:cancel" scrobble.id %}">Cancel</a>
<a class="right" href="{% url "scrobbles:finish" scrobble.id %}">Finish</a>
</p>
{% if not forloop.last %}<hr/>{% endif %}
</div>
{% endfor %}
</ul>
{% if now_playing_list|length > 1 %}<hr/>{% endif %}