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.

Files changed (70) hide show
  1. GameSentenceMiner/__init__.py +39 -0
  2. GameSentenceMiner/anki.py +6 -3
  3. GameSentenceMiner/gametext.py +13 -2
  4. GameSentenceMiner/gsm.py +40 -3
  5. GameSentenceMiner/locales/en_us.json +4 -0
  6. GameSentenceMiner/locales/ja_jp.json +4 -0
  7. GameSentenceMiner/locales/zh_cn.json +4 -0
  8. GameSentenceMiner/obs.py +4 -1
  9. GameSentenceMiner/owocr/owocr/ocr.py +304 -134
  10. GameSentenceMiner/owocr/owocr/run.py +1 -1
  11. GameSentenceMiner/ui/anki_confirmation.py +4 -2
  12. GameSentenceMiner/ui/config_gui.py +12 -0
  13. GameSentenceMiner/util/configuration.py +6 -2
  14. GameSentenceMiner/util/cron/__init__.py +12 -0
  15. GameSentenceMiner/util/cron/daily_rollup.py +613 -0
  16. GameSentenceMiner/util/cron/jiten_update.py +397 -0
  17. GameSentenceMiner/util/cron/populate_games.py +154 -0
  18. GameSentenceMiner/util/cron/run_crons.py +148 -0
  19. GameSentenceMiner/util/cron/setup_populate_games_cron.py +118 -0
  20. GameSentenceMiner/util/cron_table.py +334 -0
  21. GameSentenceMiner/util/db.py +236 -49
  22. GameSentenceMiner/util/ffmpeg.py +23 -4
  23. GameSentenceMiner/util/games_table.py +340 -93
  24. GameSentenceMiner/util/jiten_api_client.py +188 -0
  25. GameSentenceMiner/util/stats_rollup_table.py +216 -0
  26. GameSentenceMiner/web/anki_api_endpoints.py +438 -220
  27. GameSentenceMiner/web/database_api.py +955 -1259
  28. GameSentenceMiner/web/jiten_database_api.py +1015 -0
  29. GameSentenceMiner/web/rollup_stats.py +672 -0
  30. GameSentenceMiner/web/static/css/dashboard-shared.css +75 -13
  31. GameSentenceMiner/web/static/css/overview.css +604 -47
  32. GameSentenceMiner/web/static/css/search.css +226 -0
  33. GameSentenceMiner/web/static/css/shared.css +762 -0
  34. GameSentenceMiner/web/static/css/stats.css +221 -0
  35. GameSentenceMiner/web/static/js/components/bar-chart.js +339 -0
  36. GameSentenceMiner/web/static/js/database-bulk-operations.js +320 -0
  37. GameSentenceMiner/web/static/js/database-game-data.js +390 -0
  38. GameSentenceMiner/web/static/js/database-game-operations.js +213 -0
  39. GameSentenceMiner/web/static/js/database-helpers.js +44 -0
  40. GameSentenceMiner/web/static/js/database-jiten-integration.js +750 -0
  41. GameSentenceMiner/web/static/js/database-popups.js +89 -0
  42. GameSentenceMiner/web/static/js/database-tabs.js +64 -0
  43. GameSentenceMiner/web/static/js/database-text-management.js +371 -0
  44. GameSentenceMiner/web/static/js/database.js +86 -718
  45. GameSentenceMiner/web/static/js/goals.js +79 -18
  46. GameSentenceMiner/web/static/js/heatmap.js +29 -23
  47. GameSentenceMiner/web/static/js/overview.js +1205 -339
  48. GameSentenceMiner/web/static/js/regex-patterns.js +100 -0
  49. GameSentenceMiner/web/static/js/search.js +215 -18
  50. GameSentenceMiner/web/static/js/shared.js +193 -39
  51. GameSentenceMiner/web/static/js/stats.js +1536 -179
  52. GameSentenceMiner/web/stats.py +1142 -269
  53. GameSentenceMiner/web/stats_api.py +2104 -0
  54. GameSentenceMiner/web/templates/anki_stats.html +4 -18
  55. GameSentenceMiner/web/templates/components/date-range.html +118 -3
  56. GameSentenceMiner/web/templates/components/html-head.html +40 -6
  57. GameSentenceMiner/web/templates/components/js-config.html +8 -8
  58. GameSentenceMiner/web/templates/components/regex-input.html +160 -0
  59. GameSentenceMiner/web/templates/database.html +564 -117
  60. GameSentenceMiner/web/templates/goals.html +41 -5
  61. GameSentenceMiner/web/templates/overview.html +159 -129
  62. GameSentenceMiner/web/templates/search.html +78 -9
  63. GameSentenceMiner/web/templates/stats.html +159 -5
  64. GameSentenceMiner/web/texthooking_page.py +280 -111
  65. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/METADATA +43 -2
  66. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/RECORD +70 -47
  67. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/WHEEL +0 -0
  68. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/entry_points.txt +0 -0
  69. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/licenses/LICENSE +0 -0
  70. {gamesentenceminer-2.19.16.dist-info → gamesentenceminer-2.20.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,320 @@
1
+ // Database Bulk Operations Functions
2
+ // Dependencies: shared.js (provides escapeHtml, openModal, closeModal), database-popups.js, database-game-data.js
3
+
4
+ /**
5
+ * Select all games in the bulk operations list
6
+ */
7
+ function selectAllGames() {
8
+ // Clear current merge target
9
+ if (typeof databaseManager !== 'undefined') {
10
+ databaseManager.mergeTargetGame = null;
11
+ }
12
+ document.querySelectorAll('.checkbox-container').forEach(container => {
13
+ container.classList.remove('merge-target');
14
+ });
15
+
16
+ const checkboxes = document.querySelectorAll('.game-checkbox');
17
+ checkboxes.forEach((cb, index) => {
18
+ cb.checked = true;
19
+ // Mark the first checkbox as merge target
20
+ if (index === 0 && typeof databaseManager !== 'undefined') {
21
+ databaseManager.mergeTargetGame = cb.dataset.game;
22
+ cb.closest('.checkbox-container').classList.add('merge-target');
23
+ }
24
+ });
25
+ updateGameSelection();
26
+ }
27
+
28
+ /**
29
+ * Deselect all games in the bulk operations list
30
+ */
31
+ function selectNoGames() {
32
+ // Clear merge target
33
+ if (typeof databaseManager !== 'undefined') {
34
+ databaseManager.mergeTargetGame = null;
35
+ }
36
+ document.querySelectorAll('.checkbox-container').forEach(container => {
37
+ container.classList.remove('merge-target');
38
+ });
39
+
40
+ document.querySelectorAll('.game-checkbox').forEach(cb => {
41
+ cb.checked = false;
42
+ });
43
+ updateGameSelection();
44
+ }
45
+
46
+ /**
47
+ * Handle game selection change events
48
+ * @param {Event} event - Change event from checkbox
49
+ */
50
+ function handleGameSelectionChange(event) {
51
+ const checkbox = event.target;
52
+ const gameName = checkbox.dataset.game;
53
+ const isChecked = checkbox.checked;
54
+
55
+ // Get current selection count before updating
56
+ const currentSelectedCount = document.querySelectorAll('.game-checkbox:checked').length - (isChecked ? 1 : 0);
57
+
58
+ if (isChecked) {
59
+ // Game is being selected
60
+ if (currentSelectedCount === 0 && typeof databaseManager !== 'undefined') {
61
+ // This is the first game being selected, mark it as merge target
62
+ databaseManager.mergeTargetGame = gameName;
63
+ // Add visual indicator
64
+ checkbox.closest('.checkbox-container').classList.add('merge-target');
65
+ }
66
+ } else {
67
+ // Game is being deselected
68
+ if (typeof databaseManager !== 'undefined' && gameName === databaseManager.mergeTargetGame) {
69
+ // The merge target is being deselected
70
+ databaseManager.mergeTargetGame = null;
71
+ checkbox.closest('.checkbox-container').classList.remove('merge-target');
72
+
73
+ // If there are still other games selected, make the first one the new target
74
+ const remainingSelected = document.querySelectorAll('.game-checkbox:checked');
75
+ if (remainingSelected.length > 0) {
76
+ const newTargetCheckbox = remainingSelected[0];
77
+ const newTargetGame = newTargetCheckbox.dataset.game;
78
+ databaseManager.mergeTargetGame = newTargetGame;
79
+ newTargetCheckbox.closest('.checkbox-container').classList.add('merge-target');
80
+ }
81
+ } else {
82
+ // Remove merge target styling if it exists
83
+ checkbox.closest('.checkbox-container').classList.remove('merge-target');
84
+ }
85
+ }
86
+
87
+ updateGameSelection();
88
+ }
89
+
90
+ /**
91
+ * Update the state of bulk operation buttons based on selection
92
+ */
93
+ function updateGameSelection() {
94
+ const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
95
+ const deleteBtn = document.getElementById('deleteSelectedGamesBtn');
96
+ const mergeBtn = document.getElementById('mergeSelectedGamesBtn');
97
+
98
+ if (deleteBtn) {
99
+ // Update delete button
100
+ deleteBtn.disabled = selectedCheckboxes.length === 0;
101
+ deleteBtn.textContent = selectedCheckboxes.length > 0 ? `Delete Selected (${selectedCheckboxes.length})` : 'Delete Selected';
102
+ }
103
+
104
+ if (mergeBtn) {
105
+ // Update merge button - only enable when 2 or more games are selected
106
+ mergeBtn.disabled = selectedCheckboxes.length < 2;
107
+ mergeBtn.textContent = selectedCheckboxes.length >= 2 ? `Merge Selected (${selectedCheckboxes.length})` : 'Merge Selected Games';
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Delete selected games after confirmation
113
+ */
114
+ async function deleteSelectedGames() {
115
+ const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
116
+ const gameNames = Array.from(selectedCheckboxes).map(cb => cb.dataset.game);
117
+
118
+ if (gameNames.length === 0) return;
119
+
120
+ showDatabaseConfirmPopup(
121
+ `Are you sure you want to delete ${gameNames.length} game(s)? This action cannot be undone.`,
122
+ async () => {
123
+ try {
124
+ const response = await fetch('/api/delete-games', {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({ game_names: gameNames })
128
+ });
129
+
130
+ const result = await response.json();
131
+
132
+ if (response.ok) {
133
+ showDatabaseSuccessPopup(`Successfully deleted ${result.successful_games.length} games!`);
134
+ closeModal('gamesDeletionModal');
135
+ if (typeof databaseManager !== 'undefined') {
136
+ await databaseManager.loadDashboardStats();
137
+ }
138
+ } else {
139
+ showDatabaseErrorPopup(`Error: ${result.error}`);
140
+ }
141
+ } catch (error) {
142
+ console.error('Error deleting games:', error);
143
+ showDatabaseErrorPopup('Failed to delete games');
144
+ }
145
+ }
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Open game merge modal with selected games
151
+ */
152
+ async function openGameMergeModal() {
153
+ const selectedCheckboxes = document.querySelectorAll('.game-checkbox:checked');
154
+ const gameNames = Array.from(selectedCheckboxes).map(cb => cb.dataset.game);
155
+
156
+ if (gameNames.length < 2) {
157
+ showDatabaseErrorPopup('Please select at least 2 games to merge.');
158
+ return;
159
+ }
160
+
161
+ try {
162
+ // Get detailed game information
163
+ const response = await fetch('/api/games-list');
164
+ const data = await response.json();
165
+
166
+ if (response.ok && data.games) {
167
+ const selectedGames = data.games.filter(game => gameNames.includes(game.name));
168
+
169
+ // Use the tracked merge target as primary game, or fall back to first selected
170
+ let primaryGame = selectedGames.find(game =>
171
+ typeof databaseManager !== 'undefined' && game.name === databaseManager.mergeTargetGame
172
+ );
173
+ if (!primaryGame) {
174
+ primaryGame = selectedGames[0];
175
+ }
176
+
177
+ // Secondary games are all selected games except the primary
178
+ const secondaryGames = selectedGames.filter(game => game.name !== primaryGame.name);
179
+
180
+ // Calculate totals
181
+ const totalSentences = selectedGames.reduce((sum, game) => sum + game.sentence_count, 0);
182
+ const totalCharacters = selectedGames.reduce((sum, game) => sum + game.total_characters, 0);
183
+
184
+ // Populate primary game info
185
+ document.getElementById('primaryGameName').textContent = primaryGame.name;
186
+ document.getElementById('primaryGameStats').textContent =
187
+ `${primaryGame.sentence_count} sentences, ${primaryGame.total_characters.toLocaleString()} characters`;
188
+
189
+ // Populate secondary games list
190
+ const secondaryList = document.getElementById('secondaryGamesList');
191
+ secondaryList.innerHTML = '';
192
+ secondaryGames.forEach(game => {
193
+ const gameDiv = document.createElement('div');
194
+ gameDiv.className = 'game-item';
195
+ gameDiv.innerHTML = `
196
+ <div class="game-name">${escapeHtml(game.name)}</div>
197
+ <div class="game-stats">${game.sentence_count} sentences, ${game.total_characters.toLocaleString()} characters</div>
198
+ `;
199
+ secondaryList.appendChild(gameDiv);
200
+ });
201
+
202
+ // Update merge statistics
203
+ document.getElementById('totalSentencesAfterMerge').textContent = totalSentences.toLocaleString();
204
+ document.getElementById('totalCharactersAfterMerge').textContent = totalCharacters.toLocaleString();
205
+ document.getElementById('gamesBeingMerged').textContent = gameNames.length;
206
+
207
+ // Reset modal state
208
+ document.getElementById('mergeError').style.display = 'none';
209
+ document.getElementById('mergeSuccess').style.display = 'none';
210
+ document.getElementById('mergeLoadingIndicator').style.display = 'none';
211
+ document.getElementById('confirmMergeBtn').disabled = false;
212
+
213
+ // Store selected games for the merge operation
214
+ window.selectedGamesForMerge = gameNames;
215
+
216
+ openModal('gameMergeModal');
217
+ }
218
+ } catch (error) {
219
+ console.error('Error loading game data for merge:', error);
220
+ showDatabaseErrorPopup('Failed to load game data for merge');
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Confirm and execute game merge operation
226
+ */
227
+ async function confirmGameMerge() {
228
+ const gameNames = window.selectedGamesForMerge;
229
+
230
+ if (!gameNames || gameNames.length < 2) {
231
+ showDatabaseErrorPopup('Invalid game selection for merge');
232
+ return;
233
+ }
234
+
235
+ const errorDiv = document.getElementById('mergeError');
236
+ const successDiv = document.getElementById('mergeSuccess');
237
+ const loadingDiv = document.getElementById('mergeLoadingIndicator');
238
+ const confirmBtn = document.getElementById('confirmMergeBtn');
239
+
240
+ // Reset state
241
+ errorDiv.style.display = 'none';
242
+ successDiv.style.display = 'none';
243
+
244
+ // Show loading state
245
+ loadingDiv.style.display = 'flex';
246
+ confirmBtn.disabled = true;
247
+
248
+ try {
249
+ const target_game = (typeof databaseManager !== 'undefined' && databaseManager.mergeTargetGame) || gameNames[0];
250
+ const response = await fetch('/api/merge_games', {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({
254
+ target_game: target_game,
255
+ games_to_merge: gameNames.filter(name => name !== target_game)
256
+ })
257
+ });
258
+
259
+ const result = await response.json();
260
+
261
+ if (response.ok) {
262
+ // Show success message
263
+ successDiv.textContent = `Successfully merged ${result.merged_games.length} games into "${result.primary_game}"! Moved ${result.lines_moved} sentences.`;
264
+ successDiv.style.display = 'block';
265
+
266
+ // Auto-close modal after 2 seconds and refresh
267
+ setTimeout(async () => {
268
+ closeModal('gameMergeModal');
269
+ closeModal('gamesDeletionModal');
270
+ if (typeof databaseManager !== 'undefined') {
271
+ await databaseManager.loadDashboardStats();
272
+ }
273
+ }, 2000);
274
+
275
+ } else {
276
+ // Show error message
277
+ errorDiv.textContent = result.error || 'Failed to merge games';
278
+ errorDiv.style.display = 'block';
279
+ confirmBtn.disabled = false;
280
+ }
281
+ } catch (error) {
282
+ console.error('Error merging games:', error);
283
+ errorDiv.textContent = 'Network error occurred while merging games';
284
+ errorDiv.style.display = 'block';
285
+ confirmBtn.disabled = false;
286
+ } finally {
287
+ loadingDiv.style.display = 'none';
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Initialize bulk operations event handlers
293
+ */
294
+ function initializeBulkOperations() {
295
+ // Bulk operations handlers
296
+ const selectAllBtn = document.querySelector('[data-action="selectAllGames"]');
297
+ if (selectAllBtn) {
298
+ selectAllBtn.addEventListener('click', selectAllGames);
299
+ }
300
+
301
+ const selectNoneBtn = document.querySelector('[data-action="selectNoGames"]');
302
+ if (selectNoneBtn) {
303
+ selectNoneBtn.addEventListener('click', selectNoGames);
304
+ }
305
+
306
+ const deleteSelectedBtn = document.querySelector('[data-action="deleteSelectedGames"]');
307
+ if (deleteSelectedBtn) {
308
+ deleteSelectedBtn.addEventListener('click', deleteSelectedGames);
309
+ }
310
+
311
+ const mergeSelectedBtn = document.querySelector('[data-action="mergeSelectedGames"]');
312
+ if (mergeSelectedBtn) {
313
+ mergeSelectedBtn.addEventListener('click', openGameMergeModal);
314
+ }
315
+
316
+ const confirmMergeBtn = document.querySelector('[data-action="confirmGameMerge"]');
317
+ if (confirmMergeBtn) {
318
+ confirmMergeBtn.addEventListener('click', confirmGameMerge);
319
+ }
320
+ }