Compare commits

..

9 Commits
18.5 ... 18.12

Author SHA1 Message Date
1531b77b5c [tests] Fix metadata test 2025-07-30 13:59:11 -04:00
9437fdba60 [scrobbles] Fix log data parsing for tasks and boardgames
Add pagination to task and board game detail pages
2025-07-30 11:37:57 -04:00
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
2f6ed3770f [books] Fix bad import after moving webdav to importers 2025-07-26 01:49:37 -04:00
24 changed files with 216 additions and 79 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

@ -7,23 +7,24 @@ from rest_framework.authtoken.models import Token
from boardgames.models import BoardGame from boardgames.models import BoardGame
from music.models import Track, Artist from music.models import Track, Artist
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from people.models import Person
User = get_user_model() User = get_user_model()
@pytest.fixture @pytest.fixture
def boardgame_scrobble(): def boardgame_scrobble():
user = User.objects.create( first = Person.objects.create(name="First Player")
email="test@exmaple.com", first_name="Test", last_name="User" second = Person.objects.create(name="Second Player")
)
return Scrobble.objects.create( return Scrobble.objects.create(
board_game=BoardGame.objects.create(title="Test Board Game"), board_game=BoardGame.objects.create(title="Test Board Game"),
media_type="BoardGame", media_type="BoardGame",
played_to_completion=True, played_to_completion=True,
log={ log={
"players": [ "players": [
{"user_id": user.id, "win": True, "score": 30, "color": "Blue"} {"person_id": first.id, "win": True, "score": 30, "color": "Blue"},
] {"person_id": second.id, "win": False, "score": 28, "color": "Red"}
],
}, },
) )

View File

