[scrobbles] Fix display of task notes
All checks were successful
build & deploy / test (push) Successful in 1m40s
build & deploy / deploy (push) Successful in 21s

This commit is contained in:
2026-03-24 15:10:33 -04:00
parent cce2db0ea1
commit 4121915aa3
4 changed files with 149 additions and 33 deletions

View File

@ -92,7 +92,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 [16/35] * Backlog [17/37]
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement: ** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement:
:PROPERTIES: :PROPERTIES:
@ -431,11 +431,14 @@ https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-li
** TODO [#A] When creating org-mode tasks, don't copy comments :vrobbler:bug:scrobbles:tasks: ** TODO [#A] When creating org-mode tasks, don't copy comments :vrobbler:bug:scrobbles:tasks:
** TODO Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: ** TODO Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
** DONE Fix genearting chart records :vrobbler:bug:personal:project:chartrecords: ** STRT Fix display of notes so they look like stickies :vrobbler:project:personal:notes:scrobbles:
:PROPERTIES: :PROPERTIES:
:ID: e749d762-44ac-dd7e-017e-2423016737ff :ID: 0ace8814-e96e-fa54-28d1-c57dcb508f1e
:END:
** DONE Add searching to scrobbles :vrobbler:project:personal:search:scrobbles:
:PROPERTIES:
:ID: b4690c76-d741-4320-20e8-c003251fe035
:END: :END:
** DONE Fix uniqueness of imdb_id messing up youtube videos :vrobbler:project:bug:videos: ** DONE Fix uniqueness of imdb_id messing up youtube videos :vrobbler:project:bug:videos:
:PROPERTIES: :PROPERTIES:
:ID: 610b8c4c-abf1-8630-a4fd-08d9939da9f5 :ID: 610b8c4c-abf1-8630-a4fd-08d9939da9f5
@ -445,6 +448,11 @@ Turns out you can't make imdb_id unique by itself or you never get to create
youtube videos. Rather, we should make imdb_id and youtube_id unique together so youtube videos. Rather, we should make imdb_id and youtube_id unique together so
imdb_id can be empty for every youtube_id and vice versa. imdb_id can be empty for every youtube_id and vice versa.
** DONE Fix genearting chart records :vrobbler:bug:personal:project:chartrecords:
:PROPERTIES:
:ID: e749d762-44ac-dd7e-017e-2423016737ff
:END:
** DONE [#A] Save raw scrobble request data to every scrobble log :vrobbler:personal:feature:scrobbles: ** DONE [#A] Save raw scrobble request data to every scrobble log :vrobbler:personal:feature:scrobbles:
:PROPERTIES: :PROPERTIES:
:ID: b4686db4-1319-399c-9b84-f0b1036c6815 :ID: b4686db4-1319-399c-9b84-f0b1036c6815

View File

@ -792,6 +792,7 @@ class ScrobbleDetailView(DetailView):
model = Scrobble model = Scrobble
slug_field = "uuid" slug_field = "uuid"
slug_url_kwarg = "uuid" slug_url_kwarg = "uuid"
paginate_by = 100
def get_form_class(self): def get_form_class(self):
return self.object.media_obj.logdata_cls().form() return self.object.media_obj.logdata_cls().form()
@ -830,10 +831,55 @@ class ScrobbleDetailView(DetailView):
context = self.get_context_data(log_form=form) context = self.get_context_data(log_form=form)
return self.render_to_response(context) return self.render_to_response(context)
MEDIA_FK_MAP = {
"Video": "video",
"Track": "track",
"PodcastEpisode": "podcast_episode",
"Book": "book",
"Paper": "paper",
"VideoGame": "video_game",
"BoardGame": "board_game",
"Beer": "beer",
"Food": "food",
"Puzzle": "puzzle",
"Trail": "trail",
"GeoLocation": "geo_location",
"Task": "task",
"WebPage": "web_page",
"LifeEvent": "life_event",
"Mood": "mood",
"BrickSet": "brick_set",
}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if "log_form" not in context: if "log_form" not in context:
context["log_form"] = self.get_form() context["log_form"] = self.get_form()
media_obj = self.object.media_obj
user = self.object.user
media_type = self.object.media_type
fk_field = self.MEDIA_FK_MAP.get(media_type)
if fk_field:
related_scrobbles = (
Scrobble.objects.filter(user=user)
.filter(**{fk_field: media_obj})
.exclude(id=self.object.id)
.order_by("-timestamp")
)
else:
related_scrobbles = Scrobble.objects.none()
page = self.request.GET.get("page")
paginator = Paginator(related_scrobbles, self.paginate_by)
try:
context["related_scrobbles"] = paginator.page(page)
except PageNotAnInteger:
context["related_scrobbles"] = paginator.page(1)
except EmptyPage:
context["related_scrobbles"] = paginator.page(paginator.num_pages)
return context return context

View File

@ -1,3 +1,4 @@
import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
@ -45,15 +46,20 @@ class TaskLogData(BaseLogData):
lines = [] lines = []
if self.notes: if self.notes:
for note in self.notes: notes = self.notes
if isinstance(notes, str):
notes = [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()))
# Flatten newlines and clean whitespace # Flatten newlines and clean whitespace
note_text = " ".join(note_text.strip().split()) note_text = " ".join(note_text.strip().split())
lines.append(f"{timestamp}: {note_text} [{labels_str}]") lines.append(f"{timestamp}: {note_text} [{labels_str}]")
if isinstance(note, list):
lines += note
if isinstance(note, str): if isinstance(note, str):
lines.append(note) lines.append(note)
return "\n".join(lines).encode("utf-8").decode("unicode_escape") return "\n".join(lines).encode("utf-8").decode("unicode_escape")

View File

@ -1,6 +1,7 @@
{% extends "base_list.html" %} {% extends "base_list.html" %}
{% load form_tags %} {% load form_tags %}
{% load mathfilters %} {% load mathfilters %}
{% load naturalduration %}
{% load static %} {% load static %}
{% block title %}{{object.name}}{% endblock %} {% block title %}{{object.name}}{% endblock %}
@ -11,40 +12,95 @@
<h1>{{ object.media_obj }} - {{object.media_type}}</h1> <h1>{{ object.media_obj }} - {{object.media_type}}</h1>
{% if object.notes %} {% with notes_string=object.logdata.notes_as_str %}
<details class="mb-3"> {% if notes_string %}
<summary>Notes ({{ object.notes|length }})</summary> <div class="mb-3">
<ul class="mt-2"> <h5>Notes</h5>
{% for note in object.notes %} <pre>{{ notes_string }}</pre>
<li>{{ note }}</li> </div>
{% endfor %}
</ul>
</details>
{% endif %} {% endif %}
{% endwith %}
<!-- Your existing detail page content -->
{% if object.logdata.avg_seconds_per_page %} {% if object.logdata.avg_seconds_per_page %}
<p>Rate: {{object.logdata.avg_seconds_per_page}}s per page</p> <p>Rate: {{object.logdata.avg_seconds_per_page}}s per page</p>
{% endif %} {% endif %}
<h2>Edit Log</h2> <details class="mb-3">
<form method="post" class="needs-validation" novalidate> <summary>Edit Log</summary>
{% csrf_token %} <form method="post" class="needs-validation mt-3" novalidate>
{% for field in log_form %} {% csrf_token %}
<div class="mb-3"> {% for field in log_form %}
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label> <div class="mb-3">
{{ field|add_class:"form-control" }} <label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{% if field.help_text %} {{ field|add_class:"form-control" }}
<div class="form-text">{{ field.help_text }}</div> {% if field.help_text %}
{% endif %} <div class="form-text">{{ field.help_text }}</div>
{% for error in field.errors %} {% endif %}
<div class="invalid-feedback d-block">{{ error }}</div> {% for error in field.errors %}
{% endfor %} <div class="invalid-feedback d-block">{{ error }}</div>
</div> {% endfor %}
{% endfor %} </div>
{% endfor %}
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>
</form> </form>
</details>
<h2 class="mt-4">Other Scrobbles of This Media</h2>
{% if related_scrobbles %}
<p class="text-muted">{{ related_scrobbles.paginator.count }} scrobble{{ related_scrobbles.paginator.count|pluralize }}</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
<th scope="col">Time</th>
</tr>
</thead>
<tbody>
{% for scrobble in related_scrobbles %}
<tr>
<td><a href="{% url 'scrobbles:detail' scrobble.uuid %}">{{ scrobble.timestamp|date:"M d, Y" }}</a></td>
<td>
{% if scrobble.media_type == "Task" and scrobble.logdata.title %}{{ scrobble.media_obj.title }}: {{ scrobble.logdata.title }}{% else %}{{ scrobble.media_obj.title|default:scrobble.media_obj }}{% endif %}
</td>
<td>
{% if scrobble.playback_position_seconds %}{{ scrobble.playback_position_seconds|natural_duration }}{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if related_scrobbles.has_other_pages %}
<nav>
<ul class="pagination">
{% if related_scrobbles.has_previous %}
<li class="page-item"><a class="page-link" href="?page={{ related_scrobbles.previous_page_number }}">Previous</a></li>
{% else %}
<li class="page-item disabled"><span class="page-link">Previous</span></li>
{% endif %}
{% for num in related_scrobbles.paginator.page_range %}
{% if related_scrobbles.number == num %}
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
{% elif num > related_scrobbles.number|add:'-3' and num < related_scrobbles.number|add:'3' %}
<li class="page-item"><a class="page-link" href="?page={{ num }}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if related_scrobbles.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ related_scrobbles.next_page_number }}">Next</a></li>
{% else %}
<li class="page-item disabled"><span class="page-link">Next</span></li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<p class="text-muted">No other scrobbles of this media.</p>
{% endif %}
</div> </div>