Compare commits

...

11 Commits
18.8 ... 19.1

Author SHA1 Message Date
fc72b23b11 [music] Fix timezones for TSV imports 2025-08-02 23:35:22 -04:00
a681b4d63b [notifications] Fix a few typos 2025-07-30 18:34:22 -04:00
c452ac24e0 [notifications] Send mood check-in 2025-07-30 18:30:18 -04:00
ae889bff7d [tasks] Fix bug in note str method 2025-07-30 17:50:59 -04:00
99dc86dc27 [moods] Fix mood list view 2025-07-30 16:05:48 -04:00
8eefcb8290 [tasks] Fix emacs metadata 2025-07-30 16:05:34 -04:00
ad0f9a54d0 [tasks] Fix dataclass models 2025-07-30 15:46:18 -04:00
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
20 changed files with 280 additions and 64 deletions

View File

@ -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 19.0
** DONE Add periodic check for mood :vrobbler:feature:moods:personal:project:
:PROPERTIES:
:ID: 55404488-c69f-0dd5-838e-1d1e15c873eb
:END:
* Version 18.7 * Version 18.7
** DONE Use the timezone history log to fix old Scrobbles that fall into those timezone blocks :vrobbler:chore:scrobbles:project:personal: ** DONE Use the timezone history log to fix old Scrobbles that fall into those timezone blocks :vrobbler:chore:scrobbles:project:personal:
:PROPERTIES: :PROPERTIES:

File diff suppressed because one or more lines are too long

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

@ -3,7 +3,7 @@ from boardgames.models import BoardGame
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import ScrobbleNtfyNotification
User = get_user_model() User = get_user_model()
@ -124,5 +124,5 @@ def import_chess_games_for_all_users():
if scrobbles_to_create: if scrobbles_to_create:
created = Scrobble.objects.bulk_create(scrobbles_to_create) created = Scrobble.objects.bulk_create(scrobbles_to_create)
for scrobble in created: for scrobble in created:
NtfyNotification(scrobble).send() ScrobbleNtfyNotification(scrobble).send()
return scrobbles_to_create return scrobbles_to_create

View File

@ -9,7 +9,7 @@ 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 scrobbles.notifications import NtfyNotification from scrobbles.notifications import ScrobbleNtfyNotification
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
@ -409,7 +409,7 @@ def process_koreader_sqlite_file(file_path, user_id) -> list:
if new_scrobbles: if new_scrobbles:
created = Scrobble.objects.bulk_create(new_scrobbles) created = Scrobble.objects.bulk_create(new_scrobbles)
if created: if created:
NtfyNotification(created[-1]).send() ScrobbleNtfyNotification(created[-1]).send()
fix_long_play_stats_for_scrobbles(created) fix_long_play_stats_for_scrobbles(created)
logger.info( logger.info(
f"Created {len(created)} scrobbles", f"Created {len(created)} scrobbles",

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.19 on 2025-07-30 22:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('profiles', '0026_userprofile_timezone_change_log'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='mood_checkin_enabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='userprofile',
name='mood_checkin_frequency',
field=models.CharField(default='hourly', max_length=20),
),
]

View File

@ -1,6 +1,5 @@
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import pendulum import pendulum
from datetime import datetime
from django.utils import timezone from django.utils import timezone
import logging import logging
from django.conf import settings from django.conf import settings
@ -56,6 +55,9 @@ class UserProfile(TimeStampedModel):
imap_pass = EncryptedField(**BNULL) imap_pass = EncryptedField(**BNULL)
imap_auto_import = models.BooleanField(default=False) imap_auto_import = models.BooleanField(default=False)
mood_checkin_enabled = models.BooleanField(default=False)
mood_checkin_frequency = models.CharField(max_length=20, default="hourly")
ntfy_url = models.CharField(max_length=255, **BNULL) ntfy_url = models.CharField(max_length=255, **BNULL)
ntfy_enabled = models.BooleanField(default=False) ntfy_enabled = models.BooleanField(default=False)

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,27 @@ 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
rank: Optional[int] = None
seat_order: Optional[int] = None
role: Optional[str] = None
lichess_username: Optional[str] = None
# Legacy
user_id: Optional[int] = None
name_str: 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 +114,34 @@ 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
moves: Optional[list] = None
rated: Optional[str] = None
speed: Optional[str] = None
variant: Optional[str] = None
lichess_id: Optional[int] = None
board: Optional[str] = None
rounds: Optional[int] = None
details: Optional[str] = None
# Legacy
learning: Optional[bool] = None
location: Optional[str] = None
geo_location_id: Optional[int] = None
scenario: Optional[str] = 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:
if self.players:
return ", ".join([BoardGameScoreLogData(**player).__str__() for player in self.players])
return ""
@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

@ -2,6 +2,7 @@ import codecs
import csv import csv
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import requests import requests
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -61,10 +62,10 @@ def import_audioscrobbler_tsv_file(file_path, user_id):
}, },
) )
continue continue
timestamp = datetime.fromtimestamp(
timestamp = user.profile.get_timestamp_with_tz( int(row[AsTsvColumn["TIMESTAMP"].value])
datetime.fromtimestamp(int(row[AsTsvColumn["TIMESTAMP"].value])) ).astimezone(ZoneInfo("UTC"))
) timestamp = user.profile.get_timestamp_with_tz(timestamp)
stop_timestamp = timestamp + timedelta(seconds=track.run_time_seconds) stop_timestamp = timestamp + timedelta(seconds=track.run_time_seconds)
new_scrobble = Scrobble( new_scrobble = Scrobble(

View File

@ -0,0 +1,10 @@
from django.core.management.base import BaseCommand
from vrobbler.apps.scrobbles.utils import (
send_mood_checkin_reminders
)
class Command(BaseCommand):
def handle(self, *args, **options):
sent_count = send_mood_checkin_reminders()
print(f"Sent {sent_count} mood check-in notifications")

View File

@ -42,7 +42,7 @@ from puzzles.models import Puzzle
from scrobbles import dataclasses as logdata from scrobbles import dataclasses as logdata
from scrobbles.constants import LONG_PLAY_MEDIA, MEDIA_END_PADDING_SECONDS from scrobbles.constants import LONG_PLAY_MEDIA, MEDIA_END_PADDING_SECONDS
from scrobbles.importers.lastfm import LastFM from scrobbles.importers.lastfm import LastFM
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import ScrobbleNtfyNotification
from scrobbles.stats import build_charts from scrobbles.stats import build_charts
from scrobbles.utils import get_file_md5_hash, media_class_to_foreign_key from scrobbles.utils import get_file_md5_hash, media_class_to_foreign_key
from sports.models import SportEvent from sports.models import SportEvent
@ -736,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()
@ -1316,7 +1316,7 @@ class Scrobble(TimeStampedModel):
scrobble_data: dict, scrobble_data: dict,
) -> "Scrobble": ) -> "Scrobble":
scrobble = cls.objects.create(**scrobble_data) scrobble = cls.objects.create(**scrobble_data)
NtfyNotification(scrobble).send() ScrobbleNtfyNotification(scrobble).send()
return scrobble return scrobble
def stop(self, timestamp=None, force_finish=False) -> None: def stop(self, timestamp=None, force_finish=False) -> None:

