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,188 @@
1
+ """
2
+ Jiten.moe API Client
3
+
4
+ Centralized client for interacting with the jiten.moe API.
5
+ Provides both search and detail endpoints with consistent error handling and logging.
6
+ """
7
+
8
+ import time
9
+ import base64
10
+ from typing import Optional, Dict, List
11
+ import requests
12
+
13
+ from GameSentenceMiner.util.configuration import logger
14
+
15
+
16
+ class JitenApiClient:
17
+ """
18
+ Centralized client for jiten.moe API interactions.
19
+
20
+ Provides methods for:
21
+ - Searching media decks by title
22
+ - Getting detailed deck information by deck_id
23
+ - Downloading and encoding cover images
24
+ """
25
+
26
+ BASE_URL = "https://api.jiten.moe/api/media-deck"
27
+ TIMEOUT = 10
28
+
29
+ @classmethod
30
+ def search_media_decks(
31
+ cls,
32
+ title_filter: str,
33
+ sort_by: str = "title",
34
+ sort_order: int = 0,
35
+ offset: int = 0,
36
+ ) -> Optional[Dict]:
37
+ """
38
+ Search jiten.moe media decks by title.
39
+
40
+ Args:
41
+ title_filter: Title to search for
42
+ sort_by: Sort field (default: 'title')
43
+ sort_order: Sort order (default: 0)
44
+ offset: Pagination offset (default: 0)
45
+
46
+ Returns:
47
+ Dictionary with search results, or None if request fails
48
+ """
49
+ try:
50
+ url = f"{cls.BASE_URL}/get-media-decks"
51
+ params = {
52
+ "titleFilter": title_filter,
53
+ "sortBy": sort_by,
54
+ "sortOrder": sort_order,
55
+ "offset": offset,
56
+ }
57
+
58
+ logger.debug(f"Searching jiten.moe for title: {title_filter}")
59
+ response = requests.get(url, params=params, timeout=cls.TIMEOUT)
60
+
61
+ if response.status_code != 200:
62
+ logger.debug(f"Jiten search API returned status {response.status_code}")
63
+ return None
64
+
65
+ data = response.json()
66
+ logger.debug(f"Jiten search returned {len(data.get('data', []))} results")
67
+ return data
68
+
69
+ except requests.RequestException as e:
70
+ logger.debug(f"Jiten search API request failed: {e}")
71
+ return None
72
+ except Exception as e:
73
+ logger.debug(f"Unexpected error in jiten search: {e}")
74
+ return None
75
+
76
+ @classmethod
77
+ def get_deck_detail(cls, deck_id: int, offset: int = 0) -> Optional[Dict]:
78
+ """
79
+ Get detailed information for a specific deck by deck_id.
80
+
81
+ Args:
82
+ deck_id: The jiten.moe deck ID
83
+ offset: Pagination offset (default: 0)
84
+
85
+ Returns:
86
+ Dictionary with deck details, or None if request fails
87
+ """
88
+ try:
89
+ url = f"{cls.BASE_URL}/{deck_id}/detail"
90
+ params = {"offset": offset}
91
+
92
+ logger.debug(f"Fetching jiten.moe deck detail for deck_id: {deck_id}")
93
+ response = requests.get(url, params=params, timeout=cls.TIMEOUT)
94
+
95
+ if response.status_code != 200:
96
+ logger.debug(
97
+ f"Jiten detail API returned status {response.status_code} for deck {deck_id}"
98
+ )
99
+ return None
100
+
101
+ data = response.json()
102
+ logger.debug(f"Successfully fetched deck detail for deck_id: {deck_id}")
103
+ return data
104
+
105
+ except requests.RequestException as e:
106
+ logger.debug(f"Jiten detail API request failed for deck {deck_id}: {e}")
107
+ return None
108
+ except Exception as e:
109
+ logger.debug(
110
+ f"Unexpected error fetching deck detail for deck {deck_id}: {e}"
111
+ )
112
+ return None
113
+
114
+ @classmethod
115
+ def normalize_deck_data(cls, deck_data: Dict) -> Dict:
116
+ """
117
+ Normalize deck data from jiten.moe API response to consistent format.
118
+
119
+ Args:
120
+ deck_data: Raw deck data from API
121
+
122
+ Returns:
123
+ Normalized deck data with snake_case keys
124
+ """
125
+ # Map media type integer to human-readable string
126
+ media_type_raw = deck_data.get("mediaType")
127
+ media_type_map = {
128
+ 1: "Anime",
129
+ 2: "Manga",
130
+ 3: "Light Novel",
131
+ 4: "Web Novel",
132
+ 5: "Book",
133
+ 6: "Game",
134
+ 7: "Visual Novel"
135
+ }
136
+ media_type_string = media_type_map.get(media_type_raw, f"Type {media_type_raw}" if media_type_raw else "")
137
+
138
+ return {
139
+ "deck_id": deck_data.get("deckId"),
140
+ "title_original": deck_data.get("originalTitle", ""),
141
+ "title_romaji": deck_data.get("romajiTitle", ""),
142
+ "title_english": deck_data.get("englishTitle", ""),
143
+ "description": deck_data.get("description", ""),
144
+ "cover_name": deck_data.get("coverName", ""),
145
+ "media_type": media_type_raw, # Keep raw integer for backend processing
146
+ "media_type_string": media_type_string, # Add human-readable string
147
+ "character_count": deck_data.get("characterCount", 0),
148
+ "difficulty": deck_data.get("difficulty", 0),
149
+ "difficulty_raw": deck_data.get("difficultyRaw", 0),
150
+ "links": deck_data.get("links", []),
151
+ "aliases": deck_data.get("aliases", []),
152
+ "release_date": deck_data.get("releaseDate", ""),
153
+ }
154
+
155
+ @classmethod
156
+ def download_cover_image(cls, cover_url: str) -> Optional[str]:
157
+ """
158
+ Download and encode cover image from jiten.moe.
159
+
160
+ Args:
161
+ cover_url: URL of the cover image
162
+
163
+ Returns:
164
+ Base64 encoded image with data URI prefix, or None if download fails
165
+
166
+ Note:
167
+ Jiten.moe guarantees JPG format, so we assume image/jpeg MIME type.
168
+ """
169
+ try:
170
+ logger.debug(f"Downloading cover image: {cover_url}")
171
+ response = requests.get(cover_url, timeout=cls.TIMEOUT)
172
+
173
+ if response.status_code != 200:
174
+ logger.debug(
175
+ f"Failed to download cover image: HTTP {response.status_code}"
176
+ )
177
+ return None
178
+
179
+ # Encode to base64 - jiten.moe guarantees JPG format
180
+ img_base64 = base64.b64encode(response.content).decode("utf-8")
181
+ data_uri = f"data:image/jpeg;base64,{img_base64}"
182
+
183
+ logger.debug(f"Successfully downloaded and encoded cover image")
184
+ return data_uri
185
+
186
+ except Exception as e:
187
+ logger.debug(f"Failed to download cover image: {e}")
188
+ return None
@@ -0,0 +1,216 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ import time
4
+
5
+ from GameSentenceMiner.util.db import SQLiteDBTable
6
+
7
+
8
+ class StatsRollupTable(SQLiteDBTable):
9
+ _table = "daily_stats_rollup"
10
+ _fields = [
11
+ "date",
12
+ "total_lines",
13
+ "total_characters",
14
+ "total_sessions",
15
+ "unique_games_played",
16
+ "total_reading_time_seconds",
17
+ "total_active_time_seconds",
18
+ "longest_session_seconds",
19
+ "shortest_session_seconds",
20
+ "average_session_seconds",
21
+ "average_reading_speed_chars_per_hour",
22
+ "peak_reading_speed_chars_per_hour",
23
+ "games_completed",
24
+ "games_started",
25
+ "anki_cards_created",
26
+ "lines_with_screenshots",
27
+ "lines_with_audio",
28
+ "lines_with_translations",
29
+ "unique_kanji_seen",
30
+ "kanji_frequency_data",
31
+ "hourly_activity_data",
32
+ "hourly_reading_speed_data",
33
+ "game_activity_data",
34
+ "games_played_ids",
35
+ "max_chars_in_session",
36
+ "max_time_in_session_seconds",
37
+ "created_at",
38
+ "updated_at",
39
+ ]
40
+ _types = [
41
+ int, # id (primary key)
42
+ str, # date
43
+ int,
44
+ int,
45
+ int,
46
+ int, # basic counts: total_lines, total_characters, total_sessions, unique_games_played
47
+ float,
48
+ float, # time tracking: total_reading_time_seconds, total_active_time_seconds
49
+ float,
50
+ float,
51
+ float, # session stats: longest_session_seconds, shortest_session_seconds, average_session_seconds
52
+ float,
53
+ float, # reading performance: average_reading_speed_chars_per_hour, peak_reading_speed_chars_per_hour
54
+ int,
55
+ int,
56
+ int, # game progress: games_completed, games_started, anki_cards_created
57
+ int,
58
+ int,
59
+ int, # anki integration: lines_with_screenshots, lines_with_audio, lines_with_translations
60
+ int,
61
+ str, # kanji stats: unique_kanji_seen, kanji_frequency_data (JSON)
62
+ str,
63
+ str,
64
+ str, # JSON data fields: hourly_activity_data, hourly_reading_speed_data, game_activity_data
65
+ str, # games_played_ids (JSON)
66
+ int,
67
+ float, # peak performance: max_chars_in_session, max_time_in_session_seconds
68
+ float,
69
+ float, # metadata: created_at, updated_at
70
+ ]
71
+ _pk = "id"
72
+ _auto_increment = True
73
+
74
+ def __init__(
75
+ self,
76
+ id: Optional[int] = None,
77
+ date: Optional[str] = None,
78
+ total_lines: int = 0,
79
+ total_characters: int = 0,
80
+ total_sessions: int = 0,
81
+ unique_games_played: int = 0,
82
+ total_reading_time_seconds: float = 0.0,
83
+ total_active_time_seconds: float = 0.0,
84
+ longest_session_seconds: float = 0.0,
85
+ shortest_session_seconds: float = 0.0,
86
+ average_session_seconds: float = 0.0,
87
+ average_reading_speed_chars_per_hour: float = 0.0,
88
+ peak_reading_speed_chars_per_hour: float = 0.0,
89
+ games_completed: int = 0,
90
+ games_started: int = 0,
91
+ anki_cards_created: int = 0,
92
+ lines_with_screenshots: int = 0,
93
+ lines_with_audio: int = 0,
94
+ lines_with_translations: int = 0,
95
+ unique_kanji_seen: int = 0,
96
+ kanji_frequency_data: Optional[str] = None,
97
+ hourly_activity_data: Optional[str] = None,
98
+ hourly_reading_speed_data: Optional[str] = None,
99
+ game_activity_data: Optional[str] = None,
100
+ games_played_ids: Optional[str] = None,
101
+ max_chars_in_session: int = 0,
102
+ max_time_in_session_seconds: float = 0.0,
103
+ created_at: Optional[float] = None,
104
+ updated_at: Optional[float] = None,
105
+ ):
106
+ self.id = id
107
+ self.date = date if date is not None else datetime.now().strftime("%Y-%m-%d")
108
+ self.total_lines = total_lines
109
+ self.total_characters = total_characters
110
+ self.total_sessions = total_sessions
111
+ self.unique_games_played = unique_games_played
112
+ self.total_reading_time_seconds = total_reading_time_seconds
113
+ self.total_active_time_seconds = total_active_time_seconds
114
+ self.longest_session_seconds = longest_session_seconds
115
+ self.shortest_session_seconds = shortest_session_seconds
116
+ self.average_session_seconds = average_session_seconds
117
+ self.average_reading_speed_chars_per_hour = average_reading_speed_chars_per_hour
118
+ self.peak_reading_speed_chars_per_hour = peak_reading_speed_chars_per_hour
119
+ self.games_completed = games_completed
120
+ self.games_started = games_started
121
+ self.anki_cards_created = anki_cards_created
122
+ self.lines_with_screenshots = lines_with_screenshots
123
+ self.lines_with_audio = lines_with_audio
124
+ self.lines_with_translations = lines_with_translations
125
+ self.unique_kanji_seen = unique_kanji_seen
126
+ self.kanji_frequency_data = (
127
+ kanji_frequency_data if kanji_frequency_data is not None else "{}"
128
+ )
129
+ self.hourly_activity_data = (
130
+ hourly_activity_data if hourly_activity_data is not None else "{}"
131
+ )
132
+ self.hourly_reading_speed_data = (
133
+ hourly_reading_speed_data if hourly_reading_speed_data is not None else "{}"
134
+ )
135
+ self.game_activity_data = (
136
+ game_activity_data if game_activity_data is not None else "{}"
137
+ )
138
+ self.games_played_ids = (
139
+ games_played_ids if games_played_ids is not None else "[]"
140
+ )
141
+ self.max_chars_in_session = max_chars_in_session
142
+ self.max_time_in_session_seconds = max_time_in_session_seconds
143
+ self.created_at = created_at if created_at is not None else time.time()
144
+ self.updated_at = updated_at if updated_at is not None else time.time()
145
+
146
+ @classmethod
147
+ def get_stats_for_date(cls, date: str) -> Optional["StatsRollupTable"]:
148
+ """Get rollup statistics for a specific date."""
149
+ row = cls._db.fetchone(f"SELECT * FROM {cls._table} WHERE date=?", (date,))
150
+ return cls.from_row(row) if row else None
151
+
152
+ @classmethod
153
+ def get_by_date(cls, date: str) -> Optional["StatsRollupTable"]:
154
+ """Get rollup statistics for a specific date (alias for get_stats_for_date)."""
155
+ return cls.get_stats_for_date(date)
156
+
157
+ @classmethod
158
+ def date_exists(cls, date: str) -> bool:
159
+ """Check if a rollup exists for a specific date."""
160
+ row = cls._db.fetchone(f"SELECT id FROM {cls._table} WHERE date=?", (date,))
161
+ return row is not None
162
+
163
+ @classmethod
164
+ def get_date_range(cls, start_date: str, end_date: str) -> list["StatsRollupTable"]:
165
+ """Get rollup statistics for a date range (inclusive)."""
166
+ rows = cls._db.fetchall(
167
+ f"SELECT * FROM {cls._table} WHERE date >= ? AND date <= ? ORDER BY date ASC",
168
+ (start_date, end_date),
169
+ )
170
+ return [cls.from_row(row) for row in rows]
171
+
172
+ @classmethod
173
+ def get_first_date(cls) -> Optional[str]:
174
+ """Get the earliest date with rollup data."""
175
+ from GameSentenceMiner.util.configuration import logger
176
+ row = cls._db.fetchone(
177
+ f"SELECT date FROM {cls._table} ORDER BY date ASC LIMIT 1"
178
+ )
179
+ result = row[0] if row else None
180
+ return result
181
+
182
+ @classmethod
183
+ def get_last_date(cls) -> Optional[str]:
184
+ """Get the most recent date with rollup data."""
185
+ row = cls._db.fetchone(
186
+ f"SELECT date FROM {cls._table} ORDER BY date DESC LIMIT 1"
187
+ )
188
+ return row[0] if row else None
189
+
190
+ @classmethod
191
+ def update_stats(
192
+ cls,
193
+ date: str,
194
+ games_played: int = 0,
195
+ lines_mined: int = 0,
196
+ anki_cards_created: int = 0,
197
+ time_spent_mining: float = 0.0,
198
+ ):
199
+ """Legacy method for backward compatibility - updates basic stats only."""
200
+ stats = cls.get_stats_for_date(date)
201
+ if not stats:
202
+ new_stats = cls(
203
+ date=date,
204
+ unique_games_played=games_played,
205
+ total_lines=lines_mined,
206
+ anki_cards_created=anki_cards_created,
207
+ total_reading_time_seconds=time_spent_mining,
208
+ )
209
+ new_stats.save()
210
+ return
211
+ stats.unique_games_played += games_played
212
+ stats.total_lines += lines_mined
213
+ stats.anki_cards_created += anki_cards_created
214
+ stats.total_reading_time_seconds += time_spent_mining
215
+ stats.updated_at = time.time()
216
+ stats.save()