From d23ee1ce181f16aa753c0ecf01d10cfa713fbfa6 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Thu, 10 Oct 2024 23:50:48 +0200 Subject: [PATCH] Support importing series events --- obsidian_import/__init__.py | 57 ++++++++++++++++++++++++++++--------- obsidian_import/obsidian.py | 33 ++++++++++++--------- personal_data/util.py | 2 +- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/obsidian_import/__init__.py b/obsidian_import/__init__.py index d116f6d..a38cb3e 100644 --- a/obsidian_import/__init__.py +++ b/obsidian_import/__init__.py @@ -14,7 +14,10 @@ from .obsidian import ObsidianVault, Event logger = getLogger(__name__) -def import_workout_csv(vault: ObsidianVault, rows: list[dict[str,Any]]) -> int: +Row = dict[str,Any] +Rows = list[Row] + +def import_workout_csv(vault: ObsidianVault, rows: Rows) -> int: num_updated = 0 for row in rows: date = row['Date'] @@ -40,22 +43,22 @@ def import_workout_csv(vault: ObsidianVault, rows: list[dict[str,Any]]) -> int: del row, date return num_updated -def import_step_counts_csv(vault: ObsidianVault, rows: list[dict[str,Any]]) -> int: +def import_step_counts_csv(vault: ObsidianVault, rows: Rows) -> int: MINIMUM = 300 num_updated = 0 - rows_per_day = {} + rows_per_date = {} for row in rows: date = row['Start'].date() - rows_per_day.setdefault(date, []) - rows_per_day[date].append(row) + rows_per_date.setdefault(date, []) + rows_per_date[date].append(row) del date, row - steps_per_day = { date: sum(row['Steps'] for row in rows) for date, rows in rows_per_day.items()} + steps_per_date = { date: sum(row['Steps'] for row in rows) for date, rows in rows_per_date.items()} - for date, steps in steps_per_day.items(): + for date, steps in steps_per_date.items(): if steps < MINIMUM: continue was_updated = vault.add_statistic(date, 'Steps', steps) @@ -65,12 +68,36 @@ def import_step_counts_csv(vault: ObsidianVault, rows: list[dict[str,Any]]) -> i return num_updated -def import_played_dates_csv(vault: ObsidianVault, rows: list[dict[str,Any]]) -> int: - date = datetime.date(2024,10,9) - event = Event(datetime.time(12,00), datetime.time(12,00), 'Tested', 'Obsidian Import') - updated = vault.add_event(date, event) +def import_watched_series_csv(vault: ObsidianVault, rows: Rows) -> int: + # TODO: Update to using git_time_tracker event parsing system + verb = 'Watched' - return updated and 1 or 0 + num_updated = 0 + + rows_per_date = {} + for row in rows: + date = row['me.last_played_time'].date() + rows_per_date.setdefault(date, []) + rows_per_date[date].append(row) + del date, row + del rows + + + def map_to_event(row: Row) -> Event: + start = row['me.last_played_time'].time().replace(second=0, microsecond=0, fold=0) + end = start + comment = '{} Episode {}: *{}*'.format(row['season.name'], row['episode.index'], row['episode.name']) + return Event(start, end, verb, row['series.name'], comment) + + for date, rows in rows_per_date.items(): + events = [map_to_event(row) for row in rows] + was_updated = vault.add_events(date, events) + + if was_updated: + num_updated += 1 + del date, was_updated + + return num_updated def import_data(obsidian_path: Path, dry_run=True): vault = ObsidianVault(obsidian_path, read_only=dry_run and 'silent' or None) @@ -90,7 +117,11 @@ def import_data(obsidian_path: Path, dry_run=True): logger.info('Updated %d files', num_updated) if True: - num_updated = import_played_dates_csv(vault, []) # TODO + data_path = Path('output/show_episodes_watched.csv') + rows = load_csv_file(data_path) + logger.info('Loaded CSV with %d lines', len(rows)) + rows = rows[:7] + num_updated = import_watched_series_csv(vault, rows) logger.info('Updated %d files', num_updated) diff --git a/obsidian_import/obsidian.py b/obsidian_import/obsidian.py index 0aa873d..8ce0beb 100644 --- a/obsidian_import/obsidian.py +++ b/obsidian_import/obsidian.py @@ -21,6 +21,7 @@ class Event: end_time: datetime.time | None verb: str subject: str + comment: str @dataclasses.dataclass(frozen = True) class FileContents: @@ -92,20 +93,16 @@ class ObsidianVault: self._save_contents(date, contents) return True - def add_event(self, date: datetime.date, event: Event) -> bool: + def add_events(self, date: datetime.date, events: list[Event]) -> bool: if self.read_only == 'silent': logger.info( - 'Read-only ObsidianVault ignoring add_event(%s, "%s", ?)', date, event, + 'Read-only ObsidianVault ignoring add_event(%s, "%s", ?)', date, events, ) - return + return False self._create_date_if_not_present(date) contents = self._get_date_contents(date) - if event in contents.events: - logger.info('Events already exist in "%s"', date) - return False - contents.events.append(event) - + contents.events.extend(events) self._save_contents(date, contents) return True @@ -128,6 +125,7 @@ class ObsidianVault: return FileContents(file_frontmatter.metadata, pre_events, events, post_events) def _save_contents(self, date: datetime.date, contents: FileContents) -> None: + logger.info('Formatting file "%s"', date) blocks_pre_events = ''.join(MARKDOWN_RENDERER.render(b) for b in contents.blocks_pre_events) blocks_post_events = ''.join(MARKDOWN_RENDERER.render(b) for b in contents.blocks_post_events) block_events = '\n'.join('- ' + format_event_string(e) for e in unique(contents.events)) @@ -174,16 +172,23 @@ def find_events_list_block(ast) -> tuple[list, list[str], list]: return (blocks, [], []) def format_event_string(event: Event) -> str: - return f'{event.start_time}: {event.verb} [[{event.subject}]]' + assert event is not None + if event.start_time is None and event.end_time is None and event.subject is None and event.verb is None: + return event.comment + + return f'{event.start_time:%H:%M} | {event.verb} [[{event.subject}]]. {event.comment}'.strip() + +RE_TIME = r'(\d\d:\d\d(?::\d\d(?:\.\d+?))?)' def parse_event_string(event_str: str) -> Event: - if m := re.match(r'^\s*(\d\d:\d\d(?::\d\d)?):?\s+(\w+ed)\s+\[([^\]]*)\]\([^)]*\)\.?\s*$', event_str): + if m := re.match(r'^\s*'+RE_TIME+r'[ :\|-]*(\w+ed)\s+\[([^\]]*)\]\([^)]*\)\.?\s*(.*)$', event_str): start = datetime.time.fromisoformat(m.group(1)) - return Event(start, start, m.group(2), m.group(3)) - if m := re.match(r'^\s*(\d\d:\d\d(?::\d\d)?):?\s+(\w+ed)\s+\[\[([^\]]*)\]\]$', event_str): + return Event(start, start, m.group(2), m.group(3), m.group(4)) + if m := re.match(r'^\s*'+RE_TIME+'[ :\|-]*(\w+ed)\s+\[\[([^\]]*)\]\]\.?\s*(.*)$', event_str): start = datetime.time.fromisoformat(m.group(1)) - return Event(start, start, m.group(2), m.group(3)) - return None + return Event(start, start, m.group(2), m.group(3), m.group(4)) + logger.info('Could not parse format: %s', event_str) + return Event(None,None,None,None,event_str) def unique(ls: list) -> list: return list(dict.fromkeys(ls)) diff --git a/personal_data/util.py b/personal_data/util.py index 5107527..b0a800f 100644 --- a/personal_data/util.py +++ b/personal_data/util.py @@ -151,7 +151,7 @@ def normalize_dict(d: dict[str, typing.Any]) -> frozendict[str, typing.Any]: ) -def load_csv_file(csv_file: Path) -> list[frozendict]: +def load_csv_file(csv_file: Path) -> list[frozendict[str, typing.Any]]: dicts: list[frozendict] = [] with open(csv_file) as csvfile: dialect = csv.Sniffer().sniff(csvfile.read(1024))