Compare commits

...

6 Commits
18.6 ... 18.10

Author SHA1 Message Date
a7551ef162 [music] Weird hack to get timezone for LFM scrobbles
Last.fm seems to send timestamps for scrobbles with a timezone of UTC
but the actual timezone is already localized. But that means we can't
extract the timezone we want, even though the timestamp is already in
the right timezone for storage.
2025-07-28 10:52:02 -04:00
c20204a6ea [music] Turns out lastfm already has our timeszone 2025-07-28 09:14:25 -04:00
685de842ea [views] Fix showing only a users scrobbles 2025-07-26 21:31:44 -04:00
7d13967708 [scrobbles] Fix admin filtering 2025-07-26 20:57:23 -04:00
109697a746 [project] Bump version 2025-07-26 10:19:34 -04:00
dde28f4aff [importers] Fix setting timezones before all imports 2025-07-26 10:18:43 -04:00
19 changed files with 91 additions and 44 deletions

View File

@ -79,7 +79,7 @@ fetching and simple saving.
:LOGBOOK:
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
:END:
* Backlog [7/28]
* Backlog [1/22]
** TODO [#A] Add classmethod for metadata fetching to tracks :vrobbler:feature:music:personal:project:
:PROPERTIES:
:ID: bc4b45e5-4c65-13c5-ab7b-1937d3fbf5c2
@ -443,6 +443,11 @@ it's annoying.
** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature:
https://codepen.io/oliviale/pen/QYqybo
** TODO [#C] Come up with a possible flow using WebDAV and super-productivity for tasks :personal:feature:project:vrobbler:tasks:
* Version 18.7
** DONE Use the timezone history log to fix old Scrobbles that fall into those timezone blocks :vrobbler:chore:scrobbles:project:personal:
:PROPERTIES:
:ID: 9d055ac1-584b-20c8-7ad9-9ce36b329dc7
:END:
* Version 18.4
** DONE Track timezone changes for profiles :vrobbler:feature:profiles:personal:project:
:PROPERTIES:

View File

@ -9,7 +9,6 @@ import requests
from books.constants import BOOKS_TITLES_TO_IGNORE
from django.apps import apps
from django.contrib.auth import get_user_model
from profiles.utils import one_off_fix_colins_profile
from scrobbles.notifications import NtfyNotification
from stream_sqlite import stream_sqlite
from webdav.client import get_webdav_client
@ -286,9 +285,6 @@ def build_scrobbles_from_book_map(
datetime.fromtimestamp(int(last_page.get("end_ts")))
)
if user.id == 1 and not user.profile.timezone_change_log:
one_off_fix_colins_profile(user.profile)
# Adjust for Daylight Saving Time
if timestamp.dst() == timedelta(
0

View File

@ -115,6 +115,28 @@ class UserProfile(TimeStampedModel):
return timestamp.replace(tzinfo=timezone)
def adjust_timezone_of_scrobbles(self, commit=False):
current_dt = None
scrobbles_to_change_qs_list = []
for boundry_dt in self.historic_timezone_changes:
if current_dt and boundry_dt:
logger.info(
f"Checking for scrobbles between {current_dt} and {boundry_dt} to update to {current_dt.tzinfo.name}"
)
scrobbles = self.user.scrobble_set.filter(
timestamp__gte=current_dt,
timestamp__lt=boundry_dt,
).exclude(timezone=current_dt.tzinfo.name)
scrobbles_to_change_qs_list.append(scrobbles)
logger.info(
f"Updating {scrobbles.count()} scrobble timezones to {current_dt.tzinfo.name}"
)
if commit:
scrobbles.update(timezone=current_dt.tzinfo.name)
current_dt = boundry_dt
return scrobbles_to_change_qs_list
@cached_property
def task_context_tags(self) -> list[str]:
tag_list = [

View File

@ -59,15 +59,15 @@ def start_of_year(dt, profile) -> datetime:
return start_of_day(dt, profile).replace(month=1, day=1)
def one_off_fix_colins_profile(profile):
def fix_profile_historic_timezones(profile):
home_tz = "America/New_York"
europe = "2022-10-15 06:00:00"
europe = "2023-10-15 06:00:00"
europe_end = "2023-12-16 12:00:00"
europe_tz = "Europe/Paris"
washington = "2023-04-28 06:00:00"
washington_end = "2023-05-04 12:00:00"
washington = "2024-04-28 06:00:00"
washington_end = "2024-05-04 12:00:00"
washington_tz = "America/Los_Angeles"
camp = "2024-08-04 17:00:00"
@ -78,6 +78,8 @@ def one_off_fix_colins_profile(profile):
summer_end = "2025-07-11 23:30:00"
summer_tz = "America/Los_Angeles"
profile.timezone_change_log = None
profile.timezone_change_log = ""
profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n"
profile.timezone_change_log += (

View File

@ -102,7 +102,7 @@ class ChartRecordAdmin(admin.ModelAdmin):
@admin.register(Scrobble)
class ScrobbleAdmin(admin.ModelAdmin):
# date_hierarchy = "timestamp"
date_hierarchy = "timestamp"
list_display = (
"timestamp",
"media_name",
@ -112,6 +112,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"in_progress",
"is_paused",
"played_to_completion",
"user",
)
raw_id_fields = (
"video",
@ -140,6 +141,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"long_play_complete",
"source",
"timezone",
"user",
)
ordering = ("-timestamp",)
@ -148,3 +150,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
def playback_percent(self, obj):
return obj.percent_played
def get_queryset(self, request):
qs = super().get_queryset(request).exclude(timestamp__year=None)
return qs

View File

@ -50,9 +50,10 @@ class LastFM:
enrich=True,
)
timestamp = self.vrobbler_user.profile.get_timestamp_with_tz(
tz_timestamp = self.vrobbler_user.profile.get_timestamp_with_tz(
lfm_scrobble.get("timestamp")
)
timestamp = lfm_scrobble.get("timestamp")
stop_timestamp = timestamp + timedelta(
seconds=track.run_time_seconds
)
@ -65,7 +66,7 @@ class LastFM:
played_to_completion=True,
in_progress=False,
media_type=Scrobble.MediaType.TRACK,
timezone=timestamp.tzinfo.name,
timezone=tz_timestamp.tzinfo.name,
)
# Vrobbler scrobbles on finish, LastFM scrobbles on start
seconds_eariler = timestamp - timedelta(seconds=20)

View File

@ -33,6 +33,7 @@ from profiles.utils import (
end_of_day,
end_of_month,
end_of_week,
fix_profile_historic_timezones,
start_of_day,
start_of_month,
start_of_week,
@ -205,6 +206,9 @@ class KoReaderImport(BaseFileImportMixin):
def process(self, force=False):
if self.user.id == 1:
fix_profile_historic_timezones(self.user.profile)
if self.processed_finished and not force:
logger.info(
f"{self} already processed on {self.processed_finished}"
@ -250,6 +254,9 @@ class AudioScrobblerTSVImport(BaseFileImportMixin):
def process(self, force=False):
from scrobbles.importers.tsv import import_audioscrobbler_tsv_file
if self.user.id == 1:
fix_profile_historic_timezones(self.user.profile)
if self.processed_finished and not force:
logger.info(
f"{self} already processed on {self.processed_finished}"
@ -280,6 +287,10 @@ class LastFmImport(BaseFileImportMixin):
def process(self, import_all=False):
"""Import scrobbles found on LastFM"""
if self.user.id == 1:
fix_profile_historic_timezones(self.user.profile)
if self.processed_finished:
logger.info(
f"{self} already processed on {self.processed_finished}"
@ -327,6 +338,9 @@ class RetroarchImport(BaseFileImportMixin):
def process(self, import_all=False, force=False):
"""Import scrobbles found on Retroarch"""
if self.user.id == 1:
fix_profile_historic_timezones(self.user.profile)
if self.processed_finished and not force:
logger.info(
f"{self} already processed on {self.processed_finished}"

View File

@ -64,15 +64,16 @@ class ScrobbleableListView(ListView):
def get_queryset(self):
queryset = super().get_queryset()
user_filter = Q()
if not self.request.user.is_anonymous:
queryset = queryset.annotate(
user_filter = Q(scrobble__user=self.request.user)
queryset = (
queryset.annotate(
scrobble_count=Count("scrobble"),
filter=Q(scrobble__user=self.request.user),
).order_by("-scrobble_count")
else:
queryset = queryset.annotate(
scrobble_count=Count("scrobble")
).order_by("-scrobble_count")
)
.filter(user_filter, scrobble_count__gt=0)
.order_by("-scrobble_count")
)
return queryset
@ -86,7 +87,7 @@ class ScrobbleableDetailView(DetailView):
if not self.request.user.is_anonymous:
context_data["scrobbles"] = self.object.scrobble_set.filter(
user=self.request.user
)
).order_by("-timestamp")
return context_data
@ -201,7 +202,7 @@ class RecentScrobbleList(ListView):
processed_finished__isnull=True,
user=self.request.user,
)
data["counts"] = [] #scrobble_counts(user)
data["counts"] = [] # scrobble_counts(user)
else:
data["weekly_data"] = week_of_scrobbles()
data["counts"] = scrobble_counts()

View File

@ -39,7 +39,7 @@
</div>
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
<p>{{scrobbles.count}} scrobbles</p>
<p>
<a href="{{object.start_url}}">Drink again</a>
</p>
@ -55,7 +55,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
</tr>

View File

@ -42,7 +42,7 @@
</div>
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
<p>{{scrobbles.count}} scrobbles</p>
<p>
<a href="{{object.start_url}}">Play again</a>
</p>
@ -60,7 +60,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
<td>{{scrobble.media_obj.publisher}}</td>

View File

@ -26,10 +26,10 @@
</div>
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
<p>Read {{object.scrobble_set.last.book_pages_read}} pages{% if object.scrobble_set.last.long_play_complete %} and completed{% else %}{% endif %}</p>
<p>{{scrobbles.count}} scrobbles</p>
<p>Read {{scrobbles.last.book_pages_read}} pages{% if scrobbles.last.long_play_complete %} and completed{% else %}{% endif %}</p>
<p>
{% if object.scrobble_set.last.long_play_complete == True %}
{% if scrobbles.last.long_play_complete == True %}
<a href="">Read again</a>
{% else %}
<a href="">Resume reading</a>
@ -50,7 +50,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
<td>{% if scrobble.long_play_complete == True %}Yes{% endif %}</td>

View File

@ -48,7 +48,7 @@
</div>
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
<p>{{scrobbles.count}} scrobbles</p>
<div class="row">
<div class="col-md">
<h3>Last scrobbles</h3>
@ -60,7 +60,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr>
<td>{{scrobble.local_timestamp|naturaltime}}</td>
</tr>

View File

@ -26,7 +26,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
{% for scrobble in scrobbles.all %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
</tr>

View File

@ -9,7 +9,7 @@
{% endif %}
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
<p>{{scrobbles.count}} scrobbles</p>
{% if charts %}
<p>{% for chart in charts %}<em><a href="{{chart.link}}">{{chart}}</a></em>{% if forloop.last %}{% else %} | {% endif %}{% endfor %}</p>
{% endif %}
@ -26,7 +26,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
{% for scrobble in scrobbles.all %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
<td><a href="{{scrobble.track.get_absolute_url}}">{{scrobble.track.title}}</a></td>

View File

@ -18,7 +18,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
{% for scrobble in scrobbles.all %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
<td>{{scrobble.media_obj.round.season.name}}</td>

View File

@ -39,7 +39,7 @@
</div>
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
<p>{{scrobbles.count}} scrobbles</p>
<p>
<a href="{{object.start_url}}">Play again</a>
</p>
@ -57,7 +57,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
<td><a href="{{scrobble.get_media_source_url}}">{{scrobble.logdata.description}}</a></td>

View File

@ -59,12 +59,12 @@
</div>
</div>
<div class="row">
<p>{{object.scrobble_set.count}} scrobbles</p>
{% if object.scrobble_set.last.long_play_seconds %}
<p>{{object.scrobble_set.last.long_play_seconds|natural_duration}}{% if object.scrobble_set.last.long_play_complete %} and completed{% else %} spent playing{% endif %}</p>
<p>{{scrobbles.count}} scrobbles</p>
{% if scrobbles.last.long_play_seconds %}
<p>{{scrobbles.last.long_play_seconds|natural_duration}}{% if scrobbles.last.long_play_complete %} and completed{% else %} spent playing{% endif %}</p>
{% endif %}
<p>
{% if object.scrobble_set.last.long_play_complete == True %}
{% if scrobbles.last.long_play_complete == True %}
<a href="">Play again</a>
{% else %}
<a href="{{object.start_url}}">Resume playing</a>
@ -86,7 +86,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all|dictsortreversed:"timestamp" %}
{% for scrobble in scrobbles.all|dictsortreversed:"timestamp" %}
<tr>
<td>{{scrobble.local-timestamp}}</td>
<td>{% if scrobble.long_play_complete == True %}Yes{% else %}Not yet{% endif %}</td>

View File

@ -85,7 +85,7 @@ dd {
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
{% for scrobble in scrobbles.all %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
</tr>

View File

@ -51,7 +51,7 @@
</tr>
</thead>
<tbody>
{% for scrobble in object.scrobble_set.all %}
{% for scrobble in scrobbles.all %}
<tr>
<td>{{scrobble.local_timestamp}}</td>
</tr>