GameSentenceMiner 2.17.7__py3-none-any.whl → 2.18.1__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/ai/ai_prompting.py +6 -6
- GameSentenceMiner/anki.py +236 -152
- GameSentenceMiner/gametext.py +7 -4
- GameSentenceMiner/gsm.py +49 -10
- GameSentenceMiner/locales/en_us.json +7 -3
- GameSentenceMiner/locales/ja_jp.json +8 -4
- GameSentenceMiner/locales/zh_cn.json +8 -4
- GameSentenceMiner/obs.py +238 -59
- GameSentenceMiner/ocr/owocr_helper.py +1 -1
- GameSentenceMiner/tools/ss_selector.py +7 -8
- GameSentenceMiner/ui/__init__.py +0 -0
- GameSentenceMiner/ui/anki_confirmation.py +187 -0
- GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
- GameSentenceMiner/ui/screenshot_selector.py +215 -0
- GameSentenceMiner/util/configuration.py +124 -22
- GameSentenceMiner/util/db.py +22 -13
- GameSentenceMiner/util/downloader/download_tools.py +2 -2
- GameSentenceMiner/util/ffmpeg.py +24 -30
- GameSentenceMiner/util/get_overlay_coords.py +34 -34
- GameSentenceMiner/util/gsm_utils.py +31 -1
- GameSentenceMiner/util/text_log.py +11 -9
- GameSentenceMiner/vad.py +31 -12
- GameSentenceMiner/web/database_api.py +742 -123
- GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- GameSentenceMiner/web/static/css/overview.css +850 -0
- GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- GameSentenceMiner/web/static/css/shared.css +97 -0
- GameSentenceMiner/web/static/css/stats.css +192 -597
- GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- GameSentenceMiner/web/static/js/database.js +209 -5
- GameSentenceMiner/web/static/js/goals.js +610 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
- GameSentenceMiner/web/static/js/overview.js +1176 -0
- GameSentenceMiner/web/static/js/shared.js +25 -0
- GameSentenceMiner/web/static/js/stats.js +154 -1459
- GameSentenceMiner/web/stats.py +2 -2
- GameSentenceMiner/web/templates/anki_stats.html +5 -0
- GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
- GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
- GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
- GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
- GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
- GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
- GameSentenceMiner/web/templates/components/navigation.html +3 -1
- GameSentenceMiner/web/templates/database.html +73 -1
- GameSentenceMiner/web/templates/goals.html +376 -0
- GameSentenceMiner/web/templates/index.html +13 -11
- GameSentenceMiner/web/templates/overview.html +416 -0
- GameSentenceMiner/web/templates/stats.html +46 -251
- GameSentenceMiner/web/texthooking_page.py +18 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
- gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
- gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/top_level.txt +0 -0
|
@@ -3,8 +3,11 @@ import datetime
|
|
|
3
3
|
import re
|
|
4
4
|
import csv
|
|
5
5
|
import io
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
6
8
|
from collections import defaultdict
|
|
7
9
|
import time
|
|
10
|
+
from pathlib import Path
|
|
8
11
|
|
|
9
12
|
import flask
|
|
10
13
|
from flask import request, jsonify
|
|
@@ -212,7 +215,7 @@ def register_database_api_routes(app):
|
|
|
212
215
|
return jsonify({'games': games_data}), 200
|
|
213
216
|
|
|
214
217
|
except Exception as e:
|
|
215
|
-
logger.error(f"Error fetching games list: {e}")
|
|
218
|
+
logger.error(f"Error fetching games list: {e}", exc_info=True)
|
|
216
219
|
return jsonify({'error': 'Failed to fetch games list'}), 500
|
|
217
220
|
|
|
218
221
|
@app.route('/api/delete-games', methods=['POST'])
|
|
@@ -306,6 +309,9 @@ def register_database_api_routes(app):
|
|
|
306
309
|
'reading_hours_target': config.reading_hours_target,
|
|
307
310
|
'character_count_target': config.character_count_target,
|
|
308
311
|
'games_target': config.games_target,
|
|
312
|
+
'reading_hours_target_date': config.reading_hours_target_date,
|
|
313
|
+
'character_count_target_date': config.character_count_target_date,
|
|
314
|
+
'games_target_date': config.games_target_date,
|
|
309
315
|
}), 200
|
|
310
316
|
except Exception as e:
|
|
311
317
|
logger.error(f"Error getting settings: {e}")
|
|
@@ -328,6 +334,9 @@ def register_database_api_routes(app):
|
|
|
328
334
|
reading_hours_target = data.get('reading_hours_target')
|
|
329
335
|
character_count_target = data.get('character_count_target')
|
|
330
336
|
games_target = data.get('games_target')
|
|
337
|
+
reading_hours_target_date = data.get('reading_hours_target_date')
|
|
338
|
+
character_count_target_date = data.get('character_count_target_date')
|
|
339
|
+
games_target_date = data.get('games_target_date')
|
|
331
340
|
|
|
332
341
|
# Validate input - only require the settings that are provided
|
|
333
342
|
settings_to_update = {}
|
|
@@ -386,6 +395,37 @@ def register_database_api_routes(app):
|
|
|
386
395
|
except (ValueError, TypeError):
|
|
387
396
|
return jsonify({'error': 'Games target must be a valid integer'}), 400
|
|
388
397
|
|
|
398
|
+
# Validate target dates (ISO format: YYYY-MM-DD)
|
|
399
|
+
if reading_hours_target_date is not None:
|
|
400
|
+
if reading_hours_target_date == '':
|
|
401
|
+
settings_to_update['reading_hours_target_date'] = ''
|
|
402
|
+
else:
|
|
403
|
+
try:
|
|
404
|
+
datetime.datetime.strptime(reading_hours_target_date, '%Y-%m-%d')
|
|
405
|
+
settings_to_update['reading_hours_target_date'] = reading_hours_target_date
|
|
406
|
+
except ValueError:
|
|
407
|
+
return jsonify({'error': 'Reading hours target date must be in YYYY-MM-DD format'}), 400
|
|
408
|
+
|
|
409
|
+
if character_count_target_date is not None:
|
|
410
|
+
if character_count_target_date == '':
|
|
411
|
+
settings_to_update['character_count_target_date'] = ''
|
|
412
|
+
else:
|
|
413
|
+
try:
|
|
414
|
+
datetime.datetime.strptime(character_count_target_date, '%Y-%m-%d')
|
|
415
|
+
settings_to_update['character_count_target_date'] = character_count_target_date
|
|
416
|
+
except ValueError:
|
|
417
|
+
return jsonify({'error': 'Character count target date must be in YYYY-MM-DD format'}), 400
|
|
418
|
+
|
|
419
|
+
if games_target_date is not None:
|
|
420
|
+
if games_target_date == '':
|
|
421
|
+
settings_to_update['games_target_date'] = ''
|
|
422
|
+
else:
|
|
423
|
+
try:
|
|
424
|
+
datetime.datetime.strptime(games_target_date, '%Y-%m-%d')
|
|
425
|
+
settings_to_update['games_target_date'] = games_target_date
|
|
426
|
+
except ValueError:
|
|
427
|
+
return jsonify({'error': 'Games target date must be in YYYY-MM-DD format'}), 400
|
|
428
|
+
|
|
389
429
|
if not settings_to_update:
|
|
390
430
|
return jsonify({'error': 'No valid settings provided'}), 400
|
|
391
431
|
|
|
@@ -404,6 +444,12 @@ def register_database_api_routes(app):
|
|
|
404
444
|
config.character_count_target = settings_to_update['character_count_target']
|
|
405
445
|
if 'games_target' in settings_to_update:
|
|
406
446
|
config.games_target = settings_to_update['games_target']
|
|
447
|
+
if 'reading_hours_target_date' in settings_to_update:
|
|
448
|
+
config.reading_hours_target_date = settings_to_update['reading_hours_target_date']
|
|
449
|
+
if 'character_count_target_date' in settings_to_update:
|
|
450
|
+
config.character_count_target_date = settings_to_update['character_count_target_date']
|
|
451
|
+
if 'games_target_date' in settings_to_update:
|
|
452
|
+
config.games_target_date = settings_to_update['games_target_date']
|
|
407
453
|
|
|
408
454
|
save_stats_config(config)
|
|
409
455
|
|
|
@@ -786,146 +832,596 @@ def register_database_api_routes(app):
|
|
|
786
832
|
logger.error(f"Error in deduplication: {e}")
|
|
787
833
|
return jsonify({'error': f'Deduplication failed: {str(e)}'}), 500
|
|
788
834
|
|
|
835
|
+
@app.route('/api/merge_games', methods=['POST'])
|
|
836
|
+
def api_merge_games():
|
|
837
|
+
"""
|
|
838
|
+
Merges multiple selected games into a single game entry.
|
|
839
|
+
The first game in the list becomes the primary game that retains its name.
|
|
840
|
+
All lines from secondary games are moved to the primary game.
|
|
841
|
+
"""
|
|
842
|
+
try:
|
|
843
|
+
data = request.get_json()
|
|
844
|
+
target_game = data.get('target_game', None)
|
|
845
|
+
games_to_merge = data.get('games_to_merge', [])
|
|
846
|
+
|
|
847
|
+
logger.info(f"Merge request received: target_game='{target_game}', games_to_merge={games_to_merge}")
|
|
848
|
+
|
|
849
|
+
# Validation
|
|
850
|
+
if not target_game:
|
|
851
|
+
return jsonify({'error': 'No target game specified for merging'}), 400
|
|
852
|
+
|
|
853
|
+
if not games_to_merge:
|
|
854
|
+
return jsonify({'error': 'No games specified for merging'}), 400
|
|
855
|
+
|
|
856
|
+
if not isinstance(games_to_merge, list):
|
|
857
|
+
return jsonify({'error': 'game_names must be a list'}), 400
|
|
858
|
+
|
|
859
|
+
if len(games_to_merge) < 1:
|
|
860
|
+
return jsonify({'error': 'At least 1 game must be selected for merging'}), 400
|
|
861
|
+
|
|
862
|
+
# Validate that all games exist
|
|
863
|
+
existing_games = GameLinesTable.get_all_games_with_lines()
|
|
864
|
+
invalid_games = [name for name in games_to_merge if name not in existing_games]
|
|
865
|
+
|
|
866
|
+
if invalid_games:
|
|
867
|
+
return jsonify({'error': f'Games not found: {", ".join(invalid_games)}'}), 400
|
|
868
|
+
|
|
869
|
+
# Check for duplicate game names
|
|
870
|
+
if len(set(games_to_merge)) != len(games_to_merge):
|
|
871
|
+
return jsonify({'error': 'Duplicate game names found in selection'}), 400
|
|
872
|
+
|
|
873
|
+
# Identify primary and secondary games
|
|
874
|
+
|
|
875
|
+
# Collect pre-merge statistics
|
|
876
|
+
primary_lines_before = GameLinesTable.get_all_lines_for_scene(target_game)
|
|
877
|
+
total_lines_to_merge = 0
|
|
878
|
+
merge_summary = {
|
|
879
|
+
'primary_game': target_game,
|
|
880
|
+
'secondary_games': games_to_merge,
|
|
881
|
+
'lines_moved': 0,
|
|
882
|
+
'total_lines_after_merge': 0
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
# Calculate lines to be moved and store counts
|
|
886
|
+
secondary_game_line_counts = {}
|
|
887
|
+
for game_name in games_to_merge:
|
|
888
|
+
secondary_lines = GameLinesTable.get_all_lines_for_scene(game_name)
|
|
889
|
+
line_count = len(secondary_lines)
|
|
890
|
+
secondary_game_line_counts[game_name] = line_count
|
|
891
|
+
total_lines_to_merge += line_count
|
|
892
|
+
|
|
893
|
+
if total_lines_to_merge == 0:
|
|
894
|
+
return jsonify({'error': 'No lines found in secondary games to merge'}), 400
|
|
895
|
+
|
|
896
|
+
# Begin database transaction for merge
|
|
897
|
+
try:
|
|
898
|
+
# Perform the merge operation within transaction
|
|
899
|
+
lines_moved = 0
|
|
900
|
+
for game_name in games_to_merge:
|
|
901
|
+
# Update game_name for all lines belonging to this secondary game
|
|
902
|
+
# Also set original_game_name to preserve the original title
|
|
903
|
+
# Ensure the table name is as expected to prevent SQL injection
|
|
904
|
+
if GameLinesTable._table != "game_lines":
|
|
905
|
+
raise ValueError("Unexpected table name in GameLinesTable._table")
|
|
906
|
+
GameLinesTable._db.execute(
|
|
907
|
+
"UPDATE game_lines SET game_name=?, original_game_name=COALESCE(original_game_name, ?) WHERE game_name=?",
|
|
908
|
+
(target_game, game_name, game_name),
|
|
909
|
+
commit=True
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# Add the count we calculated earlier
|
|
913
|
+
lines_moved += secondary_game_line_counts[game_name]
|
|
914
|
+
|
|
915
|
+
# Update merge summary
|
|
916
|
+
merge_summary['lines_moved'] = lines_moved
|
|
917
|
+
merge_summary['total_lines_after_merge'] = len(primary_lines_before) + lines_moved
|
|
918
|
+
|
|
919
|
+
# Log the successful merge
|
|
920
|
+
logger.info(f"Successfully merged {len(games_to_merge)} games into '{target_game}': moved {lines_moved} lines")
|
|
921
|
+
|
|
922
|
+
# Prepare success response
|
|
923
|
+
response_data = {
|
|
924
|
+
'message': f'Successfully merged {len(games_to_merge)} games into "{target_game}"',
|
|
925
|
+
'primary_game': target_game,
|
|
926
|
+
'merged_games': games_to_merge,
|
|
927
|
+
'lines_moved': lines_moved,
|
|
928
|
+
'total_lines_in_primary': merge_summary['total_lines_after_merge'],
|
|
929
|
+
'merge_summary': merge_summary
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return jsonify(response_data), 200
|
|
933
|
+
|
|
934
|
+
except Exception as db_error:
|
|
935
|
+
logger.error(f"Database error during game merge: {db_error}", exc_info=True)
|
|
936
|
+
return jsonify({
|
|
937
|
+
'error': f'Failed to merge games due to database error: {str(db_error)}'
|
|
938
|
+
}), 500
|
|
939
|
+
|
|
940
|
+
except Exception as e:
|
|
941
|
+
logger.error(f"Error in game merge API: {e}")
|
|
942
|
+
return jsonify({'error': f'Game merge failed: {str(e)}'}), 500
|
|
943
|
+
|
|
789
944
|
@app.route('/api/stats')
|
|
790
945
|
def api_stats():
|
|
791
946
|
"""
|
|
792
947
|
Provides aggregated, cumulative stats for charting.
|
|
793
948
|
Accepts optional 'year' parameter to filter heatmap data.
|
|
794
949
|
"""
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
950
|
+
try:
|
|
951
|
+
punctionation_regex = regex.compile(r'[\p{P}\p{S}\p{Z}]')
|
|
952
|
+
# Get optional year filter parameter
|
|
953
|
+
filter_year = request.args.get('year', None)
|
|
798
954
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
955
|
+
# Get Start and End time as unix timestamp
|
|
956
|
+
start_timestamp = request.args.get('start', None)
|
|
957
|
+
end_timestamp = request.args.get('end', None)
|
|
958
|
+
|
|
959
|
+
# Convert timestamps to float if provided
|
|
960
|
+
start_timestamp = float(start_timestamp) if start_timestamp else None
|
|
961
|
+
end_timestamp = float(end_timestamp) if end_timestamp else None
|
|
806
962
|
|
|
807
|
-
|
|
808
|
-
|
|
963
|
+
# 1. Fetch all lines and sort them chronologically
|
|
964
|
+
all_lines = GameLinesTable.get_lines_filtered_by_timestamp(start=start_timestamp, end=end_timestamp)
|
|
965
|
+
|
|
966
|
+
if not all_lines:
|
|
967
|
+
return jsonify({"labels": [], "datasets": []})
|
|
968
|
+
|
|
969
|
+
# 2. Process data into daily totals for each game
|
|
970
|
+
# Structure: daily_data[date_str][game_name] = {'lines': N, 'chars': N}
|
|
971
|
+
daily_data = defaultdict(lambda: defaultdict(lambda: {'lines': 0, 'chars': 0}))
|
|
972
|
+
wrong_instance_found = False
|
|
973
|
+
for line in all_lines:
|
|
974
|
+
day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
|
|
975
|
+
game = line.game_name or "Unknown Game"
|
|
976
|
+
# Remove punctuation and symbols from line text before counting characters
|
|
977
|
+
clean_text = punctionation_regex.sub('', str(line.line_text)) if line.line_text else ''
|
|
978
|
+
if not isinstance(clean_text, str) and not wrong_instance_found:
|
|
979
|
+
logger.info(f"Non-string line_text encountered: {clean_text} (type: {type(clean_text)})")
|
|
980
|
+
wrong_instance_found = True
|
|
981
|
+
|
|
982
|
+
line.line_text = clean_text # Update line text to cleaned version for future use
|
|
983
|
+
daily_data[day_str][game]['lines'] += 1
|
|
984
|
+
daily_data[day_str][game]['chars'] += len(clean_text)
|
|
985
|
+
|
|
986
|
+
# 3. Create cumulative datasets for Chart.js
|
|
987
|
+
sorted_days = sorted(daily_data.keys())
|
|
988
|
+
game_names = GameLinesTable.get_all_games_with_lines()
|
|
989
|
+
|
|
990
|
+
# Keep track of the running total for each metric for each game
|
|
991
|
+
cumulative_totals = defaultdict(lambda: {'lines': 0, 'chars': 0})
|
|
992
|
+
|
|
993
|
+
# Structure for final data: final_data[game_name][metric] = [day1_val, day2_val, ...]
|
|
994
|
+
final_data = defaultdict(lambda: defaultdict(list))
|
|
809
995
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
996
|
+
for day in sorted_days:
|
|
997
|
+
for game in game_names:
|
|
998
|
+
# Add the day's total to the cumulative total
|
|
999
|
+
cumulative_totals[game]['lines'] += daily_data[day][game]['lines']
|
|
1000
|
+
cumulative_totals[game]['chars'] += daily_data[day][game]['chars']
|
|
1001
|
+
|
|
1002
|
+
# Append the new cumulative total to the list for that day
|
|
1003
|
+
final_data[game]['lines'].append(cumulative_totals[game]['lines'])
|
|
1004
|
+
final_data[game]['chars'].append(cumulative_totals[game]['chars'])
|
|
1005
|
+
|
|
1006
|
+
# 4. Format into Chart.js dataset structure
|
|
1007
|
+
datasets = []
|
|
1008
|
+
# A simple color palette for the chart lines
|
|
1009
|
+
colors = ['#3498db', '#e74c3c', '#2ecc71', '#f1c40f', '#9b59b6', '#1abc9c', '#e67e22']
|
|
1010
|
+
|
|
1011
|
+
for i, game in enumerate(game_names):
|
|
1012
|
+
color = colors[i % len(colors)]
|
|
1013
|
+
|
|
1014
|
+
# 1. Fetch all lines and sort them chronologically
|
|
1015
|
+
try:
|
|
1016
|
+
all_lines = sorted(GameLinesTable.all(), key=lambda line: line.timestamp)
|
|
1017
|
+
except Exception as e:
|
|
1018
|
+
logger.error(f"Error fetching lines from database: {e}")
|
|
1019
|
+
return jsonify({'error': 'Failed to fetch data from database'}), 500
|
|
1020
|
+
|
|
1021
|
+
if not all_lines:
|
|
1022
|
+
return jsonify({"labels": [], "datasets": []})
|
|
815
1023
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
# start_time = time.perf_counter()
|
|
824
|
-
# for line in all_lines:
|
|
825
|
-
# day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
|
|
826
|
-
# game = line.game_name or "Unknown Game"
|
|
827
|
-
# daily_data[day_str][game]['lines'] += 1
|
|
828
|
-
# daily_data[day_str][game]['chars'] += len(line.line_text) if line.line_text else 0
|
|
829
|
-
# end_time = time.perf_counter()
|
|
830
|
-
# logger.info(f"Without Punctuation removal and daily aggregation took {end_time - start_time:.4f} seconds for {len(all_lines)} lines")
|
|
1024
|
+
# 2. Process data into daily totals for each game
|
|
1025
|
+
# Structure: daily_data[date_str][game_name] = {'lines': N, 'chars': N}
|
|
1026
|
+
daily_data = defaultdict(lambda: defaultdict(lambda: {'lines': 0, 'chars': 0}))
|
|
831
1027
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
logger.info(f"
|
|
841
|
-
wrong_instance_found = True
|
|
1028
|
+
try:
|
|
1029
|
+
# start_time = time.perf_counter()
|
|
1030
|
+
# for line in all_lines:
|
|
1031
|
+
# day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
|
|
1032
|
+
# game = line.game_name or "Unknown Game"
|
|
1033
|
+
# daily_data[day_str][game]['lines'] += 1
|
|
1034
|
+
# daily_data[day_str][game]['chars'] += len(line.line_text) if line.line_text else 0
|
|
1035
|
+
# end_time = time.perf_counter()
|
|
1036
|
+
# logger.info(f"Without Punctuation removal and daily aggregation took {end_time - start_time:.4f} seconds for {len(all_lines)} lines")
|
|
842
1037
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1038
|
+
# start_time = time.perf_counter()
|
|
1039
|
+
wrong_instance_found = False
|
|
1040
|
+
for line in all_lines:
|
|
1041
|
+
day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
|
|
1042
|
+
game = line.game_name or "Unknown Game"
|
|
1043
|
+
# Remove punctuation and symbols from line text before counting characters
|
|
1044
|
+
clean_text = punctionation_regex.sub('', str(line.line_text)) if line.line_text else ''
|
|
1045
|
+
if not isinstance(clean_text, str) and not wrong_instance_found:
|
|
1046
|
+
logger.info(f"Non-string line_text encountered: {clean_text} (type: {type(clean_text)})")
|
|
1047
|
+
wrong_instance_found = True
|
|
848
1048
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
final_data = defaultdict(lambda: defaultdict(list))
|
|
1049
|
+
line.line_text = clean_text # Update line text to cleaned version for future use
|
|
1050
|
+
daily_data[day_str][game]['lines'] += 1
|
|
1051
|
+
daily_data[day_str][game]['chars'] += len(clean_text)
|
|
1052
|
+
# end_time = time.perf_counter()
|
|
1053
|
+
# logger.info(f"With Punctuation removal and daily aggregation took {end_time - start_time:.4f} seconds for {len(all_lines)} lines")
|
|
1054
|
+
except Exception as e:
|
|
1055
|
+
logger.error(f"Error processing daily data: {e}")
|
|
1056
|
+
return jsonify({'error': 'Failed to process daily data'}), 500
|
|
858
1057
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
cumulative_totals[game]['chars'] += daily_data[day][game]['chars']
|
|
1058
|
+
# 3. Create cumulative datasets for Chart.js
|
|
1059
|
+
try:
|
|
1060
|
+
sorted_days = sorted(daily_data.keys())
|
|
1061
|
+
game_names = GameLinesTable.get_all_games_with_lines()
|
|
864
1062
|
|
|
865
|
-
#
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
"
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1063
|
+
# Keep track of the running total for each metric for each game
|
|
1064
|
+
cumulative_totals = defaultdict(lambda: {'lines': 0, 'chars': 0})
|
|
1065
|
+
|
|
1066
|
+
# Structure for final data: final_data[game_name][metric] = [day1_val, day2_val, ...]
|
|
1067
|
+
final_data = defaultdict(lambda: defaultdict(list))
|
|
1068
|
+
|
|
1069
|
+
for day in sorted_days:
|
|
1070
|
+
for game in game_names:
|
|
1071
|
+
# Add the day's total to the cumulative total
|
|
1072
|
+
cumulative_totals[game]['lines'] += daily_data[day][game]['lines']
|
|
1073
|
+
cumulative_totals[game]['chars'] += daily_data[day][game]['chars']
|
|
1074
|
+
|
|
1075
|
+
# Append the new cumulative total to the list for that day
|
|
1076
|
+
final_data[game]['lines'].append(cumulative_totals[game]['lines'])
|
|
1077
|
+
final_data[game]['chars'].append(cumulative_totals[game]['chars'])
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
logger.error(f"Error creating cumulative datasets: {e}")
|
|
1080
|
+
return jsonify({'error': 'Failed to create datasets'}), 500
|
|
1081
|
+
|
|
1082
|
+
# 4. Format into Chart.js dataset structure
|
|
1083
|
+
try:
|
|
1084
|
+
datasets = []
|
|
1085
|
+
# A simple color palette for the chart lines
|
|
1086
|
+
colors = ['#3498db', '#e74c3c', '#2ecc71', '#f1c40f', '#9b59b6', '#1abc9c', '#e67e22']
|
|
1087
|
+
|
|
1088
|
+
for i, game in enumerate(game_names):
|
|
1089
|
+
color = colors[i % len(colors)]
|
|
1090
|
+
|
|
1091
|
+
datasets.append({
|
|
1092
|
+
"label": f"{game} - Lines Received",
|
|
1093
|
+
"data": final_data[game]['lines'],
|
|
1094
|
+
"borderColor": color,
|
|
1095
|
+
"backgroundColor": f"{color}33", # Semi-transparent for fill
|
|
1096
|
+
"fill": False,
|
|
1097
|
+
"tension": 0.1,
|
|
1098
|
+
"for": "Lines Received"
|
|
1099
|
+
})
|
|
1100
|
+
datasets.append({
|
|
1101
|
+
"label": f"{game} - Characters Read",
|
|
1102
|
+
"data": final_data[game]['chars'],
|
|
1103
|
+
"borderColor": color,
|
|
1104
|
+
"backgroundColor": f"{color}33",
|
|
1105
|
+
"fill": False,
|
|
1106
|
+
"tension": 0.1,
|
|
1107
|
+
"hidden": True, # Hide by default to not clutter the chart
|
|
1108
|
+
"for": "Characters Read"
|
|
1109
|
+
})
|
|
1110
|
+
except Exception as e:
|
|
1111
|
+
logger.error(f"Error formatting Chart.js datasets: {e}")
|
|
1112
|
+
return jsonify({'error': 'Failed to format chart data'}), 500
|
|
1113
|
+
|
|
1114
|
+
# 5. Calculate additional chart data
|
|
1115
|
+
try:
|
|
1116
|
+
kanji_grid_data = calculate_kanji_frequency(all_lines)
|
|
1117
|
+
except Exception as e:
|
|
1118
|
+
logger.error(f"Error calculating kanji frequency: {e}")
|
|
1119
|
+
kanji_grid_data = []
|
|
1120
|
+
|
|
1121
|
+
try:
|
|
1122
|
+
heatmap_data = calculate_heatmap_data(all_lines, filter_year)
|
|
1123
|
+
except Exception as e:
|
|
1124
|
+
logger.error(f"Error calculating heatmap data: {e}")
|
|
1125
|
+
heatmap_data = []
|
|
1126
|
+
|
|
1127
|
+
try:
|
|
1128
|
+
total_chars_data = calculate_total_chars_per_game(all_lines)
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
logger.error(f"Error calculating total chars per game: {e}")
|
|
1131
|
+
total_chars_data = {}
|
|
1132
|
+
|
|
1133
|
+
try:
|
|
1134
|
+
reading_time_data = calculate_reading_time_per_game(all_lines)
|
|
1135
|
+
except Exception as e:
|
|
1136
|
+
logger.error(f"Error calculating reading time per game: {e}")
|
|
1137
|
+
reading_time_data = {}
|
|
1138
|
+
|
|
1139
|
+
try:
|
|
1140
|
+
reading_speed_per_game_data = calculate_reading_speed_per_game(all_lines)
|
|
1141
|
+
except Exception as e:
|
|
1142
|
+
logger.error(f"Error calculating reading speed per game: {e}")
|
|
1143
|
+
reading_speed_per_game_data = {}
|
|
1144
|
+
|
|
1145
|
+
# 6. Calculate dashboard statistics
|
|
1146
|
+
try:
|
|
1147
|
+
current_game_stats = calculate_current_game_stats(all_lines)
|
|
1148
|
+
except Exception as e:
|
|
1149
|
+
logger.error(f"Error calculating current game stats: {e}")
|
|
1150
|
+
current_game_stats = {}
|
|
1151
|
+
|
|
1152
|
+
try:
|
|
1153
|
+
all_games_stats = calculate_all_games_stats(all_lines)
|
|
1154
|
+
except Exception as e:
|
|
1155
|
+
logger.error(f"Error calculating all games stats: {e}")
|
|
1156
|
+
all_games_stats = {}
|
|
896
1157
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1158
|
+
# 7. Prepare allLinesData for frontend calculations (needed for average daily time)
|
|
1159
|
+
try:
|
|
1160
|
+
all_lines_data = []
|
|
1161
|
+
for line in all_lines:
|
|
1162
|
+
all_lines_data.append({
|
|
1163
|
+
'timestamp': float(line.timestamp),
|
|
1164
|
+
'game_name': line.game_name or 'Unknown Game',
|
|
1165
|
+
'characters': len(line.line_text) if line.line_text else 0
|
|
1166
|
+
})
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
logger.error(f"Error preparing all lines data: {e}")
|
|
1169
|
+
all_lines_data = []
|
|
907
1170
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1171
|
+
return jsonify({
|
|
1172
|
+
"labels": sorted_days,
|
|
1173
|
+
"datasets": datasets,
|
|
1174
|
+
"kanjiGridData": kanji_grid_data,
|
|
1175
|
+
"heatmapData": heatmap_data,
|
|
1176
|
+
"totalCharsPerGame": total_chars_data,
|
|
1177
|
+
"readingTimePerGame": reading_time_data,
|
|
1178
|
+
"readingSpeedPerGame": reading_speed_per_game_data,
|
|
1179
|
+
"currentGameStats": current_game_stats,
|
|
1180
|
+
"allGamesStats": all_games_stats,
|
|
1181
|
+
"allLinesData": all_lines_data
|
|
915
1182
|
})
|
|
1183
|
+
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
logger.error(f"Unexpected error in api_stats: {e}")
|
|
1186
|
+
return jsonify({'error': 'Failed to generate statistics'}), 500
|
|
1187
|
+
|
|
1188
|
+
@app.route('/api/goals-today', methods=['GET'])
|
|
1189
|
+
def api_goals_today():
|
|
1190
|
+
"""
|
|
1191
|
+
Calculate daily requirements and current progress for today based on goal target dates.
|
|
1192
|
+
Returns what needs to be accomplished today to stay on track.
|
|
1193
|
+
"""
|
|
1194
|
+
try:
|
|
1195
|
+
config = get_stats_config()
|
|
1196
|
+
today = datetime.date.today()
|
|
1197
|
+
|
|
1198
|
+
# Get all lines for overall progress
|
|
1199
|
+
all_lines = GameLinesTable.all()
|
|
1200
|
+
if not all_lines:
|
|
1201
|
+
return jsonify({
|
|
1202
|
+
'hours': {'required': 0, 'progress': 0, 'has_target': False},
|
|
1203
|
+
'characters': {'required': 0, 'progress': 0, 'has_target': False},
|
|
1204
|
+
'games': {'required': 0, 'progress': 0, 'has_target': False}
|
|
1205
|
+
}), 200
|
|
1206
|
+
|
|
1207
|
+
# Calculate overall current progress
|
|
1208
|
+
timestamps = [float(line.timestamp) for line in all_lines]
|
|
1209
|
+
total_time_seconds = calculate_actual_reading_time(timestamps)
|
|
1210
|
+
total_hours = total_time_seconds / 3600
|
|
1211
|
+
total_characters = sum(len(line.line_text) if line.line_text else 0 for line in all_lines)
|
|
1212
|
+
total_games = len(set(line.game_name or "Unknown Game" for line in all_lines))
|
|
1213
|
+
|
|
1214
|
+
# Get today's lines for progress
|
|
1215
|
+
today_lines = [line for line in all_lines
|
|
1216
|
+
if datetime.date.fromtimestamp(float(line.timestamp)) == today]
|
|
1217
|
+
|
|
1218
|
+
today_timestamps = [float(line.timestamp) for line in today_lines]
|
|
1219
|
+
today_time_seconds = calculate_actual_reading_time(today_timestamps) if len(today_timestamps) >= 2 else 0
|
|
1220
|
+
today_hours = today_time_seconds / 3600
|
|
1221
|
+
today_characters = sum(len(line.line_text) if line.line_text else 0 for line in today_lines)
|
|
1222
|
+
|
|
1223
|
+
result = {}
|
|
1224
|
+
|
|
1225
|
+
# Calculate hours requirement
|
|
1226
|
+
if config.reading_hours_target_date:
|
|
1227
|
+
try:
|
|
1228
|
+
target_date = datetime.datetime.strptime(config.reading_hours_target_date, '%Y-%m-%d').date()
|
|
1229
|
+
days_remaining = (target_date - today).days + 1 # +1 to include today
|
|
1230
|
+
if days_remaining > 0:
|
|
1231
|
+
hours_needed = max(0, config.reading_hours_target - total_hours)
|
|
1232
|
+
daily_hours_required = hours_needed / days_remaining
|
|
1233
|
+
result['hours'] = {
|
|
1234
|
+
'required': round(daily_hours_required, 2),
|
|
1235
|
+
'progress': round(today_hours, 2),
|
|
1236
|
+
'has_target': True,
|
|
1237
|
+
'target_date': config.reading_hours_target_date,
|
|
1238
|
+
'days_remaining': days_remaining
|
|
1239
|
+
}
|
|
1240
|
+
else:
|
|
1241
|
+
result['hours'] = {'required': 0, 'progress': round(today_hours, 2), 'has_target': True, 'expired': True}
|
|
1242
|
+
except ValueError:
|
|
1243
|
+
result['hours'] = {'required': 0, 'progress': round(today_hours, 2), 'has_target': False}
|
|
1244
|
+
else:
|
|
1245
|
+
result['hours'] = {'required': 0, 'progress': round(today_hours, 2), 'has_target': False}
|
|
1246
|
+
|
|
1247
|
+
# Calculate characters requirement
|
|
1248
|
+
if config.character_count_target_date:
|
|
1249
|
+
try:
|
|
1250
|
+
target_date = datetime.datetime.strptime(config.character_count_target_date, '%Y-%m-%d').date()
|
|
1251
|
+
days_remaining = (target_date - today).days + 1
|
|
1252
|
+
if days_remaining > 0:
|
|
1253
|
+
chars_needed = max(0, config.character_count_target - total_characters)
|
|
1254
|
+
daily_chars_required = int(chars_needed / days_remaining)
|
|
1255
|
+
result['characters'] = {
|
|
1256
|
+
'required': daily_chars_required,
|
|
1257
|
+
'progress': today_characters,
|
|
1258
|
+
'has_target': True,
|
|
1259
|
+
'target_date': config.character_count_target_date,
|
|
1260
|
+
'days_remaining': days_remaining
|
|
1261
|
+
}
|
|
1262
|
+
else:
|
|
1263
|
+
result['characters'] = {'required': 0, 'progress': today_characters, 'has_target': True, 'expired': True}
|
|
1264
|
+
except ValueError:
|
|
1265
|
+
result['characters'] = {'required': 0, 'progress': today_characters, 'has_target': False}
|
|
1266
|
+
else:
|
|
1267
|
+
result['characters'] = {'required': 0, 'progress': today_characters, 'has_target': False}
|
|
1268
|
+
|
|
1269
|
+
# Calculate games requirement
|
|
1270
|
+
if config.games_target_date:
|
|
1271
|
+
try:
|
|
1272
|
+
target_date = datetime.datetime.strptime(config.games_target_date, '%Y-%m-%d').date()
|
|
1273
|
+
days_remaining = (target_date - today).days + 1
|
|
1274
|
+
if days_remaining > 0:
|
|
1275
|
+
games_needed = max(0, config.games_target - total_games)
|
|
1276
|
+
daily_games_required = games_needed / days_remaining
|
|
1277
|
+
result['games'] = {
|
|
1278
|
+
'required': round(daily_games_required, 2),
|
|
1279
|
+
'progress': total_games,
|
|
1280
|
+
'has_target': True,
|
|
1281
|
+
'target_date': config.games_target_date,
|
|
1282
|
+
'days_remaining': days_remaining
|
|
1283
|
+
}
|
|
1284
|
+
else:
|
|
1285
|
+
result['games'] = {'required': 0, 'progress': total_games, 'has_target': True, 'expired': True}
|
|
1286
|
+
except ValueError:
|
|
1287
|
+
result['games'] = {'required': 0, 'progress': total_games, 'has_target': False}
|
|
1288
|
+
else:
|
|
1289
|
+
result['games'] = {'required': 0, 'progress': total_games, 'has_target': False}
|
|
1290
|
+
|
|
1291
|
+
return jsonify(result), 200
|
|
1292
|
+
|
|
1293
|
+
except Exception as e:
|
|
1294
|
+
logger.error(f"Error calculating goals today: {e}")
|
|
1295
|
+
return jsonify({'error': 'Failed to calculate daily goals'}), 500
|
|
916
1296
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1297
|
+
@app.route('/api/goals-projection', methods=['GET'])
|
|
1298
|
+
def api_goals_projection():
|
|
1299
|
+
"""
|
|
1300
|
+
Calculate projections based on 30-day rolling average.
|
|
1301
|
+
Returns projected stats by target dates.
|
|
1302
|
+
"""
|
|
1303
|
+
try:
|
|
1304
|
+
config = get_stats_config()
|
|
1305
|
+
today = datetime.date.today()
|
|
1306
|
+
thirty_days_ago = today - datetime.timedelta(days=30)
|
|
1307
|
+
|
|
1308
|
+
# Get all lines
|
|
1309
|
+
all_lines = GameLinesTable.all()
|
|
1310
|
+
if not all_lines:
|
|
1311
|
+
return jsonify({
|
|
1312
|
+
'hours': {'projection': 0, 'daily_average': 0},
|
|
1313
|
+
'characters': {'projection': 0, 'daily_average': 0},
|
|
1314
|
+
'games': {'projection': 0, 'daily_average': 0}
|
|
1315
|
+
}), 200
|
|
1316
|
+
|
|
1317
|
+
# Get last 30 days of lines
|
|
1318
|
+
recent_lines = [line for line in all_lines
|
|
1319
|
+
if datetime.date.fromtimestamp(float(line.timestamp)) >= thirty_days_ago]
|
|
1320
|
+
|
|
1321
|
+
# Calculate 30-day averages
|
|
1322
|
+
if recent_lines:
|
|
1323
|
+
# Group by day for accurate averaging
|
|
1324
|
+
daily_data = defaultdict(lambda: {'timestamps': [], 'characters': 0, 'games': set()})
|
|
1325
|
+
for line in recent_lines:
|
|
1326
|
+
day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
|
|
1327
|
+
daily_data[day_str]['timestamps'].append(float(line.timestamp))
|
|
1328
|
+
daily_data[day_str]['characters'] += len(line.line_text) if line.line_text else 0
|
|
1329
|
+
daily_data[day_str]['games'].add(line.game_name or "Unknown Game")
|
|
1330
|
+
|
|
1331
|
+
# Calculate daily averages
|
|
1332
|
+
total_hours = 0
|
|
1333
|
+
total_chars = 0
|
|
1334
|
+
total_unique_games = set()
|
|
1335
|
+
|
|
1336
|
+
for day_data in daily_data.values():
|
|
1337
|
+
if len(day_data['timestamps']) >= 2:
|
|
1338
|
+
day_seconds = calculate_actual_reading_time(day_data['timestamps'])
|
|
1339
|
+
total_hours += day_seconds / 3600
|
|
1340
|
+
total_chars += day_data['characters']
|
|
1341
|
+
total_unique_games.update(day_data['games'])
|
|
1342
|
+
|
|
1343
|
+
# Average over ALL 30 days (including days with 0 activity)
|
|
1344
|
+
avg_daily_hours = total_hours / 30
|
|
1345
|
+
avg_daily_chars = total_chars / 30
|
|
1346
|
+
# Calculate average daily unique games correctly
|
|
1347
|
+
today = datetime.date.today()
|
|
1348
|
+
daily_unique_games_counts = []
|
|
1349
|
+
for i in range(30):
|
|
1350
|
+
day = (today - datetime.timedelta(days=i)).strftime('%Y-%m-%d')
|
|
1351
|
+
daily_unique_games_counts.append(len(daily_data[day]['games']) if day in daily_data else 0)
|
|
1352
|
+
avg_daily_games = sum(daily_unique_games_counts) / 30
|
|
1353
|
+
else:
|
|
1354
|
+
avg_daily_hours = 0
|
|
1355
|
+
avg_daily_chars = 0
|
|
1356
|
+
avg_daily_games = 0
|
|
1357
|
+
|
|
1358
|
+
# Calculate current totals
|
|
1359
|
+
timestamps = [float(line.timestamp) for line in all_lines]
|
|
1360
|
+
current_hours = calculate_actual_reading_time(timestamps) / 3600
|
|
1361
|
+
current_chars = sum(len(line.line_text) if line.line_text else 0 for line in all_lines)
|
|
1362
|
+
current_games = len(set(line.game_name or "Unknown Game" for line in all_lines))
|
|
1363
|
+
|
|
1364
|
+
result = {}
|
|
1365
|
+
|
|
1366
|
+
# Project hours by target date
|
|
1367
|
+
if config.reading_hours_target_date:
|
|
1368
|
+
try:
|
|
1369
|
+
target_date = datetime.datetime.strptime(config.reading_hours_target_date, '%Y-%m-%d').date()
|
|
1370
|
+
days_until_target = (target_date - today).days
|
|
1371
|
+
projected_hours = current_hours + (avg_daily_hours * days_until_target)
|
|
1372
|
+
result['hours'] = {
|
|
1373
|
+
'projection': round(projected_hours, 2),
|
|
1374
|
+
'daily_average': round(avg_daily_hours, 2),
|
|
1375
|
+
'target_date': config.reading_hours_target_date,
|
|
1376
|
+
'target': config.reading_hours_target,
|
|
1377
|
+
'current': round(current_hours, 2)
|
|
1378
|
+
}
|
|
1379
|
+
except ValueError:
|
|
1380
|
+
result['hours'] = {'projection': 0, 'daily_average': round(avg_daily_hours, 2)}
|
|
1381
|
+
else:
|
|
1382
|
+
result['hours'] = {'projection': 0, 'daily_average': round(avg_daily_hours, 2)}
|
|
1383
|
+
|
|
1384
|
+
# Project characters by target date
|
|
1385
|
+
if config.character_count_target_date:
|
|
1386
|
+
try:
|
|
1387
|
+
target_date = datetime.datetime.strptime(config.character_count_target_date, '%Y-%m-%d').date()
|
|
1388
|
+
days_until_target = (target_date - today).days
|
|
1389
|
+
projected_chars = int(current_chars + (avg_daily_chars * days_until_target))
|
|
1390
|
+
result['characters'] = {
|
|
1391
|
+
'projection': projected_chars,
|
|
1392
|
+
'daily_average': int(avg_daily_chars),
|
|
1393
|
+
'target_date': config.character_count_target_date,
|
|
1394
|
+
'target': config.character_count_target,
|
|
1395
|
+
'current': current_chars
|
|
1396
|
+
}
|
|
1397
|
+
except ValueError:
|
|
1398
|
+
result['characters'] = {'projection': 0, 'daily_average': int(avg_daily_chars)}
|
|
1399
|
+
else:
|
|
1400
|
+
result['characters'] = {'projection': 0, 'daily_average': int(avg_daily_chars)}
|
|
1401
|
+
|
|
1402
|
+
# Project games by target date
|
|
1403
|
+
if config.games_target_date:
|
|
1404
|
+
try:
|
|
1405
|
+
target_date = datetime.datetime.strptime(config.games_target_date, '%Y-%m-%d').date()
|
|
1406
|
+
days_until_target = (target_date - today).days
|
|
1407
|
+
projected_games = int(current_games + (avg_daily_games * days_until_target))
|
|
1408
|
+
result['games'] = {
|
|
1409
|
+
'projection': projected_games,
|
|
1410
|
+
'daily_average': round(avg_daily_games, 2),
|
|
1411
|
+
'target_date': config.games_target_date,
|
|
1412
|
+
'target': config.games_target,
|
|
1413
|
+
'current': current_games
|
|
1414
|
+
}
|
|
1415
|
+
except ValueError:
|
|
1416
|
+
result['games'] = {'projection': 0, 'daily_average': round(avg_daily_games, 2)}
|
|
1417
|
+
else:
|
|
1418
|
+
result['games'] = {'projection': 0, 'daily_average': round(avg_daily_games, 2)}
|
|
1419
|
+
|
|
1420
|
+
return jsonify(result), 200
|
|
1421
|
+
|
|
1422
|
+
except Exception as e:
|
|
1423
|
+
logger.error(f"Error calculating goal projections: {e}")
|
|
1424
|
+
return jsonify({'error': 'Failed to calculate projections'}), 500
|
|
929
1425
|
|
|
930
1426
|
@app.route('/api/import-exstatic', methods=['POST'])
|
|
931
1427
|
def api_import_exstatic():
|
|
@@ -1111,4 +1607,127 @@ def register_database_api_routes(app):
|
|
|
1111
1607
|
|
|
1112
1608
|
except Exception as e:
|
|
1113
1609
|
logger.error(f"Error in ExStatic import: {e}")
|
|
1114
|
-
return jsonify({'error': f'Import failed: {str(e)}'}), 500
|
|
1610
|
+
return jsonify({'error': f'Import failed: {str(e)}'}), 500
|
|
1611
|
+
|
|
1612
|
+
@app.route('/api/kanji-sorting-configs', methods=['GET'])
|
|
1613
|
+
def api_kanji_sorting_configs():
|
|
1614
|
+
"""
|
|
1615
|
+
List available kanji sorting configuration JSON files.
|
|
1616
|
+
Returns metadata for each available sorting option.
|
|
1617
|
+
"""
|
|
1618
|
+
try:
|
|
1619
|
+
# Get the kanji_grid directory path
|
|
1620
|
+
template_dir = Path(__file__).parent / 'templates' / 'components' / 'kanji_grid'
|
|
1621
|
+
|
|
1622
|
+
if not template_dir.exists():
|
|
1623
|
+
logger.warning(f"Kanji grid directory does not exist: {template_dir}")
|
|
1624
|
+
return jsonify({'configs': []}), 200
|
|
1625
|
+
|
|
1626
|
+
configs = []
|
|
1627
|
+
|
|
1628
|
+
# Scan for JSON files in the directory
|
|
1629
|
+
for json_file in template_dir.glob('*.json'):
|
|
1630
|
+
try:
|
|
1631
|
+
with open(json_file, 'r', encoding='utf-8') as f:
|
|
1632
|
+
data = json.load(f)
|
|
1633
|
+
|
|
1634
|
+
# Extract metadata from JSON
|
|
1635
|
+
configs.append({
|
|
1636
|
+
'filename': json_file.name,
|
|
1637
|
+
'name': data.get('name', json_file.stem),
|
|
1638
|
+
'version': data.get('version', 1),
|
|
1639
|
+
'lang': data.get('lang', 'ja'),
|
|
1640
|
+
'source': data.get('source', ''),
|
|
1641
|
+
'group_count': len(data.get('groups', []))
|
|
1642
|
+
})
|
|
1643
|
+
except json.JSONDecodeError as e:
|
|
1644
|
+
logger.warning(f"Failed to parse {json_file.name}: {e}")
|
|
1645
|
+
continue
|
|
1646
|
+
except Exception as e:
|
|
1647
|
+
logger.warning(f"Error reading {json_file.name}: {e}")
|
|
1648
|
+
continue
|
|
1649
|
+
|
|
1650
|
+
# Sort by name for consistency
|
|
1651
|
+
configs.sort(key=lambda x: x['name'])
|
|
1652
|
+
|
|
1653
|
+
return jsonify({'configs': configs}), 200
|
|
1654
|
+
|
|
1655
|
+
except Exception as e:
|
|
1656
|
+
logger.error(f"Error fetching kanji sorting configs: {e}")
|
|
1657
|
+
return jsonify({'error': 'Failed to fetch sorting configurations'}), 500
|
|
1658
|
+
|
|
1659
|
+
@app.route('/api/kanji-sorting-config/<filename>', methods=['GET'])
|
|
1660
|
+
def api_kanji_sorting_config(filename):
|
|
1661
|
+
"""
|
|
1662
|
+
Get a specific kanji sorting configuration file.
|
|
1663
|
+
Returns the full JSON configuration.
|
|
1664
|
+
"""
|
|
1665
|
+
try:
|
|
1666
|
+
# Sanitize filename to prevent path traversal
|
|
1667
|
+
if '..' in filename or '/' in filename or '\\' in filename:
|
|
1668
|
+
return jsonify({'error': 'Invalid filename'}), 400
|
|
1669
|
+
|
|
1670
|
+
if not filename.endswith('.json'):
|
|
1671
|
+
filename += '.json'
|
|
1672
|
+
|
|
1673
|
+
# Get the kanji_grid directory path
|
|
1674
|
+
template_dir = Path(__file__).parent / 'templates' / 'components' / 'kanji_grid'
|
|
1675
|
+
config_file = template_dir / filename
|
|
1676
|
+
|
|
1677
|
+
if not config_file.exists() or not config_file.is_file():
|
|
1678
|
+
return jsonify({'error': 'Configuration file not found'}), 404
|
|
1679
|
+
|
|
1680
|
+
# Read and return the JSON configuration
|
|
1681
|
+
with open(config_file, 'r', encoding='utf-8') as f:
|
|
1682
|
+
config_data = json.load(f)
|
|
1683
|
+
|
|
1684
|
+
return jsonify(config_data), 200
|
|
1685
|
+
|
|
1686
|
+
except json.JSONDecodeError as e:
|
|
1687
|
+
logger.error(f"Failed to parse {filename}: {e}")
|
|
1688
|
+
return jsonify({'error': 'Invalid JSON configuration'}), 500
|
|
1689
|
+
except Exception as e:
|
|
1690
|
+
logger.error(f"Error fetching config {filename}: {e}")
|
|
1691
|
+
return jsonify({'error': 'Failed to fetch configuration'}), 500
|
|
1692
|
+
|
|
1693
|
+
@app.route('/api/debug-db', methods=['GET'])
|
|
1694
|
+
def api_debug_db():
|
|
1695
|
+
"""Debug endpoint to check database structure and content."""
|
|
1696
|
+
try:
|
|
1697
|
+
# Check table structure
|
|
1698
|
+
columns_info = GameLinesTable._db.fetchall("PRAGMA table_info(game_lines)")
|
|
1699
|
+
table_structure = [{'name': col[1], 'type': col[2], 'notnull': col[3], 'default': col[4]} for col in columns_info]
|
|
1700
|
+
|
|
1701
|
+
# Check if we have any data
|
|
1702
|
+
count_result = GameLinesTable._db.fetchone("SELECT COUNT(*) FROM game_lines")
|
|
1703
|
+
total_count = count_result[0] if count_result else 0
|
|
1704
|
+
|
|
1705
|
+
# Try to get a sample record
|
|
1706
|
+
sample_record = None
|
|
1707
|
+
if total_count > 0:
|
|
1708
|
+
sample_row = GameLinesTable._db.fetchone("SELECT * FROM game_lines LIMIT 1")
|
|
1709
|
+
if sample_row:
|
|
1710
|
+
sample_record = {
|
|
1711
|
+
'row_length': len(sample_row),
|
|
1712
|
+
'sample_data': sample_row[:5] if len(sample_row) > 5 else sample_row # First 5 columns only
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
# Test the model
|
|
1716
|
+
model_info = {
|
|
1717
|
+
'fields_count': len(GameLinesTable._fields),
|
|
1718
|
+
'types_count': len(GameLinesTable._types),
|
|
1719
|
+
'fields': GameLinesTable._fields,
|
|
1720
|
+
'types': [str(t) for t in GameLinesTable._types]
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
return jsonify({
|
|
1724
|
+
'table_structure': table_structure,
|
|
1725
|
+
'total_records': total_count,
|
|
1726
|
+
'sample_record': sample_record,
|
|
1727
|
+
'model_info': model_info
|
|
1728
|
+
}), 200
|
|
1729
|
+
|
|
1730
|
+
except Exception as e:
|
|
1731
|
+
logger.error(f"Error in debug endpoint: {e}")
|
|
1732
|
+
return jsonify({'error': f'Debug failed: {str(e)}'}), 500
|
|
1733
|
+
|