[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: :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 [4/19] :vrobbler:project:personal: * Backlog [5/19] :vrobbler:project:personal:
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: ** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
:PROPERTIES: :PROPERTIES:
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85 :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 different in the webhook than what's in the scrobble.log, update the comment in
the scrobble.log 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: :PROPERTIES:
:ID: c0dcf9da-227f-4a22-bcd9-9d46053607d9 :ID: c0dcf9da-227f-4a22-bcd9-9d46053607d9
:END: :END:

View File

@ -83,43 +83,110 @@ class BaseLogData(JSONDataclass):
return "" 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: def notes_as_html(self) -> str:
import html import bleach
import markdown
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
if not self.notes: if not self.notes:
return "" 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): if isinstance(self.notes, dict):
for ts, text in self.notes.items(): for ts, text in sorted(self.notes.items(), reverse=True):
note_text = " ".join(text.strip().split()) note_items.append((ts, text.strip()))
html_notes.append(
f'<div class="sticky-note">{ts}: {note_text}</div>'
)
elif isinstance(self.notes, str): elif isinstance(self.notes, str):
for line in self.notes.split("\n"): for line in self.notes.split("\n"):
if line.strip(): if line.strip():
escaped_line = html.escape(line) note_items.append((None, line.strip()))
html_notes.append(f'<div class="sticky-note">{escaped_line}</div>')
elif isinstance(self.notes, list): elif isinstance(self.notes, list):
for note in self.notes: for note in self.notes:
if isinstance(note, dict): if isinstance(note, dict):
timestamp, note_text = next(iter(note.items())) timestamp, note_text = next(iter(note.items()))
note_text = " ".join(note_text.strip().split()) note_items.append((timestamp, note_text.strip()))
html_notes.append(
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
)
elif isinstance(note, str): elif isinstance(note, str):
for line in note.split("\n"): for line in note.split("\n"):
if line.strip(): if line.strip():
escaped_line = html.escape(line) note_items.append((None, line.strip()))
html_notes.append(
f'<div class="sticky-note">{escaped_line}</div>'
)
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 @dataclass

View File

@ -76,33 +76,62 @@ class TaskLogData(BaseLogData):
return separator.join(lines).encode("utf-8").decode("unicode_escape") return separator.join(lines).encode("utf-8").decode("unicode_escape")
def notes_as_html(self) -> str: 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: if not self.notes:
return "" 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 notes = self.notes
if isinstance(notes, dict): if isinstance(notes, dict):
notes = [{k: v} for k, v in notes.items()] notes = [{k: v} for k, v in notes.items()]
if isinstance(notes, str): if isinstance(notes, str):
notes = [notes] notes = [notes]
html_notes = [] note_items = []
for note in notes: for note in notes:
if isinstance(note, dict): if isinstance(note, dict):
timestamp, note_text = next(iter(note.items())) timestamp, note_text = next(iter(note.items()))
note_text = " ".join(note_text.strip().split()) note_items.append((timestamp, note_text.strip()))
html_notes.append(
f'<div class="sticky-note">{timestamp}: {note_text}</div>'
)
elif isinstance(note, str): elif isinstance(note, str):
escaped = note.encode("utf-8").decode("unicode_escape") escaped = note.encode("utf-8").decode("unicode_escape")
for line in escaped.split("\n"): for line in escaped.split("\n"):
if line.strip(): if line.strip():
html_notes.append(f'<div class="sticky-note">{line}</div>') note_items.append((None, line.strip()))
elif isinstance(note, list): elif isinstance(note, list):
for item in note: for item in note:
if isinstance(item, str): if isinstance(item, str):
html_notes.append(f'<div class="sticky-note">{item}</div>') note_items.append((None, item.strip()))
return "".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">{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): class Task(LongPlayScrobblableMixin):

View File

@ -168,6 +168,35 @@
} }
#scrobble-form { width: 100% } #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 { .sticky-notes-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -218,6 +247,9 @@
a:not(.nav-link):not(.btn):not(.page-link):hover { a:not(.nav-link):not(.btn):not(.page-link):hover {
color: var(--accent); color: var(--accent);
} }
a.badge {
color: #fff !important;
}
.table-striped > tbody > tr:nth-of-type(odd) { .table-striped > tbody > tr:nth-of-type(odd) {
background-color: color-mix(in srgb, var(--accent) 6%, #fff); background-color: color-mix(in srgb, var(--accent) 6%, #fff);
} }

View File

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