Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b8e027c30 | |||
| 1bd9f0d942 | |||
| fa7890cb21 | |||
| 957c32e3a7 | |||
| 8d069df9d1 |
56
PROJECT.org
56
PROJECT.org
@ -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:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vrobbler"
|
||||
version = "39.0"
|
||||
version = "39.2"
|
||||
description = ""
|
||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
Reference in New Issue
Block a user