Compare commits

...

5 Commits
39.0 ... 39.2

Author SHA1 Message Date
0b8e027c30 [release] Bump to version 39.2
Some checks failed
build & deploy / test (push) Successful in 1m59s
build & deploy / build-and-deploy (push) Failing after 24s
- Releases do not pin commit to the repo for display
- Fix the way timestamps are stored for notes on tasks
2026-06-01 10:11:02 -04:00
1bd9f0d942 [tooling] Fix commit stamping on release
All checks were successful
build & deploy / test (push) Successful in 1m56s
build & deploy / build-and-deploy (push) Has been skipped
2026-06-01 09:58:59 -04:00
fa7890cb21 [tasks] Fix timestamps for notes and syncing from org-mode (32973bb3)
All checks were successful
build & deploy / test (push) Successful in 1m54s
build & deploy / build-and-deploy (push) Has been skipped
2026-06-01 09:37:41 -04:00
957c32e3a7 [release] Bump to version 39.1
All checks were successful
build & deploy / test (push) Successful in 1m56s
build & deploy / build-and-deploy (push) Has been skipped
- Fix bug in tests for notes saving
2026-05-31 23:25:34 -04:00
8d069df9d1 [scrobbles] Fix log saving tests 2026-05-31 23:24:22 -04:00
8 changed files with 128 additions and 28 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 [0/15] :vrobbler:project:personal:
* Backlog [0/13] :vrobbler:project:personal:
** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment:
:PROPERTIES:
:ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85
@ -437,12 +437,21 @@ AllTrails is the best source, though having TrailForks is nice to.
Would be nice to have some loose connection to the actual event in my Garmin
profile.
** TODO [#B] Explore a way to add metadata editing to scrobbles after saving :vrobbler:spike:scrobbling:personal:project:
** TODO [#B] Fix how we show notes and descriptions from scrobbles to users :metadata:notes:tasks:
:PROPERTIES:
:ID: adf4c513-a417-4ec9-8831-f01ffcf63276
:END:
*** Description
Currently the display of notes leaves something to be desired. The biggest issue
is that they don't look good on mobile and are probably trying to be too cute.
Rather than post-it note style, we should just put notes in a list under the
description, above the Edit Log toggle, with timestamps for when they were
added.
They should also probably support markdown formatting and that should be
displayed in the template.
Could be as simple as a JSON form on the scrobble detail page (do I have have
one of those yet?).
** TODO [#B] Explore a good way to show notes and descriptions from scrobbles to users :personal:project:scrobbling:vrobbler:spike:
** TODO [#B] Add webdav syncing to retroarch imports :vrobbler:videogames:webdav:feature:project:personal:
** TODO [#B] Add CSV endpoint for book scrobbles that LibraryThing can ingest :personal:project:books:feature:export:
https://app.todoist.com/app/task/add-a-csv-endpoint-for-users-book-reads-that-library-thing-can-ingest-6X7QPMRp265xMXqg#comment-6X7QrXq6gJjMP4hg
** TODO [#B] Scrape ComicBookRoundUp ratings for comic book metadata :vrobbler:books:feature:comicbook:personal:project:
@ -480,6 +489,41 @@ whatever time KoReader reports, we need to know, given the date and the user
profile's historic timezone, how many hours to adjust the KoReader time to get
to GMT to save it in the database.
* Version 39.2 [2/2]
** DONE [#B] Releases do not pin commit to the repo for display :bug:tooling:releases:
:PROPERTIES:
:ID: 2a9f2ff5-2642-47ab-ba1d-e41825411713
:END:
*** Description
Somewhere in implementing the justfile release flow, we lost the capture of the
latest commit in the relesae flow so the footer now always says: vXX.x (unknown)
It should have the first bit of the commit in the parens at the end.
** DONE [#B] Fix the way timestamps are stored for notes on tasks :bug:scrobbles:tasks:
:PROPERTIES:
:ID: 32973bb3-079b-8cdf-6495-82f8ae907299
:END:
*** Description
:PROPERTIES:
:ID: d833a3df-6eb2-36bb-1863-e438e5d36151
:END:
Turns out Todoist uses a human-readable timestamp format for comments. We should
adapt that for use from org-mode as well.
Format should be: "%Y-%m-%dT%H:%M:%S.%fZ"
* Version 39.1 [1/1]
** DONE [#A] Fix bug in tests for notes saving :bug:scrobbles:forms:
:PROPERTIES:
:ID: 68a011b2-bb6f-3ba8-2312-5947c41db9ac
:END:
* Version 39.0 [3/3]
** DONE [#B] Clean up org-mode tasks metadata :bug:tasks:metadata:
:PROPERTIES:

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "vrobbler"
version = "39.0"
version = "39.2"
description = ""
authors = ["Colin Powell <colin@unbl.ink>"]

View File

@ -607,7 +607,8 @@ def test_scrobble_detail_view_post_updates_log(client):
scrobble.refresh_from_db()
assert scrobble.log["description"] == "Updated description"
assert scrobble.log["notes"] == ["Updated note"]
assert isinstance(scrobble.log["notes"], dict)
assert list(scrobble.log["notes"].values()) == ["Updated note"]
@pytest.mark.skip("Need to refactor")

View File

@ -112,11 +112,14 @@ class NotesDictWidget(forms.Widget):
def value_from_datadict(self, data, files, name):
timestamps = data.getlist(f"{name}_timestamps")
contents = data.getlist(f"{name}_contents")
return {
"timestamps": timestamps,
"contents": contents,
}
if timestamps:
contents = data.getlist(f"{name}_contents")
result = {}
for i, ts in enumerate(timestamps):
if i < len(contents) and ts:
result[ts] = contents[i]
return result if result else ""
return data.get(name, "")
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
@ -139,10 +142,14 @@ class NotesDictField(forms.Field):
def clean(self, value):
if not value:
return {}
result = {}
timestamps = value.get("timestamps", [])
contents = value.get("contents", [])
for i, ts in enumerate(timestamps):
if i < len(contents) and ts and contents[i].strip():
result[ts] = contents[i].strip()
return result if result else {}
if isinstance(value, str):
if value.strip():
from scrobbles.utils import make_note_timestamp
return {make_note_timestamp(): value.strip()}
return {}
if isinstance(value, dict):
return value
return {}

View File

@ -35,6 +35,7 @@ from scrobbles.notifications import ScrobbleNtfyNotification
from scrobbles.utils import (
convert_to_seconds,
extract_domain,
make_note_timestamp,
next_url_if_exists,
remove_last_part,
)
@ -528,7 +529,7 @@ def email_scrobble_board_game(
stop_timestamp = timestamp + timedelta(seconds=duration_seconds)
if comments:
log_data["notes"] = {str(int(stop_timestamp.timestamp())): comments}
log_data["notes"] = {make_note_timestamp(stop_timestamp): comments}
logger.info(f"Creating scrobble for {base_game} at {timestamp}")
log_data["raw_data"] = bgstat_data
@ -688,7 +689,7 @@ def todoist_scrobble_update_task(
)
return
timestamp = todoist_note.get("posted_at") or str(int(timezone.now().timestamp()))
timestamp = todoist_note.get("posted_at") or make_note_timestamp()
if not scrobble.log.get("notes"):
scrobble.log["notes"] = {}
scrobble.log["notes"][timestamp] = todoist_note.get("notes")
@ -786,12 +787,37 @@ def todoist_scrobble_task(
return scrobble
ORG_HEADING_RE = re.compile(r"^(\*+\s+.*)$", re.MULTILINE)
NOTE_CONTENT_SPLIT = re.compile(r"^\*{3,}\s", re.MULTILINE)
def _truncate_at_org_header(text: str) -> str:
"""Truncate text at the first org-mode heading (*** or more)."""
parts = NOTE_CONTENT_SPLIT.split(text, maxsplit=1)
return parts[0].strip()
def _extract_org_section(body: str | None, heading: str) -> str | None:
"""Extract content under a specific org-mode sub-heading (e.g. '*** Description')."""
if not body:
return None
sections = ORG_HEADING_RE.split(body)
# sections alternates: [prefix, heading1, content1, heading2, content2, ...]
for i, section in enumerate(sections):
if section.strip().startswith(heading):
if i + 1 < len(sections):
return sections[i + 1].strip()
return ""
return None
def emacs_scrobble_update_task(
emacs_id: str,
emacs_notes: list,
user_id: int,
description: Optional[str] = None,
) -> Optional[Scrobble]:
description = _extract_org_section(description, "*** Description")
scrobble = Scrobble.objects.filter(
in_progress=True,
user_id=user_id,
@ -818,6 +844,11 @@ def emacs_scrobble_update_task(
for note in emacs_notes:
timestamp = note.get("timestamp")
content = note.get("content")
if not content:
continue
content = _truncate_at_org_header(content)
if not content:
continue
if timestamp:
existing = scrobble.log["notes"].get(timestamp)
if existing != content:
@ -896,7 +927,7 @@ def emacs_scrobble_task(
task_data.pop("notes", None)
task_data["title"] = task_data.pop("description")
task_data["description"] = task_data.pop("body")
task_data["description"] = _extract_org_section(task_data.pop("body"), "*** Description")
task_data["labels"] = task_data.pop("labels")
task_data["orgmode_id"] = task_data.pop("source_id")

View File

@ -32,6 +32,16 @@ from webdav.client import get_webdav_client
if TYPE_CHECKING:
from scrobbles.models import Scrobble
NOTE_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
def make_note_timestamp(dt: datetime | None = None) -> str:
if dt is None:
dt = timezone.now()
return dt.strftime(NOTE_TIMESTAMP_FORMAT)
logger = logging.getLogger(__name__)
User = get_user_model()

View File

@ -64,6 +64,8 @@ def convert_old_todoist_log_to_new(commit=False):
def convert_notes_to_dict(commit=False):
from scrobbles.utils import make_note_timestamp
scrobbles = Scrobble.objects.filter(log__notes__isnull=False)
count = 0
for scrobble in scrobbles:
@ -71,7 +73,7 @@ def convert_notes_to_dict(commit=False):
print(f"Converting {scrobble} string note to dict")
if scrobble.log.get("notes") == "":
scrobble.log.pop("notes")
key = str(int(scrobble.timestamp.timestamp()))
key = make_note_timestamp(scrobble.timestamp)
notes = scrobble.log.pop("notes")
scrobble.log = {}
scrobble.log["notes"] = {key: notes}
@ -92,12 +94,14 @@ def convert_notes_to_dict(commit=False):
def convert_tasks_notes_list_to_dict(commit=False):
from scrobbles.utils import make_note_timestamp
scrobbles = Scrobble.objects.filter(task__isnull=False, log__notes__isnull=False)
count = 0
for scrobble in scrobbles:
notes = scrobble.log.get("notes")
if isinstance(notes, list):
key = str(int(scrobble.timestamp.timestamp()) + 10)
key = make_note_timestamp(scrobble.timestamp + timedelta(seconds=10))
parts = []
for note in notes:
if isinstance(note, dict):
@ -114,6 +118,8 @@ def convert_tasks_notes_list_to_dict(commit=False):
def convert_old_boardgame_log_to_new(commit=False):
from scrobbles.utils import make_note_timestamp
scrobbles = Scrobble.objects.filter(board_game__isnull=False, log__has_key="notes")
count = 0
for scrobble in scrobbles:
@ -123,7 +129,7 @@ def convert_old_boardgame_log_to_new(commit=False):
notes = [notes]
if isinstance(notes, list):
key_ts = scrobble.stop_timestamp or scrobble.timestamp
scrobble.log["notes"] = {str(int(key_ts.timestamp())): "\n".join(
scrobble.log["notes"] = {make_note_timestamp(key_ts): "\n".join(
str(n) for n in notes
)}
count += 1

View File

@ -14,11 +14,12 @@ def version_info(request):
if not commit:
# Try to import from _commit.py module first
try:
from vrobbler._commit import commit
from vrobbler._commit import commit as _commit
except ImportError:
pass
else:
return {"app_version": app_version, "git_commit": commit}
if _commit and _commit != "unknown":
return {"app_version": app_version, "git_commit": _commit}
# Try to read from commit file (written during deploy)
commit_file = Path("/var/lib/vrobbler/commit.txt")