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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import uuid
|
|
2
|
+
import re
|
|
3
|
+
from difflib import SequenceMatcher
|
|
2
4
|
from typing import Optional, List, Dict
|
|
3
5
|
|
|
4
6
|
from GameSentenceMiner.util.db import SQLiteDBTable
|
|
@@ -6,29 +8,41 @@ from GameSentenceMiner.util.configuration import logger
|
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class GamesTable(SQLiteDBTable):
|
|
9
|
-
_table =
|
|
11
|
+
_table = "games"
|
|
10
12
|
_fields = [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
"deck_id",
|
|
14
|
+
"title_original",
|
|
15
|
+
"title_romaji",
|
|
16
|
+
"title_english",
|
|
17
|
+
"type",
|
|
18
|
+
"description",
|
|
19
|
+
"image",
|
|
20
|
+
"character_count",
|
|
21
|
+
"difficulty",
|
|
22
|
+
"links",
|
|
23
|
+
"completed",
|
|
24
|
+
"release_date",
|
|
25
|
+
"manual_overrides",
|
|
26
|
+
"obs_scene_name",
|
|
15
27
|
]
|
|
16
28
|
_types = [
|
|
17
|
-
str,
|
|
18
|
-
int,
|
|
19
|
-
str,
|
|
20
|
-
str,
|
|
21
|
-
str,
|
|
22
|
-
str,
|
|
23
|
-
str,
|
|
24
|
-
str,
|
|
25
|
-
int,
|
|
26
|
-
int,
|
|
27
|
-
list,
|
|
28
|
-
bool,
|
|
29
|
-
|
|
29
|
+
str, # id (primary key)
|
|
30
|
+
int, # deck_id
|
|
31
|
+
str, # title_original
|
|
32
|
+
str, # title_romaji
|
|
33
|
+
str, # title_english
|
|
34
|
+
str, # type (string)
|
|
35
|
+
str, # description
|
|
36
|
+
str, # image (base64)
|
|
37
|
+
int, # character_count
|
|
38
|
+
int, # difficulty
|
|
39
|
+
list, # links (stored as JSON)
|
|
40
|
+
bool, # completed
|
|
41
|
+
str, # release_date (ISO date string)
|
|
42
|
+
list, # manual_overrides (stored as JSON)
|
|
43
|
+
str, # obs_scene_name (immutable OBS scene name)
|
|
30
44
|
]
|
|
31
|
-
_pk =
|
|
45
|
+
_pk = "id"
|
|
32
46
|
_auto_increment = False # UUID-based primary key
|
|
33
47
|
|
|
34
48
|
def __init__(
|
|
@@ -38,85 +52,235 @@ class GamesTable(SQLiteDBTable):
|
|
|
38
52
|
title_original: Optional[str] = None,
|
|
39
53
|
title_romaji: Optional[str] = None,
|
|
40
54
|
title_english: Optional[str] = None,
|
|
41
|
-
|
|
55
|
+
game_type: Optional[str] = None,
|
|
42
56
|
description: Optional[str] = None,
|
|
43
57
|
image: Optional[str] = None,
|
|
44
58
|
character_count: int = 0,
|
|
45
59
|
difficulty: Optional[int] = None,
|
|
46
60
|
links: Optional[List[Dict]] = None,
|
|
47
61
|
completed: bool = False,
|
|
48
|
-
|
|
62
|
+
release_date: Optional[str] = None,
|
|
63
|
+
manual_overrides: Optional[List[str]] = None,
|
|
64
|
+
obs_scene_name: Optional[str] = None,
|
|
49
65
|
):
|
|
50
66
|
self.id = id if id else str(uuid.uuid4())
|
|
51
67
|
self.deck_id = deck_id
|
|
52
|
-
self.title_original = title_original if title_original else
|
|
53
|
-
self.title_romaji = title_romaji if title_romaji else
|
|
54
|
-
self.title_english = title_english if title_english else
|
|
55
|
-
self.type =
|
|
56
|
-
self.description = description if description else
|
|
57
|
-
self.image = image if image else
|
|
68
|
+
self.title_original = title_original if title_original else ""
|
|
69
|
+
self.title_romaji = title_romaji if title_romaji else ""
|
|
70
|
+
self.title_english = title_english if title_english else ""
|
|
71
|
+
self.type = game_type if game_type else ""
|
|
72
|
+
self.description = description if description else ""
|
|
73
|
+
self.image = image if image else ""
|
|
58
74
|
self.character_count = character_count
|
|
59
75
|
self.difficulty = difficulty
|
|
60
76
|
self.links = links if links else []
|
|
61
77
|
self.completed = completed
|
|
78
|
+
self.release_date = release_date if release_date else ""
|
|
62
79
|
self.manual_overrides = manual_overrides if manual_overrides else []
|
|
80
|
+
self.obs_scene_name = obs_scene_name if obs_scene_name else ""
|
|
63
81
|
|
|
64
82
|
@classmethod
|
|
65
|
-
def get_by_deck_id(cls, deck_id: int) -> Optional[
|
|
83
|
+
def get_by_deck_id(cls, deck_id: int) -> Optional["GamesTable"]:
|
|
66
84
|
"""Get a game by its jiten.moe deck ID."""
|
|
67
85
|
row = cls._db.fetchone(
|
|
68
|
-
f"SELECT * FROM {cls._table} WHERE deck_id=?", (deck_id,)
|
|
86
|
+
f"SELECT * FROM {cls._table} WHERE deck_id=?", (deck_id,)
|
|
87
|
+
)
|
|
69
88
|
return cls.from_row(row) if row else None
|
|
70
89
|
|
|
71
90
|
@classmethod
|
|
72
|
-
def get_by_title(cls, title_original: str) -> Optional[
|
|
91
|
+
def get_by_title(cls, title_original: str) -> Optional["GamesTable"]:
|
|
73
92
|
"""Get a game by its original title."""
|
|
74
93
|
row = cls._db.fetchone(
|
|
75
|
-
f"SELECT * FROM {cls._table} WHERE title_original=?", (title_original,)
|
|
94
|
+
f"SELECT * FROM {cls._table} WHERE title_original=?", (title_original,)
|
|
95
|
+
)
|
|
76
96
|
return cls.from_row(row) if row else None
|
|
77
97
|
|
|
78
98
|
@classmethod
|
|
79
|
-
def
|
|
99
|
+
def get_by_obs_scene_name(cls, obs_scene_name: str) -> Optional["GamesTable"]:
|
|
100
|
+
"""Get a game by its OBS scene name."""
|
|
101
|
+
row = cls._db.fetchone(
|
|
102
|
+
f"SELECT * FROM {cls._table} WHERE obs_scene_name=?", (obs_scene_name,)
|
|
103
|
+
)
|
|
104
|
+
return cls.from_row(row) if row else None
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def normalize_game_name(cls, name: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Normalize a game name for fuzzy matching.
|
|
110
|
+
Removes version numbers, extra whitespace, and converts to lowercase.
|
|
111
|
+
"""
|
|
112
|
+
if not name:
|
|
113
|
+
return ""
|
|
114
|
+
# Remove version patterns like "ver1.00", "v1.0", "Ver.1.0", etc.
|
|
115
|
+
normalized = re.sub(
|
|
116
|
+
r"\s*v(?:er)?\.?\s*\d+(?:\.\d+)*", "", name, flags=re.IGNORECASE
|
|
117
|
+
)
|
|
118
|
+
# Remove extra whitespace
|
|
119
|
+
normalized = " ".join(normalized.split())
|
|
120
|
+
# Convert to lowercase for comparison
|
|
121
|
+
return normalized.lower().strip()
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def fuzzy_match_game_name(
|
|
125
|
+
cls, name1: str, name2: str, threshold: float = 0.85
|
|
126
|
+
) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Check if two game names are similar using fuzzy matching.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
name1: First game name
|
|
132
|
+
name2: Second game name
|
|
133
|
+
threshold: Similarity threshold (0.0 to 1.0), default 0.85
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if names are similar enough, False otherwise
|
|
137
|
+
"""
|
|
138
|
+
if not name1 or not name2:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
# Normalize both names
|
|
142
|
+
norm1 = cls.normalize_game_name(name1)
|
|
143
|
+
norm2 = cls.normalize_game_name(name2)
|
|
144
|
+
|
|
145
|
+
# Calculate similarity ratio
|
|
146
|
+
similarity = SequenceMatcher(None, norm1, norm2).ratio()
|
|
147
|
+
|
|
148
|
+
logger.debug(
|
|
149
|
+
f"[FUZZY_MATCH] Comparing '{name1}' vs '{name2}': normalized='{norm1}' vs '{norm2}', similarity={similarity:.2f}, threshold={threshold}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return similarity >= threshold
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def find_similar_game(
|
|
156
|
+
cls, game_name: str, threshold: float = 0.85
|
|
157
|
+
) -> Optional["GamesTable"]:
|
|
158
|
+
"""
|
|
159
|
+
Find a game with a similar name using fuzzy matching.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
game_name: The game name to search for
|
|
163
|
+
threshold: Similarity threshold (default 0.85)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
GamesTable: The similar game if found, None otherwise
|
|
167
|
+
"""
|
|
168
|
+
# Get all games
|
|
169
|
+
all_games = cls.all()
|
|
170
|
+
|
|
171
|
+
for game in all_games:
|
|
172
|
+
# Check against title_original
|
|
173
|
+
if cls.fuzzy_match_game_name(game_name, game.title_original, threshold):
|
|
174
|
+
logger.debug(
|
|
175
|
+
f"[FUZZY_MATCH] Found similar game by title_original: '{game_name}' matches '{game.title_original}' (id={game.id})"
|
|
176
|
+
)
|
|
177
|
+
return game
|
|
178
|
+
|
|
179
|
+
# Check against obs_scene_name if it exists
|
|
180
|
+
if game.obs_scene_name and cls.fuzzy_match_game_name(
|
|
181
|
+
game_name, game.obs_scene_name, threshold
|
|
182
|
+
):
|
|
183
|
+
logger.debug(
|
|
184
|
+
f"[FUZZY_MATCH] Found similar game by obs_scene_name: '{game_name}' matches '{game.obs_scene_name}' (id={game.id})"
|
|
185
|
+
)
|
|
186
|
+
return game
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def get_or_create_by_name(cls, game_name: str) -> "GamesTable":
|
|
80
192
|
"""
|
|
81
193
|
Get an existing game by name, or create a new one if it doesn't exist.
|
|
82
194
|
This is the primary method for automatically linking game_lines to games.
|
|
83
|
-
|
|
195
|
+
|
|
84
196
|
Args:
|
|
85
|
-
game_name: The original game name (from game_lines.game_name)
|
|
86
|
-
|
|
197
|
+
game_name: The original game name (from game_lines.game_name / OBS scene name)
|
|
198
|
+
|
|
87
199
|
Returns:
|
|
88
200
|
GamesTable: The existing or newly created game record
|
|
89
201
|
"""
|
|
90
|
-
|
|
202
|
+
logger.debug(f"[GET_OR_CREATE] Looking up game: '{game_name}'")
|
|
203
|
+
|
|
204
|
+
# Try exact match on title_original first
|
|
91
205
|
existing = cls.get_by_title(game_name)
|
|
92
206
|
if existing:
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"[GET_OR_CREATE] Found exact match in games table: id={existing.id}, deck_id={existing.deck_id}"
|
|
209
|
+
)
|
|
93
210
|
return existing
|
|
94
|
-
|
|
95
|
-
|
|
211
|
+
|
|
212
|
+
logger.debug(
|
|
213
|
+
f"[GET_OR_CREATE] No exact match found, checking game_lines for existing mapping..."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Check if existing game_lines already have this game_name mapped to a game_id
|
|
217
|
+
# This handles cases where OBS scene name != game title_original
|
|
218
|
+
from GameSentenceMiner.util.db import GameLinesTable
|
|
219
|
+
|
|
220
|
+
# First, let's see what game_names exist in game_lines
|
|
221
|
+
all_game_names = GameLinesTable._db.fetchall(
|
|
222
|
+
f"SELECT DISTINCT game_name FROM {GameLinesTable._table} LIMIT 10"
|
|
223
|
+
)
|
|
224
|
+
logger.debug(
|
|
225
|
+
f"[GET_OR_CREATE] Sample game_names in game_lines: {[row[0] for row in all_game_names]}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Now try to find our specific game_name
|
|
229
|
+
existing_line = GameLinesTable._db.fetchone(
|
|
230
|
+
f"SELECT game_id FROM {GameLinesTable._table} WHERE game_name=? AND game_id IS NOT NULL AND game_id != '' LIMIT 1",
|
|
231
|
+
(game_name,),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if existing_line and existing_line[0]:
|
|
235
|
+
game_id = existing_line[0]
|
|
236
|
+
logger.debug(
|
|
237
|
+
f"[GET_OR_CREATE] Found existing mapping in game_lines: '{game_name}' -> game_id={game_id}"
|
|
238
|
+
)
|
|
239
|
+
existing_game = cls.get(game_id)
|
|
240
|
+
if existing_game:
|
|
241
|
+
logger.debug(
|
|
242
|
+
f"[GET_OR_CREATE] ✓ Reusing existing game: '{game_name}' -> game_id={game_id} ('{existing_game.title_original}', deck_id={existing_game.deck_id})"
|
|
243
|
+
)
|
|
244
|
+
return existing_game
|
|
245
|
+
else:
|
|
246
|
+
logger.warning(
|
|
247
|
+
f"[GET_OR_CREATE] game_id {game_id} found in game_lines but not in games table!"
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
logger.debug(
|
|
251
|
+
f"[GET_OR_CREATE] No existing mapping found in game_lines for '{game_name}'"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# No existing mapping found - create new UNLINKED game with minimal info
|
|
255
|
+
# Store the OBS scene name in obs_scene_name field for future linking
|
|
96
256
|
new_game = cls(
|
|
97
257
|
title_original=game_name,
|
|
98
|
-
title_romaji=
|
|
99
|
-
title_english=
|
|
100
|
-
description=
|
|
258
|
+
title_romaji="",
|
|
259
|
+
title_english="",
|
|
260
|
+
description="",
|
|
101
261
|
difficulty=None,
|
|
102
|
-
completed=False
|
|
262
|
+
completed=False,
|
|
263
|
+
obs_scene_name=game_name, # Store original OBS scene name
|
|
264
|
+
)
|
|
265
|
+
new_game.add() # Use add() instead of save() for new records with UUID primary keys
|
|
266
|
+
logger.debug(
|
|
267
|
+
f"[GET_OR_CREATE] ✗ Created new UNLINKED game record: '{game_name}' (id={new_game.id}, obs_scene_name='{game_name}')"
|
|
268
|
+
)
|
|
269
|
+
logger.debug(
|
|
270
|
+
f"[GET_OR_CREATE] ℹ️ This game needs to be manually linked to jiten.moe via the Games Management interface"
|
|
103
271
|
)
|
|
104
|
-
new_game.save()
|
|
105
|
-
logger.info(f"Auto-created new game record: {game_name} (id={new_game.id})")
|
|
106
272
|
return new_game
|
|
107
273
|
|
|
108
274
|
@classmethod
|
|
109
|
-
def get_all_completed(cls) -> List[
|
|
275
|
+
def get_all_completed(cls) -> List["GamesTable"]:
|
|
110
276
|
"""Get all completed games."""
|
|
111
|
-
rows = cls._db.fetchall(
|
|
112
|
-
f"SELECT * FROM {cls._table} WHERE completed=1")
|
|
277
|
+
rows = cls._db.fetchall(f"SELECT * FROM {cls._table} WHERE completed=1")
|
|
113
278
|
return [cls.from_row(row) for row in rows]
|
|
114
279
|
|
|
115
280
|
@classmethod
|
|
116
|
-
def get_all_in_progress(cls) -> List[
|
|
281
|
+
def get_all_in_progress(cls) -> List["GamesTable"]:
|
|
117
282
|
"""Get all games that are in progress (not completed)."""
|
|
118
|
-
rows = cls._db.fetchall(
|
|
119
|
-
f"SELECT * FROM {cls._table} WHERE completed=0")
|
|
283
|
+
rows = cls._db.fetchall(f"SELECT * FROM {cls._table} WHERE completed=0")
|
|
120
284
|
return [cls.from_row(row) for row in rows]
|
|
121
285
|
|
|
122
286
|
@classmethod
|
|
@@ -126,9 +290,10 @@ class GamesTable(SQLiteDBTable):
|
|
|
126
290
|
Returns Unix timestamp (float) or None if no lines exist.
|
|
127
291
|
"""
|
|
128
292
|
from GameSentenceMiner.util.db import GameLinesTable
|
|
293
|
+
|
|
129
294
|
result = GameLinesTable._db.fetchone(
|
|
130
295
|
f"SELECT MIN(timestamp) FROM {GameLinesTable._table} WHERE game_id=?",
|
|
131
|
-
(game_id,)
|
|
296
|
+
(game_id,),
|
|
132
297
|
)
|
|
133
298
|
return result[0] if result and result[0] else None
|
|
134
299
|
|
|
@@ -139,34 +304,37 @@ class GamesTable(SQLiteDBTable):
|
|
|
139
304
|
Returns Unix timestamp (float) or None if no lines exist.
|
|
140
305
|
"""
|
|
141
306
|
from GameSentenceMiner.util.db import GameLinesTable
|
|
307
|
+
|
|
142
308
|
result = GameLinesTable._db.fetchone(
|
|
143
309
|
f"SELECT MAX(timestamp) FROM {GameLinesTable._table} WHERE game_id=?",
|
|
144
|
-
(game_id,)
|
|
310
|
+
(game_id,),
|
|
145
311
|
)
|
|
146
312
|
return result[0] if result and result[0] else None
|
|
147
313
|
|
|
148
314
|
def is_field_manual(self, field_name: str) -> bool:
|
|
149
315
|
"""
|
|
150
316
|
Check if a field has been manually edited and should not be auto-updated.
|
|
151
|
-
|
|
317
|
+
|
|
152
318
|
Args:
|
|
153
319
|
field_name: The name of the field to check
|
|
154
|
-
|
|
320
|
+
|
|
155
321
|
Returns:
|
|
156
322
|
True if the field is manually overridden, False otherwise
|
|
157
323
|
"""
|
|
158
324
|
return field_name in self.manual_overrides
|
|
159
|
-
|
|
325
|
+
|
|
160
326
|
def mark_field_manual(self, field_name: str):
|
|
161
327
|
"""
|
|
162
328
|
Mark a field as manually edited so it won't be auto-updated.
|
|
163
|
-
|
|
329
|
+
|
|
164
330
|
Args:
|
|
165
331
|
field_name: The name of the field to mark as manual
|
|
166
332
|
"""
|
|
167
333
|
if field_name not in self.manual_overrides and field_name in self._fields:
|
|
168
334
|
self.manual_overrides.append(field_name)
|
|
169
|
-
logger.debug(
|
|
335
|
+
logger.debug(
|
|
336
|
+
f"Marked field '{field_name}' as manually overridden for game {self.id}"
|
|
337
|
+
)
|
|
170
338
|
|
|
171
339
|
def update_all_fields_manual(
|
|
172
340
|
self,
|
|
@@ -174,66 +342,72 @@ class GamesTable(SQLiteDBTable):
|
|
|
174
342
|
title_original: Optional[str] = None,
|
|
175
343
|
title_romaji: Optional[str] = None,
|
|
176
344
|
title_english: Optional[str] = None,
|
|
177
|
-
|
|
345
|
+
game_type: Optional[str] = None,
|
|
178
346
|
description: Optional[str] = None,
|
|
179
347
|
image: Optional[str] = None,
|
|
180
348
|
character_count: Optional[int] = None,
|
|
181
349
|
difficulty: Optional[int] = None,
|
|
182
350
|
links: Optional[List[Dict]] = None,
|
|
183
|
-
completed: Optional[bool] = None
|
|
351
|
+
completed: Optional[bool] = None,
|
|
352
|
+
release_date: Optional[str] = None,
|
|
184
353
|
):
|
|
185
354
|
"""
|
|
186
355
|
Update all fields of the game at once. Only provided fields will be updated.
|
|
187
356
|
Fields that are updated will be automatically marked as manually overridden.
|
|
188
|
-
|
|
357
|
+
|
|
189
358
|
Args:
|
|
190
359
|
deck_id: jiten.moe deck ID
|
|
191
360
|
title_original: Original Japanese title
|
|
192
361
|
title_romaji: Romanized title
|
|
193
362
|
title_english: English translated title
|
|
363
|
+
game_type: Game type (string)
|
|
194
364
|
description: Game description
|
|
195
365
|
image: Base64-encoded image data
|
|
196
366
|
character_count: Total character count
|
|
197
367
|
difficulty: Difficulty rating
|
|
198
368
|
links: List of link objects
|
|
199
369
|
completed: Whether the game is completed
|
|
370
|
+
release_date: Release date (ISO format string)
|
|
200
371
|
"""
|
|
201
372
|
if deck_id is not None:
|
|
202
373
|
self.deck_id = deck_id
|
|
203
|
-
self.mark_field_manual(
|
|
374
|
+
self.mark_field_manual("deck_id")
|
|
204
375
|
if title_original is not None:
|
|
205
376
|
self.title_original = title_original
|
|
206
|
-
self.mark_field_manual(
|
|
377
|
+
self.mark_field_manual("title_original")
|
|
207
378
|
if title_romaji is not None:
|
|
208
379
|
self.title_romaji = title_romaji
|
|
209
|
-
self.mark_field_manual(
|
|
380
|
+
self.mark_field_manual("title_romaji")
|
|
210
381
|
if title_english is not None:
|
|
211
382
|
self.title_english = title_english
|
|
212
|
-
self.mark_field_manual(
|
|
213
|
-
if
|
|
214
|
-
self.type =
|
|
215
|
-
self.mark_field_manual(
|
|
383
|
+
self.mark_field_manual("title_english")
|
|
384
|
+
if game_type is not None:
|
|
385
|
+
self.type = game_type
|
|
386
|
+
self.mark_field_manual("type")
|
|
216
387
|
if description is not None:
|
|
217
388
|
self.description = description
|
|
218
|
-
self.mark_field_manual(
|
|
389
|
+
self.mark_field_manual("description")
|
|
219
390
|
if image is not None:
|
|
220
391
|
self.image = image
|
|
221
|
-
self.mark_field_manual(
|
|
392
|
+
self.mark_field_manual("image")
|
|
222
393
|
if character_count is not None:
|
|
223
394
|
self.character_count = character_count
|
|
224
|
-
self.mark_field_manual(
|
|
395
|
+
self.mark_field_manual("character_count")
|
|
225
396
|
if difficulty is not None:
|
|
226
397
|
self.difficulty = difficulty
|
|
227
|
-
self.mark_field_manual(
|
|
398
|
+
self.mark_field_manual("difficulty")
|
|
228
399
|
if links is not None:
|
|
229
400
|
self.links = links
|
|
230
|
-
self.mark_field_manual(
|
|
401
|
+
self.mark_field_manual("links")
|
|
231
402
|
if completed is not None:
|
|
232
403
|
self.completed = completed
|
|
233
|
-
self.mark_field_manual(
|
|
234
|
-
|
|
404
|
+
self.mark_field_manual("completed")
|
|
405
|
+
if release_date is not None:
|
|
406
|
+
self.release_date = release_date
|
|
407
|
+
self.mark_field_manual("release_date")
|
|
408
|
+
|
|
235
409
|
self.save()
|
|
236
|
-
logger.
|
|
410
|
+
logger.debug(f"Updated game {self.id} ({self.title_original})")
|
|
237
411
|
|
|
238
412
|
def update_all_fields_from_jiten(
|
|
239
413
|
self,
|
|
@@ -241,28 +415,31 @@ class GamesTable(SQLiteDBTable):
|
|
|
241
415
|
title_original: Optional[str] = None,
|
|
242
416
|
title_romaji: Optional[str] = None,
|
|
243
417
|
title_english: Optional[str] = None,
|
|
244
|
-
|
|
418
|
+
game_type: Optional[str] = None,
|
|
245
419
|
description: Optional[str] = None,
|
|
246
420
|
image: Optional[str] = None,
|
|
247
421
|
character_count: Optional[int] = None,
|
|
248
422
|
difficulty: Optional[int] = None,
|
|
249
423
|
links: Optional[List[Dict]] = None,
|
|
250
|
-
completed: Optional[bool] = None
|
|
424
|
+
completed: Optional[bool] = None,
|
|
425
|
+
release_date: Optional[str] = None,
|
|
251
426
|
):
|
|
252
427
|
"""
|
|
253
428
|
Update all fields of the game at once. Only provided fields will be updated.
|
|
254
|
-
|
|
429
|
+
|
|
255
430
|
Args:
|
|
256
431
|
deck_id: jiten.moe deck ID
|
|
257
432
|
title_original: Original Japanese title
|
|
258
433
|
title_romaji: Romanized title
|
|
259
434
|
title_english: English translated title
|
|
435
|
+
game_type: Game type (string)
|
|
260
436
|
description: Game description
|
|
261
437
|
image: Base64-encoded image data
|
|
262
438
|
character_count: Total character count
|
|
263
439
|
difficulty: Difficulty rating
|
|
264
440
|
links: List of link objects
|
|
265
441
|
completed: Whether the game is completed
|
|
442
|
+
release_date: Release date (ISO format string)
|
|
266
443
|
"""
|
|
267
444
|
if deck_id is not None:
|
|
268
445
|
self.deck_id = deck_id
|
|
@@ -272,8 +449,8 @@ class GamesTable(SQLiteDBTable):
|
|
|
272
449
|
self.title_romaji = title_romaji
|
|
273
450
|
if title_english is not None:
|
|
274
451
|
self.title_english = title_english
|
|
275
|
-
if
|
|
276
|
-
self.type =
|
|
452
|
+
if game_type is not None:
|
|
453
|
+
self.type = game_type
|
|
277
454
|
if description is not None:
|
|
278
455
|
self.description = description
|
|
279
456
|
if image is not None:
|
|
@@ -285,36 +462,106 @@ class GamesTable(SQLiteDBTable):
|
|
|
285
462
|
if links is not None:
|
|
286
463
|
self.links = links
|
|
287
464
|
if completed is not None:
|
|
288
|
-
self.completed = completed
|
|
465
|
+
self.completed = completed
|
|
466
|
+
if release_date is not None:
|
|
467
|
+
logger.debug(
|
|
468
|
+
f"📅 GamesTable.update_all_fields_from_jiten: Setting release_date for game {self.id} to '{release_date}' (type: {type(release_date)})"
|
|
469
|
+
)
|
|
470
|
+
self.release_date = release_date
|
|
471
|
+
else:
|
|
472
|
+
logger.debug(
|
|
473
|
+
f"⏭️ GamesTable.update_all_fields_from_jiten: release_date is None for game {self.id}"
|
|
474
|
+
)
|
|
289
475
|
self.save()
|
|
290
|
-
logger.
|
|
476
|
+
logger.debug(
|
|
477
|
+
f"Updated game {self.id} ({self.title_original}) - final release_date: '{self.release_date}'"
|
|
478
|
+
)
|
|
291
479
|
|
|
292
480
|
def add_link(self, link_type: int, url: str, link_id: Optional[int] = None):
|
|
293
481
|
"""
|
|
294
482
|
Add a link to the game's links array and persist to database.
|
|
295
|
-
|
|
483
|
+
|
|
296
484
|
Args:
|
|
297
485
|
link_type: Type of link (e.g., 4 for AniList, 5 for MyAnimeList)
|
|
298
486
|
url: URL of the link
|
|
299
487
|
link_id: Optional link ID
|
|
300
|
-
|
|
488
|
+
|
|
301
489
|
Note:
|
|
302
490
|
Changes are automatically saved to the database.
|
|
303
491
|
"""
|
|
304
|
-
new_link = {
|
|
305
|
-
'linkType': link_type,
|
|
306
|
-
'url': url,
|
|
307
|
-
'deckId': self.deck_id
|
|
308
|
-
}
|
|
492
|
+
new_link = {"linkType": link_type, "url": url, "deckId": self.deck_id}
|
|
309
493
|
if link_id is not None:
|
|
310
|
-
new_link[
|
|
311
|
-
|
|
494
|
+
new_link["linkId"] = link_id
|
|
495
|
+
|
|
312
496
|
self.links.append(new_link)
|
|
313
497
|
self.save()
|
|
314
498
|
|
|
315
499
|
def get_lines(self) -> List:
|
|
316
500
|
"""Get all lines associated with this game."""
|
|
317
501
|
from GameSentenceMiner.util.db import GameLinesTable
|
|
502
|
+
|
|
318
503
|
rows = GameLinesTable._db.fetchall(
|
|
319
|
-
f"SELECT * FROM {GameLinesTable._table} WHERE game_id=?", (self.id,)
|
|
320
|
-
|
|
504
|
+
f"SELECT * FROM {GameLinesTable._table} WHERE game_id=?", (self.id,)
|
|
505
|
+
)
|
|
506
|
+
return [GameLinesTable.from_row(row) for row in rows]
|
|
507
|
+
|
|
508
|
+
@classmethod
|
|
509
|
+
def get_by_game_line(cls, game_line) -> Optional["GamesTable"]:
|
|
510
|
+
"""
|
|
511
|
+
Get game metadata from a game_line record using the game_id relationship.
|
|
512
|
+
Falls back to name-based lookup if game_id is missing or invalid.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
game_line: A GameLinesTable record
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
GamesTable: The game record, or None if not found
|
|
519
|
+
"""
|
|
520
|
+
logger.debug(
|
|
521
|
+
f"[GET_BY_GAME_LINE] Looking up game for line with game_name='{game_line.game_name if hasattr(game_line, 'game_name') else 'N/A'}', game_id='{game_line.game_id if hasattr(game_line, 'game_id') else 'N/A'}'"
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# First try using game_id relationship if it exists
|
|
525
|
+
if (
|
|
526
|
+
hasattr(game_line, "game_id")
|
|
527
|
+
and game_line.game_id
|
|
528
|
+
and game_line.game_id.strip()
|
|
529
|
+
):
|
|
530
|
+
logger.debug(
|
|
531
|
+
f"[GET_BY_GAME_LINE] Attempting lookup by game_id: '{game_line.game_id}'"
|
|
532
|
+
)
|
|
533
|
+
game = cls.get(game_line.game_id)
|
|
534
|
+
if game:
|
|
535
|
+
logger.debug(
|
|
536
|
+
f"[GET_BY_GAME_LINE] ✓ Found game by game_id: title_original='{game.title_original}', deck_id={game.deck_id}, has_image={bool(game.image)}"
|
|
537
|
+
)
|
|
538
|
+
return game
|
|
539
|
+
else:
|
|
540
|
+
logger.warning(
|
|
541
|
+
f"[GET_BY_GAME_LINE] game_id '{game_line.game_id}' not found in games table, falling back to name lookup"
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
logger.debug(
|
|
545
|
+
f"[GET_BY_GAME_LINE] No valid game_id, falling back to name lookup"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Fallback to name-based lookup
|
|
549
|
+
if hasattr(game_line, "game_name") and game_line.game_name:
|
|
550
|
+
logger.debug(
|
|
551
|
+
f"[GET_BY_GAME_LINE] Attempting lookup by game_name: '{game_line.game_name}'"
|
|
552
|
+
)
|
|
553
|
+
game = cls.get_by_title(game_line.game_name)
|
|
554
|
+
if game:
|
|
555
|
+
logger.debug(
|
|
556
|
+
f"[GET_BY_GAME_LINE] ✓ Found game by name: title_original='{game.title_original}', deck_id={game.deck_id}, has_image={bool(game.image)}"
|
|
557
|
+
)
|
|
558
|
+
else:
|
|
559
|
+
logger.debug(
|
|
560
|
+
f"[GET_BY_GAME_LINE] ✗ No game found by name: '{game_line.game_name}'"
|
|
561
|
+
)
|
|
562
|
+
return game
|
|
563
|
+
|
|
564
|
+
logger.warning(
|
|
565
|
+
f"[GET_BY_GAME_LINE] ✗ No game found for line (no game_id or game_name)"
|
|
566
|
+
)
|
|
567
|
+
return None
|