Compare commits

...

7 Commits
18.4 ... 18.9

20 changed files with 96 additions and 51 deletions

View File

@ -79,7 +79,7 @@ fetching and simple saving.
:LOGBOOK: :LOGBOOK:
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
:END: :END:
* Backlog [7/28] * Backlog [1/22]
** TODO [#A] Add classmethod for metadata fetching to tracks :vrobbler:feature:music:personal:project: ** TODO [#A] Add classmethod for metadata fetching to tracks :vrobbler:feature:music:personal:project:
:PROPERTIES: :PROPERTIES:
:ID: bc4b45e5-4c65-13c5-ab7b-1937d3fbf5c2 :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: ** TODO [#C] Allow users to see tasks on calendar view :vrobbler:personal:project:templates:feature:
https://codepen.io/oliviale/pen/QYqybo 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: ** 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 * Version 18.4
** DONE Track timezone changes for profiles :vrobbler:feature:profiles:personal:project: ** DONE Track timezone changes for profiles :vrobbler:feature:profiles:personal:project:
:PROPERTIES: :PROPERTIES:

View File

@ -3,16 +3,14 @@ import re
import sqlite3 import sqlite3
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
import pendulum
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import requests import requests
from books.constants import BOOKS_TITLES_TO_IGNORE from books.constants import BOOKS_TITLES_TO_IGNORE
from django.apps import apps from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from stream_sqlite import stream_sqlite
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import NtfyNotification
from vrobbler.apps.profiles.utils import one_off_fix_colins_profile from stream_sqlite import stream_sqlite
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -287,9 +285,6 @@ def build_scrobbles_from_book_map(
datetime.fromtimestamp(int(last_page.get("end_ts"))) 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 # Adjust for Daylight Saving Time
if timestamp.dst() == timedelta( if timestamp.dst() == timedelta(
0 0

View File

@ -115,6 +115,28 @@ class UserProfile(TimeStampedModel):
return timestamp.replace(tzinfo=timezone) 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 @cached_property
def task_context_tags(self) -> list[str]: def task_context_tags(self) -> list[str]:
tag_list = [ 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) 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" 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_end = "2023-12-16 12:00:00"
europe_tz = "Europe/Paris" europe_tz = "Europe/Paris"
washington = "2023-04-28 06:00:00" washington = "2024-04-28 06:00:00"
washington_end = "2023-05-04 12:00:00" washington_end = "2024-05-04 12:00:00"
washington_tz = "America/Los_Angeles" washington_tz = "America/Los_Angeles"
camp = "2024-08-04 17:00:00" 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_end = "2025-07-11 23:30:00"
summer_tz = "America/Los_Angeles" summer_tz = "America/Los_Angeles"
profile.timezone_change_log = None
profile.timezone_change_log = "" profile.timezone_change_log = ""
profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n" profile.timezone_change_log += f"{europe_tz} - {pendulum.parse(europe)}\n"
profile.timezone_change_log += ( profile.timezone_change_log += (

View File

@ -102,7 +102,7 @@ class ChartRecordAdmin(admin.ModelAdmin):
@admin.register(Scrobble) @admin.register(Scrobble)
class ScrobbleAdmin(admin.ModelAdmin): class ScrobbleAdmin(admin.ModelAdmin):
# date_hierarchy = "timestamp" date_hierarchy = "timestamp"
list_display = ( list_display = (
"timestamp", "timestamp",
"media_name", "media_name",
@ -112,6 +112,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"in_progress", "in_progress",
"is_paused", "is_paused",
"played_to_completion", "played_to_completion",
"user",
) )
raw_id_fields = ( raw_id_fields = (
"video", "video",
@ -140,6 +141,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
"long_play_complete", "long_play_complete",
"source", "source",
"timezone", "timezone",
"user",
) )
ordering = ("-timestamp",) ordering = ("-timestamp",)
@ -148,3 +150,7 @@ class ScrobbleAdmin(admin.ModelAdmin):
def playback_percent(self, obj): def playback_percent(self, obj):
return obj.percent_played 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,7 @@ class LastFM:
enrich=True, enrich=True,
) )
timestamp = self.vrobbler_user.profile.get_timestamp_with_tz( timestamp = lfm_scrobble.get("timestamp")
lfm_scrobble.get("timestamp")
)
stop_timestamp = timestamp + timedelta( stop_timestamp = timestamp + timedelta(
seconds=track.run_time_seconds seconds=track.run_time_seconds
) )

View File

@ -1,9 +1,11 @@
import logging
from books.koreader import fetch_file_from_webdav from books.koreader import fetch_file_from_webdav
from profiles.models import UserProfile from profiles.models import UserProfile
from scrobbles.models import KoReaderImport from scrobbles.models import KoReaderImport
from scrobbles.tasks import process_koreader_import from scrobbles.tasks import process_koreader_import
from scrobbles.utils import get_file_md5_hash
from webdav.client import get_webdav_client from webdav.client import get_webdav_client
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -11,7 +13,7 @@ logger = logging.getLogger(__name__)
def import_from_webdav_for_all_users(restart=False): def import_from_webdav_for_all_users(restart=False):
"""Grab a list of all users with WebDAV enabled and kickoff imports for them""" """Grab a list of all users with WebDAV enabled and kickoff imports for them"""
# LastFmImport = apps.get_model("scrobbles", "LastFMImport") # WebDavImport = apps.get_model("scrobbles", "WebDavImport")
webdav_enabled_user_ids = UserProfile.objects.filter( webdav_enabled_user_ids = UserProfile.objects.filter(
webdav_url__isnull=False, webdav_url__isnull=False,
webdav_user__isnull=False, webdav_user__isnull=False,
@ -42,7 +44,7 @@ def import_from_webdav_for_all_users(restart=False):
KoReaderImport.objects.filter( KoReaderImport.objects.filter(
user_id=user_id, processed_finished__isnull=False user_id=user_id, processed_finished__isnull=False
) )
.order_by("Processed_finished") .order_by("processed_finished")
.last() .last()
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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