easyrip 3.13.2__py3-none-any.whl → 4.9.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.
Files changed (36) hide show
  1. easyrip/__init__.py +5 -1
  2. easyrip/__main__.py +124 -15
  3. easyrip/easyrip_command.py +457 -148
  4. easyrip/easyrip_config/config.py +269 -0
  5. easyrip/easyrip_config/config_key.py +28 -0
  6. easyrip/easyrip_log.py +120 -42
  7. easyrip/easyrip_main.py +509 -259
  8. easyrip/easyrip_mlang/__init__.py +20 -45
  9. easyrip/easyrip_mlang/global_lang_val.py +18 -16
  10. easyrip/easyrip_mlang/lang_en.py +1 -1
  11. easyrip/easyrip_mlang/lang_zh_Hans_CN.py +101 -77
  12. easyrip/easyrip_mlang/translator.py +12 -10
  13. easyrip/easyrip_prompt.py +73 -0
  14. easyrip/easyrip_web/__init__.py +2 -1
  15. easyrip/easyrip_web/http_server.py +56 -42
  16. easyrip/easyrip_web/third_party_api.py +60 -8
  17. easyrip/global_val.py +21 -1
  18. easyrip/ripper/media_info.py +10 -3
  19. easyrip/ripper/param.py +482 -0
  20. easyrip/ripper/ripper.py +260 -574
  21. easyrip/ripper/sub_and_font/__init__.py +10 -0
  22. easyrip/ripper/{font_subset → sub_and_font}/ass.py +95 -84
  23. easyrip/ripper/{font_subset → sub_and_font}/font.py +72 -79
  24. easyrip/ripper/{font_subset → sub_and_font}/subset.py +122 -81
  25. easyrip/utils.py +129 -27
  26. easyrip-4.9.1.dist-info/METADATA +92 -0
  27. easyrip-4.9.1.dist-info/RECORD +31 -0
  28. easyrip/easyrip_config.py +0 -198
  29. easyrip/ripper/__init__.py +0 -10
  30. easyrip/ripper/font_subset/__init__.py +0 -7
  31. easyrip-3.13.2.dist-info/METADATA +0 -89
  32. easyrip-3.13.2.dist-info/RECORD +0 -29
  33. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/WHEEL +0 -0
  34. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/entry_points.txt +0 -0
  35. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/licenses/LICENSE +0 -0
  36. {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/top_level.txt +0 -0
easyrip/easyrip_main.py CHANGED
@@ -1,3 +1,4 @@
1
+ import ast
1
2
  import ctypes
2
3
  import itertools
3
4
  import json
@@ -7,19 +8,23 @@ import shlex
7
8
  import shutil
8
9
  import subprocess
9
10
  import sys
11
+ import textwrap
12
+ import threading
10
13
  import tkinter as tk
14
+ import tomllib
15
+ from collections.abc import Callable, Iterable
16
+ from concurrent.futures import ThreadPoolExecutor
11
17
  from datetime import datetime
12
18
  from multiprocessing import shared_memory
13
19
  from pathlib import Path
14
20
  from threading import Thread
15
21
  from time import sleep
16
22
  from tkinter import filedialog
17
-
18
- import tomllib
23
+ from typing import Final, Literal
19
24
 
20
25
  from . import easyrip_mlang, easyrip_web, global_val
21
26
  from .easyrip_command import Cmd_type, Opt_type, get_help_doc
22
- from .easyrip_config import config
27
+ from .easyrip_config.config import Config_key, config
23
28
  from .easyrip_log import Event as LogEvent
24
29
  from .easyrip_log import log
25
30
  from .easyrip_mlang import (
@@ -30,7 +35,11 @@ from .easyrip_mlang import (
30
35
  gettext,
31
36
  translate_subtitles,
32
37
  )
33
- from .ripper import Media_info, Ripper
38
+ from .easyrip_prompt import easyrip_prompt
39
+ from .ripper.media_info import Media_info
40
+ from .ripper.param import DEFAULT_PRESET_PARAMS, PRESET_OPT_NAME
41
+ from .ripper.ripper import Ripper
42
+ from .ripper.sub_and_font import load_fonts
34
43
  from .utils import change_title, check_ver, read_text
35
44
 
36
45
  __all__ = ["init", "run_command"]
@@ -42,40 +51,44 @@ PROJECT_TITLE = global_val.PROJECT_TITLE
42
51
  PROJECT_URL = global_val.PROJECT_URL
43
52
 
44
53
 
45
- def log_new_ver(new_ver: str | None, old_ver: str, program_name: str, dl_url: str):
54
+ def log_new_ver(
55
+ new_ver: str | None, old_ver: str, program_name: str, dl_url: str
56
+ ) -> None:
46
57
  if new_ver is None:
47
58
  return
48
59
  try:
49
60
  if check_ver(new_ver, old_ver):
50
- print()
51
61
  log.info(
52
- "{} has new version {}. You can download it: {}",
53
- program_name,
54
- new_ver,
55
- dl_url,
62
+ "\n"
63
+ + gettext(
64
+ "{} has new version ({} -> {}). Suggest upgrading it: {}",
65
+ program_name,
66
+ old_ver,
67
+ new_ver,
68
+ dl_url,
69
+ )
56
70
  )
57
- print(get_input_prompt(True), end="")
58
71
  except Exception as e:
59
- print()
60
- log.warning(e, deep=True)
61
- print(get_input_prompt(True), end="")
72
+ log.warning(f"\n{e}", is_format=False, deep=True)
62
73
 
63
74
 
64
- def check_env():
75
+ def check_env() -> None:
65
76
  try:
66
77
  change_title(f"{gettext('Check env...')} {PROJECT_TITLE}")
67
78
 
68
- if config.get_user_profile("check_dependent"):
79
+ if config.get_user_profile(Config_key.check_dependent):
69
80
  _url = "https://ffmpeg.org/download.html"
70
81
  for _name in ("FFmpeg", "FFprobe"):
71
82
  if not shutil.which(_name):
72
- print()
73
83
  log.error(
74
- "{} not found, download it: {}",
75
- _name,
76
- f"(full build ver) {_url}",
84
+ "\n"
85
+ + gettext(
86
+ "{} not found, download it: {}",
87
+ _name,
88
+ f"(full build ver) {_url}",
89
+ )
77
90
  )
78
- print(get_input_prompt(True), end="")
91
+ log.print(get_input_prompt(True), end="")
79
92
  else:
80
93
  _new_ver = (
81
94
  subprocess.run(
@@ -86,19 +99,21 @@ def check_env():
86
99
  )
87
100
 
88
101
  if "." in _new_ver:
89
- log_new_ver("8.0", _new_ver.split("-")[0], _name, _url)
102
+ log_new_ver("8.0.1", _new_ver.split("-")[0], _name, _url)
90
103
  else:
91
104
  log_new_ver(
92
- "2025.08.22", ".".join(_new_ver.split("-")[:3]), _name, _url
105
+ "2025.11.20", ".".join(_new_ver.split("-")[:3]), _name, _url
93
106
  )
94
107
 
95
108
  _name, _url = "flac", "https://github.com/xiph/flac/releases"
96
109
  if not shutil.which(_name):
97
- print()
98
110
  log.warning(
99
- "{} not found, download it: {}", _name, f"(ver >= 1.5.0) {_url}"
111
+ "\n"
112
+ + gettext(
113
+ "{} not found, download it: {}", _name, f"(ver >= 1.5.0) {_url}"
114
+ )
100
115
  )
101
- print(get_input_prompt(True), end="")
116
+ log.print(get_input_prompt(True), end="")
102
117
 
103
118
  elif check_ver(
104
119
  "1.5.0",
@@ -112,7 +127,7 @@ def check_env():
112
127
 
113
128
  else:
114
129
  log_new_ver(
115
- easyrip_web.github.get_release_ver(
130
+ easyrip_web.github.get_latest_release_ver(
116
131
  "https://api.github.com/repos/xiph/flac/releases/latest"
117
132
  ),
118
133
  old_ver_str,
@@ -122,12 +137,13 @@ def check_env():
122
137
 
123
138
  _name, _url = "mp4fpsmod", "https://github.com/nu774/mp4fpsmod/releases"
124
139
  if not shutil.which(_name):
125
- print()
126
- log.warning("{} not found, download it: {}", _name, _url)
127
- print(get_input_prompt(True), end="")
140
+ log.warning(
141
+ "\n" + gettext("{} not found, download it: {}", _name, _url)
142
+ )
143
+ log.print(get_input_prompt(True), end="")
128
144
  else:
129
145
  log_new_ver(
130
- easyrip_web.github.get_release_ver(
146
+ easyrip_web.github.get_latest_release_ver(
131
147
  "https://api.github.com/repos/nu774/mp4fpsmod/releases/latest"
132
148
  ),
133
149
  subprocess.run(_name, capture_output=True, text=True).stderr.split(
@@ -139,9 +155,10 @@ def check_env():
139
155
 
140
156
  _name, _url = "MP4Box", "https://gpac.io/downloads/gpac-nightly-builds/"
141
157
  if not shutil.which(_name):
142
- print()
143
- log.warning("{} not found, download it: {}", _name, _url)
144
- print(get_input_prompt(True), end="")
158
+ log.warning(
159
+ "\n" + gettext("{} not found, download it: {}", _name, _url)
160
+ )
161
+ log.print(get_input_prompt(True), end="")
145
162
  else:
146
163
  log_new_ver(
147
164
  "2.5",
@@ -156,12 +173,13 @@ def check_env():
156
173
  _url = "https://mkvtoolnix.download/downloads.html"
157
174
  for _name in ("mkvpropedit", "mkvmerge"):
158
175
  if not shutil.which(_name):
159
- print()
160
- log.warning("{} not found, download it: {}", _name, _url)
161
- print(get_input_prompt(True), end="")
176
+ log.warning(
177
+ "\n" + gettext("{} not found, download it: {}", _name, _url)
178
+ )
179
+ log.print(get_input_prompt(True), end="")
162
180
  else:
163
181
  log_new_ver(
164
- "95",
182
+ easyrip_web.mkvtoolnix.get_latest_release_ver(),
165
183
  subprocess.run(
166
184
  f"{_name} --version", capture_output=True, text=True
167
185
  ).stdout.split(maxsplit=2)[1],
@@ -169,28 +187,29 @@ def check_env():
169
187
  _url,
170
188
  )
171
189
 
172
- # _name, _url = 'MediaInfo', 'https://mediaarea.net/en/MediaInfo/Download'
173
- # if not shutil.which(_name):
174
- # print()
175
- # log.warning('{} not found, download it: {}', _name, f'(CLI ver) {_url}')
176
- # print(get_input_prompt(), end='')
177
- # elif not subprocess.run('mediainfo --version', capture_output=True, text=True).stdout:
178
- # log.error("The MediaInfo must be CLI ver")
179
-
180
- if config.get_user_profile("check_update"):
190
+ if config.get_user_profile(Config_key.check_update):
181
191
  log_new_ver(
182
- easyrip_web.github.get_release_ver(global_val.PROJECT_RELEASE_API),
192
+ easyrip_web.github.get_latest_release_ver(
193
+ global_val.PROJECT_RELEASE_API
194
+ ),
183
195
  PROJECT_VERSION,
184
196
  PROJECT_NAME,
185
- f"{global_val.PROJECT_URL} {gettext("or run '{}' when you use pip", 'pip install -U easyrip')}",
197
+ f"{global_val.PROJECT_URL}\n{
198
+ gettext(
199
+ 'Suggest running the following command to upgrade using pip: {}',
200
+ f'{f'"{sys.executable}" -m ' if sys.executable.lower().endswith("python.exe") else ""}pip install -U easyrip',
201
+ )
202
+ }",
186
203
  )
187
204
 
188
- sys.stdout.flush()
189
- sys.stderr.flush()
190
205
  change_title(PROJECT_TITLE)
191
206
 
192
207
  except Exception as e:
193
- log.error(f"The def check_env error: {e!r} {e}", deep=True)
208
+ log.error(
209
+ f"The function {check_env.__name__} error: {e!r} {e}",
210
+ is_format=False,
211
+ deep=True,
212
+ )
194
213
 
195
214
 
196
215
  def get_input_prompt(is_color: bool = False) -> str:
@@ -209,17 +228,29 @@ if os.name == "nt":
209
228
  log.warning("Windows DPI Aware failed")
210
229
 
211
230
 
212
- def file_dialog(initialdir=None):
231
+ def file_dialog(
232
+ *,
233
+ is_askdir: bool = False,
234
+ initialdir=None,
235
+ ) -> tuple[str, ...]:
213
236
  tkRoot = tk.Tk()
237
+
214
238
  tkRoot.withdraw()
215
- file_paths = filedialog.askopenfilenames(initialdir=initialdir)
239
+ if is_askdir:
240
+ file_paths = (filedialog.askdirectory(initialdir=initialdir),)
241
+ else:
242
+ file_paths = filedialog.askopenfilenames(initialdir=initialdir)
243
+
216
244
  tkRoot.destroy()
217
- return file_paths
245
+ return file_paths if file_paths else ()
218
246
 
219
247
 
220
248
  def run_ripper_list(
221
- is_exit_when_run_finished: bool = False, shutdow_sec_str: str | None = None
222
- ):
249
+ *,
250
+ is_exit_when_run_finished: bool = False,
251
+ shutdow_sec_str: str | None = None,
252
+ enable_multithreading: bool = False,
253
+ ) -> None:
223
254
  shutdown_sec: int | None = None
224
255
  if shutdow_sec_str is not None:
225
256
  try:
@@ -236,8 +267,10 @@ def run_ripper_list(
236
267
  create=True,
237
268
  size=_size,
238
269
  )
270
+ assert path_lock_shm.buf is not None
239
271
  except FileExistsError:
240
272
  _shm = shared_memory.SharedMemory(name=_name)
273
+ assert _shm.buf is not None
241
274
  _res: dict = json.loads(
242
275
  bytes(_shm.buf[: len(_shm.buf)]).decode("utf-8").rstrip("\0")
243
276
  )
@@ -258,27 +291,55 @@ def run_ripper_list(
258
291
  ).encode("utf-8")
259
292
  path_lock_shm.buf[: len(_data)] = _data
260
293
 
261
- total = len(Ripper.ripper_list)
262
- warning_num = log.warning_num
263
- error_num = log.error_num
264
- for i, ripper in enumerate(Ripper.ripper_list, 1):
265
- progress = f"{i} / {total} - {PROJECT_TITLE}"
266
- log.info(progress)
267
- change_title(progress)
268
- try:
269
- if ripper.run() is False:
270
- log.error("Run {} failed", "Ripper")
271
- except Exception as e:
272
- log.error(e, deep=True)
273
- log.warning("Stop run Ripper")
274
- sleep(1)
294
+ total: Final[int] = len(Ripper.ripper_list)
295
+ warning_num: Final[int] = log.warning_num
296
+ error_num: Final[int] = log.error_num
297
+
298
+ if enable_multithreading:
299
+ threading_lock = threading.Lock()
300
+ progress_num: int = 0
301
+
302
+ def _executor_submit_ripper_run(ripper: Ripper) -> None:
303
+ nonlocal progress_num
304
+ with threading_lock:
305
+ progress_num += 1
306
+
307
+ progress = f"{progress_num} / {total} - {PROJECT_TITLE}"
308
+ log.info(progress)
309
+ change_title(progress)
310
+
311
+ try:
312
+ if ripper.run() is False:
313
+ log.error("Run {} failed", "Ripper")
314
+ except Exception as e:
315
+ log.error(e, deep=True)
316
+ log.warning("Stop run Ripper")
317
+
318
+ with ThreadPoolExecutor() as executor:
319
+ for ripper in Ripper.ripper_list:
320
+ executor.submit(_executor_submit_ripper_run, ripper)
321
+ sleep(0.1)
322
+
323
+ else:
324
+ for i, ripper in enumerate(Ripper.ripper_list, 1):
325
+ progress = f"{i} / {total} - {PROJECT_TITLE}"
326
+ log.info(progress)
327
+ change_title(progress)
328
+ try:
329
+ if ripper.run() is False:
330
+ log.error("Run {} failed", "Ripper")
331
+ except Exception as e:
332
+ log.error(e, deep=True)
333
+ log.warning("Stop run Ripper")
334
+ sleep(0.5)
335
+
275
336
  if log.warning_num > warning_num:
276
337
  log.warning(
277
338
  "There are {} {} during run", log.warning_num - warning_num, "warning"
278
339
  )
279
340
  if log.error_num > error_num:
280
341
  log.error("There are {} {} during run", log.error_num - error_num, "error")
281
- Ripper.ripper_list = []
342
+ Ripper.ripper_list.clear()
282
343
  path_lock_shm.close()
283
344
 
284
345
  if shutdown_sec:
@@ -293,25 +354,52 @@ def run_ripper_list(
293
354
  os.system(_cmd[0])
294
355
 
295
356
  if is_exit_when_run_finished:
296
- sys.exit()
357
+ sys.exit(0)
297
358
 
298
359
  change_title(f"End - {PROJECT_TITLE}")
299
360
  log.info("Run completed")
300
361
 
301
362
 
302
- def run_command(command: list[str] | str) -> bool:
303
- if isinstance(command, list):
304
- cmd_list = command
363
+ def get_web_server_params(
364
+ opt: str,
365
+ ) -> Literal[False] | tuple[str, int, str | None]:
366
+ """[<address>]:[<port>]@[<password>]"""
367
+ if easyrip_web.http_server.Event.is_run_command:
368
+ log.error("Can not start multiple services")
369
+ return False
305
370
 
306
- else:
307
- try:
308
- cmd_list = [
309
- cmd.strip('"').strip("'").replace("\\\\", "\\")
310
- for cmd in shlex.split(command, posix=False)
311
- ]
312
- except ValueError as e:
313
- log.error(e)
314
- return False
371
+ if ":" not in opt:
372
+ log.error("{} param illegal", f"':' not in '{opt}':")
373
+ return False
374
+
375
+ opt_list: list[str] = opt.split(":")
376
+ opt_list = [opt_list[0], *opt_list[1].split("@")]
377
+
378
+ if len(opt_list) != 3:
379
+ log.error("{} param illegal", f"len('{opt}') != 3:")
380
+ return False
381
+
382
+ host, port, password = opt_list
383
+
384
+ port = port or "0"
385
+
386
+ if not port.isdigit():
387
+ log.error("{} param illegal", f"The port in '{opt}' not a digit:")
388
+ return False
389
+
390
+ return (host or "", int(port), password)
391
+
392
+
393
+ def run_command(command: Iterable[str] | str) -> bool:
394
+ try:
395
+ cmd_list: list[str] = (
396
+ shlex.split(command.replace("\\", "\\\\") if os.name == "nt" else command)
397
+ if isinstance(command, str)
398
+ else list(command)
399
+ )
400
+ except ValueError as e:
401
+ log.error(e)
402
+ return False
315
403
 
316
404
  if len(cmd_list) == 0:
317
405
  return True
@@ -321,7 +409,7 @@ def run_command(command: list[str] | str) -> bool:
321
409
  cmd_list.append("")
322
410
 
323
411
  cmd_type: Cmd_type | None = None
324
- if cmd_list[0] in Cmd_type._member_map_.keys():
412
+ if cmd_list[0] in Cmd_type._member_map_:
325
413
  cmd_type = Cmd_type[cmd_list[0]]
326
414
  elif len(cmd_list[0]) > 0 and cmd_list[0].startswith("$"):
327
415
  cmd_type = Cmd_type._run_any
@@ -332,10 +420,54 @@ def run_command(command: list[str] | str) -> bool:
332
420
  _want_doc_cmd_type: Cmd_type | Opt_type | None = Cmd_type.from_str(
333
421
  cmd_list[1]
334
422
  ) or Opt_type.from_str(cmd_list[1])
335
- if _want_doc_cmd_type is None:
336
- log.error("'{}' does not exist", cmd_list[1])
337
- else:
338
- log.send(_want_doc_cmd_type.value.to_doc(), is_format=False)
423
+ match _want_doc_cmd_type:
424
+ case Opt_type._preset:
425
+ if not cmd_list[2]:
426
+ log.send(_want_doc_cmd_type.value.to_doc(), is_format=False)
427
+ elif cmd_list[2] in Ripper.Preset_name._value2member_map_:
428
+ _preset = Ripper.Preset_name(cmd_list[2])
429
+ if (
430
+ _preset in DEFAULT_PRESET_PARAMS
431
+ or _preset in PRESET_OPT_NAME
432
+ ):
433
+ if _preset in PRESET_OPT_NAME:
434
+ log.send(
435
+ "Params that can be directly used:\n{}",
436
+ textwrap.indent(
437
+ "\n".join(
438
+ f"-{n}"
439
+ for n in PRESET_OPT_NAME[_preset]
440
+ ),
441
+ prefix=" ",
442
+ ),
443
+ )
444
+ if _preset in DEFAULT_PRESET_PARAMS:
445
+ _default_params = DEFAULT_PRESET_PARAMS[_preset]
446
+ max_name_len = (
447
+ max(len(str(n)) for n in _default_params) + 1
448
+ )
449
+ log.send(
450
+ "Default val:\n{}",
451
+ textwrap.indent(
452
+ "\n".join(
453
+ f"{f'-{n}':>{max_name_len}} {v}"
454
+ for n, v in _default_params.items()
455
+ ),
456
+ prefix=" ",
457
+ ),
458
+ )
459
+ else:
460
+ log.send(
461
+ "The preset '{}' has no separate help", cmd_list[2]
462
+ )
463
+ else:
464
+ log.error("'{}' is not a member of preset", cmd_list[2])
465
+
466
+ case None:
467
+ log.error("'{}' does not exist", cmd_list[1])
468
+
469
+ case _:
470
+ log.send(_want_doc_cmd_type.value.to_doc(), is_format=False)
339
471
  else:
340
472
  log.send(get_help_doc(), is_format=False)
341
473
 
@@ -368,35 +500,52 @@ def run_command(command: list[str] | str) -> bool:
368
500
  log.error("Your input command has error:\n{}", e)
369
501
 
370
502
  case Cmd_type.exit:
371
- sys.exit()
503
+ sys.exit(0)
372
504
 
373
- case Cmd_type.cd | Cmd_type.mediainfo:
374
- _path = None
505
+ case Cmd_type.cd | Cmd_type.mediainfo | Cmd_type.fontinfo:
506
+ _path_tuple: tuple[str, ...] | None = None
375
507
 
376
- if isinstance(command, str):
377
- _path = command.split(" ", maxsplit=1)
378
- if len(_path) <= 1:
379
- _path = None
380
- else:
381
- _path = _path[1].strip('"').strip("'")
508
+ match cmd_list[1]:
509
+ case "fd" | "cfd" as fd_param:
510
+ if easyrip_web.http_server.Event.is_run_command:
511
+ log.error("Disable the use of '{}' on the web", fd_param)
512
+ return False
513
+ _path_tuple = file_dialog(
514
+ is_askdir=cmd_type is Cmd_type.cd,
515
+ initialdir=os.getcwd() if fd_param == "cfd" else None,
516
+ )
517
+ case _:
518
+ if isinstance(command, str):
519
+ _path = command.split(None, maxsplit=1)
520
+ _path_tuple = (
521
+ None
522
+ if len(_path) <= 1
523
+ else tuple(_path[1].strip('"').strip("'").split("?"))
524
+ )
382
525
 
383
- if _path is None:
384
- _path = cmd_list[1]
526
+ if _path_tuple is None:
527
+ _path_tuple = tuple(cmd_list[1].split("?"))
385
528
 
386
529
  match cmd_type:
387
530
  case Cmd_type.cd:
388
531
  try:
389
- os.chdir(_path)
532
+ os.chdir(_path_tuple[0])
390
533
  except OSError as e:
391
534
  log.error(e)
392
535
  case Cmd_type.mediainfo:
393
- mediainfo = Media_info.from_path(_path)
394
- log.send(mediainfo)
536
+ for _path in _path_tuple:
537
+ log.send(f"{_path}: {Media_info.from_path(_path)}")
538
+ case Cmd_type.fontinfo:
539
+ for _font in itertools.chain.from_iterable(
540
+ load_fonts(_path) for _path in _path_tuple
541
+ ):
542
+ log.send(
543
+ f"{_font.pathname}: {_font.familys} / {_font.font_type.name}"
544
+ )
395
545
 
396
546
  case Cmd_type.dir:
397
547
  files = os.listdir(os.getcwd())
398
- for f_and_s in files:
399
- print(f_and_s)
548
+ log.print("\n".join(files))
400
549
  log.send(" | ".join(files))
401
550
 
402
551
  case Cmd_type.mkdir:
@@ -407,11 +556,12 @@ def run_command(command: list[str] | str) -> bool:
407
556
 
408
557
  case Cmd_type.cls:
409
558
  os.system("cls") if os.name == "nt" else os.system("clear")
559
+ easyrip_web.http_server.Event.log_queue.clear()
410
560
 
411
561
  case Cmd_type.list:
412
562
  match cmd_list[1]:
413
563
  case "clear" | "clean":
414
- Ripper.ripper_list = []
564
+ Ripper.ripper_list.clear()
415
565
  case "del" | "pop":
416
566
  try:
417
567
  del Ripper.ripper_list[int(cmd_list[2]) - 1]
@@ -437,15 +587,14 @@ def run_command(command: list[str] | str) -> bool:
437
587
  reverse=reverse,
438
588
  )
439
589
  case "":
440
- msg = f"Ripper list ({len(Ripper.ripper_list)}):"
441
- if Ripper.ripper_list:
442
- msg += "\n" + f"\n {log.hr}\n".join(
443
- [
444
- f" {i}.\n {ripper}"
445
- for i, ripper in enumerate(Ripper.ripper_list, 1)
446
- ]
447
- )
448
- log.send(msg, is_format=False)
590
+ log.send(
591
+ f"Ripper list ({len(Ripper.ripper_list)}):"
592
+ + f"\n {'─' * (shutil.get_terminal_size().columns - 2)}".join(
593
+ f"\n {i}.\n {ripper}"
594
+ for i, ripper in enumerate(Ripper.ripper_list, 1)
595
+ ),
596
+ is_format=False,
597
+ )
449
598
  case _:
450
599
  try:
451
600
  i1, i2 = int(cmd_list[1]), int(cmd_list[2])
@@ -457,109 +606,138 @@ def run_command(command: list[str] | str) -> bool:
457
606
  Ripper.ripper_list[i2],
458
607
  Ripper.ripper_list[i1],
459
608
  )
460
- except Exception as e:
461
- log.error(f"{e!r} {e}", deep=True)
609
+ except ValueError:
610
+ log.error("2 int must be inputed")
611
+ except IndexError:
612
+ log.error("list index out of range")
462
613
 
463
614
  case Cmd_type.run:
464
- is_run_exit = False
465
- match cmd_list[1]:
466
- case "":
467
- pass
468
- case "exit":
469
- is_run_exit = True
470
- case "shutdown":
471
- if _shutdown_sec_str := cmd_list[2] or "60":
472
- log.info(
473
- "Will shutdown in {}s after run finished", _shutdown_sec_str
474
- )
475
- case _ as param:
476
- log.error("Unsupported param: {}", param)
477
- return False
615
+ is_run_exit: bool = False
478
616
 
479
- run_ripper_list(is_run_exit)
617
+ _web_server_params = None
480
618
 
481
- case Cmd_type.server:
482
- if easyrip_web.http_server.Event.is_run_command:
483
- log.error("Can not start multiple services")
484
- return False
619
+ _enable_multithreading: bool = False
485
620
 
486
- address, password = None, None
621
+ _shutdown_sec_str: str | None = None
487
622
 
488
- for i in range(1, len(cmd_list)):
489
- match cmd_list[i]:
490
- case "-a" | "-address":
491
- address = cmd_list[i + 1]
492
- case "-p" | "-password":
493
- password = cmd_list[i + 1]
494
- case _:
495
- if address is None:
496
- address = cmd_list[i]
497
- elif password is None:
498
- password = cmd_list[i]
499
- if address:
500
- res = re.match(
501
- r"^([a-zA-Z0-9.-]+)(:(\d+))?$",
502
- address,
623
+ _skip_run_param: int = 0
624
+
625
+ for i, cmd in enumerate(cmd_list[1:]):
626
+ if _skip_run_param:
627
+ _skip_run_param -= 1
628
+ continue
629
+
630
+ match cmd:
631
+ case "":
632
+ pass
633
+
634
+ case "exit":
635
+ is_run_exit = True
636
+
637
+ case "shutdown":
638
+ _skip_run_param += 1
639
+ if i + 1 < len(cmd_list[1:]):
640
+ _shutdown_sec_str = cmd_list[i + 1] or "60"
641
+ log.info(
642
+ "Will shutdown in {}s after run finished",
643
+ _shutdown_sec_str,
644
+ )
645
+ else:
646
+ log.error("{} need param", cmd)
647
+ return False
648
+
649
+ case "server":
650
+ _skip_run_param += 1
651
+ if (
652
+ _web_server_params := get_web_server_params(cmd_list[2])
653
+ ) is False:
654
+ return False
655
+
656
+ case "-multithreading":
657
+ _skip_run_param += 1
658
+ if i + 1 < len(cmd_list[1:]):
659
+ _enable_multithreading = cmd_list[i + 1] != "0"
660
+ else:
661
+ log.error("{} need param", cmd)
662
+ return False
663
+
664
+ case _ as param:
665
+ log.error("Unsupported param: {}", param)
666
+ return False
667
+
668
+ if _web_server_params is None:
669
+ run_ripper_list(
670
+ is_exit_when_run_finished=is_run_exit,
671
+ shutdow_sec_str=_shutdown_sec_str,
672
+ enable_multithreading=_enable_multithreading,
503
673
  )
504
- if res:
505
- host = res.group(1)
506
- port = res.group(2)
507
- if port:
508
- port = int(port.lstrip(":"))
509
- elif host.isdigit():
510
- port = int(host)
511
- host = None
512
- else:
513
- port = None
514
- host = None
515
- else:
516
- host, port = "localhost", 0
517
674
  else:
518
- host, port = "localhost", 0
675
+ easyrip_web.run_server(
676
+ *_web_server_params,
677
+ after_start_server_hook=lambda: run_ripper_list(
678
+ is_exit_when_run_finished=is_run_exit,
679
+ shutdow_sec_str=_shutdown_sec_str,
680
+ enable_multithreading=_enable_multithreading,
681
+ ),
682
+ )
519
683
 
520
- easyrip_web.run_server(host=host or "", port=port or 0, password=password)
684
+ case Cmd_type.server:
685
+ if (_params := get_web_server_params(cmd_list[1])) is False:
686
+ return False
687
+ easyrip_web.run_server(*_params)
521
688
 
522
689
  case Cmd_type.config:
523
690
  match cmd_list[1]:
524
- case "clear" | "clean" | "reset" | "regenerate":
691
+ case "list" | "":
692
+ config.show_config_list()
693
+ case "regenerate" | "clear" | "clean":
525
694
  config.regenerate_config()
526
695
  init()
527
696
  case "open":
528
697
  config.open_config_dir()
529
698
  case "set":
699
+ _key = cmd_list[2]
530
700
  _val = cmd_list[3]
701
+
702
+ if (_old_val := config.get_user_profile(_key)) is None:
703
+ return False
704
+
531
705
  try:
532
- _val = int(_val)
533
- except ValueError:
706
+ _val = ast.literal_eval(_val)
707
+ except (ValueError, SyntaxError):
534
708
  pass
535
- try:
536
- _val = float(_val)
537
- except ValueError:
538
- pass
539
- match _val:
540
- case "true" | "True":
541
- _val = True
542
- case "false" | "False":
543
- _val = False
544
709
 
545
- if (_old_val := config.get_user_profile(cmd_list[2])) == _val:
710
+ if _old_val == _val:
546
711
  log.info(
547
712
  "The new value is the same as the old value, cancel the modification",
548
713
  )
549
- elif config.set_user_profile(cmd_list[2], _val):
714
+ elif config.set_user_profile(_key, _val):
550
715
  init()
551
716
  log.info(
552
- "'config set {}' successful: {} -> {}",
553
- cmd_list[2],
554
- _old_val,
555
- _val,
717
+ "'{}' successfully: {}",
718
+ f"config set {_key}",
719
+ f"{f'"{_old_val}"' if isinstance(_old_val, str) else _old_val} -> {f'"{_val}"' if isinstance(_val, str) else _val}"
720
+ + (
721
+ ""
722
+ if type(_old_val) is type(_val)
723
+ else f" ({type(_old_val).__name__} -> {type(_val).__name__})"
724
+ ),
556
725
  )
557
- case "list":
558
- config.show_config_list()
559
726
  case _ as param:
560
727
  log.error("Unsupported param: {}", param)
561
728
  return False
562
729
 
730
+ case Cmd_type.prompt:
731
+ match cmd_list[1]:
732
+ case "history":
733
+ with easyrip_prompt.PROMPT_HISTORY_FILE.open(
734
+ "rt", encoding="utf-8"
735
+ ) as f:
736
+ for line in f.read().splitlines():
737
+ log.send(line, is_format=False)
738
+ case "clear" | "clean":
739
+ easyrip_prompt.clear()
740
+
563
741
  case Cmd_type.translate:
564
742
  if not (_infix := cmd_list[1]):
565
743
  log.error("Need target infix")
@@ -604,22 +782,32 @@ def run_command(command: list[str] | str) -> bool:
604
782
  return True
605
783
 
606
784
  case _:
785
+ if shutil.which(cmd_list[0]):
786
+ if easyrip_web.http_server.Event.is_run_command:
787
+ log.error("Disable the use of '{}' on the web", cmd_list[0])
788
+ return False
789
+
790
+ os.system(command if isinstance(command, str) else " ".join(command))
791
+ return True
792
+
607
793
  input_pathname_org_list: list[str] = []
608
- output_basename = None
609
- output_dir = None
610
- preset_name = None
794
+ output_basename: str | None = None
795
+ output_dir: str | None = None
796
+ preset_name: str | Ripper.Preset_name | None = None
611
797
  option_map: dict[str, str] = {}
612
- is_run = False
613
- is_exit_when_run_finished = False
798
+ is_run: bool = False
799
+ web_server_params = None
800
+ is_exit_when_run_finished: bool = False
614
801
  shutdown_sec_str: str | None = None
802
+ enable_multithreading: bool = False
615
803
 
616
- _skip: bool = False
804
+ _skip: int = 0
617
805
  for i in range(len(cmd_list)):
618
806
  if _skip:
619
- _skip = False
807
+ _skip -= 1
620
808
  continue
621
809
 
622
- _skip = True
810
+ _skip += 1
623
811
 
624
812
  match cmd_list[i]:
625
813
  case "-i":
@@ -631,7 +819,9 @@ def run_command(command: list[str] | str) -> bool:
631
819
  )
632
820
  return False
633
821
  input_pathname_org_list += file_dialog(
634
- os.getcwd() if fd_param == "cfd" else None
822
+ initialdir=(
823
+ os.getcwd() if fd_param == "cfd" else None
824
+ )
635
825
  )
636
826
  case _:
637
827
  input_pathname_org_list += [
@@ -674,18 +864,51 @@ def run_command(command: list[str] | str) -> bool:
674
864
  is_exit_when_run_finished = True
675
865
  case "shutdown":
676
866
  shutdown_sec_str = cmd_list[i + 2] or "60"
867
+ _skip += 1
868
+ case "server":
869
+ web_server_params = get_web_server_params(
870
+ cmd_list[i + 2]
871
+ )
872
+ if web_server_params is False:
873
+ return False
874
+ _skip += 1
875
+ case _:
876
+ _skip -= 1
877
+
878
+ case "-multithreading":
879
+ match cmd_list[i + 1]:
880
+ case "0":
881
+ enable_multithreading = False
882
+ case "1":
883
+ enable_multithreading = True
677
884
  case _:
678
- _skip = False
885
+ log.error("Unsupported param: {}", cmd_list[i + 1])
886
+ return False
679
887
 
680
888
  case str() as s if len(s) > 1 and s.startswith("-"):
681
889
  option_map[s[1:]] = cmd_list[i + 1]
682
890
 
683
891
  case _:
684
- _skip = False
892
+ _skip -= 1
685
893
 
686
894
  if not preset_name:
687
895
  log.warning("Missing '-preset' option, set to default value 'custom'")
688
896
  preset_name = "custom"
897
+ if preset_name not in Ripper.Preset_name._value2member_map_:
898
+ log.error("'{}' is not a member of preset", preset_name)
899
+ return False
900
+
901
+ preset_name = Ripper.Preset_name(preset_name)
902
+
903
+ if (
904
+ preset_name is Ripper.Preset_name.custom
905
+ and easyrip_web.http_server.Event.is_run_command
906
+ ):
907
+ log.error(
908
+ "Disable the use of '{}' on the web",
909
+ f"-preset {Ripper.Preset_name.custom}",
910
+ )
911
+ return False
689
912
 
690
913
  try:
691
914
  if len(input_pathname_org_list) == 0:
@@ -695,17 +918,21 @@ def run_command(command: list[str] | str) -> bool:
695
918
  for i, input_pathname in enumerate(input_pathname_org_list):
696
919
  new_option_map = option_map.copy()
697
920
 
698
- def _iterator_fmt_replace(match: re.Match[str]):
699
- s = match.group(1)
700
- match s:
701
- case str() as s if s.startswith("time:"):
702
- try:
703
- return _time.strftime(s[5:])
704
- except Exception as e:
705
- log.error(f"{e!r} {e}", deep=True)
706
- return ""
707
- case _:
708
- try:
921
+ fmt_time = datetime.now()
922
+
923
+ def _create_iterator_fmt_replace(
924
+ time: datetime, num: int
925
+ ) -> Callable[[re.Match[str]], str]:
926
+ def _iterator_fmt_replace(match: re.Match[str]) -> str:
927
+ s = match.group(1)
928
+ match s:
929
+ case str() as s if s.startswith("time:"):
930
+ try:
931
+ return time.strftime(s[5:])
932
+ except Exception as e:
933
+ log.error(f"{e!r} {e}", deep=True)
934
+ return ""
935
+ case _:
709
936
  d = {
710
937
  k: v
711
938
  for s1 in s.split(",")
@@ -714,35 +941,38 @@ def run_command(command: list[str] | str) -> bool:
714
941
  start = int(d.get("start", 0))
715
942
  padding = int(d.get("padding", 0))
716
943
  increment = int(d.get("increment", 1))
717
- return str(start + i * increment).zfill(padding)
718
- except Exception as e:
719
- log.error(f"{e!r} {e}", deep=True)
720
- return ""
944
+ return str(start + num * increment).zfill(padding)
721
945
 
722
- if output_basename is None:
723
- new_output_basename = None
724
- else:
725
- _time = datetime.now()
946
+ return _iterator_fmt_replace
726
947
 
727
- new_output_basename = re.sub(
728
- r"\?\{([^}]*)\}",
729
- _iterator_fmt_replace,
730
- output_basename,
731
- )
732
-
733
- if chapters := option_map.get("chapters"):
734
- chapters = re.sub(
735
- r"\?\{([^}]*)\}",
736
- _iterator_fmt_replace,
737
- chapters,
948
+ try:
949
+ new_output_basename = (
950
+ None
951
+ if output_basename is None
952
+ else re.sub(
953
+ r"\?\{([^}]*)\}",
954
+ _create_iterator_fmt_replace(fmt_time, i),
955
+ output_basename,
956
+ )
738
957
  )
739
958
 
740
- if not Path(chapters).is_file():
741
- log.warning(
742
- "The '-chapters' file {} does not exist", chapters
959
+ if chapters := option_map.get("chapters"):
960
+ chapters = re.sub(
961
+ r"\?\{([^}]*)\}",
962
+ _create_iterator_fmt_replace(fmt_time, i),
963
+ chapters,
743
964
  )
744
965
 
745
- new_option_map["chapters"] = chapters
966
+ if not Path(chapters).is_file():
967
+ log.warning(
968
+ "The '-chapters' file {} does not exist", chapters
969
+ )
970
+
971
+ new_option_map["chapters"] = chapters
972
+
973
+ except ValueError as e:
974
+ log.error("Unsupported param: {}", e)
975
+ return False
746
976
 
747
977
  input_pathname_list: list[str] = input_pathname.split("?")
748
978
  for path in input_pathname_list:
@@ -801,7 +1031,7 @@ def run_command(command: list[str] | str) -> bool:
801
1031
  f"{new_output_basename or _input_basename[0]}{_output_base_suffix_name}"
802
1032
  ],
803
1033
  output_dir,
804
- Ripper.PresetName(preset_name),
1034
+ preset_name,
805
1035
  new_option_map,
806
1036
  )
807
1037
 
@@ -818,7 +1048,7 @@ def run_command(command: list[str] | str) -> bool:
818
1048
  input_pathname_list,
819
1049
  [new_output_basename],
820
1050
  output_dir,
821
- Ripper.PresetName(preset_name),
1051
+ preset_name,
822
1052
  new_option_map,
823
1053
  )
824
1054
 
@@ -827,7 +1057,7 @@ def run_command(command: list[str] | str) -> bool:
827
1057
  input_pathname_list,
828
1058
  [new_output_basename],
829
1059
  output_dir,
830
- Ripper.PresetName(preset_name),
1060
+ preset_name,
831
1061
  new_option_map,
832
1062
  )
833
1063
 
@@ -836,38 +1066,56 @@ def run_command(command: list[str] | str) -> bool:
836
1066
  return False
837
1067
 
838
1068
  if is_run:
839
- run_ripper_list(is_exit_when_run_finished, shutdown_sec_str)
1069
+ if web_server_params is None:
1070
+ run_ripper_list(
1071
+ is_exit_when_run_finished=is_exit_when_run_finished,
1072
+ shutdow_sec_str=shutdown_sec_str,
1073
+ enable_multithreading=enable_multithreading,
1074
+ )
1075
+ else:
1076
+ easyrip_web.run_server(
1077
+ *web_server_params,
1078
+ after_start_server_hook=lambda: run_ripper_list(
1079
+ is_exit_when_run_finished=is_exit_when_run_finished,
1080
+ shutdow_sec_str=shutdown_sec_str,
1081
+ enable_multithreading=enable_multithreading,
1082
+ ),
1083
+ )
840
1084
 
841
1085
  return True
842
1086
 
843
1087
 
844
- def init(is_first_run: bool = False):
1088
+ def init(is_first_run: bool = False) -> None:
845
1089
  if is_first_run:
846
1090
  # 当前路径添加到环境变量
847
1091
  new_path = os.path.realpath(os.getcwd())
848
- if os.pathsep in (current_path := os.environ.get("PATH", "")):
849
- if new_path not in current_path.split(os.pathsep):
850
- updated_path = f"{new_path}{os.pathsep}{current_path}"
851
- os.environ["PATH"] = updated_path
1092
+ if os.pathsep in (
1093
+ current_path := os.environ.get("PATH", "")
1094
+ ) and new_path not in current_path.split(os.pathsep):
1095
+ updated_path = f"{new_path}{os.pathsep}{current_path}"
1096
+ os.environ["PATH"] = updated_path
852
1097
 
853
1098
  # 设置语言
854
1099
  _sys_lang = get_system_language()
855
1100
  Global_lang_val.gettext_target_lang = _sys_lang
856
- if (_lang_config := config.get_user_profile("language")) not in {"auto", None}:
1101
+ if (_lang_config := config.get_user_profile(Config_key.language)) not in {
1102
+ "auto",
1103
+ None,
1104
+ }:
857
1105
  Global_lang_val.gettext_target_lang = Lang_tag.from_str(str(_lang_config))
858
1106
 
859
1107
  # 设置日志文件路径名
860
- log.html_filename = gettext("encoding_log.html")
861
- if _path := str(config.get_user_profile("force_log_file_path") or ""):
1108
+ log.html_filename = gettext(log.html_filename)
1109
+ if _path := str(config.get_user_profile(Config_key.force_log_file_path) or ""):
862
1110
  log.html_filename = os.path.join(_path, log.html_filename)
863
1111
 
864
1112
  # 设置日志级别
865
1113
  try:
866
1114
  log.print_level = getattr(
867
- log.LogLevel, str(config.get_user_profile("log_print_level"))
1115
+ log.LogLevel, str(config.get_user_profile(Config_key.log_print_level))
868
1116
  )
869
1117
  log.write_level = getattr(
870
- log.LogLevel, str(config.get_user_profile("log_write_level"))
1118
+ log.LogLevel, str(config.get_user_profile(Config_key.log_write_level))
871
1119
  )
872
1120
  except Exception as e:
873
1121
  log.error(f"{e!r} {e}", deep=True)
@@ -875,8 +1123,18 @@ def init(is_first_run: bool = False):
875
1123
  if is_first_run:
876
1124
  # 设置启动目录
877
1125
  try:
878
- if _path := str(config.get_user_profile("startup_directory")):
879
- os.chdir(_path)
1126
+ if _startup_dir := config.get_user_profile(Config_key.startup_dir):
1127
+ if _startup_dir_blacklist := config.get_user_profile(
1128
+ Config_key.startup_dir_blacklist
1129
+ ):
1130
+ if any(
1131
+ Path.cwd().samefile(d)
1132
+ for d in map(Path, _startup_dir_blacklist)
1133
+ if d.is_dir()
1134
+ ):
1135
+ os.chdir(_startup_dir)
1136
+ else:
1137
+ os.chdir(_startup_dir)
880
1138
  except Exception as e:
881
1139
  log.error(f"{e!r} {e}", deep=True)
882
1140
 
@@ -900,9 +1158,7 @@ def init(is_first_run: bool = False):
900
1158
  if (
901
1159
  lang_tag := Lang_tag.from_str(file.stem[5:])
902
1160
  ).language is not Lang_tag_language.Unknown:
903
- easyrip_mlang.all_supported_lang_map[lang_tag] = {
904
- k: v for k, v in lang_map.items()
905
- }
1161
+ easyrip_mlang.all_supported_lang_map[lang_tag] = lang_map
906
1162
 
907
1163
  log.debug("Loading \"{}\" as '{}' language successfully", file, lang_tag)
908
1164
 
@@ -911,11 +1167,5 @@ def init(is_first_run: bool = False):
911
1167
  Thread(target=check_env, daemon=True).start()
912
1168
 
913
1169
  LogEvent.append_http_server_log_queue = (
914
- lambda message: easyrip_web.http_server.Event.log_queue.append(message)
1170
+ easyrip_web.http_server.Event.log_queue.append
915
1171
  )
916
-
917
- def _post_run_event(cmd: str):
918
- run_command(cmd)
919
- easyrip_web.http_server.Event.is_run_command = False
920
-
921
- easyrip_web.http_server.Event.post_run_event = _post_run_event