@ -3,14 +3,13 @@ import pytest
from scrobbles.dataclasses import BoardGameLogData, BoardGameScoreLogData from scrobbles.dataclasses import BoardGameLogData, BoardGameScoreLogData
@pytest.mark.skip("Need to get local tests running working again")
@pytest.mark.django_db @pytest.mark.django_db
def test_boardgame_log_data(boardgame_scrobble): def test_boardgame_log_data(boardgame_scrobble):
assert not boardgame_scrobble.geo_location
assert boardgame_scrobble.logdata == BoardGameLogData( assert boardgame_scrobble.logdata == BoardGameLogData(
players=[ players=[
BoardGameScoreLogData( BoardGameScoreLogData(
user_id=1, person_id=1,
name_str="",
bgg_username="", bgg_username="",
color="Blue", color="Blue",
character=None, character=None,
@ -18,10 +17,24 @@ def test_boardgame_log_data(boardgame_scrobble):
score=30, score=30,
win=True, win=True,
new=None, new=None,
) rank=None,
seat_order=None,
role=None
),
BoardGameScoreLogData(
person_id=2,
bgg_username="",
color="Red",
character=None,
team=None,
score=28,
win=False,
new=None,
rank=None,
seat_order=None,
role=None
),
], ],
location=None,
geo_location_id=None,
difficulty=None, difficulty=None,
solo=None, solo=None,
two_handed=None, two_handed=None,

View File

@ -9,7 +9,6 @@ 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 profiles.utils import one_off_fix_colins_profile
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import NtfyNotification
from stream_sqlite import stream_sqlite from stream_sqlite import stream_sqlite
from webdav.client import get_webdav_client 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"))) 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

@ -7,6 +7,7 @@ from typing import Optional
from dataclass_wizard import JSONWizard from dataclass_wizard import JSONWizard
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from locations.models import GeoLocation from locations.models import GeoLocation
from people.models import Person
User = get_user_model() User = get_user_model()
@ -66,8 +67,7 @@ class WithOthersLogData(JSONDataclass):
@dataclass @dataclass
class BoardGameScoreLogData(JSONDataclass): class BoardGameScoreLogData(JSONDataclass):
user_id: Optional[int] = None person_id: Optional[int] = None
name_str: str = ""
bgg_username: str = "" bgg_username: str = ""
color: Optional[str] = None color: Optional[str] = None
character: Optional[str] = None character: Optional[str] = None
@ -75,19 +75,19 @@ class BoardGameScoreLogData(JSONDataclass):
score: Optional[int] = None score: Optional[int] = None
win: Optional[bool] = None win: Optional[bool] = None
new: Optional[bool] = None new: Optional[bool] = None
rank: Optional[int] = None
seat_order: Optional[int] = None
role: Optional[str] = None
@property @property
def user(self) -> Optional[User]: def person(self) -> Optional[Person]:
user = None return Person.objects.filter(id=self.person_id).first()
if self.user_id:
user = User.objects.filter(id=self.user_id).first()
return user
@property @property
def name(self) -> str: def name(self) -> str:
name = self.name_str name = ""
if self.user_id: if self.person:
name = self.user.first_name name = self.person.name
return name return name
def __str__(self) -> str: def __str__(self) -> str:
@ -106,17 +106,20 @@ class BoardGameLogData(LongPlayLogData):
serial_scrobble_id: Optional[int] = None serial_scrobble_id: Optional[int] = None
long_play_complete: Optional[bool] = None long_play_complete: Optional[bool] = None
players: Optional[list[BoardGameScoreLogData]] = None players: Optional[list[BoardGameScoreLogData]] = None
location: Optional[str] = None location_id: Optional[int] = None
geo_location_id: Optional[int] = None
difficulty: Optional[int] = None difficulty: Optional[int] = None
solo: Optional[bool] = None solo: Optional[bool] = None
two_handed: Optional[bool] = None two_handed: Optional[bool] = None
expansion_ids: Optional[int] = None
@cached_property @cached_property
def geo_location(self) -> Optional[GeoLocation]: def geo_location(self) -> Optional[GeoLocation]:
if self.geo_location_id: if self.geo_location_id:
return GeoLocation.objects.filter(id=self.geo_location_id).first() return GeoLocation.objects.filter(id=self.geo_location_id).first()
@cached_property
def player_log(self) -> str:
return ", ".join([BoardGameScoreLogData(**player).__str__() for player in self.players])
@dataclass @dataclass
class BookPageLogData(JSONDataclass): class BookPageLogData(JSONDataclass):

View File

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

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,

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}"
@ -722,7 +736,7 @@ class Scrobble(TimeStampedModel):
if not log_dict: if not log_dict:
log_dict = {} log_dict = {}
return logdata_cls.from_dict(log_dict) return logdata_cls(**log_dict)
def redirect_url(self, user_id) -> str: def redirect_url(self, user_id) -> str:
user = User.objects.filter(id=user_id).first() user = User.objects.filter(id=user_id).first()

View File

@ -19,6 +19,8 @@ from django.views.generic import DetailView, FormView, TemplateView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from music.aggregators import live_charts, scrobble_counts, week_of_scrobbles from music.aggregators import live_charts, scrobble_counts, week_of_scrobbles
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from rest_framework import status from rest_framework import status
from rest_framework.decorators import ( from rest_framework.decorators import (
api_view, api_view,
@ -64,29 +66,46 @@ 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
class ScrobbleableDetailView(DetailView): class ScrobbleableDetailView(DetailView):
model = None model = None
slug_field = "uuid" slug_field = "uuid"
paginate_by = 200 # You can set this to whatever page size you want
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs) context_data = super().get_context_data(**kwargs)
context_data["scrobbles"] = list() scrobbles = []
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous:
context_data["scrobbles"] = self.object.scrobble_set.filter( scrobbles = self.object.scrobble_set.filter(
user=self.request.user user=self.request.user
) ).order_by("-timestamp")
paginator = Paginator(scrobbles, self.paginate_by)
page_number = self.request.GET.get("page")
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
context_data["page_obj"] = page_obj
context_data["scrobbles"] = page_obj.object_list
context_data["is_paginated"] = paginator.num_pages > 1
return context_data return context_data
@ -201,7 +220,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

@ -23,6 +23,16 @@ class TaskLogData(JSONDataclass):
todoist_type: Optional[str] = None todoist_type: Optional[str] = None
notes: Optional[dict] = None notes: Optional[dict] = None
def notes_as_str(self) -> str:
"""Return formatted notes with line breaks and no keys"""
note_block = ""
if not self.notes:
return note_block
for id, content in self.notes.items():
note_block += content + "</br>"
return note_block
class Task(LongPlayScrobblableMixin): class Task(LongPlayScrobblableMixin):
"""Basically a holder for Todoist Tasks """Basically a holder for Todoist Tasks
@ -42,9 +52,9 @@ class Task(LongPlayScrobblableMixin):
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Doing", tags="memo") return ScrobblableConstants(verb="Doing", tags="memo")
# @property @property
# def logdata_cls(self): def logdata_cls(self):
# return TaskLogData return TaskLogData
def source_url_for_user(self, user_id) -> str: def source_url_for_user(self, user_id) -> str:
url = "" url = ""

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>
@ -56,20 +56,40 @@
<tr> <tr>
<th scope="col">Date</th> <th scope="col">Date</th>
<th scope="col">Publisher</th> <th scope="col">Publisher</th>
<th scope="col">Screenshot</th> <th scope="col">Players</th>
</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>
<td>{% if scrobble.screenshot%}<img src="{{scrobble.screenshot.url}}" width=250 />{% endif %}</td> <td>{% if scrobble.logdata.player_log %}{{scrobble.logdata.player_log}}{% else %}No data{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">&laquo; Previous</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if num == page_obj.number %}
<strong>{{ num }}</strong>
{% else %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next &raquo;</a>
{% endif %}
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

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

@ -22,6 +22,7 @@
width: 600px; width: 600px;
margin-left: 10px; margin-left: 10px;
} }
.pagination a { padding: 0 5px 0 5px; }
</style> </style>
{% endblock %} {% endblock %}
@ -39,7 +40,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>
@ -47,27 +48,49 @@
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<h3>Last scrobbles</h3> <h3>Last scrobbles</h3>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">Date</th> <th scope="col">Date</th>
<th scope="col">Description</th> <th scope="col">Description</th>
<th scope="col">Notes</th>
<th scope="col">Source</th> <th scope="col">Source</th>
</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>
<td>{{scrobble.logdata.notes_as_str|safe}}</td>
<td>{{scrobble.source}}</td> <td>{{scrobble.source}}</td>
<td>{{scrobble.log.notes}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">&laquo; Previous</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if num == page_obj.number %}
<strong>{{ num }}</strong>
{% else %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next &raquo;</a>
{% endif %}
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

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>