Track your Anki study sessions directly into Yōten.
at master 120 lines 3.7 kB view raw
1import time 2from dataclasses import dataclass 3from datetime import datetime 4from typing import Optional 5 6 7@dataclass 8class StudySession: 9 deck_name: str 10 cards_studied: int 11 duration_seconds: int 12 session_end: datetime 13 14 @property 15 def description(self) -> str: 16 return f"Anki: {self.deck_name} - {self.cards_studied} cards" 17 18 @property 19 def formatted_duration(self) -> str: 20 mins, secs = divmod(self.duration_seconds, 60) 21 if mins > 0: 22 return f"{mins}m {secs}s" 23 return f"{secs}s" 24 25 @property 26 def duration_parts(self) -> tuple[int, int, int]: 27 hours = self.duration_seconds // 3600 28 minutes = (self.duration_seconds % 3600) // 60 29 seconds = self.duration_seconds % 60 30 return (hours, minutes, seconds) 31 32 33class SessionTracker: 34 def __init__(self, min_cards: int = 5, min_duration: int = 30): 35 self.min_cards = min_cards 36 self.min_duration = min_duration 37 self.reset() 38 39 def reset(self): 40 self.session_active = False 41 self.session_start: Optional[float] = None 42 self.cards_count = 0 43 self.deck_name: Optional[str] = None 44 self.deck_id: Optional[int] = None 45 46 def _start_new_session(self, deck_id: int, deck_name: str) -> None: 47 self.session_active = True 48 self.session_start = time.time() 49 self.cards_count = 1 50 self.deck_name = deck_name 51 self.deck_id = deck_id 52 53 def on_card_shown(self, card): 54 from aqt import mw 55 56 current_deck = mw.col.decks.current() 57 current_deck_id = current_deck['id'] 58 current_deck_name = current_deck['name'] 59 60 if not self.session_active: 61 # Start new session 62 self._start_new_session(current_deck_id, current_deck_name) 63 elif self.deck_id != current_deck_id: 64 # Deck changed - end current session and start new one 65 if self.cards_count > 0: 66 self.end_session() 67 self._start_new_session(current_deck_id, current_deck_name) 68 else: 69 # Same deck, increment counter 70 self.cards_count += 1 71 72 def on_state_change(self, new_state: str, old_state: str): 73 if old_state == "review" and self.session_active: 74 if self.cards_count > 0: 75 self.end_session() 76 77 def end_session(self): 78 if not self.session_active or self.session_start is None: 79 return 80 81 duration = int(time.time() - self.session_start) 82 83 # Too few cards - don't post 84 if self.cards_count < self.min_cards: 85 self.reset() 86 return 87 88 # Too short - don't post 89 if duration < self.min_duration: 90 self.reset() 91 return 92 93 session = StudySession( 94 deck_name=self.deck_name, 95 cards_studied=self.cards_count, 96 duration_seconds=duration, 97 session_end=datetime.utcnow() 98 ) 99 100 self.reset() 101 102 # Imported here to avoid circular import 103 from aqt import mw 104 105 # Schedule on main thread to ensure UI updates properly 106 mw.taskman.run_on_main( 107 lambda: self._show_dialog(session) 108 ) 109 110 def _show_dialog(self, session: StudySession): 111 # Imported here to avoid circular import 112 from . import show_post_dialog 113 show_post_dialog(session) 114 115 def update_thresholds(self, min_cards: Optional[int] = None, 116 min_duration: Optional[int] = None): 117 if min_cards is not None: 118 self.min_cards = min_cards 119 if min_duration is not None: 120 self.min_duration = min_duration