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,750 @@
1
+ // Database Jiten.moe Integration Functions
2
+ // Dependencies: shared.js (provides escapeHtml, openModal, closeModal, safeJoinArray, logApiResponse), database-popups.js, database-helpers.js
3
+
4
+ // Global flag to prevent concurrent link operations
5
+ let isLinkingInProgress = false;
6
+
7
+ /**
8
+ * Open jiten.moe search modal for a specific game
9
+ * @param {string} gameId - Game ID to search for
10
+ * @param {string} gameTitle - Game title to search for
11
+ */
12
+ function openJitenSearch(gameId, gameTitle) {
13
+ // Validate gameId
14
+ if (!gameId || gameId === 'undefined' || gameId === 'null') {
15
+ showDatabaseErrorPopup(`Cannot link game: Invalid game ID. Please refresh the page and try again.`);
16
+ console.error(`Invalid gameId provided to openJitenSearch: ${gameId}`);
17
+ return;
18
+ }
19
+
20
+ currentGameForSearch = currentGames.find(game => game.id === gameId);
21
+ if (!currentGameForSearch) {
22
+ showDatabaseErrorPopup(`Cannot find game with ID: ${gameId}. Please refresh the page and try again.`);
23
+ console.error(`Game not found in currentGames: ${gameId}`);
24
+ return;
25
+ }
26
+
27
+ // Additional validation
28
+ if (!currentGameForSearch.id) {
29
+ showDatabaseErrorPopup(`Game data is incomplete (missing ID). Please refresh the page and try again.`);
30
+ console.error(`Game found but has no ID:`, currentGameForSearch);
31
+ return;
32
+ }
33
+
34
+ document.getElementById('searchingForGame').textContent = gameTitle;
35
+ document.getElementById('jitenSearchInput').value = gameTitle;
36
+ document.getElementById('jitenSearchResults').style.display = 'none';
37
+ document.getElementById('jitenSearchError').style.display = 'none';
38
+
39
+ openModal('jitenSearchModal');
40
+ }
41
+
42
+ /**
43
+ * Search jiten.moe database
44
+ */
45
+ async function searchJitenMoe() {
46
+ const searchInput = document.getElementById('jitenSearchInput');
47
+ const resultsDiv = document.getElementById('jitenSearchResults');
48
+ const resultsListDiv = document.getElementById('jitenResultsList');
49
+ const errorDiv = document.getElementById('jitenSearchError');
50
+ const loadingDiv = document.getElementById('jitenSearchLoading');
51
+
52
+ const searchTerm = searchInput.value.trim();
53
+ if (!searchTerm) {
54
+ errorDiv.textContent = 'Please enter a search term';
55
+ errorDiv.style.display = 'block';
56
+ return;
57
+ }
58
+
59
+ // Remove all punctuation from the search term before sending to API
60
+ const searchTermNoPunctuation = searchTerm.replace(/[^\p{L}\p{N}\s]/gu, '');
61
+
62
+ errorDiv.style.display = 'none';
63
+ resultsDiv.style.display = 'none';
64
+ loadingDiv.style.display = 'flex';
65
+
66
+ try {
67
+ const response = await fetch(`/api/jiten-search?title=${encodeURIComponent(searchTermNoPunctuation)}`);
68
+ const data = await response.json();
69
+
70
+ if (response.ok) {
71
+ if (data.results && data.results.length > 0) {
72
+ renderJitenResults(data.results);
73
+ resultsDiv.style.display = 'block';
74
+ } else {
75
+ errorDiv.textContent = 'No results found. Try a different search term.';
76
+ errorDiv.style.display = 'block';
77
+ }
78
+ } else {
79
+ errorDiv.textContent = data.error || 'Search failed';
80
+ errorDiv.style.display = 'block';
81
+ }
82
+ } catch (error) {
83
+ console.error('Error searching jiten.moe:', error);
84
+ errorDiv.textContent = 'Search failed. Please try again.';
85
+ errorDiv.style.display = 'block';
86
+ } finally {
87
+ loadingDiv.style.display = 'none';
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Render jiten.moe search results
93
+ * @param {Array} results - Search results from jiten.moe
94
+ */
95
+ function renderJitenResults(results) {
96
+ const resultsListDiv = document.getElementById('jitenResultsList');
97
+
98
+ // Store results globally for easy access
99
+ jitenSearchResults = results;
100
+
101
+ resultsListDiv.innerHTML = '';
102
+
103
+ results.forEach((result, index) => {
104
+ const resultItem = document.createElement('div');
105
+ resultItem.className = 'jiten-result-item';
106
+
107
+ const mediaTypeMap = {1: 'Anime', 7: 'Visual Novel', 2: 'Manga'};
108
+ const mediaTypeText = mediaTypeMap[result.media_type] || 'Unknown';
109
+
110
+ resultItem.innerHTML = `
111
+ <div class="jiten-result-header">
112
+ ${result.cover_name ? `<img src="${result.cover_name}" class="jiten-thumbnail" alt="Cover">` : '<div class="jiten-thumbnail-placeholder">🎮</div>'}
113
+ <div class="jiten-info">
114
+ <h5 class="jiten-title">${escapeHtml(result.title_original)}</h5>
115
+ ${result.title_english ? `<p class="jiten-title-en">${escapeHtml(result.title_english)}</p>` : ''}
116
+ ${result.title_romaji ? `<p class="jiten-title-rom">${escapeHtml(result.title_romaji)}</p>` : ''}
117
+ <div class="jiten-meta">
118
+ <span class="jiten-type">${mediaTypeText}</span>
119
+ ${result.difficulty ? `<span class="jiten-difficulty">Difficulty: ${result.difficulty}</span>` : ''}
120
+ <span class="jiten-chars">Total: ${result.character_count.toLocaleString()} chars</span>
121
+ </div>
122
+ </div>
123
+ <div class="jiten-actions">
124
+ <button class="action-btn primary" onclick="selectJitenGame(${index})">Select</button>
125
+ </div>
126
+ </div>
127
+ ${result.description ? `<div class="jiten-description">${escapeHtml(result.description.substring(0, 200))}${result.description.length > 200 ? '...' : ''}</div>` : ''}
128
+ `;
129
+
130
+ resultsListDiv.appendChild(resultItem);
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Select a jiten.moe game result
136
+ * @param {number} resultIndex - Index of the selected result
137
+ */
138
+ function selectJitenGame(resultIndex) {
139
+ selectedJitenGame = jitenSearchResults[resultIndex];
140
+
141
+ // Check if we're linking an existing game or creating from potential
142
+ if (window.currentPotentialGame) {
143
+ if (typeof showPotentialGameLinkConfirmation === 'function') {
144
+ showPotentialGameLinkConfirmation();
145
+ } else {
146
+ showLinkConfirmation();
147
+ }
148
+ } else {
149
+ showLinkConfirmation();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Show link confirmation modal
155
+ */
156
+ function showLinkConfirmation() {
157
+ if (!currentGameForSearch || !selectedJitenGame) return;
158
+
159
+ // Populate current game preview
160
+ const currentGamePreview = document.getElementById('currentGamePreview');
161
+ currentGamePreview.innerHTML = `
162
+ <div class="preview-header">
163
+ <h5>${escapeHtml(currentGameForSearch.title_original)}</h5>
164
+ <div class="preview-stats">
165
+ ${currentGameForSearch.line_count.toLocaleString()} lines,
166
+ ${currentGameForSearch.mined_character_count.toLocaleString()} mined characters
167
+ ${currentGameForSearch.jiten_character_count > 0 ? `<br>Game Total: ${currentGameForSearch.jiten_character_count.toLocaleString()} chars` : ''}
168
+ </div>
169
+ </div>
170
+ `;
171
+
172
+ // Populate jiten game preview
173
+ const jitenGamePreview = document.getElementById('jitenGamePreview');
174
+ const mediaTypeMap = {1: 'Anime', 7: 'Visual Novel', 2: 'Manga'};
175
+ jitenGamePreview.innerHTML = `
176
+ <div class="preview-header">
177
+ ${selectedJitenGame.cover_name ? `<img src="${selectedJitenGame.cover_name}" style="width: 60px; height: 60px; object-fit: cover; border-radius: 4px; margin-right: 10px;">` : ''}
178
+ <div>
179
+ <h5>${escapeHtml(selectedJitenGame.title_original)}</h5>
180
+ ${selectedJitenGame.title_english ? `<p>${escapeHtml(selectedJitenGame.title_english)}</p>` : ''}
181
+ <div class="preview-stats">
182
+ ${mediaTypeMap[selectedJitenGame.media_type] || 'Unknown'} |
183
+ Deck ID: ${selectedJitenGame.deck_id} |
184
+ Difficulty: ${selectedJitenGame.difficulty}
185
+ </div>
186
+ </div>
187
+ </div>
188
+ ${selectedJitenGame.description ? `<div style="margin-top: 10px; color: var(--text-secondary); font-size: 14px;">${escapeHtml(selectedJitenGame.description.substring(0, 150))}${selectedJitenGame.description.length > 150 ? '...' : ''}</div>` : ''}
189
+ `;
190
+
191
+ // Show manual overrides warning if any
192
+ const warningDiv = document.getElementById('manualOverridesWarning');
193
+ const overriddenFieldsList = document.getElementById('overriddenFieldsList');
194
+
195
+ if (currentGameForSearch.has_manual_overrides && currentGameForSearch.manual_overrides) {
196
+ const overridesStr = safeJoinArray(currentGameForSearch.manual_overrides, ', ');
197
+ if (overridesStr) {
198
+ overriddenFieldsList.innerHTML = `<div>Fields: ${overridesStr}</div>`;
199
+ warningDiv.style.display = 'block';
200
+ } else {
201
+ warningDiv.style.display = 'none';
202
+ }
203
+ } else {
204
+ warningDiv.style.display = 'none';
205
+ }
206
+
207
+ // Close search modal and open confirmation modal
208
+ closeModal('jitenSearchModal');
209
+ openModal('gameLinkConfirmModal');
210
+ }
211
+
212
+ /**
213
+ * Confirm and execute game linking to jiten.moe
214
+ */
215
+ async function confirmLinkGame() {
216
+ if (!currentGameForSearch || !selectedJitenGame) {
217
+ showDatabaseErrorPopup('Missing game or jiten data. Please try again.');
218
+ return;
219
+ }
220
+
221
+ // Prevent concurrent link operations
222
+ if (isLinkingInProgress) {
223
+ console.log('Link operation already in progress, ignoring request');
224
+ return;
225
+ }
226
+
227
+ // Validate game ID before making API call
228
+ if (!currentGameForSearch.id || currentGameForSearch.id === 'undefined' || currentGameForSearch.id === 'null') {
229
+ showDatabaseErrorPopup(`Cannot link game: Invalid game ID (${currentGameForSearch.id}). Please refresh the page and try again.`);
230
+ console.error('Invalid game ID in confirmLinkGame:', currentGameForSearch);
231
+ return;
232
+ }
233
+
234
+ const errorDiv = document.getElementById('linkConfirmError');
235
+ const loadingDiv = document.getElementById('linkConfirmLoading');
236
+ const confirmBtn = document.getElementById('confirmLinkBtn');
237
+
238
+ // Set global lock
239
+ isLinkingInProgress = true;
240
+
241
+ errorDiv.style.display = 'none';
242
+ loadingDiv.style.display = 'flex';
243
+ confirmBtn.disabled = true;
244
+
245
+ try {
246
+ const apiUrl = `/api/games/${currentGameForSearch.id}/link-jiten`;
247
+ console.log(`Linking game to jiten.moe: ${apiUrl}`);
248
+
249
+ const response = await fetch(apiUrl, {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({
253
+ deck_id: selectedJitenGame.deck_id,
254
+ jiten_data: selectedJitenGame
255
+ })
256
+ });
257
+
258
+ const result = await response.json();
259
+
260
+ if (response.ok) {
261
+ // Success! Close modal
262
+ closeModal('gameLinkConfirmModal');
263
+
264
+ // Log the complete API response for debugging
265
+ logApiResponse('Link Game to Jiten', response, result);
266
+
267
+ // Show success message with line count
268
+ const lineCount = result.lines_linked || currentGameForSearch.line_count || 0;
269
+ console.log(`✅ Game linking successful: ${lineCount} lines linked`);
270
+ showDatabaseSuccessPopup(`Successfully linked "${currentGameForSearch.title_original}" to jiten.moe! ${lineCount} lines linked.`);
271
+
272
+ // Refresh data without page reload
273
+ await refreshAfterLinking();
274
+
275
+ // Reset state for next operation
276
+ confirmBtn.disabled = false;
277
+ } else {
278
+ const errorMessage = result.error || 'Failed to link game';
279
+ errorDiv.textContent = errorMessage;
280
+ errorDiv.style.display = 'block';
281
+ confirmBtn.disabled = false;
282
+ console.error('Link game API error:', result);
283
+ }
284
+ } catch (error) {
285
+ console.error('Error linking game:', error);
286
+ errorDiv.textContent = `Network error: ${error.message || 'Failed to connect to server'}`;
287
+ errorDiv.style.display = 'block';
288
+ confirmBtn.disabled = false;
289
+ } finally {
290
+ loadingDiv.style.display = 'none';
291
+ // Release global lock
292
+ isLinkingInProgress = false;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Refresh data after successful game linking without page reload
298
+ */
299
+ async function refreshAfterLinking() {
300
+ console.log('🔄 Refreshing data after game linking...');
301
+
302
+ try {
303
+ // Fetch updated game data from API
304
+ const gamesResponse = await fetch('/api/games-management');
305
+ const gamesData = await gamesResponse.json();
306
+
307
+ if (gamesResponse.ok && gamesData.games) {
308
+ // Update the currentGames array with fresh data
309
+ currentGames = gamesData.games;
310
+ console.log(`✅ Updated currentGames array with ${currentGames.length} games`);
311
+
312
+ // Get the current filter state
313
+ const activeFilterBtn = document.querySelector('.game-data-filters button.primary');
314
+ const currentFilter = activeFilterBtn ? activeFilterBtn.dataset.filter : 'all';
315
+
316
+ // Re-render the games list with the current filter (this is smooth, no loading indicator)
317
+ renderGamesList(currentGames, currentFilter);
318
+
319
+ // Silently update dashboard stats in the background
320
+ if (typeof databaseManager !== 'undefined' && databaseManager.loadGameManagementStats) {
321
+ await databaseManager.loadGameManagementStats();
322
+ }
323
+ }
324
+
325
+ console.log('✅ Data refresh completed successfully');
326
+ } catch (error) {
327
+ console.error('Error refreshing data after linking:', error);
328
+ // Don't show error to user since the link operation itself succeeded
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Mark a game as completed
334
+ * @param {string} gameId - Game ID to mark as completed
335
+ */
336
+ async function markGameCompleted(gameId) {
337
+ showDatabaseConfirmPopup('Mark this game as completed?', async () => {
338
+ try {
339
+ const response = await fetch(`/api/games/${gameId}`, {
340
+ method: 'PUT',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({ completed: true })
343
+ });
344
+
345
+ const result = await response.json();
346
+
347
+ if (response.ok) {
348
+ await loadGamesForDataManagement();
349
+ showDatabaseSuccessPopup('Game marked as completed!');
350
+ } else {
351
+ showDatabaseErrorPopup(`Error: ${result.error}`);
352
+ }
353
+ } catch (error) {
354
+ console.error('Error marking game as completed:', error);
355
+ showDatabaseErrorPopup('Failed to mark game as completed');
356
+ }
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Edit a game's information
362
+ * @param {string} gameId - Game ID to edit
363
+ */
364
+ function editGame(gameId) {
365
+ const game = currentGames.find(g => g.id === gameId);
366
+ if (!game) {
367
+ showDatabaseErrorPopup('Game not found');
368
+ return;
369
+ }
370
+
371
+ openEditGameModal(game);
372
+ }
373
+
374
+ /**
375
+ * Open edit game modal with game data
376
+ * @param {Object} game - Game object to edit
377
+ */
378
+ function openEditGameModal(game) {
379
+ // Populate form fields with current game data
380
+ document.getElementById('editGameId').value = game.id;
381
+ document.getElementById('editTitleOriginal').value = game.title_original || '';
382
+ document.getElementById('editTitleRomaji').value = game.title_romaji || '';
383
+ document.getElementById('editTitleEnglish').value = game.title_english || '';
384
+ document.getElementById('editType').value = game.type || '';
385
+ document.getElementById('editDescription').value = game.description || '';
386
+ document.getElementById('editDifficulty').value = game.difficulty || '';
387
+ document.getElementById('editDeckId').value = game.deck_id || '';
388
+ document.getElementById('editCharacterCount').value = game.jiten_character_count || '';
389
+ document.getElementById('editCompleted').checked = !!game.completed;
390
+
391
+ // Handle release date - convert ISO format to date input format (YYYY-MM-DD)
392
+ if (game.release_date) {
393
+ try {
394
+ const date = new Date(game.release_date);
395
+ if (!isNaN(date.getTime())) {
396
+ document.getElementById('editReleaseDate').value = date.toISOString().split('T')[0];
397
+ } else {
398
+ document.getElementById('editReleaseDate').value = '';
399
+ }
400
+ } catch (error) {
401
+ console.warn('Error parsing release date:', game.release_date, error);
402
+ document.getElementById('editReleaseDate').value = '';
403
+ }
404
+ } else {
405
+ document.getElementById('editReleaseDate').value = '';
406
+ }
407
+
408
+ // Handle links JSON - keep the hidden field updated for compatibility
409
+ if (game.links && game.links.length > 0) {
410
+ document.getElementById('editLinks').value = JSON.stringify(game.links, null, 2);
411
+
412
+ // Extract URLs from links array and populate the list textarea
413
+ // Handle both array of objects and array of strings
414
+ const urls = game.links.map(link => {
415
+ if (typeof link === 'string') {
416
+ return link;
417
+ } else if (link && link.url) {
418
+ return link.url;
419
+ }
420
+ return null;
421
+ }).filter(url => url);
422
+
423
+ document.getElementById('editLinksList').value = urls.join('\n');
424
+ } else {
425
+ document.getElementById('editLinks').value = '';
426
+ document.getElementById('editLinksList').value = '';
427
+ }
428
+
429
+ // Handle image preview
430
+ const imagePreview = document.getElementById('editImagePreview');
431
+ const imagePreviewImg = document.getElementById('editImagePreviewImg');
432
+ if (game.image) {
433
+ imagePreviewImg.src = game.image.startsWith('data:') ? game.image : `data:image/png;base64,${game.image}`;
434
+ imagePreview.style.display = 'block';
435
+ } else {
436
+ imagePreview.style.display = 'none';
437
+ }
438
+
439
+ // Reset file input
440
+ document.getElementById('editImageUpload').value = '';
441
+
442
+ // Reset error display
443
+ document.getElementById('editGameError').style.display = 'none';
444
+
445
+ // Open the modal
446
+ openModal('editGameModal');
447
+ }
448
+
449
+ /**
450
+ * Convert any image file to PNG format using Canvas API
451
+ * @param {File} file - The image file to convert
452
+ * @returns {Promise<string>} Base64 PNG data (without data URI prefix)
453
+ */
454
+ async function convertImageToPNG(file) {
455
+ return new Promise((resolve, reject) => {
456
+ const img = new Image();
457
+ const reader = new FileReader();
458
+
459
+ reader.onload = (e) => {
460
+ img.onload = () => {
461
+ try {
462
+ // Create canvas and draw image
463
+ const canvas = document.createElement('canvas');
464
+ canvas.width = img.width;
465
+ canvas.height = img.height;
466
+ const ctx = canvas.getContext('2d');
467
+ ctx.drawImage(img, 0, 0);
468
+
469
+ // Convert to PNG base64 (remove data:image/png;base64, prefix)
470
+ const pngDataUrl = canvas.toDataURL('image/png');
471
+ const pngBase64 = pngDataUrl.split(',')[1];
472
+ resolve(pngBase64);
473
+ } catch (error) {
474
+ reject(new Error(`Failed to convert image to PNG: ${error.message}`));
475
+ }
476
+ };
477
+ img.onerror = () => reject(new Error('Failed to load image for conversion'));
478
+ img.src = e.target.result;
479
+ };
480
+ reader.onerror = () => reject(new Error('Failed to read image file'));
481
+ reader.readAsDataURL(file);
482
+ });
483
+ }
484
+
485
+ /**
486
+ * Save game edits
487
+ */
488
+ async function saveGameEdits() {
489
+ const gameId = document.getElementById('editGameId').value;
490
+ const errorDiv = document.getElementById('editGameError');
491
+ const loadingDiv = document.getElementById('editGameLoading');
492
+ const saveBtn = document.getElementById('saveGameEditsBtn');
493
+
494
+ // Reset error display
495
+ errorDiv.style.display = 'none';
496
+
497
+ // Validate required fields
498
+ const titleOriginal = document.getElementById('editTitleOriginal').value.trim();
499
+ if (!titleOriginal) {
500
+ errorDiv.textContent = 'Original title is required';
501
+ errorDiv.style.display = 'block';
502
+ return;
503
+ }
504
+
505
+ // Convert links list to JSON array
506
+ const linksListText = document.getElementById('editLinksList').value.trim();
507
+ let linksArray = [];
508
+ if (linksListText) {
509
+ // Split by newlines and filter out empty lines
510
+ const urls = linksListText.split('\n')
511
+ .map(url => url.trim())
512
+ .filter(url => url.length > 0);
513
+
514
+ // Convert each URL to the required JSON format
515
+ linksArray = urls.map(url => ({
516
+ deckId: 1,
517
+ linkId: 1,
518
+ linkType: 2,
519
+ url: url
520
+ }));
521
+ }
522
+
523
+ // Validate difficulty
524
+ const difficulty = document.getElementById('editDifficulty').value;
525
+ if (difficulty && (parseInt(difficulty) < 1 || parseInt(difficulty) > 5)) {
526
+ errorDiv.textContent = 'Difficulty must be between 1 and 5';
527
+ errorDiv.style.display = 'block';
528
+ return;
529
+ }
530
+
531
+ // Show loading state
532
+ loadingDiv.style.display = 'flex';
533
+ saveBtn.disabled = true;
534
+
535
+ try {
536
+ // Prepare update data
537
+ const updateData = {
538
+ title_original: titleOriginal,
539
+ title_romaji: document.getElementById('editTitleRomaji').value.trim(),
540
+ title_english: document.getElementById('editTitleEnglish').value.trim(),
541
+ type: document.getElementById('editType').value,
542
+ description: document.getElementById('editDescription').value.trim(),
543
+ completed: document.getElementById('editCompleted').checked
544
+ };
545
+
546
+ // Add release date if provided
547
+ const releaseDate = document.getElementById('editReleaseDate').value;
548
+ if (releaseDate) {
549
+ // Convert date input (YYYY-MM-DD) to ISO format for storage
550
+ updateData.release_date = releaseDate + 'T00:00:00';
551
+ }
552
+
553
+ // Add optional numeric fields
554
+ const deckId = document.getElementById('editDeckId').value;
555
+ if (deckId) {
556
+ updateData.deck_id = parseInt(deckId);
557
+ }
558
+
559
+ if (difficulty) {
560
+ updateData.difficulty = parseInt(difficulty);
561
+ }
562
+
563
+ const characterCount = document.getElementById('editCharacterCount').value;
564
+ if (characterCount) {
565
+ updateData.character_count = parseInt(characterCount);
566
+ }
567
+
568
+ // Add links if provided
569
+ if (linksArray.length > 0) {
570
+ updateData.links = linksArray;
571
+ }
572
+
573
+ // Handle image upload - convert to PNG format
574
+ const imageFile = document.getElementById('editImageUpload').files[0];
575
+ if (imageFile) {
576
+ try {
577
+ const pngBase64 = await convertImageToPNG(imageFile);
578
+ updateData.image = pngBase64;
579
+ } catch (error) {
580
+ console.error('Error converting image:', error);
581
+ errorDiv.textContent = `Failed to process image: ${error.message}`;
582
+ errorDiv.style.display = 'block';
583
+ saveBtn.disabled = false;
584
+ loadingDiv.style.display = 'none';
585
+ return;
586
+ }
587
+ }
588
+
589
+ // Send update request
590
+ const response = await fetch(`/api/games/${gameId}`, {
591
+ method: 'PUT',
592
+ headers: { 'Content-Type': 'application/json' },
593
+ body: JSON.stringify(updateData)
594
+ });
595
+
596
+ const result = await response.json();
597
+
598
+ if (response.ok) {
599
+ // Success! Close modal and refresh
600
+ closeModal('editGameModal');
601
+ await loadGamesForDataManagement();
602
+ showDatabaseSuccessPopup('Game updated successfully! All edited fields marked as manual overrides.');
603
+ } else {
604
+ errorDiv.textContent = result.error || 'Failed to update game';
605
+ errorDiv.style.display = 'block';
606
+ saveBtn.disabled = false;
607
+ }
608
+ } catch (error) {
609
+ console.error('Error saving game edits:', error);
610
+ errorDiv.textContent = `Error: ${error.message}`;
611
+ errorDiv.style.display = 'block';
612
+ saveBtn.disabled = false;
613
+ } finally {
614
+ loadingDiv.style.display = 'none';
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Repull data from jiten.moe for a game
620
+ * @param {string} gameId - Game ID to repull data for
621
+ * @param {string} gameName - Game name for display
622
+ */
623
+ async function repullJitenData(gameId, gameName) {
624
+ console.log(`🔄 Starting repull operation for game: ${gameName} (ID: ${gameId})`);
625
+
626
+ showDatabaseConfirmPopup(
627
+ `Repull data from jiten.moe for "${gameName}"? This will update all non-manually edited fields with fresh data from jiten.moe.`,
628
+ async () => {
629
+ console.log(`✅ User confirmed repull for ${gameName}`);
630
+
631
+ try {
632
+ console.log(`📡 Making API request to /api/games/${gameId}/repull-jiten`);
633
+
634
+ const response = await fetch(`/api/games/${gameId}/repull-jiten`, {
635
+ method: 'POST',
636
+ headers: { 'Content-Type': 'application/json' }
637
+ });
638
+
639
+ console.log(`📥 Received response:`, {
640
+ status: response.status,
641
+ statusText: response.statusText,
642
+ ok: response.ok,
643
+ headers: Object.fromEntries(response.headers.entries())
644
+ });
645
+
646
+ const result = await response.json();
647
+
648
+ // Log the complete API response for debugging
649
+ logApiResponse('Repull Jiten Data', response, result);
650
+
651
+ if (response.ok) {
652
+ console.log(`✅ Repull operation successful for ${gameName}`);
653
+
654
+ let message = result.message || 'Repull completed successfully';
655
+
656
+ // Safe handling of updated_fields
657
+ if (result.updated_fields) {
658
+ const updatedFieldsStr = safeJoinArray(result.updated_fields, ', ');
659
+ if (updatedFieldsStr) {
660
+ message += ` Updated fields: ${updatedFieldsStr}.`;
661
+ console.log(`📝 Updated fields: ${updatedFieldsStr}`);
662
+ }
663
+ }
664
+
665
+ // Safe handling of skipped_fields
666
+ if (result.skipped_fields) {
667
+ const skippedFieldsStr = safeJoinArray(result.skipped_fields, ', ');
668
+ if (skippedFieldsStr) {
669
+ message += ` Skipped (manually edited): ${skippedFieldsStr}.`;
670
+ console.log(`⏭️ Skipped fields: ${skippedFieldsStr}`);
671
+ }
672
+ }
673
+
674
+ console.log(`📢 Final success message: ${message}`);
675
+ showDatabaseSuccessPopup(message);
676
+
677
+ // Refresh the current tab to show updated data
678
+ console.log(`🔄 Refreshing current tab to show updated data`);
679
+ const activeTab = document.querySelector('.tab-btn.active');
680
+ if (activeTab) {
681
+ console.log(`🔄 Switching to tab: ${activeTab.dataset.tab}`);
682
+ switchTab(activeTab.dataset.tab);
683
+ }
684
+
685
+ // Update dashboard stats
686
+ console.log(`📊 Updating dashboard stats`);
687
+ if (typeof databaseManager !== 'undefined') {
688
+ await databaseManager.loadDashboardStats();
689
+ }
690
+
691
+ console.log(`✅ Repull operation completed successfully for ${gameName}`);
692
+ } else {
693
+ console.error(`❌ Repull operation failed for ${gameName}:`, result);
694
+ const errorMessage = result.error || 'Unknown error occurred';
695
+ showDatabaseErrorPopup(`Error: ${errorMessage}`);
696
+ }
697
+ } catch (error) {
698
+ console.error(`💥 Exception during repull operation for ${gameName}:`, error);
699
+ console.error('Error stack:', error.stack);
700
+ showDatabaseErrorPopup('Failed to repull data from jiten.moe');
701
+ }
702
+ }
703
+ );
704
+ }
705
+
706
+ /**
707
+ * Initialize jiten integration event handlers
708
+ */
709
+ function initializeJitenIntegration() {
710
+ const jitenSearchBtn = document.getElementById('jitenSearchBtn');
711
+ if (jitenSearchBtn) {
712
+ jitenSearchBtn.addEventListener('click', searchJitenMoe);
713
+ }
714
+
715
+ // Add Enter key support for jiten search input
716
+ const jitenSearchInput = document.getElementById('jitenSearchInput');
717
+ if (jitenSearchInput) {
718
+ jitenSearchInput.addEventListener('keypress', function(e) {
719
+ if (e.key === 'Enter') {
720
+ e.preventDefault();
721
+ searchJitenMoe();
722
+ }
723
+ });
724
+ }
725
+
726
+ const confirmLinkBtn = document.getElementById('confirmLinkBtn');
727
+ if (confirmLinkBtn) {
728
+ confirmLinkBtn.addEventListener('click', confirmLinkGame);
729
+ }
730
+
731
+ // Handle image upload preview - convert to PNG for preview
732
+ const imageUpload = document.getElementById('editImageUpload');
733
+ if (imageUpload) {
734
+ imageUpload.addEventListener('change', async function(e) {
735
+ const file = e.target.files[0];
736
+ if (file) {
737
+ try {
738
+ const pngBase64 = await convertImageToPNG(file);
739
+ const imagePreview = document.getElementById('editImagePreview');
740
+ const imagePreviewImg = document.getElementById('editImagePreviewImg');
741
+ imagePreviewImg.src = `data:image/png;base64,${pngBase64}`;
742
+ imagePreview.style.display = 'block';
743
+ } catch (error) {
744
+ console.error('Error previewing image:', error);
745
+ alert(`Failed to preview image: ${error.message}`);
746
+ }
747
+ }
748
+ });
749
+ }
750
+ }