GameSentenceMiner 2.18.2__py3-none-any.whl → 2.18.4__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/util/db.py +107 -41
- GameSentenceMiner/util/get_overlay_coords.py +32 -32
- GameSentenceMiner/web/database_api.py +1 -1
- {gamesentenceminer-2.18.2.dist-info → gamesentenceminer-2.18.4.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.18.2.dist-info → gamesentenceminer-2.18.4.dist-info}/RECORD +9 -9
- {gamesentenceminer-2.18.2.dist-info → gamesentenceminer-2.18.4.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.18.2.dist-info → gamesentenceminer-2.18.4.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.18.2.dist-info → gamesentenceminer-2.18.4.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.18.2.dist-info → gamesentenceminer-2.18.4.dist-info}/top_level.txt +0 -0
GameSentenceMiner/util/db.py
CHANGED
|
@@ -93,6 +93,7 @@ class SQLiteDBTable:
|
|
|
93
93
|
_types: List[type] = []
|
|
94
94
|
_pk: str = 'id'
|
|
95
95
|
_auto_increment: bool = True
|
|
96
|
+
_column_order_cache: Optional[List[str]] = None # Cache for actual column order
|
|
96
97
|
|
|
97
98
|
def __init_subclass__(cls, **kwargs):
|
|
98
99
|
super().__init_subclass__(**kwargs)
|
|
@@ -104,6 +105,7 @@ class SQLiteDBTable:
|
|
|
104
105
|
@classmethod
|
|
105
106
|
def set_db(cls, db: SQLiteDB):
|
|
106
107
|
cls._db = db
|
|
108
|
+
cls._column_order_cache = None # Reset cache when database changes
|
|
107
109
|
# Ensure table exists
|
|
108
110
|
if not db.table_exists(cls._table):
|
|
109
111
|
fields_def = ', '.join([f"{field} TEXT" for field in cls._fields])
|
|
@@ -115,6 +117,7 @@ class SQLiteDBTable:
|
|
|
115
117
|
for field in cls._fields:
|
|
116
118
|
if field not in existing_columns:
|
|
117
119
|
db.execute(f"ALTER TABLE {cls._table} ADD COLUMN {field} TEXT", commit=True)
|
|
120
|
+
cls._column_order_cache = None # Reset cache when schema changes
|
|
118
121
|
|
|
119
122
|
@classmethod
|
|
120
123
|
def all(cls: Type[T]) -> List[T]:
|
|
@@ -137,49 +140,80 @@ class SQLiteDBTable:
|
|
|
137
140
|
if not row:
|
|
138
141
|
return None
|
|
139
142
|
obj = cls()
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Get actual column order from database schema
|
|
146
|
+
actual_columns = cls.get_actual_column_order()
|
|
147
|
+
expected_fields = [cls._pk] + cls._fields
|
|
148
|
+
|
|
149
|
+
# Create a mapping from actual column positions to expected field positions
|
|
150
|
+
column_mapping = {}
|
|
151
|
+
for i, actual_col in enumerate(actual_columns):
|
|
152
|
+
if actual_col in expected_fields:
|
|
153
|
+
expected_index = expected_fields.index(actual_col)
|
|
154
|
+
column_mapping[i] = expected_index
|
|
155
|
+
|
|
156
|
+
# Process each column in the row based on the mapping
|
|
157
|
+
for actual_pos, row_value in enumerate(row):
|
|
158
|
+
if actual_pos not in column_mapping:
|
|
159
|
+
continue # Skip unknown columns
|
|
160
|
+
|
|
161
|
+
expected_pos = column_mapping[actual_pos]
|
|
162
|
+
field = expected_fields[expected_pos]
|
|
163
|
+
field_type = cls._types[expected_pos]
|
|
164
|
+
|
|
165
|
+
cls._set_field_value(obj, field, field_type, row_value, expected_pos == 0 and field == cls._pk)
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# Fallback to original behavior if schema-based mapping fails
|
|
169
|
+
logger.warning(f"Column mapping failed for {cls._table}, falling back to positional mapping: {e}")
|
|
170
|
+
expected_fields = [cls._pk] + cls._fields
|
|
171
|
+
for i, field in enumerate(expected_fields):
|
|
172
|
+
if i >= len(row):
|
|
173
|
+
break # Safety check
|
|
174
|
+
field_type = cls._types[i]
|
|
175
|
+
cls._set_field_value(obj, field, field_type, row[i], i == 0 and field == cls._pk)
|
|
176
|
+
|
|
177
|
+
return obj
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def _set_field_value(cls, obj, field: str, field_type: type, row_value, is_pk: bool = False):
|
|
181
|
+
"""Helper method to set field value with proper type conversion."""
|
|
182
|
+
if is_pk:
|
|
183
|
+
if field_type is int:
|
|
184
|
+
setattr(obj, field, int(row_value) if row_value is not None else None)
|
|
185
|
+
elif field_type is str:
|
|
186
|
+
setattr(obj, field, str(row_value) if row_value is not None else None)
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if field_type is str:
|
|
190
|
+
if not row_value:
|
|
191
|
+
setattr(obj, field, "")
|
|
192
|
+
elif isinstance(row_value, str) and (row_value.startswith('[') or row_value.startswith('{')):
|
|
176
193
|
try:
|
|
177
|
-
setattr(obj, field, json.loads(
|
|
194
|
+
setattr(obj, field, json.loads(row_value))
|
|
178
195
|
except json.JSONDecodeError:
|
|
179
|
-
setattr(obj, field,
|
|
196
|
+
setattr(obj, field, row_value)
|
|
180
197
|
else:
|
|
181
|
-
setattr(obj, field,
|
|
182
|
-
|
|
198
|
+
setattr(obj, field, str(row_value) if row_value is not None else None)
|
|
199
|
+
elif field_type is list:
|
|
200
|
+
try:
|
|
201
|
+
setattr(obj, field, json.loads(row_value) if row_value else [])
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
setattr(obj, field, [])
|
|
204
|
+
elif field_type is int:
|
|
205
|
+
setattr(obj, field, int(row_value) if row_value is not None else None)
|
|
206
|
+
elif field_type is float:
|
|
207
|
+
setattr(obj, field, float(row_value) if row_value is not None else None)
|
|
208
|
+
elif field_type is bool:
|
|
209
|
+
setattr(obj, field, bool(row_value) if row_value is not None else None)
|
|
210
|
+
elif field_type is dict:
|
|
211
|
+
try:
|
|
212
|
+
setattr(obj, field, json.loads(row_value) if row_value else {})
|
|
213
|
+
except json.JSONDecodeError:
|
|
214
|
+
setattr(obj, field, {})
|
|
215
|
+
else:
|
|
216
|
+
setattr(obj, field, row_value)
|
|
183
217
|
|
|
184
218
|
def save(self, retry=1):
|
|
185
219
|
try:
|
|
@@ -245,9 +279,9 @@ class SQLiteDBTable:
|
|
|
245
279
|
|
|
246
280
|
def add_column(self, column_name: str, new_column_type: str = "TEXT"):
|
|
247
281
|
try:
|
|
248
|
-
index = self._fields.index(column_name) + 1
|
|
249
282
|
self._db.execute(
|
|
250
283
|
f"ALTER TABLE {self._table} ADD COLUMN {column_name} {new_column_type}", commit=True)
|
|
284
|
+
self.__class__._column_order_cache = None # Reset cache when schema changes
|
|
251
285
|
logger.info(f"Added column {column_name} to {self._table}")
|
|
252
286
|
except sqlite3.OperationalError as e:
|
|
253
287
|
if "duplicate column name" in str(e):
|
|
@@ -286,11 +320,13 @@ class SQLiteDBTable:
|
|
|
286
320
|
def rename_column(cls, old_column: str, new_column: str):
|
|
287
321
|
cls._db.execute(
|
|
288
322
|
f"ALTER TABLE {cls._table} RENAME COLUMN {old_column} TO {new_column}", commit=True)
|
|
323
|
+
cls._column_order_cache = None # Reset cache when schema changes
|
|
289
324
|
|
|
290
325
|
@classmethod
|
|
291
326
|
def drop_column(cls, column_name: str):
|
|
292
327
|
cls._db.execute(
|
|
293
328
|
f"ALTER TABLE {cls._table} DROP COLUMN {column_name}", commit=True)
|
|
329
|
+
cls._column_order_cache = None # Reset cache when schema changes
|
|
294
330
|
|
|
295
331
|
@classmethod
|
|
296
332
|
def get_column_type(cls, column_name: str) -> Optional[str]:
|
|
@@ -315,6 +351,36 @@ class SQLiteDBTable:
|
|
|
315
351
|
f"UPDATE {cls._table} SET {new_column} = CAST({old_column} AS {new_type})", commit=True)
|
|
316
352
|
cls._db.execute(
|
|
317
353
|
f"ALTER TABLE {cls._table} DROP COLUMN {old_column}", commit=True)
|
|
354
|
+
cls._column_order_cache = None # Reset cache when schema changes
|
|
355
|
+
|
|
356
|
+
@classmethod
|
|
357
|
+
def get_actual_column_order(cls) -> List[str]:
|
|
358
|
+
"""Get the actual column order from the database schema."""
|
|
359
|
+
if cls._column_order_cache is not None:
|
|
360
|
+
return cls._column_order_cache
|
|
361
|
+
|
|
362
|
+
# Use direct database access to avoid recursion through from_row()
|
|
363
|
+
with cls._db._lock:
|
|
364
|
+
import sqlite3
|
|
365
|
+
with sqlite3.connect(cls._db.db_path, check_same_thread=False) as conn:
|
|
366
|
+
cursor = conn.cursor()
|
|
367
|
+
cursor.execute(f"PRAGMA table_info({cls._table})")
|
|
368
|
+
columns_info = cursor.fetchall()
|
|
369
|
+
|
|
370
|
+
# Each row is (cid, name, type, notnull, dflt_value, pk)
|
|
371
|
+
# Sort by column id (cid) to get the actual order
|
|
372
|
+
sorted_columns = sorted(columns_info, key=lambda x: x[0])
|
|
373
|
+
column_order = [col[1] for col in sorted_columns]
|
|
374
|
+
|
|
375
|
+
# Cache the result
|
|
376
|
+
cls._column_order_cache = column_order
|
|
377
|
+
return column_order
|
|
378
|
+
|
|
379
|
+
@classmethod
|
|
380
|
+
def get_expected_column_list(cls) -> str:
|
|
381
|
+
"""Get comma-separated list of columns in expected order for explicit SELECT queries."""
|
|
382
|
+
expected_fields = [cls._pk] + cls._fields
|
|
383
|
+
return ', '.join(expected_fields)
|
|
318
384
|
|
|
319
385
|
|
|
320
386
|
class AIModelsTable(SQLiteDBTable):
|
|
@@ -188,41 +188,41 @@ class OverlayProcessor:
|
|
|
188
188
|
For primary monitor, excludes taskbar. For others, returns full monitor area.
|
|
189
189
|
monitor_index: 0 = primary monitor, 1+ = others (as in mss.monitors).
|
|
190
190
|
"""
|
|
191
|
-
set_dpi_awareness()
|
|
191
|
+
# set_dpi_awareness()
|
|
192
192
|
with mss.mss() as sct:
|
|
193
193
|
monitors = sct.monitors[1:]
|
|
194
|
-
return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
194
|
+
# return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
|
|
195
|
+
if is_windows() and monitor_index == 0:
|
|
196
|
+
from ctypes import wintypes
|
|
197
|
+
import ctypes
|
|
198
|
+
# Get work area for primary monitor (ignores taskbar)
|
|
199
|
+
SPI_GETWORKAREA = 0x0030
|
|
200
|
+
rect = wintypes.RECT()
|
|
201
|
+
res = ctypes.windll.user32.SystemParametersInfoW(
|
|
202
|
+
SPI_GETWORKAREA, 0, ctypes.byref(rect), 0
|
|
203
|
+
)
|
|
204
|
+
if not res:
|
|
205
|
+
raise ctypes.WinError()
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
207
|
+
return {
|
|
208
|
+
"left": rect.left,
|
|
209
|
+
"top": rect.top,
|
|
210
|
+
"width": rect.right - rect.left,
|
|
211
|
+
"height": rect.bottom - rect.top,
|
|
212
|
+
}
|
|
213
|
+
elif is_windows() and monitor_index > 0:
|
|
214
|
+
# Secondary monitors: just return with a guess of how tall the taskbar is
|
|
215
|
+
taskbar_height_guess = 48 # A common taskbar height, may vary
|
|
216
|
+
mon = monitors[monitor_index]
|
|
217
|
+
return {
|
|
218
|
+
"left": mon["left"],
|
|
219
|
+
"top": mon["top"],
|
|
220
|
+
"width": mon["width"],
|
|
221
|
+
"height": mon["height"] - taskbar_height_guess
|
|
222
|
+
}
|
|
223
|
+
else:
|
|
224
|
+
# For non-Windows systems or unspecified monitors, return the monitor area as-is
|
|
225
|
+
return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
|
|
226
226
|
|
|
227
227
|
|
|
228
228
|
def _get_full_screenshot(self) -> Tuple[Image.Image | None, int, int]:
|
|
@@ -1182,7 +1182,7 @@ def register_database_api_routes(app):
|
|
|
1182
1182
|
})
|
|
1183
1183
|
|
|
1184
1184
|
except Exception as e:
|
|
1185
|
-
logger.error(f"Unexpected error in api_stats: {e}")
|
|
1185
|
+
logger.error(f"Unexpected error in api_stats: {e}", exc_info=True)
|
|
1186
1186
|
return jsonify({'error': 'Failed to generate statistics'}), 500
|
|
1187
1187
|
|
|
1188
1188
|
@app.route('/api/goals-today', methods=['GET'])
|
|
@@ -41,10 +41,10 @@ GameSentenceMiner/ui/config_gui.py,sha256=Nt6uuJrinOrSpDdrw92zdd1GvyQNjOoGvkZQdG
|
|
|
41
41
|
GameSentenceMiner/ui/screenshot_selector.py,sha256=AKML87MpgYQeSuj1F10GngpNrn9qp06zLLzNRwrQWM8,8900
|
|
42
42
|
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
43
|
GameSentenceMiner/util/configuration.py,sha256=fYlU4Qrr6T-k_G2MY2rgoQ9EV_k7XBfbtsO8q7PKTsU,46653
|
|
44
|
-
GameSentenceMiner/util/db.py,sha256=
|
|
44
|
+
GameSentenceMiner/util/db.py,sha256=1DjGjlwWnPefmQfzvMqqFPW0a0qeO-fIXE1YqKiok18,32000
|
|
45
45
|
GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
|
|
46
46
|
GameSentenceMiner/util/ffmpeg.py,sha256=cAzztfY36Xf2WvsJDjavoiMOvA9ac2GVdCrSB4LzHk4,29007
|
|
47
|
-
GameSentenceMiner/util/get_overlay_coords.py,sha256=
|
|
47
|
+
GameSentenceMiner/util/get_overlay_coords.py,sha256=c37zVKPxFqwIi5XjZKjxRCW_Y8W4e1MX7vSLLwcOhs4,22116
|
|
48
48
|
GameSentenceMiner/util/gsm_utils.py,sha256=mASECTmN10c2yPL4NEfLg0Y0YWwFso1i6r_hhJPR3MY,10974
|
|
49
49
|
GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
|
|
50
50
|
GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
|
|
@@ -59,7 +59,7 @@ GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=l3s9Z-x1b57GX048o5h-MVv0UT
|
|
|
59
59
|
GameSentenceMiner/util/win10toast/__init__.py,sha256=6TL2w6rzNmpJEp6_v2cAJP_7ExA3UsKzwdM08pNcVfE,5341
|
|
60
60
|
GameSentenceMiner/util/win10toast/__main__.py,sha256=5MYnBcFj8y_6Dyc1kiPd0_FsUuh4yl1cv5wsleU6V4w,668
|
|
61
61
|
GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
|
-
GameSentenceMiner/web/database_api.py,sha256=
|
|
62
|
+
GameSentenceMiner/web/database_api.py,sha256=Ph30uGAJFSxRkBdrZglXKsuwuKP46RxjzGODMO5aaLc,84827
|
|
63
63
|
GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZsA,5119
|
|
64
64
|
GameSentenceMiner/web/gsm_websocket.py,sha256=B0VKpxmsRu0WRh5nFWlpDPBQ6-K2ed7TEIa0O6YWeoo,4166
|
|
65
65
|
GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
|
|
@@ -124,9 +124,9 @@ GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic
|
|
|
124
124
|
GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json,sha256=8wjnnaYQqmho6t5tMxrIAc03512A2tYhQh5dfsQnfAM,11372
|
|
125
125
|
GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json,sha256=wRkqZNPzz6DT9OTPHpXwfqW96Qb96stCQNNgOL-ZdKk,17535
|
|
126
126
|
GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
127
|
-
gamesentenceminer-2.18.
|
|
128
|
-
gamesentenceminer-2.18.
|
|
129
|
-
gamesentenceminer-2.18.
|
|
130
|
-
gamesentenceminer-2.18.
|
|
131
|
-
gamesentenceminer-2.18.
|
|
132
|
-
gamesentenceminer-2.18.
|
|
127
|
+
gamesentenceminer-2.18.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
128
|
+
gamesentenceminer-2.18.4.dist-info/METADATA,sha256=SXw7P4busY00L4EwV-Avnt2C29HYJGLoePm5WBEgIo8,7487
|
|
129
|
+
gamesentenceminer-2.18.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
130
|
+
gamesentenceminer-2.18.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
|
131
|
+
gamesentenceminer-2.18.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
|
132
|
+
gamesentenceminer-2.18.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|