View File

@ -3,13 +3,26 @@ import requests
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.urls import reverse
class Notification(ABC): class BasicNtfyNotification(ABC):
scrobble: "Scrobble"
ntfy_headers: dict = {} ntfy_headers: dict = {}
ntfy_url: str = "" ntfy_url: str = ""
title: str = "" title: str = ""
def __init__(self, profile: "UserProfile"):
self.profile = profile.user
protocol = "http" if settings.DEBUG else "https"
domain = Site.objects.get_current().domain
self.url_tmpl = f'{protocol}://{domain}' + '{path}'
@abstractmethod
def send(self) -> None:
pass
class ScrobbleNotification(BasicNtfyNotification):
scrobble: "Scrobble"
def __init__(self, scrobble: "Scrobble"): def __init__(self, scrobble: "Scrobble"):
self.scrobble = scrobble self.scrobble = scrobble
self.user = scrobble.user self.user = scrobble.user
@ -19,13 +32,12 @@ class Notification(ABC):
self.url_tmpl = f'{protocol}://{domain}' + '{path}' self.url_tmpl = f'{protocol}://{domain}' + '{path}'
@abstractmethod @abstractmethod
def send(self) -> None: def send(self) -> None:
pass pass
class NtfyNotification(Notification): class ScrobbleNtfyNotification(ScrobbleNotification):
def __init__(self, scrobble, **kwargs): def __init__(self, scrobble, **kwargs):
super().__init__(scrobble) super().__init__(scrobble)
self.ntfy_str: str = f"{self.scrobble.media_obj}" self.ntfy_str: str = f"{self.scrobble.media_obj}"
@ -55,3 +67,27 @@ class NtfyNotification(Notification):
"Click": self.click_url, "Click": self.click_url,
}, },
) )
class MoodNtfyNotification(BasicNtfyNotification):
def __init__(self, profile, **kwargs):
super().__init__(profile)
self.ntfy_str: str = "Would you like to check in about your mood?"
self.click_url = self.url_tmpl.format(path=reverse("moods:mood-list"))
self.title = "Mood Check-in!"
def send(self):
if (
self.profile
and self.profile.ntfy_enabled
and self.profile.ntfy_url
):
requests.post(
self.profile.ntfy_url,
data=self.ntfy_str.encode(encoding="utf-8"),
headers={
"Title": self.title,
"Priority": "high",
"Tags": "smiley, check",
"Click": self.click_url,
},
)

