GameSentenceMiner 2.6.5__py3-none-any.whl → 2.7.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.
- GameSentenceMiner/anki.py +2 -2
- GameSentenceMiner/config_gui.py +7 -0
- GameSentenceMiner/configuration.py +4 -1
- GameSentenceMiner/gametext.py +28 -14
- GameSentenceMiner/gsm.py +8 -4
- GameSentenceMiner/obs.py +41 -8
- GameSentenceMiner/ocr/__init__.py +0 -0
- GameSentenceMiner/ocr/ocrconfig.py +130 -0
- GameSentenceMiner/ocr/owocr_area_selector.py +275 -0
- GameSentenceMiner/ocr/owocr_helper.py +309 -0
- GameSentenceMiner/owocr/owocr/__init__.py +1 -0
- GameSentenceMiner/owocr/owocr/__main__.py +8 -0
- GameSentenceMiner/owocr/owocr/config.py +134 -0
- GameSentenceMiner/owocr/owocr/lens_betterproto.py +1238 -0
- GameSentenceMiner/owocr/owocr/ocr.py +975 -0
- GameSentenceMiner/owocr/owocr/run.py +1096 -0
- GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +100 -0
- {gamesentenceminer-2.6.5.dist-info → gamesentenceminer-2.7.1.dist-info}/METADATA +8 -2
- gamesentenceminer-2.7.1.dist-info/RECORD +42 -0
- gamesentenceminer-2.6.5.dist-info/RECORD +0 -31
- {gamesentenceminer-2.6.5.dist-info → gamesentenceminer-2.7.1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.6.5.dist-info → gamesentenceminer-2.7.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.6.5.dist-info → gamesentenceminer-2.7.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.6.5.dist-info → gamesentenceminer-2.7.1.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -7,7 +7,7 @@ import urllib.request
|
|
7
7
|
from datetime import datetime, timedelta
|
8
8
|
from requests import post
|
9
9
|
|
10
|
-
from GameSentenceMiner import obs, util, notification, ffmpeg
|
10
|
+
from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
|
11
11
|
from GameSentenceMiner.ai.gemini import translate_with_context
|
12
12
|
from GameSentenceMiner.configuration import *
|
13
13
|
from GameSentenceMiner.configuration import get_config
|
@@ -279,7 +279,7 @@ def update_new_card():
|
|
279
279
|
if use_prev_audio:
|
280
280
|
lines = get_utility_window().get_selected_lines()
|
281
281
|
with util.lock:
|
282
|
-
update_anki_card(last_card, note=get_initial_card_info(last_card, lines), reuse_audio=True)
|
282
|
+
update_anki_card(last_card, note=get_initial_card_info(last_card, lines), game_line=gametext.get_mined_line(last_card, lines), reuse_audio=True)
|
283
283
|
get_utility_window().reset_checkboxes()
|
284
284
|
else:
|
285
285
|
logger.info("New card(s) detected! Added to Processing Queue!")
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -201,6 +201,7 @@ class ConfigApp:
|
|
201
201
|
show_screenshot_buttons=self.show_screenshot_button.get(),
|
202
202
|
multi_line_line_break=self.multi_line_line_break.get(),
|
203
203
|
multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
|
204
|
+
ocr_sends_to_clipboard=self.ocr_sends_to_clipboard.get(),
|
204
205
|
),
|
205
206
|
ai=Ai(
|
206
207
|
enabled=self.ai_enabled.get(),
|
@@ -970,6 +971,12 @@ class ConfigApp:
|
|
970
971
|
self.multi_line_sentence_storage_field.grid(row=self.current_row, column=1)
|
971
972
|
self.add_label_and_increment_row(advanced_frame, "Field in Anki for storing the multi-line sentence temporarily.", row=self.current_row, column=2)
|
972
973
|
|
974
|
+
ttk.Label(advanced_frame, text="OCR Sends to Clipboard:").grid(row=self.current_row, column=0, sticky='W')
|
975
|
+
self.ocr_sends_to_clipboard = tk.BooleanVar(value=self.settings.advanced.ocr_sends_to_clipboard)
|
976
|
+
ttk.Checkbutton(advanced_frame, variable=self.ocr_sends_to_clipboard).grid(row=self.current_row, column=1, sticky='W')
|
977
|
+
self.add_label_and_increment_row(advanced_frame, "Enable to send OCR results to clipboard.", row=self.current_row, column=2)
|
978
|
+
|
979
|
+
|
973
980
|
|
974
981
|
@new_tab
|
975
982
|
def create_ai_tab(self):
|
@@ -177,6 +177,7 @@ class Advanced:
|
|
177
177
|
show_screenshot_buttons: bool = False
|
178
178
|
multi_line_line_break: str = '<br>'
|
179
179
|
multi_line_sentence_storage_field: str = ''
|
180
|
+
ocr_sends_to_clipboard: bool = True
|
180
181
|
|
181
182
|
@dataclass_json
|
182
183
|
@dataclass
|
@@ -392,7 +393,9 @@ def get_app_directory():
|
|
392
393
|
|
393
394
|
|
394
395
|
def get_log_path():
|
395
|
-
|
396
|
+
path = os.path.join(get_app_directory(), "logs", 'gamesentenceminer.log')
|
397
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
398
|
+
return path
|
396
399
|
|
397
400
|
temp_directory = ''
|
398
401
|
|
GameSentenceMiner/gametext.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import asyncio
|
2
|
+
import difflib
|
2
3
|
import re
|
3
4
|
import threading
|
4
5
|
import time
|
5
|
-
from datetime import datetime
|
6
|
+
from datetime import datetime, timedelta
|
6
7
|
|
7
8
|
import pyperclip
|
8
9
|
import websockets
|
9
|
-
from websockets import
|
10
|
+
from websockets import InvalidStatus
|
10
11
|
|
11
12
|
from GameSentenceMiner import util
|
12
13
|
from GameSentenceMiner.model import AnkiCard
|
@@ -66,12 +67,15 @@ class GameText:
|
|
66
67
|
return matches[occurrence]
|
67
68
|
return None
|
68
69
|
|
69
|
-
def add_line(self, line_text):
|
70
|
-
|
70
|
+
def add_line(self, line_text, line_time=None):
|
71
|
+
if not line_text:
|
72
|
+
return
|
73
|
+
new_line = GameLine(line_text, line_time if line_time else datetime.now(), self.values[-1] if self.values else None, None, self.game_line_index)
|
71
74
|
self.game_line_index += 1
|
72
75
|
if self.values:
|
73
76
|
self.values[-1].next = new_line
|
74
77
|
self.values.append(new_line)
|
78
|
+
# self.remove_old_events(datetime.now() - timedelta(minutes=10))
|
75
79
|
|
76
80
|
def has_line(self, line_text) -> bool:
|
77
81
|
for game_line in self.values:
|
@@ -79,6 +83,9 @@ class GameText:
|
|
79
83
|
return True
|
80
84
|
return False
|
81
85
|
|
86
|
+
# def remove_old_events(self, cutoff_time: datetime):
|
87
|
+
# self.values = [line for line in self.values if line.time >= cutoff_time]
|
88
|
+
|
82
89
|
line_history = GameText()
|
83
90
|
|
84
91
|
class ClipboardMonitor(threading.Thread):
|
@@ -111,7 +118,7 @@ class ClipboardMonitor(threading.Thread):
|
|
111
118
|
async def listen_websocket():
|
112
119
|
global current_line, current_line_time, line_history, reconnecting, websocket_connected
|
113
120
|
try_other = False
|
114
|
-
websocket_url = f'ws://{get_config().general.websocket_uri}'
|
121
|
+
websocket_url = f'ws://{get_config().general.websocket_uri}/gsm'
|
115
122
|
while True:
|
116
123
|
if try_other:
|
117
124
|
websocket_url = f'ws://{get_config().general.websocket_uri}/api/ws/text/origin'
|
@@ -123,6 +130,7 @@ async def listen_websocket():
|
|
123
130
|
reconnecting = False
|
124
131
|
websocket_connected = True
|
125
132
|
try_other = True
|
133
|
+
line_time = None
|
126
134
|
while True:
|
127
135
|
message = await websocket.recv()
|
128
136
|
logger.debug(message)
|
@@ -130,14 +138,17 @@ async def listen_websocket():
|
|
130
138
|
data = json.loads(message)
|
131
139
|
if "sentence" in data:
|
132
140
|
current_clipboard = data["sentence"]
|
141
|
+
if "time" in data:
|
142
|
+
line_time = datetime.fromisoformat(data["time"])
|
143
|
+
print(line_time)
|
133
144
|
except json.JSONDecodeError or TypeError:
|
134
145
|
current_clipboard = message
|
135
146
|
if current_clipboard != current_line:
|
136
|
-
handle_new_text_event(current_clipboard)
|
137
|
-
except (websockets.ConnectionClosed, ConnectionError,
|
138
|
-
if isinstance(e,
|
139
|
-
e:
|
140
|
-
if e.status_code == 404:
|
147
|
+
handle_new_text_event(current_clipboard, line_time if line_time else None)
|
148
|
+
except (websockets.ConnectionClosed, ConnectionError, InvalidStatus) as e:
|
149
|
+
if isinstance(e, InvalidStatus):
|
150
|
+
e: InvalidStatus
|
151
|
+
if e.response.status_code == 404:
|
141
152
|
logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
|
142
153
|
try_other = True
|
143
154
|
|
@@ -148,7 +159,7 @@ async def listen_websocket():
|
|
148
159
|
reconnecting = True
|
149
160
|
await asyncio.sleep(5)
|
150
161
|
|
151
|
-
def handle_new_text_event(current_clipboard):
|
162
|
+
def handle_new_text_event(current_clipboard, line_time=None):
|
152
163
|
global current_line, current_line_time, line_history, current_line_after_regex
|
153
164
|
current_line = current_clipboard
|
154
165
|
if get_config().general.texthook_replacement_regex:
|
@@ -156,9 +167,10 @@ def handle_new_text_event(current_clipboard):
|
|
156
167
|
else:
|
157
168
|
current_line_after_regex = current_line
|
158
169
|
logger.info(f"Line Received: {current_line_after_regex}")
|
159
|
-
current_line_time = datetime.now()
|
160
|
-
line_history.add_line(current_line_after_regex)
|
161
|
-
get_utility_window()
|
170
|
+
current_line_time = line_time if line_time else datetime.now()
|
171
|
+
line_history.add_line(current_line_after_regex, line_time)
|
172
|
+
if get_utility_window():
|
173
|
+
get_utility_window().add_text(line_history[-1])
|
162
174
|
|
163
175
|
|
164
176
|
def reset_line_hotkey_pressed():
|
@@ -232,6 +244,8 @@ def get_line_and_future_lines(last_note):
|
|
232
244
|
def get_mined_line(last_note: AnkiCard, lines):
|
233
245
|
if not last_note:
|
234
246
|
return lines[-1]
|
247
|
+
if not lines:
|
248
|
+
lines = get_all_lines()
|
235
249
|
|
236
250
|
sentence = last_note.get_field(get_config().anki.sentence_field)
|
237
251
|
for line in lines:
|
GameSentenceMiner/gsm.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
import os.path
|
3
2
|
import signal
|
4
3
|
from subprocess import Popen
|
@@ -496,6 +495,7 @@ def cleanup():
|
|
496
495
|
icon.stop()
|
497
496
|
|
498
497
|
settings_window.window.destroy()
|
498
|
+
time.sleep(5)
|
499
499
|
logger.info("Cleanup complete.")
|
500
500
|
|
501
501
|
|
@@ -594,8 +594,8 @@ def main(reloading=False):
|
|
594
594
|
win32api.SetConsoleCtrlHandler(handle_exit())
|
595
595
|
|
596
596
|
try:
|
597
|
-
if get_config().general.open_config_on_startup:
|
598
|
-
|
597
|
+
# if get_config().general.open_config_on_startup:
|
598
|
+
# root.after(0, settings_window.show)
|
599
599
|
if get_config().general.open_multimine_on_startup:
|
600
600
|
root.after(0, get_utility_window().show)
|
601
601
|
root.after(0, post_init)
|
@@ -614,4 +614,8 @@ def main(reloading=False):
|
|
614
614
|
|
615
615
|
if __name__ == "__main__":
|
616
616
|
logger.info("Starting GSM")
|
617
|
-
|
617
|
+
try:
|
618
|
+
main()
|
619
|
+
except Exception as e:
|
620
|
+
logger.exception(e)
|
621
|
+
time.sleep(5)
|
GameSentenceMiner/obs.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import os.path
|
3
3
|
import subprocess
|
4
|
+
import tempfile
|
4
5
|
import time
|
5
6
|
|
6
7
|
import psutil
|
@@ -72,20 +73,27 @@ def get_obs_websocket_config_values():
|
|
72
73
|
if get_config().obs.password == 'your_password':
|
73
74
|
logger.info("OBS WebSocket password is not set. Setting it now...")
|
74
75
|
config = get_master_config()
|
75
|
-
config.
|
76
|
-
config.
|
76
|
+
config.get_scene_ocr_config().obs.port = server_port
|
77
|
+
config.get_scene_ocr_config().obs.password = server_password
|
77
78
|
with open(get_config_path(), 'w') as file:
|
78
79
|
json.dump(config.to_dict(), file, indent=4)
|
79
80
|
reload_config()
|
80
81
|
|
81
82
|
|
83
|
+
reconnecting = False
|
84
|
+
|
82
85
|
def on_connect(obs):
|
86
|
+
global reconnecting
|
83
87
|
logger.info("Reconnected to OBS WebSocket.")
|
84
|
-
|
88
|
+
if reconnecting:
|
89
|
+
start_replay_buffer()
|
90
|
+
reconnecting = False
|
85
91
|
|
86
92
|
|
87
93
|
def on_disconnect(obs):
|
94
|
+
global reconnecting
|
88
95
|
logger.error("OBS Connection Lost!")
|
96
|
+
reconnecting = True
|
89
97
|
|
90
98
|
|
91
99
|
def connect_to_obs():
|
@@ -113,6 +121,7 @@ def do_obs_call(request, from_dict = None, retry=10):
|
|
113
121
|
if not client:
|
114
122
|
time.sleep(1)
|
115
123
|
return do_obs_call(request, from_dict, retry - 1)
|
124
|
+
logger.debug("Sending obs call: " + str(request))
|
116
125
|
response = client.call(request)
|
117
126
|
if not response.status and retry > 0:
|
118
127
|
time.sleep(1)
|
@@ -142,8 +151,8 @@ def toggle_replay_buffer():
|
|
142
151
|
# Start replay buffer
|
143
152
|
def start_replay_buffer(retry=5):
|
144
153
|
try:
|
145
|
-
|
146
|
-
|
154
|
+
if not get_replay_buffer_status()['outputActive']:
|
155
|
+
do_obs_call(requests.StartReplayBuffer(), retry=0)
|
147
156
|
except Exception as e:
|
148
157
|
if "socket is already closed" in str(e):
|
149
158
|
if retry > 0:
|
@@ -152,6 +161,12 @@ def start_replay_buffer(retry=5):
|
|
152
161
|
else:
|
153
162
|
logger.error(f"Error starting replay buffer: {e}")
|
154
163
|
|
164
|
+
def get_replay_buffer_status():
|
165
|
+
try:
|
166
|
+
return do_obs_call(requests.GetReplayBufferStatus())
|
167
|
+
except Exception as e:
|
168
|
+
logger.error(f"Error getting replay buffer status: {e}")
|
169
|
+
|
155
170
|
|
156
171
|
# Stop replay buffer
|
157
172
|
def stop_replay_buffer():
|
@@ -176,7 +191,7 @@ def save_replay_buffer():
|
|
176
191
|
|
177
192
|
def get_current_scene():
|
178
193
|
try:
|
179
|
-
return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict).sceneName
|
194
|
+
return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict, retry=0).sceneName
|
180
195
|
except Exception as e:
|
181
196
|
logger.error(f"Couldn't get scene: {e}")
|
182
197
|
return ''
|
@@ -197,7 +212,7 @@ def get_record_directory():
|
|
197
212
|
return ''
|
198
213
|
|
199
214
|
|
200
|
-
def get_screenshot():
|
215
|
+
def get_screenshot(compression=-1):
|
201
216
|
try:
|
202
217
|
screenshot = util.make_unique_file_name(os.path.abspath(
|
203
218
|
configuration.get_temporary_directory()) + '/screenshot.png')
|
@@ -207,11 +222,29 @@ def get_screenshot():
|
|
207
222
|
if not current_source_name:
|
208
223
|
logger.error("No active scene found.")
|
209
224
|
return
|
210
|
-
|
225
|
+
start = time.time()
|
226
|
+
logger.debug(f"Current source name: {current_source_name}")
|
227
|
+
response = client.call(requests.SaveSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageFilePath=screenshot, imageCompressionQuality=compression))
|
228
|
+
logger.debug(f"Screenshot response: {response}")
|
229
|
+
logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
|
211
230
|
return screenshot
|
212
231
|
except Exception as e:
|
213
232
|
logger.error(f"Error getting screenshot: {e}")
|
214
233
|
|
234
|
+
def get_screenshot_base64():
|
235
|
+
try:
|
236
|
+
update_current_game()
|
237
|
+
current_source = get_source_from_scene(get_current_game())
|
238
|
+
current_source_name = current_source.sourceName
|
239
|
+
if not current_source_name:
|
240
|
+
logger.error("No active scene found.")
|
241
|
+
return
|
242
|
+
response = do_obs_call(requests.GetSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageCompressionQuality=0))
|
243
|
+
with open('screenshot_response.txt', 'wb') as f:
|
244
|
+
f.write(str(response).encode())
|
245
|
+
return response['imageData']
|
246
|
+
except Exception as e:
|
247
|
+
logger.error(f"Error getting screenshot: {e}")
|
215
248
|
|
216
249
|
def update_current_game():
|
217
250
|
configuration.current_game = get_current_scene()
|
File without changes
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import configparser
|
2
|
+
import os
|
3
|
+
import re
|
4
|
+
|
5
|
+
class OCRConfig:
|
6
|
+
def __init__(self, config_file=os.path.expanduser("~/.config/owocr_config.ini")):
|
7
|
+
self.config_file = config_file
|
8
|
+
self.config = configparser.ConfigParser(allow_no_value=True)
|
9
|
+
self.raw_config = {} # Store the raw lines of the config file
|
10
|
+
self.load_config()
|
11
|
+
|
12
|
+
def load_config(self):
|
13
|
+
if os.path.exists(self.config_file):
|
14
|
+
self.raw_config = self._read_config_with_comments()
|
15
|
+
self.config.read_dict(self._parse_config_to_dict())
|
16
|
+
else:
|
17
|
+
self.create_default_config()
|
18
|
+
|
19
|
+
def create_default_config(self):
|
20
|
+
self.raw_config = {
|
21
|
+
"general": [
|
22
|
+
";engines = avision,alivetext,bing,glens,glensweb,gvision,azure,mangaocr,winrtocr,oneocr,easyocr,rapidocr,ocrspace",
|
23
|
+
";engine = glens",
|
24
|
+
"read_from = screencapture",
|
25
|
+
"write_to = websocket",
|
26
|
+
";note: this specifies an amount of seconds to wait for auto pausing the program after a successful text recognition. Will be ignored when reading with screen capture. 0 to disable.",
|
27
|
+
";auto_pause = 0",
|
28
|
+
";pause_at_startup = False",
|
29
|
+
";logger_format = <green>{time:HH:mm:ss.SSS}</green> | <level>{message}</level>",
|
30
|
+
";engine_color = cyan",
|
31
|
+
"websocket_port = 7331",
|
32
|
+
";delay_secs = 0.5",
|
33
|
+
";notifications = False",
|
34
|
+
";ignore_flag = False",
|
35
|
+
";delete_images = False",
|
36
|
+
";note: this specifies a combo to wait on for pausing the program. As an example: <ctrl>+<shift>+p. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key",
|
37
|
+
";combo_pause = <ctrl>+<shift>+p",
|
38
|
+
";note: this specifies a combo to wait on for switching the OCR engine. As an example: <ctrl>+<shift>+a. To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key",
|
39
|
+
";combo_engine_switch = <ctrl>+<shift>+a",
|
40
|
+
";note: screen_capture_area can be empty for the coordinate picker, \"screen_N\" (where N is the screen number starting from 1) for an entire screen, have a manual set of coordinates (x,y,width,height) or a window name (the first matching window title will be used).",
|
41
|
+
";screen_capture_area = ",
|
42
|
+
";screen_capture_area = screen_1",
|
43
|
+
";screen_capture_area = 400,200,1500,600",
|
44
|
+
";screen_capture_area = OBS",
|
45
|
+
";note: if screen_capture_area is a window name, this can be changed to capture inactive windows too.",
|
46
|
+
";screen_capture_only_active_windows = True",
|
47
|
+
";screen_capture_delay_secs = 3",
|
48
|
+
";note: this specifies a combo to wait on for taking a screenshot instead of using the delay. As an example: <ctrl>+<shift>+s. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key",
|
49
|
+
";screen_capture_combo = <ctrl>+<shift>+s",
|
50
|
+
],
|
51
|
+
"winrtocr": [";url = http://aaa.xxx.yyy.zzz:8000"],
|
52
|
+
"oneocr": [";url = http://aaa.xxx.yyy.zzz:8001"],
|
53
|
+
"azure": [";api_key = api_key_here", ";endpoint = https://YOURPROJECT.cognitiveservices.azure.com/"],
|
54
|
+
"mangaocr": ["pretrained_model_name_or_path = kha-white/manga-ocr-base", "force_cpu = False"],
|
55
|
+
"easyocr": ["gpu = True"],
|
56
|
+
"ocrspace": [";api_key = api_key_here"],
|
57
|
+
}
|
58
|
+
self.config.read_dict(self._parse_config_to_dict())
|
59
|
+
self.save_config()
|
60
|
+
|
61
|
+
def _read_config_with_comments(self):
|
62
|
+
with open(self.config_file, "r") as f:
|
63
|
+
lines = f.readlines()
|
64
|
+
config_data = {}
|
65
|
+
current_section = None
|
66
|
+
for line in lines:
|
67
|
+
line = line.strip()
|
68
|
+
if line.startswith("[") and line.endswith("]"):
|
69
|
+
current_section = line[1:-1]
|
70
|
+
config_data[current_section] = []
|
71
|
+
elif current_section is not None:
|
72
|
+
config_data[current_section].append(line)
|
73
|
+
return config_data
|
74
|
+
|
75
|
+
def _parse_config_to_dict(self):
|
76
|
+
parsed_config = {}
|
77
|
+
for section, lines in self.raw_config.items():
|
78
|
+
parsed_config[section] = {}
|
79
|
+
for line in lines:
|
80
|
+
if "=" in line and not line.startswith(";"):
|
81
|
+
key, value = line.split("=", 1)
|
82
|
+
parsed_config[section][key.strip()] = value.strip()
|
83
|
+
return parsed_config
|
84
|
+
|
85
|
+
def save_config(self):
|
86
|
+
with open(self.config_file, "w") as f:
|
87
|
+
for section, lines in self.raw_config.items():
|
88
|
+
f.write(f"[{section}]\n")
|
89
|
+
for line in lines:
|
90
|
+
f.write(f"{line}\n")
|
91
|
+
|
92
|
+
def get_value(self, section, key):
|
93
|
+
if section in self.config and key in self.config[section]:
|
94
|
+
return self.config[section][key]
|
95
|
+
return None
|
96
|
+
|
97
|
+
def set_value(self, section, key, value):
|
98
|
+
if section not in self.config:
|
99
|
+
self.raw_config[section] = [] #add section if it does not exist.
|
100
|
+
if section not in self.config:
|
101
|
+
self.config[section] = {}
|
102
|
+
self.config[section][key] = str(value)
|
103
|
+
|
104
|
+
# Update the raw config to keep comments
|
105
|
+
found = False
|
106
|
+
for i, line in enumerate(self.raw_config[section]):
|
107
|
+
if line.startswith(key + " ="):
|
108
|
+
self.raw_config[section][i] = f"{key} = {value}"
|
109
|
+
found = True
|
110
|
+
break
|
111
|
+
if not found:
|
112
|
+
self.raw_config[section].append(f"{key} = {value}")
|
113
|
+
|
114
|
+
self.save_config()
|
115
|
+
|
116
|
+
def get_section(self, section):
|
117
|
+
if section in self.config:
|
118
|
+
return dict(self.config[section])
|
119
|
+
return None
|
120
|
+
|
121
|
+
def set_screen_capture_area(self, screen_capture_data):
|
122
|
+
if not isinstance(screen_capture_data, dict) or "coordinates" not in screen_capture_data:
|
123
|
+
raise ValueError("Invalid screen capture data format.")
|
124
|
+
|
125
|
+
coordinates = screen_capture_data["coordinates"]
|
126
|
+
if len(coordinates) != 4:
|
127
|
+
raise ValueError("Coordinates must contain four values: x, y, width, height.")
|
128
|
+
|
129
|
+
x, y, width, height = coordinates
|
130
|
+
self.set_value("general", "screen_capture_area", f"{x},{y},{width},{height}")
|