GameSentenceMiner 2.19.16__py3-none-any.whl → 2.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of GameSentenceMiner might be problematic. Click here for more details.
- GameSentenceMiner/__init__.py +39 -0
- GameSentenceMiner/anki.py +6 -3
- GameSentenceMiner/gametext.py +13 -2
- GameSentenceMiner/gsm.py +40 -3
- GameSentenceMiner/locales/en_us.json +4 -0
- GameSentenceMiner/locales/ja_jp.json +4 -0
- GameSentenceMiner/locales/zh_cn.json +4 -0
- GameSentenceMiner/obs.py +4 -1
- GameSentenceMiner/owocr/owocr/ocr.py +304 -134
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/ui/anki_confirmation.py +4 -2
- GameSentenceMiner/ui/config_gui.py +12 -0
- GameSentenceMiner/util/configuration.py +6 -2
- GameSentenceMiner/util/cron/__init__.py +12 -0
- GameSentenceMiner/util/cron/daily_rollup.py +613 -0
- GameSentenceMiner/util/cron/jiten_update.py +397 -0
- GameSentenceMiner/util/cron/populate_games.py +154 -0
- GameSentenceMiner/util/cron/run_crons.py +148 -0
- GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
- GameSentenceMiner/util/cron_table.py +334 -0
- GameSentenceMiner/util/db.py +236 -49
- GameSentenceMiner/util/ffmpeg.py +23 -4
- GameSentenceMiner/util/games_table.py +340 -93
- GameSentenceMiner/util/jiten_api_client.py +188 -0
- GameSentenceMiner/util/stats_rollup_table.py +216 -0
- GameSentenceMiner/web/anki_api_endpoints.py +438 -220
- GameSentenceMiner/web/database_api.py +955 -1259
- GameSentenceMiner/web/jiten_database_api.py +1015 -0
- GameSentenceMiner/web/rollup_stats.py +672 -0
- GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
- GameSentenceMiner/web/static/css/overview.css +604 -47
- GameSentenceMiner/web/static/css/search.css +226 -0
- GameSentenceMiner/web/static/css/shared.css +762 -0
- GameSentenceMiner/web/static/css/stats.css +221 -0
- GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
- GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
- GameSentenceMiner/web/static/js/database-game-data.js +390 -0
- GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
- GameSentenceMiner/web/static/js/database-helpers.js +44 -0
- GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
- GameSentenceMiner/web/static/js/database-popups.js +89 -0
- GameSentenceMiner/web/static/js/database-tabs.js +64 -0
- GameSentenceMiner/web/static/js/database-text-management.js +371 -0
- GameSentenceMiner/web/static/js/database.js +86 -718
- GameSentenceMiner/web/static/js/goals.js +79 -18
- GameSentenceMiner/web/static/js/heatmap.js +29 -23
- GameSentenceMiner/web/static/js/overview.js +1205 -339
- GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
- GameSentenceMiner/web/static/js/search.js +215 -18
- GameSentenceMiner/web/static/js/shared.js +193 -39
- GameSentenceMiner/web/static/js/stats.js +1536 -179
- GameSentenceMiner/web/stats.py +1142 -269
- GameSentenceMiner/web/stats_api.py +2104 -0
- GameSentenceMiner/web/templates/anki_stats.html +4 -18
- GameSentenceMiner/web/templates/components/date-range.html +118 -3
- GameSentenceMiner/web/templates/components/html-head.html +40 -6
- GameSentenceMiner/web/templates/components/js-config.html +8 -8
- GameSentenceMiner/web/templates/components/regex-input.html +160 -0
- GameSentenceMiner/web/templates/database.html +564 -117
- GameSentenceMiner/web/templates/goals.html +41 -5
- GameSentenceMiner/web/templates/overview.html +159 -129
- GameSentenceMiner/web/templates/search.html +78 -9
- GameSentenceMiner/web/templates/stats.html +159 -5
- GameSentenceMiner/web/texthooking_page.py +280 -111
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jiten.moe API Client
|
|
3
|
+
|
|
4
|
+
Centralized client for interacting with the jiten.moe API.
|
|
5
|
+
Provides both search and detail endpoints with consistent error handling and logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import base64
|
|
10
|
+
from typing import Optional, Dict, List
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from GameSentenceMiner.util.configuration import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JitenApiClient:
|
|
17
|
+
"""
|
|
18
|
+
Centralized client for jiten.moe API interactions.
|
|
19
|
+
|
|
20
|
+
Provides methods for:
|
|
21
|
+
- Searching media decks by title
|
|
22
|
+
- Getting detailed deck information by deck_id
|
|
23
|
+
- Downloading and encoding cover images
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
BASE_URL = "https://api.jiten.moe/api/media-deck"
|
|
27
|
+
TIMEOUT = 10
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def search_media_decks(
|
|
31
|
+
cls,
|
|
32
|
+
title_filter: str,
|
|
33
|
+
sort_by: str = "title",
|
|
34
|
+
sort_order: int = 0,
|
|
35
|
+
offset: int = 0,
|
|
36
|
+
) -> Optional[Dict]:
|
|
37
|
+
"""
|
|
38
|
+
Search jiten.moe media decks by title.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
title_filter: Title to search for
|
|
42
|
+
sort_by: Sort field (default: 'title')
|
|
43
|
+
sort_order: Sort order (default: 0)
|
|
44
|
+
offset: Pagination offset (default: 0)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary with search results, or None if request fails
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
url = f"{cls.BASE_URL}/get-media-decks"
|
|
51
|
+
params = {
|
|
52
|
+
"titleFilter": title_filter,
|
|
53
|
+
"sortBy": sort_by,
|
|
54
|
+
"sortOrder": sort_order,
|
|
55
|
+
"offset": offset,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.debug(f"Searching jiten.moe for title: {title_filter}")
|
|
59
|
+
response = requests.get(url, params=params, timeout=cls.TIMEOUT)
|
|
60
|
+
|
|
61
|
+
if response.status_code != 200:
|
|
62
|
+
logger.debug(f"Jiten search API returned status {response.status_code}")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
data = response.json()
|
|
66
|
+
logger.debug(f"Jiten search returned {len(data.get('data', []))} results")
|
|
67
|
+
return data
|
|
68
|
+
|
|
69
|
+
except requests.RequestException as e:
|
|
70
|
+
logger.debug(f"Jiten search API request failed: {e}")
|
|
71
|
+
return None
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Unexpected error in jiten search: {e}")
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def get_deck_detail(cls, deck_id: int, offset: int = 0) -> Optional[Dict]:
|
|
78
|
+
"""
|
|
79
|
+
Get detailed information for a specific deck by deck_id.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
deck_id: The jiten.moe deck ID
|
|
83
|
+
offset: Pagination offset (default: 0)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary with deck details, or None if request fails
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
url = f"{cls.BASE_URL}/{deck_id}/detail"
|
|
90
|
+
params = {"offset": offset}
|
|
91
|
+
|
|
92
|
+
logger.debug(f"Fetching jiten.moe deck detail for deck_id: {deck_id}")
|
|
93
|
+
response = requests.get(url, params=params, timeout=cls.TIMEOUT)
|
|
94
|
+
|
|
95
|
+
if response.status_code != 200:
|
|
96
|
+
logger.debug(
|
|
97
|
+
f"Jiten detail API returned status {response.status_code} for deck {deck_id}"
|
|
98
|
+
)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
data = response.json()
|
|
102
|
+
logger.debug(f"Successfully fetched deck detail for deck_id: {deck_id}")
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
except requests.RequestException as e:
|
|
106
|
+
logger.debug(f"Jiten detail API request failed for deck {deck_id}: {e}")
|
|
107
|
+
return None
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"Unexpected error fetching deck detail for deck {deck_id}: {e}"
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def normalize_deck_data(cls, deck_data: Dict) -> Dict:
|
|
116
|
+
"""
|
|
117
|
+
Normalize deck data from jiten.moe API response to consistent format.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
deck_data: Raw deck data from API
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Normalized deck data with snake_case keys
|
|
124
|
+
"""
|
|
125
|
+
# Map media type integer to human-readable string
|
|
126
|
+
media_type_raw = deck_data.get("mediaType")
|
|
127
|
+
media_type_map = {
|
|
128
|
+
1: "Anime",
|
|
129
|
+
2: "Manga",
|
|
130
|
+
3: "Light Novel",
|
|
131
|
+
4: "Web Novel",
|
|
132
|
+
5: "Book",
|
|
133
|
+
6: "Game",
|
|
134
|
+
7: "Visual Novel"
|
|
135
|
+
}
|
|
136
|
+
media_type_string = media_type_map.get(media_type_raw, f"Type {media_type_raw}" if media_type_raw else "")
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"deck_id": deck_data.get("deckId"),
|
|
140
|
+
"title_original": deck_data.get("originalTitle", ""),
|
|
141
|
+
"title_romaji": deck_data.get("romajiTitle", ""),
|
|
142
|
+
"title_english": deck_data.get("englishTitle", ""),
|
|
143
|
+
"description": deck_data.get("description", ""),
|
|
144
|
+
"cover_name": deck_data.get("coverName", ""),
|
|
145
|
+
"media_type": media_type_raw, # Keep raw integer for backend processing
|
|
146
|
+
"media_type_string": media_type_string, # Add human-readable string
|
|
147
|
+
"character_count": deck_data.get("characterCount", 0),
|
|
148
|
+
"difficulty": deck_data.get("difficulty", 0),
|
|
149
|
+
"difficulty_raw": deck_data.get("difficultyRaw", 0),
|
|
150
|
+
"links": deck_data.get("links", []),
|
|
151
|
+
"aliases": deck_data.get("aliases", []),
|
|
152
|
+
"release_date": deck_data.get("releaseDate", ""),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def download_cover_image(cls, cover_url: str) -> Optional[str]:
|
|
157
|
+
"""
|
|
158
|
+
Download and encode cover image from jiten.moe.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
cover_url: URL of the cover image
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Base64 encoded image with data URI prefix, or None if download fails
|
|
165
|
+
|
|
166
|
+
Note:
|
|
167
|
+
Jiten.moe guarantees JPG format, so we assume image/jpeg MIME type.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
logger.debug(f"Downloading cover image: {cover_url}")
|
|
171
|
+
response = requests.get(cover_url, timeout=cls.TIMEOUT)
|
|
172
|
+
|
|
173
|
+
if response.status_code != 200:
|
|
174
|
+
logger.debug(
|
|
175
|
+
f"Failed to download cover image: HTTP {response.status_code}"
|
|
176
|
+
)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# Encode to base64 - jiten.moe guarantees JPG format
|
|
180
|
+
img_base64 = base64.b64encode(response.content).decode("utf-8")
|
|
181
|
+
data_uri = f"data:image/jpeg;base64,{img_base64}"
|
|
182
|
+
|
|
183
|
+
logger.debug(f"Successfully downloaded and encoded cover image")
|
|
184
|
+
return data_uri
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.debug(f"Failed to download cover image: {e}")
|
|
188
|
+
return None
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from GameSentenceMiner.util.db import SQLiteDBTable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StatsRollupTable(SQLiteDBTable):
|
|
9
|
+
_table = "daily_stats_rollup"
|
|
10
|
+
_fields = [
|
|
11
|
+
"date",
|
|
12
|
+
"total_lines",
|
|
13
|
+
"total_characters",
|
|
14
|
+
"total_sessions",
|
|
15
|
+
"unique_games_played",
|
|
16
|
+
"total_reading_time_seconds",
|
|
17
|
+
"total_active_time_seconds",
|
|
18
|
+
"longest_session_seconds",
|
|
19
|
+
"shortest_session_seconds",
|
|
20
|
+
"average_session_seconds",
|
|
21
|
+
"average_reading_speed_chars_per_hour",
|
|
22
|
+
"peak_reading_speed_chars_per_hour",
|
|
23
|
+
"games_completed",
|
|
24
|
+
"games_started",
|
|
25
|
+
"anki_cards_created",
|
|
26
|
+
"lines_with_screenshots",
|
|
27
|
+
"lines_with_audio",
|
|
28
|
+
"lines_with_translations",
|
|
29
|
+
"unique_kanji_seen",
|
|
30
|
+
"kanji_frequency_data",
|
|
31
|
+
"hourly_activity_data",
|
|
32
|
+
"hourly_reading_speed_data",
|
|
33
|
+
"game_activity_data",
|
|
34
|
+
"games_played_ids",
|
|
35
|
+
"max_chars_in_session",
|
|
36
|
+
"max_time_in_session_seconds",
|
|
37
|
+
"created_at",
|
|
38
|
+
"updated_at",
|
|
39
|
+
]
|
|
40
|
+
_types = [
|
|
41
|
+
int, # id (primary key)
|
|
42
|
+
str, # date
|
|
43
|
+
int,
|
|
44
|
+
int,
|
|
45
|
+
int,
|
|
46
|
+
int, # basic counts: total_lines, total_characters, total_sessions, unique_games_played
|
|
47
|
+
float,
|
|
48
|
+
float, # time tracking: total_reading_time_seconds, total_active_time_seconds
|
|
49
|
+
float,
|
|
50
|
+
float,
|
|
51
|
+
float, # session stats: longest_session_seconds, shortest_session_seconds, average_session_seconds
|
|
52
|
+
float,
|
|
53
|
+
float, # reading performance: average_reading_speed_chars_per_hour, peak_reading_speed_chars_per_hour
|
|
54
|
+
int,
|
|
55
|
+
int,
|
|
56
|
+
int, # game progress: games_completed, games_started, anki_cards_created
|
|
57
|
+
int,
|
|
58
|
+
int,
|
|
59
|
+
int, # anki integration: lines_with_screenshots, lines_with_audio, lines_with_translations
|
|
60
|
+
int,
|
|
61
|
+
str, # kanji stats: unique_kanji_seen, kanji_frequency_data (JSON)
|
|
62
|
+
str,
|
|
63
|
+
str,
|
|
64
|
+
str, # JSON data fields: hourly_activity_data, hourly_reading_speed_data, game_activity_data
|
|
65
|
+
str, # games_played_ids (JSON)
|
|
66
|
+
int,
|
|
67
|
+
float, # peak performance: max_chars_in_session, max_time_in_session_seconds
|
|
68
|
+
float,
|
|
69
|
+
float, # metadata: created_at, updated_at
|
|
70
|
+
]
|
|
71
|
+
_pk = "id"
|
|
72
|
+
_auto_increment = True
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
id: Optional[int] = None,
|
|
77
|
+
date: Optional[str] = None,
|
|
78
|
+
total_lines: int = 0,
|
|
79
|
+
total_characters: int = 0,
|
|
80
|
+
total_sessions: int = 0,
|
|
81
|
+
unique_games_played: int = 0,
|
|
82
|
+
total_reading_time_seconds: float = 0.0,
|
|
83
|
+
total_active_time_seconds: float = 0.0,
|
|
84
|
+
longest_session_seconds: float = 0.0,
|
|
85
|
+
shortest_session_seconds: float = 0.0,
|
|
86
|
+
average_session_seconds: float = 0.0,
|
|
87
|
+
average_reading_speed_chars_per_hour: float = 0.0,
|
|
88
|
+
peak_reading_speed_chars_per_hour: float = 0.0,
|
|
89
|
+
games_completed: int = 0,
|
|
90
|
+
games_started: int = 0,
|
|
91
|
+
anki_cards_created: int = 0,
|
|
92
|
+
lines_with_screenshots: int = 0,
|
|
93
|
+
lines_with_audio: int = 0,
|
|
94
|
+
lines_with_translations: int = 0,
|
|
95
|
+
unique_kanji_seen: int = 0,
|
|
96
|
+
kanji_frequency_data: Optional[str] = None,
|
|
97
|
+
hourly_activity_data: Optional[str] = None,
|
|
98
|
+
hourly_reading_speed_data: Optional[str] = None,
|
|
99
|
+
game_activity_data: Optional[str] = None,
|
|
100
|
+
games_played_ids: Optional[str] = None,
|
|
101
|
+
max_chars_in_session: int = 0,
|
|
102
|
+
max_time_in_session_seconds: float = 0.0,
|
|
103
|
+
created_at: Optional[float] = None,
|
|
104
|
+
updated_at: Optional[float] = None,
|
|
105
|
+
):
|
|
106
|
+
self.id = id
|
|
107
|
+
self.date = date if date is not None else datetime.now().strftime("%Y-%m-%d")
|
|
108
|
+
self.total_lines = total_lines
|
|
109
|
+
self.total_characters = total_characters
|
|
110
|
+
self.total_sessions = total_sessions
|
|
111
|
+
self.unique_games_played = unique_games_played
|
|
112
|
+
self.total_reading_time_seconds = total_reading_time_seconds
|
|
113
|
+
self.total_active_time_seconds = total_active_time_seconds
|
|
114
|
+
self.longest_session_seconds = longest_session_seconds
|
|
115
|
+
self.shortest_session_seconds = shortest_session_seconds
|
|
116
|
+
self.average_session_seconds = average_session_seconds
|
|
117
|
+
self.average_reading_speed_chars_per_hour = average_reading_speed_chars_per_hour
|
|
118
|
+
self.peak_reading_speed_chars_per_hour = peak_reading_speed_chars_per_hour
|
|
119
|
+
self.games_completed = games_completed
|
|
120
|
+
self.games_started = games_started
|
|
121
|
+
self.anki_cards_created = anki_cards_created
|
|
122
|
+
self.lines_with_screenshots = lines_with_screenshots
|
|
123
|
+
self.lines_with_audio = lines_with_audio
|
|
124
|
+
self.lines_with_translations = lines_with_translations
|
|
125
|
+
self.unique_kanji_seen = unique_kanji_seen
|
|
126
|
+
self.kanji_frequency_data = (
|
|
127
|
+
kanji_frequency_data if kanji_frequency_data is not None else "{}"
|
|
128
|
+
)
|
|
129
|
+
self.hourly_activity_data = (
|
|
130
|
+
hourly_activity_data if hourly_activity_data is not None else "{}"
|
|
131
|
+
)
|
|
132
|
+
self.hourly_reading_speed_data = (
|
|
133
|
+
hourly_reading_speed_data if hourly_reading_speed_data is not None else "{}"
|
|
134
|
+
)
|
|
135
|
+
self.game_activity_data = (
|
|
136
|
+
game_activity_data if game_activity_data is not None else "{}"
|
|
137
|
+
)
|
|
138
|
+
self.games_played_ids = (
|
|
139
|
+
games_played_ids if games_played_ids is not None else "[]"
|
|
140
|
+
)
|
|
141
|
+
self.max_chars_in_session = max_chars_in_session
|
|
142
|
+
self.max_time_in_session_seconds = max_time_in_session_seconds
|
|
143
|
+
self.created_at = created_at if created_at is not None else time.time()
|
|
144
|
+
self.updated_at = updated_at if updated_at is not None else time.time()
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def get_stats_for_date(cls, date: str) -> Optional["StatsRollupTable"]:
|
|
148
|
+
"""Get rollup statistics for a specific date."""
|
|
149
|
+
row = cls._db.fetchone(f"SELECT * FROM {cls._table} WHERE date=?", (date,))
|
|
150
|
+
return cls.from_row(row) if row else None
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def get_by_date(cls, date: str) -> Optional["StatsRollupTable"]:
|
|
154
|
+
"""Get rollup statistics for a specific date (alias for get_stats_for_date)."""
|
|
155
|
+
return cls.get_stats_for_date(date)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def date_exists(cls, date: str) -> bool:
|
|
159
|
+
"""Check if a rollup exists for a specific date."""
|
|
160
|
+
row = cls._db.fetchone(f"SELECT id FROM {cls._table} WHERE date=?", (date,))
|
|
161
|
+
return row is not None
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_date_range(cls, start_date: str, end_date: str) -> list["StatsRollupTable"]:
|
|
165
|
+
"""Get rollup statistics for a date range (inclusive)."""
|
|
166
|
+
rows = cls._db.fetchall(
|
|
167
|
+
f"SELECT * FROM {cls._table} WHERE date >= ? AND date <= ? ORDER BY date ASC",
|
|
168
|
+
(start_date, end_date),
|
|
169
|
+
)
|
|
170
|
+
return [cls.from_row(row) for row in rows]
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def get_first_date(cls) -> Optional[str]:
|
|
174
|
+
"""Get the earliest date with rollup data."""
|
|
175
|
+
from GameSentenceMiner.util.configuration import logger
|
|
176
|
+
row = cls._db.fetchone(
|
|
177
|
+
f"SELECT date FROM {cls._table} ORDER BY date ASC LIMIT 1"
|
|
178
|
+
)
|
|
179
|
+
result = row[0] if row else None
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def get_last_date(cls) -> Optional[str]:
|
|
184
|
+
"""Get the most recent date with rollup data."""
|
|
185
|
+
row = cls._db.fetchone(
|
|
186
|
+
f"SELECT date FROM {cls._table} ORDER BY date DESC LIMIT 1"
|
|
187
|
+
)
|
|
188
|
+
return row[0] if row else None
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def update_stats(
|
|
192
|
+
cls,
|
|
193
|
+
date: str,
|
|
194
|
+
games_played: int = 0,
|
|
195
|
+
lines_mined: int = 0,
|
|
196
|
+
anki_cards_created: int = 0,
|
|
197
|
+
time_spent_mining: float = 0.0,
|
|
198
|
+
):
|
|
199
|
+
"""Legacy method for backward compatibility - updates basic stats only."""
|
|
200
|
+
stats = cls.get_stats_for_date(date)
|
|
201
|
+
if not stats:
|
|
202
|
+
new_stats = cls(
|
|
203
|
+
date=date,
|
|
204
|
+
unique_games_played=games_played,
|
|
205
|
+
total_lines=lines_mined,
|
|
206
|
+
anki_cards_created=anki_cards_created,
|
|
207
|
+
total_reading_time_seconds=time_spent_mining,
|
|
208
|
+
)
|
|
209
|
+
new_stats.save()
|
|
210
|
+
return
|
|
211
|
+
stats.unique_games_played += games_played
|
|
212
|
+
stats.total_lines += lines_mined
|
|
213
|
+
stats.anki_cards_created += anki_cards_created
|
|
214
|
+
stats.total_reading_time_seconds += time_spent_mining
|
|
215
|
+
stats.updated_at = time.time()
|
|
216
|
+
stats.save()
|