[templates] Fix note parsing
Some checks failed
build / test (push) Has been cancelled

This commit is contained in:
2026-06-04 11:15:58 -04:00
parent 3f71065ad6
commit 087c7775ae
5 changed files with 158 additions and 30 deletions

View File

@ -93,7 +93,7 @@ fetching and simple saving.
:LOGBOOK:
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
:END:
* Backlog [4/19] :vrobbler:project:personal:
* Backlog [5/19] :vrobbler:project:personal:
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
:PROPERTIES:
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
@ -505,7 +505,7 @@ different. And the same for comments. If a comment (by timestamp key) is
different in the webhook than what's in the scrobble.log, update the comment in
the scrobble.log
** TODO [#B] For any scrobble detail page with notes display them better :templates:notes:scrobbles:
** DONE [#B] For any scrobble detail page with notes display them better :templates:notes:scrobbles:
:PROPERTIES:
:ID: c0dcf9da-227f-4a22-bcd9-9d46053607d9
:END:

View File

@ -83,43 +83,110 @@ class BaseLogData(JSONDataclass):
return ""
@staticmethod
def _format_timestamp(ts: str) -> str:
from datetime import datetime, timezone
import re
cleaned = ts.strip().strip("[]")
dt = None
if cleaned.isdigit():
try:
seconds = int(cleaned)
dt = datetime.fromtimestamp(seconds, tz=timezone.utc)
except (ValueError, OSError):
pass
if dt is None:
for fmt in [
"%Y-%m-%dT%H:%M:%S.%fZ",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S.%f",
"%Y-%m-%dT%H:%M:%S",
]:
try:
dt = datetime.strptime(cleaned, fmt)
break
except ValueError:
continue
if dt is None:
m = re.match(
r"(\d{4}-\d{2}-\d{2})\s+\w{3}\s+(\d{2}:\d{2})", cleaned
)
if m:
try:
dt = datetime.strptime(
f"{m.group(1)} {m.group(2)}", "%Y-%m-%d %H:%M"
)
except ValueError:
pass
if dt is None:
try:
dt = datetime.fromisoformat(cleaned)
except ValueError:
pass
if dt:
return dt.strftime("%b %-d, %Y %-I:%M %p")
return ts
def notes_as_html(self) -> str:
import html
import bleach
import markdown
from django.utils.safestring import mark_safe
if not self.notes:
return ""
html_notes = []
md = markdown.Markdown(extensions=["extra"])
allowed_tags = [
"p", "br", "strong", "em", "a", "ul", "ol", "li",
"code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6",
"hr", "img",
]
note_items = []
if isinstance(self.notes, dict):
for ts, text in self.notes.items():
note_text = " ".join(text.strip().split())
html_notes.append(
f'<div class="sticky-note">{ts}: {note_text}</div>'
)
for ts, text in sorted(self.notes.items(), reverse=True):
note_items.append((ts, text.strip()))
elif isinstance(self.notes, str):
for line in self.notes.split("\n"):
if line.strip():
escaped_line = html.escape(line)
html_notes.append(f'<div class="sticky-note">{escaped_line}</div>')
note_items.append((None, line.strip()))
elif isinstance(self.notes, list):
for note in self.notes:
if isinstance(note, dict):
timestamp, note_text = next(iter(note.items()))
note_text = " ".join(note_text.strip().split())
html_notes.append(
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
)
note_items.append((timestamp, note_text.strip()))
elif isinstance(note, str):
for line in note.split("\n"):
if line.strip():
escaped_line = html.escape(line)
html_notes.append(
f'<div class="sticky-note">{escaped_line}</div>'
)
note_items.append((None, line.strip()))
return mark_safe("".join(html_notes))
html_parts = []
for i, (ts, text) in enumerate(note_items):
if i > 0:
html_parts.append('<hr class="note-divider">')
ts_html = ""
if ts:
ts_html = f'<h5 class="note-timestamp">{self._format_timestamp(ts)}</h5>'
content_html = bleach.clean(
md.convert(text),
tags=allowed_tags,
strip=True,
)
html_parts.append(
f'<div class="note-item">{ts_html}<div class="note-content">{content_html}</div></div>'
)
return mark_safe("".join(html_parts))
@dataclass

View File

@ -76,33 +76,62 @@ class TaskLogData(BaseLogData):
return separator.join(lines).encode("utf-8").decode("unicode_escape")
def notes_as_html(self) -> str:
import bleach
import markdown
from django.utils.safestring import mark_safe
from scrobbles.dataclasses import BaseLogData
if not self.notes:
return ""
md = markdown.Markdown(extensions=["extra"])
allowed_tags = [
"p", "br", "strong", "em", "a", "ul", "ol", "li",
"code", "pre", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6",
"hr", "img",
]
notes = self.notes
if isinstance(notes, dict):
notes = [{k: v} for k, v in notes.items()]
if isinstance(notes, str):
notes = [notes]
html_notes = []
note_items = []
for note in notes:
if isinstance(note, dict):
timestamp, note_text = next(iter(note.items()))
note_text = " ".join(note_text.strip().split())
html_notes.append(
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
)
note_items.append((timestamp, note_text.strip()))
elif isinstance(note, str):
escaped = note.encode("utf-8").decode("unicode_escape")
for line in escaped.split("\n"):
if line.strip():
html_notes.append(f'<div class="sticky-note">{line}</div>')
note_items.append((None, line.strip()))
elif isinstance(note, list):
for item in note:
if isinstance(item, str):
html_notes.append(f'<div class="sticky-note">{item}</div>')
return "".join(html_notes)
note_items.append((None, item.strip()))
html_parts = []
for i, (ts, text) in enumerate(note_items):
if i > 0:
html_parts.append('<hr class="note-divider">')
ts_html = ""
if ts:
ts_html = f'<h5 class="note-timestamp">{BaseLogData._format_timestamp(ts)}</h5>'
content_html = bleach.clean(
md.convert(text),
tags=allowed_tags,
strip=True,
)
html_parts.append(
f'<div class="note-item">{ts_html}<div class="note-content">{content_html}</div></div>'
)
return mark_safe("".join(html_parts))
class Task(LongPlayScrobblableMixin):

View File

@ -168,6 +168,35 @@
}
#scrobble-form { width: 100% }
.notes-list {
max-width: 600px;
}
.note-item {
padding: 4px 0;
}
.note-timestamp {
font-size: 0.8em;
color: #666;
margin: 0 0 4px;
}
.note-content {
font-size: 0.95em;
line-height: 1.5;
}
.note-content p:last-child {
margin-bottom: 0;
}
.note-divider {
margin: 8px 0;
border: 0;
border-top: 1px solid #ddd;
}
.sticky-notes-container {
display: flex;
flex-wrap: wrap;
@ -218,6 +247,9 @@
a:not(.nav-link):not(.btn):not(.page-link):hover {
color: var(--accent);
}
a.badge {
color: #fff !important;
}
.table-striped > tbody > tr:nth-of-type(odd) {
background-color: color-mix(in srgb, var(--accent) 6%, #fff);
}

View File

@ -115,8 +115,8 @@
{% with notes_html=object.logdata.notes_as_html %}
{% if notes_html %}
<div class="mb-3">
<h5>Notes</h5>
<div class="sticky-notes-container">
<h4>Notes</h4>
<div class="notes-list">
{{ notes_html|safe }}
</div>
</div>