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,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jiten.moe Update Script for GameSentenceMiner
|
|
3
|
+
|
|
4
|
+
This module provides functions to automatically update game metadata from jiten.moe,
|
|
5
|
+
respecting manual overrides set by users.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from GameSentenceMiner.util.cron.jiten_update import update_all_jiten_games
|
|
9
|
+
|
|
10
|
+
# Update all linked games
|
|
11
|
+
result = update_all_jiten_games()
|
|
12
|
+
print(f"Updated {result['updated_games']} out of {result['total_games']} games")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from typing import Optional, Dict, List
|
|
17
|
+
|
|
18
|
+
from GameSentenceMiner.util.games_table import GamesTable
|
|
19
|
+
from GameSentenceMiner.util.configuration import logger
|
|
20
|
+
from GameSentenceMiner.util.jiten_api_client import JitenApiClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fetch_jiten_data_for_game(game: GamesTable) -> Optional[Dict]:
|
|
24
|
+
"""
|
|
25
|
+
Fetch fresh data from jiten.moe API for a specific game.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
game: GamesTable object with a deck_id
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary with normalized jiten.moe data (snake_case keys), or None if fetch fails
|
|
32
|
+
|
|
33
|
+
Example return structure:
|
|
34
|
+
{
|
|
35
|
+
'deck_id': 123,
|
|
36
|
+
'title_original': '君と彼女と彼女の恋。',
|
|
37
|
+
'title_romaji': 'Kimi to Kanojo to Kanojo no Koi.',
|
|
38
|
+
'title_english': 'You, Me, and Her',
|
|
39
|
+
'description': 'A visual novel about...',
|
|
40
|
+
'cover_name': 'https://...',
|
|
41
|
+
'media_type': 7, # 1=Anime, 7=Visual Novel, 2=Manga
|
|
42
|
+
'character_count': 50000,
|
|
43
|
+
'difficulty': 5,
|
|
44
|
+
'difficulty_raw': 5.2,
|
|
45
|
+
'links': [...],
|
|
46
|
+
'aliases': [...],
|
|
47
|
+
'release_date': '2013-06-28'
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
if not game.deck_id:
|
|
51
|
+
logger.debug(
|
|
52
|
+
f"Game {game.id} ({game.title_original}) has no deck_id, skipping jiten fetch"
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
logger.debug(
|
|
58
|
+
f"Fetching jiten.moe data for game: {game.title_original} (deck_id: {game.deck_id})"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Use direct deck detail API endpoint
|
|
62
|
+
data = JitenApiClient.get_deck_detail(game.deck_id)
|
|
63
|
+
|
|
64
|
+
if not data:
|
|
65
|
+
logger.debug(f"Failed to fetch deck detail for deck_id {game.deck_id}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Extract main deck data from the detail response
|
|
69
|
+
main_deck = data.get("data", {}).get("mainDeck")
|
|
70
|
+
if not main_deck:
|
|
71
|
+
logger.debug(f"No mainDeck found in response for deck_id {game.deck_id}")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# Normalize the deck data
|
|
75
|
+
jiten_data = JitenApiClient.normalize_deck_data(main_deck)
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"Successfully fetched jiten.moe data for: {jiten_data['title_original']}"
|
|
78
|
+
)
|
|
79
|
+
return jiten_data
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.debug(f"Unexpected error fetching jiten data for game {game.id}: {e}")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def update_single_game_from_jiten(game: GamesTable, jiten_data: Dict) -> Dict:
|
|
87
|
+
"""
|
|
88
|
+
Update a single game's fields from jiten.moe data, respecting manual overrides.
|
|
89
|
+
Always re-downloads cover images.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
game: GamesTable object to update
|
|
93
|
+
jiten_data: Dictionary with jiten.moe data (from fetch_jiten_data_for_game)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary with update summary:
|
|
97
|
+
{
|
|
98
|
+
'success': bool,
|
|
99
|
+
'updated_fields': List[str],
|
|
100
|
+
'skipped_fields': List[str],
|
|
101
|
+
'error': Optional[str]
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
update_fields = {}
|
|
106
|
+
skipped_fields = []
|
|
107
|
+
|
|
108
|
+
# Ensure manual_overrides is always a list
|
|
109
|
+
manual_overrides = (
|
|
110
|
+
game.manual_overrides if game.manual_overrides is not None else []
|
|
111
|
+
)
|
|
112
|
+
if not isinstance(manual_overrides, list):
|
|
113
|
+
logger.warning(
|
|
114
|
+
f"⚠️ manual_overrides is not a list for game {game.id}: {type(manual_overrides)}"
|
|
115
|
+
)
|
|
116
|
+
manual_overrides = []
|
|
117
|
+
|
|
118
|
+
logger.debug(
|
|
119
|
+
f"Checking fields for game {game.id} (manual overrides: {manual_overrides})"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Check each field against manual overrides
|
|
123
|
+
if "deck_id" not in manual_overrides:
|
|
124
|
+
update_fields["deck_id"] = jiten_data["deck_id"]
|
|
125
|
+
else:
|
|
126
|
+
skipped_fields.append("deck_id")
|
|
127
|
+
|
|
128
|
+
if "title_original" not in manual_overrides and jiten_data.get(
|
|
129
|
+
"title_original"
|
|
130
|
+
):
|
|
131
|
+
update_fields["title_original"] = jiten_data["title_original"]
|
|
132
|
+
elif "title_original" in manual_overrides:
|
|
133
|
+
skipped_fields.append("title_original")
|
|
134
|
+
|
|
135
|
+
if "title_romaji" not in manual_overrides and jiten_data.get("title_romaji"):
|
|
136
|
+
update_fields["title_romaji"] = jiten_data["title_romaji"]
|
|
137
|
+
elif "title_romaji" in manual_overrides:
|
|
138
|
+
skipped_fields.append("title_romaji")
|
|
139
|
+
|
|
140
|
+
if "title_english" not in manual_overrides and jiten_data.get("title_english"):
|
|
141
|
+
update_fields["title_english"] = jiten_data["title_english"]
|
|
142
|
+
elif "title_english" in manual_overrides:
|
|
143
|
+
skipped_fields.append("title_english")
|
|
144
|
+
|
|
145
|
+
if "type" not in manual_overrides and jiten_data.get("media_type"):
|
|
146
|
+
# Map media type to string
|
|
147
|
+
media_type_map = {1: "Anime", 7: "Visual Novel", 2: "Manga"}
|
|
148
|
+
update_fields["game_type"] = media_type_map.get(
|
|
149
|
+
jiten_data["media_type"], "Unknown"
|
|
150
|
+
)
|
|
151
|
+
elif "type" in manual_overrides:
|
|
152
|
+
skipped_fields.append("type")
|
|
153
|
+
|
|
154
|
+
if "description" not in manual_overrides and jiten_data.get("description"):
|
|
155
|
+
update_fields["description"] = jiten_data["description"]
|
|
156
|
+
elif "description" in manual_overrides:
|
|
157
|
+
skipped_fields.append("description")
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
"difficulty" not in manual_overrides
|
|
161
|
+
and jiten_data.get("difficulty") is not None
|
|
162
|
+
):
|
|
163
|
+
update_fields["difficulty"] = jiten_data["difficulty"]
|
|
164
|
+
elif "difficulty" in manual_overrides:
|
|
165
|
+
skipped_fields.append("difficulty")
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
"character_count" not in manual_overrides
|
|
169
|
+
and jiten_data.get("character_count") is not None
|
|
170
|
+
):
|
|
171
|
+
update_fields["character_count"] = jiten_data["character_count"]
|
|
172
|
+
elif "character_count" in manual_overrides:
|
|
173
|
+
skipped_fields.append("character_count")
|
|
174
|
+
|
|
175
|
+
if "links" not in manual_overrides and jiten_data.get("links"):
|
|
176
|
+
update_fields["links"] = jiten_data["links"]
|
|
177
|
+
elif "links" in manual_overrides:
|
|
178
|
+
skipped_fields.append("links")
|
|
179
|
+
|
|
180
|
+
if "release_date" not in manual_overrides and jiten_data.get("release_date"):
|
|
181
|
+
update_fields["release_date"] = jiten_data["release_date"]
|
|
182
|
+
elif "release_date" in manual_overrides:
|
|
183
|
+
skipped_fields.append("release_date")
|
|
184
|
+
|
|
185
|
+
# Always re-download image if not manually overridden
|
|
186
|
+
if "image" not in manual_overrides and jiten_data.get("cover_name"):
|
|
187
|
+
image_data = JitenApiClient.download_cover_image(jiten_data["cover_name"])
|
|
188
|
+
if image_data:
|
|
189
|
+
update_fields["image"] = image_data
|
|
190
|
+
logger.debug(f"Downloaded and encoded image for game {game.id}")
|
|
191
|
+
else:
|
|
192
|
+
logger.debug(f"Failed to download image for game {game.id}")
|
|
193
|
+
elif "image" in manual_overrides:
|
|
194
|
+
skipped_fields.append("image")
|
|
195
|
+
|
|
196
|
+
# Update the game using the jiten update method (doesn't mark as manual)
|
|
197
|
+
if update_fields:
|
|
198
|
+
game.update_all_fields_from_jiten(**update_fields)
|
|
199
|
+
logger.debug(
|
|
200
|
+
f"Updated game {game.id} ({game.title_original}): {len(update_fields)} fields"
|
|
201
|
+
)
|
|
202
|
+
return {
|
|
203
|
+
"success": True,
|
|
204
|
+
"updated_fields": list(update_fields.keys()),
|
|
205
|
+
"skipped_fields": skipped_fields,
|
|
206
|
+
"error": None,
|
|
207
|
+
}
|
|
208
|
+
else:
|
|
209
|
+
logger.debug(
|
|
210
|
+
f"No fields updated for game {game.id} - all fields are manually overridden"
|
|
211
|
+
)
|
|
212
|
+
return {
|
|
213
|
+
"success": True,
|
|
214
|
+
"updated_fields": [],
|
|
215
|
+
"skipped_fields": skipped_fields,
|
|
216
|
+
"error": None,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
logger.error(
|
|
221
|
+
f"💥 Error updating game {game.id} from jiten data: {e}", exc_info=True
|
|
222
|
+
)
|
|
223
|
+
return {
|
|
224
|
+
"success": False,
|
|
225
|
+
"updated_fields": [],
|
|
226
|
+
"skipped_fields": skipped_fields,
|
|
227
|
+
"error": str(e),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def update_all_jiten_games() -> Dict:
|
|
232
|
+
"""
|
|
233
|
+
Update all games that are linked to jiten.moe (have a deck_id).
|
|
234
|
+
|
|
235
|
+
This is the main entry point for the jiten update cron job.
|
|
236
|
+
- Continues on errors (individual game failures don't stop the process)
|
|
237
|
+
- Always re-downloads images
|
|
238
|
+
- Adds 1 second delay between games to avoid rate limiting
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Dictionary with summary statistics:
|
|
242
|
+
{
|
|
243
|
+
'total_games': int, # Total games in database
|
|
244
|
+
'linked_games': int, # Games with deck_id
|
|
245
|
+
'updated_games': int, # Successfully updated
|
|
246
|
+
'failed_games': int, # Failed to update
|
|
247
|
+
'skipped_games': int, # No deck_id
|
|
248
|
+
'total_fields_updated': int, # Total fields updated across all games
|
|
249
|
+
'details': List[Dict] # Per-game details
|
|
250
|
+
}
|
|
251
|
+
"""
|
|
252
|
+
logger.debug("Starting jiten.moe update for all linked games")
|
|
253
|
+
|
|
254
|
+
start_time = time.time()
|
|
255
|
+
|
|
256
|
+
# Get all games
|
|
257
|
+
all_games = GamesTable.all()
|
|
258
|
+
total_games = len(all_games)
|
|
259
|
+
|
|
260
|
+
# Filter for linked games (have deck_id)
|
|
261
|
+
linked_games = [game for game in all_games if game.deck_id]
|
|
262
|
+
linked_count = len(linked_games)
|
|
263
|
+
skipped_count = total_games - linked_count
|
|
264
|
+
|
|
265
|
+
logger.debug(
|
|
266
|
+
f"Found {total_games} total games, {linked_count} linked to jiten.moe, {skipped_count} unlinked"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if linked_count == 0:
|
|
270
|
+
logger.debug("No linked games found, nothing to update")
|
|
271
|
+
return {
|
|
272
|
+
"total_games": total_games,
|
|
273
|
+
"linked_games": 0,
|
|
274
|
+
"updated_games": 0,
|
|
275
|
+
"failed_games": 0,
|
|
276
|
+
"skipped_games": skipped_count,
|
|
277
|
+
"total_fields_updated": 0,
|
|
278
|
+
"details": [],
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Process each linked game
|
|
282
|
+
updated_count = 0
|
|
283
|
+
failed_count = 0
|
|
284
|
+
total_fields_updated = 0
|
|
285
|
+
details = []
|
|
286
|
+
|
|
287
|
+
for i, game in enumerate(linked_games, 1):
|
|
288
|
+
logger.debug(
|
|
289
|
+
f"Processing game {i}/{linked_count}: {game.title_original} (deck_id: {game.deck_id})"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
game_detail = {
|
|
293
|
+
"game_id": game.id,
|
|
294
|
+
"title": game.title_original,
|
|
295
|
+
"deck_id": game.deck_id,
|
|
296
|
+
"success": False,
|
|
297
|
+
"updated_fields": [],
|
|
298
|
+
"skipped_fields": [],
|
|
299
|
+
"error": None,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Fetch jiten data
|
|
304
|
+
jiten_data = fetch_jiten_data_for_game(game)
|
|
305
|
+
|
|
306
|
+
if jiten_data is None:
|
|
307
|
+
logger.debug(f"Failed to fetch jiten data for game {game.id}, skipping")
|
|
308
|
+
failed_count += 1
|
|
309
|
+
game_detail["error"] = "Failed to fetch jiten data"
|
|
310
|
+
details.append(game_detail)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# Update the game
|
|
314
|
+
result = update_single_game_from_jiten(game, jiten_data)
|
|
315
|
+
|
|
316
|
+
if result["success"]:
|
|
317
|
+
updated_count += 1
|
|
318
|
+
total_fields_updated += len(result["updated_fields"])
|
|
319
|
+
game_detail["success"] = True
|
|
320
|
+
game_detail["updated_fields"] = result["updated_fields"]
|
|
321
|
+
game_detail["skipped_fields"] = result["skipped_fields"]
|
|
322
|
+
else:
|
|
323
|
+
failed_count += 1
|
|
324
|
+
game_detail["error"] = result["error"]
|
|
325
|
+
|
|
326
|
+
details.append(game_detail)
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.error(
|
|
330
|
+
f"💥 Unexpected error processing game {game.id}: {e}", exc_info=True
|
|
331
|
+
)
|
|
332
|
+
failed_count += 1
|
|
333
|
+
game_detail["error"] = str(e)
|
|
334
|
+
details.append(game_detail)
|
|
335
|
+
|
|
336
|
+
# Add 1 second delay between games (except after the last one)
|
|
337
|
+
if i < linked_count:
|
|
338
|
+
logger.debug(f"Waiting 1 second before next game...")
|
|
339
|
+
time.sleep(1)
|
|
340
|
+
|
|
341
|
+
elapsed_time = time.time() - start_time
|
|
342
|
+
|
|
343
|
+
# Log summary
|
|
344
|
+
logger.debug("Jiten.moe update completed")
|
|
345
|
+
logger.debug(f"Summary:")
|
|
346
|
+
logger.debug(f" - Total games: {total_games}")
|
|
347
|
+
logger.debug(f" - Linked games: {linked_count}")
|
|
348
|
+
logger.debug(f" - Successfully updated: {updated_count}")
|
|
349
|
+
logger.debug(f" - Failed: {failed_count}")
|
|
350
|
+
logger.debug(f" - Skipped (no deck_id): {skipped_count}")
|
|
351
|
+
logger.debug(f" - Total fields updated: {total_fields_updated}")
|
|
352
|
+
logger.debug(f" - Time elapsed: {elapsed_time:.2f} seconds")
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"total_games": total_games,
|
|
356
|
+
"linked_games": linked_count,
|
|
357
|
+
"updated_games": updated_count,
|
|
358
|
+
"failed_games": failed_count,
|
|
359
|
+
"skipped_games": skipped_count,
|
|
360
|
+
"total_fields_updated": total_fields_updated,
|
|
361
|
+
"elapsed_time": elapsed_time,
|
|
362
|
+
"details": details,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Example usage for testing
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
# Run the update
|
|
369
|
+
result = update_all_jiten_games()
|
|
370
|
+
|
|
371
|
+
# Print summary
|
|
372
|
+
print("\n" + "=" * 80)
|
|
373
|
+
print("JITEN UPDATE SUMMARY")
|
|
374
|
+
print("=" * 80)
|
|
375
|
+
print(f"Total games: {result['total_games']}")
|
|
376
|
+
print(f"Linked games: {result['linked_games']}")
|
|
377
|
+
print(f"Successfully updated: {result['updated_games']}")
|
|
378
|
+
print(f"Failed: {result['failed_games']}")
|
|
379
|
+
print(f"Skipped (no deck_id): {result['skipped_games']}")
|
|
380
|
+
print(f"Total fields updated: {result['total_fields_updated']}")
|
|
381
|
+
print(f"Time elapsed: {result['elapsed_time']:.2f} seconds")
|
|
382
|
+
print("=" * 80)
|
|
383
|
+
|
|
384
|
+
# Print per-game details
|
|
385
|
+
if result["details"]:
|
|
386
|
+
print("\nPER-GAME DETAILS:")
|
|
387
|
+
print("-" * 80)
|
|
388
|
+
for detail in result["details"]:
|
|
389
|
+
status = "✅" if detail["success"] else "❌"
|
|
390
|
+
print(f"{status} {detail['title']} (deck_id: {detail['deck_id']})")
|
|
391
|
+
if detail["updated_fields"]:
|
|
392
|
+
print(f" Updated: {', '.join(detail['updated_fields'])}")
|
|
393
|
+
if detail["skipped_fields"]:
|
|
394
|
+
print(f" Skipped: {', '.join(detail['skipped_fields'])}")
|
|
395
|
+
if detail["error"]:
|
|
396
|
+
print(f" Error: {detail['error']}")
|
|
397
|
+
print("-" * 80)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
One-time Game Population Cron Job for GameSentenceMiner
|
|
3
|
+
|
|
4
|
+
This module provides a one-time cron job that auto-creates game records from game_lines.
|
|
5
|
+
This ensures the games table is populated before the daily rollup runs, so that
|
|
6
|
+
game_activity_data and games_played_ids can be properly populated in the rollup.
|
|
7
|
+
|
|
8
|
+
This job should run once and then mark itself as complete.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from GameSentenceMiner.util.cron.populate_games import populate_games_table
|
|
12
|
+
|
|
13
|
+
# Run the one-time population
|
|
14
|
+
result = populate_games_table()
|
|
15
|
+
print(f"Created {result['created']} games, linked {result['linked_lines']} lines")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from GameSentenceMiner.util.db import GameLinesTable
|
|
19
|
+
from GameSentenceMiner.util.games_table import GamesTable
|
|
20
|
+
from GameSentenceMiner.util.configuration import logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def populate_games_table():
|
|
24
|
+
"""
|
|
25
|
+
Auto-create game records for any game_lines that don't have corresponding games.
|
|
26
|
+
This is a one-time operation that ensures the games table is populated.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dictionary with execution summary:
|
|
30
|
+
{
|
|
31
|
+
'success': bool,
|
|
32
|
+
'created': int, # Number of new games created
|
|
33
|
+
'linked_lines': int, # Number of game_lines linked to games
|
|
34
|
+
'errors': int,
|
|
35
|
+
'error_message': str or None
|
|
36
|
+
}
|
|
37
|
+
"""
|
|
38
|
+
logger.info("Starting one-time game population from game_lines")
|
|
39
|
+
|
|
40
|
+
created_count = 0
|
|
41
|
+
linked_lines_count = 0
|
|
42
|
+
errors = 0
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Get all distinct game names from game_lines
|
|
46
|
+
game_names_from_lines = GameLinesTable._db.fetchall(
|
|
47
|
+
f"SELECT DISTINCT game_name FROM {GameLinesTable._table} "
|
|
48
|
+
f"WHERE game_name IS NOT NULL AND game_name != ''"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if not game_names_from_lines:
|
|
52
|
+
logger.info("No game names found in game_lines table")
|
|
53
|
+
return {
|
|
54
|
+
'success': True,
|
|
55
|
+
'created': 0,
|
|
56
|
+
'linked_lines': 0,
|
|
57
|
+
'errors': 0,
|
|
58
|
+
'error_message': None
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.info(f"Found {len(game_names_from_lines)} distinct game names in game_lines")
|
|
62
|
+
|
|
63
|
+
# Get existing game titles to avoid duplicates
|
|
64
|
+
existing_games_rows = GamesTable._db.fetchall(
|
|
65
|
+
f"SELECT title_original FROM {GamesTable._table}"
|
|
66
|
+
)
|
|
67
|
+
existing_titles = {row[0] for row in existing_games_rows}
|
|
68
|
+
|
|
69
|
+
logger.info(f"Found {len(existing_titles)} existing games in games table")
|
|
70
|
+
|
|
71
|
+
# Auto-create games for game_lines that don't have corresponding games
|
|
72
|
+
for row in game_names_from_lines:
|
|
73
|
+
game_name = row[0]
|
|
74
|
+
|
|
75
|
+
# Skip if game already exists
|
|
76
|
+
if game_name in existing_titles:
|
|
77
|
+
logger.debug(f"Game '{game_name}' already exists, skipping")
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Use get_or_create_by_name which checks for existing mappings
|
|
82
|
+
# and reuses them instead of creating duplicates
|
|
83
|
+
game = GamesTable.get_or_create_by_name(game_name)
|
|
84
|
+
|
|
85
|
+
# Link any orphaned game_lines to this game
|
|
86
|
+
GameLinesTable._db.execute(
|
|
87
|
+
f"UPDATE {GameLinesTable._table} SET game_id = ? "
|
|
88
|
+
f"WHERE game_name = ? AND (game_id IS NULL OR game_id = '')",
|
|
89
|
+
(game.id, game_name),
|
|
90
|
+
commit=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Count how many lines were linked
|
|
94
|
+
linked_count = GameLinesTable._db.fetchone(
|
|
95
|
+
f"SELECT COUNT(*) FROM {GameLinesTable._table} WHERE game_id = ?",
|
|
96
|
+
(game.id,),
|
|
97
|
+
)
|
|
98
|
+
lines_linked = linked_count[0] if linked_count else 0
|
|
99
|
+
|
|
100
|
+
logger.info(
|
|
101
|
+
f"Created game '{game_name}' (id={game.id}) and linked {lines_linked} lines"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
created_count += 1
|
|
105
|
+
linked_lines_count += lines_linked
|
|
106
|
+
existing_titles.add(game_name)
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"Error creating game for '{game_name}': {e}", exc_info=True)
|
|
110
|
+
errors += 1
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
logger.info(
|
|
114
|
+
f"Game population completed: created {created_count} games, "
|
|
115
|
+
f"linked {linked_lines_count} lines, {errors} errors"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
'success': True,
|
|
120
|
+
'created': created_count,
|
|
121
|
+
'linked_lines': linked_lines_count,
|
|
122
|
+
'errors': errors,
|
|
123
|
+
'error_message': None
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
error_msg = str(e)
|
|
128
|
+
logger.error(f"Fatal error in populate_games_table: {error_msg}", exc_info=True)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
'success': False,
|
|
132
|
+
'created': created_count,
|
|
133
|
+
'linked_lines': linked_lines_count,
|
|
134
|
+
'errors': errors + 1,
|
|
135
|
+
'error_message': error_msg
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Example usage for testing
|
|
140
|
+
if __name__ == '__main__':
|
|
141
|
+
# Run the one-time population
|
|
142
|
+
result = populate_games_table()
|
|
143
|
+
|
|
144
|
+
# Print summary
|
|
145
|
+
print("\n" + "=" * 80)
|
|
146
|
+
print("GAME POPULATION SUMMARY")
|
|
147
|
+
print("=" * 80)
|
|
148
|
+
print(f"Success: {'Yes' if result['success'] else 'No'}")
|
|
149
|
+
print(f"Games created: {result['created']}")
|
|
150
|
+
print(f"Lines linked: {result['linked_lines']}")
|
|
151
|
+
print(f"Errors: {result['errors']}")
|
|
152
|
+
if result['error_message']:
|
|
153
|
+
print(f"Error: {result['error_message']}")
|
|
154
|
+
print("=" * 80)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cron Job Runner for GameSentenceMiner
|
|
3
|
+
|
|
4
|
+
This script checks for due cron jobs and executes them.
|
|
5
|
+
Should be called periodically (e.g., every hour) by an external scheduler.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m GameSentenceMiner.util.cron.run_crons
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from GameSentenceMiner.util.cron_table import CronTable
|
|
12
|
+
from GameSentenceMiner.util.configuration import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_due_crons():
|
|
16
|
+
"""
|
|
17
|
+
Check for and execute all due cron jobs.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Dictionary with execution summary
|
|
21
|
+
"""
|
|
22
|
+
logger.info("Checking for due cron jobs...")
|
|
23
|
+
|
|
24
|
+
# Get all cron jobs that need to run
|
|
25
|
+
due_crons = CronTable.get_due_crons()
|
|
26
|
+
|
|
27
|
+
if not due_crons:
|
|
28
|
+
logger.info("No cron jobs are due to run at this time")
|
|
29
|
+
return {
|
|
30
|
+
'total_checked': 0,
|
|
31
|
+
'executed': 0,
|
|
32
|
+
'failed': 0,
|
|
33
|
+
'details': []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.info(f"📋 Found {len(due_crons)} cron job(s) due to run")
|
|
37
|
+
|
|
38
|
+
executed_count = 0
|
|
39
|
+
failed_count = 0
|
|
40
|
+
details = []
|
|
41
|
+
|
|
42
|
+
for cron in due_crons:
|
|
43
|
+
logger.info(f"Executing cron job: {cron.name}")
|
|
44
|
+
logger.info(f"Description: {cron.description}")
|
|
45
|
+
|
|
46
|
+
detail = {
|
|
47
|
+
'name': cron.name,
|
|
48
|
+
'description': cron.description,
|
|
49
|
+
'success': False,
|
|
50
|
+
'error': None
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
# Execute populate_games BEFORE daily_stats_rollup to ensure games table is populated
|
|
55
|
+
if cron.name == 'populate_games':
|
|
56
|
+
from GameSentenceMiner.util.cron.populate_games import populate_games_table
|
|
57
|
+
result = populate_games_table()
|
|
58
|
+
|
|
59
|
+
# Mark as successfully run (even if there were some errors, as long as it completed)
|
|
60
|
+
CronTable.just_ran(cron.id)
|
|
61
|
+
executed_count += 1
|
|
62
|
+
detail['success'] = True
|
|
63
|
+
detail['result'] = result
|
|
64
|
+
|
|
65
|
+
logger.info(f"Successfully executed {cron.name}")
|
|
66
|
+
logger.info(f"Created: {result['created']} games, Linked: {result['linked_lines']} lines, Errors: {result['errors']}")
|
|
67
|
+
|
|
68
|
+
# Execute the appropriate function based on cron name
|
|
69
|
+
elif cron.name == 'jiten_sync':
|
|
70
|
+
from GameSentenceMiner.util.cron.jiten_update import update_all_jiten_games
|
|
71
|
+
result = update_all_jiten_games()
|
|
72
|
+
|
|
73
|
+
# Mark as successfully run
|
|
74
|
+
CronTable.just_ran(cron.id)
|
|
75
|
+
executed_count += 1
|
|
76
|
+
detail['success'] = True
|
|
77
|
+
detail['result'] = result
|
|
78
|
+
|
|
79
|
+
logger.info(f"Successfully executed {cron.name}")
|
|
80
|
+
logger.info(f"Updated: {result['updated_games']}/{result['linked_games']} games")
|
|
81
|
+
|
|
82
|
+
elif cron.name == 'daily_stats_rollup':
|
|
83
|
+
from GameSentenceMiner.util.cron.daily_rollup import run_daily_rollup
|
|
84
|
+
result = run_daily_rollup()
|
|
85
|
+
|
|
86
|
+
# Mark as successfully run
|
|
87
|
+
CronTable.just_ran(cron.id)
|
|
88
|
+
executed_count += 1
|
|
89
|
+
detail['success'] = True
|
|
90
|
+
detail['result'] = result
|
|
91
|
+
|
|
92
|
+
logger.info(f"Successfully executed {cron.name}")
|
|
93
|
+
logger.info(f"Processed: {result['processed']} dates, Overwritten: {result['overwritten']}, Errors: {result['errors']}")
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
logger.error(f"⚠️ Unknown cron job: {cron.name}")
|
|
97
|
+
detail['error'] = f"Unknown cron job: {cron.name}"
|
|
98
|
+
failed_count += 1
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Failed to execute {cron.name}: {e}", exc_info=True)
|
|
102
|
+
detail['error'] = str(e)
|
|
103
|
+
failed_count += 1
|
|
104
|
+
|
|
105
|
+
details.append(detail)
|
|
106
|
+
logger.info("Cron job check completed")
|
|
107
|
+
logger.info(f"Total checked: {len(due_crons)}")
|
|
108
|
+
logger.info(f"Successfully executed: {executed_count}")
|
|
109
|
+
logger.info(f"Failed: {failed_count}")
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
'total_checked': len(due_crons),
|
|
113
|
+
'executed': executed_count,
|
|
114
|
+
'failed': failed_count,
|
|
115
|
+
'details': details
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# for me to manually check
|
|
120
|
+
if __name__ == '__main__':
|
|
121
|
+
# Run the cron checker
|
|
122
|
+
result = run_due_crons()
|
|
123
|
+
|
|
124
|
+
# Print summary
|
|
125
|
+
print("\n" + "=" * 80)
|
|
126
|
+
print("CRON EXECUTION SUMMARY")
|
|
127
|
+
print("=" * 80)
|
|
128
|
+
print(f"Total cron jobs checked: {result['total_checked']}")
|
|
129
|
+
print(f"Successfully executed: {result['executed']}")
|
|
130
|
+
print(f"Failed: {result['failed']}")
|
|
131
|
+
print("=" * 80)
|
|
132
|
+
|
|
133
|
+
# Print details
|
|
134
|
+
if result['details']:
|
|
135
|
+
print("\nDETAILS:")
|
|
136
|
+
print("-" * 80)
|
|
137
|
+
for detail in result['details']:
|
|
138
|
+
status = "✅" if detail['success'] else "❌"
|
|
139
|
+
print(f"{status} {detail['name']}: {detail['description']}")
|
|
140
|
+
if detail.get('error'):
|
|
141
|
+
print(f" Error: {detail['error']}")
|
|
142
|
+
elif detail.get('result'):
|
|
143
|
+
res = detail['result']
|
|
144
|
+
if 'updated_games' in res:
|
|
145
|
+
print(f" Updated {res['updated_games']}/{res['linked_games']} games")
|
|
146
|
+
elif 'processed' in res:
|
|
147
|
+
print(f" Processed {res['processed']} dates, Overwritten {res['overwritten']}, Errors {res['errors']}")
|
|
148
|
+
print("-" * 80)
|