View File

@ -26,7 +26,7 @@ from scrobbles.constants import (
SCROBBLE_CONTENT_URLS, SCROBBLE_CONTENT_URLS,
) )
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import ScrobbleNtfyNotification
from scrobbles.utils import convert_to_seconds, extract_domain from scrobbles.utils import convert_to_seconds, extract_domain
from sports.models import SportEvent from sports.models import SportEvent
from sports.thesportsdb import lookup_event_from_thesportsdb from sports.thesportsdb import lookup_event_from_thesportsdb
@ -505,7 +505,7 @@ def email_scrobble_board_game(
scrobble.played_to_completion = True scrobble.played_to_completion = True
scrobble.save() scrobble.save()
scrobbles_created.append(scrobble) scrobbles_created.append(scrobble)
NtfyNotification(scrobble).send() ScrobbleNtfyNotification(scrobble).send()
return scrobbles_created return scrobbles_created

View File

@ -13,7 +13,7 @@ from django.utils import timezone
from profiles.models import UserProfile from profiles.models import UserProfile
from profiles.utils import now_user_timezone from profiles.utils import now_user_timezone
from scrobbles.constants import LONG_PLAY_MEDIA from scrobbles.constants import LONG_PLAY_MEDIA
from scrobbles.notifications import NtfyNotification from scrobbles.notifications import MoodNtfyNotification, ScrobbleNtfyNotification
from scrobbles.tasks import ( from scrobbles.tasks import (
process_koreader_import, process_koreader_import,
process_lastfm_import, process_lastfm_import,
@ -318,7 +318,20 @@ def send_stop_notifications_for_in_progress_scrobbles() -> int:
).seconds ).seconds
if elapsed_scrobble_seconds > scrobble.media_obj.run_time_seconds: if elapsed_scrobble_seconds > scrobble.media_obj.run_time_seconds:
NtfyNotification(scrobble, end=True).send() ScrobbleNtfyNotification(scrobble, end=True).send()
notifications_sent += 1
return notifications_sent
def send_mood_checkin_reminders() -> int:
"""Get all profiles with mood check-ins enabled and checkin!"""
from profiles.models import UserProfile
now = timezone.now()
notifications_sent = 0
for profile in UserProfile.objects.filter(mood_checkin_enabled=True):
if profile.mood_checkin_frequency == "hourly" and now.minute == 0:
MoodNtfyNotification(profile).send()
notifications_sent += 1 notifications_sent += 1
return notifications_sent return notifications_sent

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,
@ -54,6 +56,7 @@ from scrobbles.utils import (
get_long_plays_completed, get_long_plays_completed,
get_long_plays_in_progress, get_long_plays_in_progress,
) )
from moods.models import Mood
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -64,30 +67,48 @@ class ScrobbleableListView(ListView):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
if self.model == Mood:
return queryset
user_filter = Q() user_filter = Q()
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous:
user_filter = Q(scrobble__user=self.request.user) user_filter = Q(scrobble__user=self.request.user)
queryset = ( queryset = (
queryset.annotate( queryset.filter(user_filter).annotate(
scrobble_count=Count("scrobble"), scrobble_count=Count("scrobble")
) ).filter(scrobble_count__gt=0).order_by("-scrobble_count")
.filter(user_filter, scrobble_count__gt=0)
.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") ).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

View File

@ -18,10 +18,34 @@ class TaskLogData(JSONDataclass):
description: Optional[str] = None description: Optional[str] = None
title: Optional[str] = None title: Optional[str] = None
project: Optional[str] = None project: Optional[str] = None
notes: Optional[dict] = None
updated_at: Optional[str] = None
todoist_id: Optional[str] = None todoist_id: Optional[str] = None
todoist_event: Optional[str] = None todoist_event: Optional[str] = None
todoist_type: Optional[str] = None todoist_type: Optional[str] = None
notes: Optional[dict] = None todoist_type: Optional[str] = None
todoist_label_list: Optional[list] = None
todoist_project_id: Optional[str] = None
body: Optional[str] = None
state: Optional[str] = None
labels: Optional[str] = None
properties: Optional[list] = None
drawers: Optional[list] = None
source: Optional[str] = None
source_id: Optional[str] = None
timestamps: Optional[list] = None
def notes_as_str(self) -> str:
"""Return formatted notes with line breaks and no keys"""
note_block = ""
if isinstance(self.notes, list):
note_block = "</br>".join(self.notes)
if isinstance(self.notes, dict):
for id, content in self.notes.items():
note_block += content + "</br>"
return note_block
class Task(LongPlayScrobblableMixin): class Task(LongPlayScrobblableMixin):
@ -42,9 +66,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

@ -56,7 +56,7 @@
<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>
@ -64,12 +64,32 @@
<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

@ -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 %}
@ -47,12 +48,14 @@
<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>
@ -61,13 +64,33 @@
<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 %}