easyrip 4.1.0__py3-none-any.whl → 4.3.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.
easyrip/__main__.py CHANGED
@@ -6,9 +6,10 @@ import Crypto
6
6
  import fontTools
7
7
  import prompt_toolkit
8
8
  from prompt_toolkit import ANSI, prompt
9
- from prompt_toolkit.completion import FuzzyCompleter, PathCompleter, merge_completers
9
+ from prompt_toolkit.completion import PathCompleter, merge_completers
10
10
  from prompt_toolkit.history import InMemoryHistory
11
- from prompt_toolkit.key_binding import KeyBindings
11
+ from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
12
+ from prompt_toolkit.keys import Keys
12
13
 
13
14
  from .easyrip_command import (
14
15
  Cmd_type,
@@ -18,7 +19,10 @@ from .easyrip_command import (
18
19
  OptCompleter,
19
20
  nested_dict,
20
21
  )
22
+ from .easyrip_config.config import Config_key, config
21
23
  from .easyrip_main import Ripper, get_input_prompt, init, log, run_command
24
+ from .easyrip_prompt import ConfigFileHistory, easyrip_prompt
25
+ from .global_val import C_D, C_Z
22
26
 
23
27
 
24
28
  def run() -> NoReturn:
@@ -36,11 +40,15 @@ def run() -> NoReturn:
36
40
 
37
41
  key_bindings = KeyBindings()
38
42
 
39
- @key_bindings.add("c-d")
40
- def _(event) -> None:
41
- event.app.current_buffer.insert_text("\x04") # ^D
43
+ @key_bindings.add(Keys.ControlC)
44
+ def _(event: KeyPressEvent) -> None:
45
+ event.app.exit(exception=KeyboardInterrupt, style="class:exiting")
42
46
 
43
- path_completer = FuzzyCompleter(PathCompleter())
47
+ @key_bindings.add(Keys.ControlD)
48
+ def _(event: KeyPressEvent) -> None:
49
+ event.app.current_buffer.insert_text(C_D)
50
+
51
+ path_completer = PathCompleter()
44
52
 
45
53
  def _ctv_to_nc(ctvs: Iterable[Cmd_type_val]) -> CmdCompleter:
46
54
  return CmdCompleter(
@@ -92,7 +100,11 @@ def run() -> NoReturn:
92
100
  }
93
101
 
94
102
  cmd_ctv_tuple = tuple(ct.value for ct in Cmd_type if ct != Cmd_type.Option)
95
- prompt_history = InMemoryHistory()
103
+ prompt_history = (
104
+ ConfigFileHistory(easyrip_prompt.PROMPT_HISTORY_FILE)
105
+ if config.get_user_profile(Config_key.prompt_history_save_file)
106
+ else InMemoryHistory()
107
+ )
96
108
  while True:
97
109
  try:
98
110
  command = prompt(
@@ -107,7 +119,7 @@ def run() -> NoReturn:
107
119
  history=prompt_history,
108
120
  complete_while_typing=True,
109
121
  )
110
- if command.startswith("\x1a"): # ^Z
122
+ if command.startswith(C_Z):
111
123
  raise EOFError
112
124
  except KeyboardInterrupt:
113
125
  continue
@@ -3,10 +3,11 @@ import itertools
3
3
  import textwrap
4
4
  from collections.abc import Iterable
5
5
  from dataclasses import dataclass
6
- from typing import Final, Self, TypeAlias, final
6
+ from typing import Final, Self, final
7
7
 
8
8
  from prompt_toolkit.completion import (
9
9
  Completer,
10
+ DeduplicateCompleter,
10
11
  FuzzyCompleter,
11
12
  FuzzyWordCompleter,
12
13
  NestedCompleter,
@@ -17,6 +18,7 @@ from prompt_toolkit.completion.base import CompleteEvent, Completion
17
18
  from prompt_toolkit.document import Document
18
19
 
19
20
  from . import global_val
21
+ from .easyrip_config.config_key import Config_key
20
22
 
21
23
 
22
24
  @final
@@ -175,19 +177,22 @@ class Cmd_type(enum.Enum):
175
177
  )
176
178
  run = Cmd_type_val(
177
179
  ("run",),
178
- param="[<run option>]",
180
+ param="[<run option>] [-multithreading <0 | 1>]",
179
181
  description=(
180
182
  "Run the Ripper in the Ripper list\n"
181
- " \n"
183
+ "\n"
182
184
  "Default:\n"
183
185
  " Only run\n"
184
- " \n"
186
+ "\n"
185
187
  "exit:\n"
186
188
  " Close program when run finished\n"
187
- " \n"
189
+ "\n"
188
190
  "shutdown [<sec>]:\n"
189
191
  " Shutdown when run finished\n"
190
- " Default: 60"
192
+ " Default: 60\n"
193
+ "\n"
194
+ "server [<address>]:[<port>]@[<password>]:\n"
195
+ " See the corresponding help for details"
191
196
  ),
192
197
  childs=(
193
198
  Cmd_type_val(("exit",)),
@@ -196,22 +201,18 @@ class Cmd_type(enum.Enum):
196
201
  )
197
202
  server = Cmd_type_val(
198
203
  ("server",),
199
- param="[[-a | -address] <address>[:<port>] [[-p | -password] <password>]]",
204
+ param="[<address>]:[<port>]@[<password>]",
200
205
  description=(
201
206
  "Boot web service\n"
202
207
  "Default: server localhost:0\n"
203
208
  "Client send command 'kill' can exit Ripper's run, note that FFmpeg needs to accept multiple ^C signals to forcibly terminate, and a single ^C signal will wait for the file output to be complete before terminating"
204
209
  ),
205
- childs=(
206
- Cmd_type_val(("-a", "-address")),
207
- Cmd_type_val(("-p", "-password")),
208
- ),
209
210
  )
210
211
  config = Cmd_type_val(
211
212
  ("config",),
212
213
  param="<config option>",
213
214
  description=(
214
- "regenerate | clear | clean | reset\n"
215
+ "regenerate | clear | clean\n"
215
216
  " Regenerate config file\n"
216
217
  "open\n"
217
218
  " Open the directory where the config file is located\n"
@@ -222,10 +223,27 @@ class Cmd_type(enum.Enum):
222
223
  " e.g. config set language zh"
223
224
  ),
224
225
  childs=(
225
- Cmd_type_val(("regenerate", "clear", "clean", "reset")),
226
+ Cmd_type_val(("regenerate", "clear", "clean")),
226
227
  Cmd_type_val(("open",)),
227
228
  Cmd_type_val(("list",)),
228
- Cmd_type_val(("set",)),
229
+ Cmd_type_val(
230
+ ("set",),
231
+ childs=tuple(Cmd_type_val((k,)) for k in Config_key._value2member_map_),
232
+ ),
233
+ ),
234
+ )
235
+ prompt = Cmd_type_val(
236
+ ("prompt",),
237
+ param="<prompt option>",
238
+ description=(
239
+ "history\n" # .
240
+ " Show prompt history\n"
241
+ "clear | clean\n"
242
+ " Delete history file"
243
+ ),
244
+ childs=(
245
+ Cmd_type_val(("history",)),
246
+ Cmd_type_val(("clear", "clean")),
229
247
  ),
230
248
  )
231
249
  translate = Cmd_type_val(
@@ -545,16 +563,19 @@ class Opt_type(enum.Enum):
545
563
  param="[<string>]",
546
564
  description=(
547
565
  "Run the Ripper from the Ripper list\n"
548
- " \n"
566
+ "\n"
549
567
  "Default:\n"
550
568
  " Only run\n"
551
- " \n"
569
+ "\n"
552
570
  "exit:\n"
553
571
  " Close program when run finished\n"
554
- " \n"
572
+ "\n"
555
573
  "shutdown [<sec>]:\n"
556
574
  " Shutdown when run finished\n"
557
575
  " Default: 60\n"
576
+ "\n"
577
+ "server [<address>]:[<port>]@[<password>]:\n"
578
+ " See the corresponding help for details"
558
579
  ),
559
580
  childs=(
560
581
  Cmd_type_val(("exit",)),
@@ -665,7 +686,7 @@ def get_help_doc() -> str:
665
686
  )
666
687
 
667
688
 
668
- nested_dict: TypeAlias = dict[str, "nested_dict | Completer"]
689
+ type nested_dict = dict[str, "nested_dict | Completer"]
669
690
  META_DICT_OPT_TYPE = {
670
691
  name: opt.value.param for opt in Opt_type for name in opt.value.names
671
692
  }
@@ -746,7 +767,17 @@ class OptCompleter(Completer):
746
767
  )
747
768
  )
748
769
 
749
- if isinstance(opt_tree_pos_list[-1], Completer):
770
+ if opt_tree_pos_list[-1] is not self.opt_tree and not text.endswith(" "):
771
+ yield from (
772
+ Completion(
773
+ text=words[-1],
774
+ start_position=-len(words[-1]),
775
+ display_meta=META_DICT_OPT_TYPE.get(words[-1], ""),
776
+ ),
777
+ Completion(text="", display="✔"),
778
+ )
779
+
780
+ elif isinstance(opt_tree_pos_list[-1], Completer):
750
781
  # 直接使用 PathCompleter 会因为上下文问题失效,所以将上文套进 NestedCompleter
751
782
  new_nd: nested_dict = {}
752
783
  new_nd_pos: nested_dict = new_nd
@@ -767,11 +798,22 @@ class OptCompleter(Completer):
767
798
 
768
799
  yield from merge_completers(
769
800
  (
770
- _nested_dict_to_nc(new_nd),
771
- FuzzyWordCompleter(
772
- words=tuple(opt_tree_pos_list[-1]),
773
- meta_dict=META_DICT_OPT_TYPE,
774
- WORD=True,
801
+ DeduplicateCompleter(
802
+ merge_completers(
803
+ (
804
+ _nested_dict_to_nc(new_nd),
805
+ FuzzyCompleter(_nested_dict_to_nc(new_nd), WORD=True),
806
+ )
807
+ )
808
+ ),
809
+ FuzzyCompleter(
810
+ WordCompleter(
811
+ words=tuple(opt_tree_pos_list[-1]),
812
+ meta_dict=META_DICT_OPT_TYPE,
813
+ WORD=True, # 匹配标点
814
+ match_middle=True,
815
+ ),
816
+ WORD=False,
775
817
  ),
776
818
  )
777
819
  ).get_completions(document=document, complete_event=complete_event)
@@ -779,10 +821,16 @@ class OptCompleter(Completer):
779
821
  else:
780
822
  yield from FuzzyCompleter(
781
823
  WordCompleter(
782
- words=tuple(opt_tree_pos_list[-1]),
824
+ words=tuple(
825
+ opt_tree_pos_list[-1] | {}
826
+ if text.endswith(" ")
827
+ or len(words) <= 1
828
+ or isinstance(opt_tree_pos_list[-2], Completer)
829
+ else opt_tree_pos_list[-2]
830
+ ),
783
831
  meta_dict=META_DICT_OPT_TYPE,
784
- WORD=True,
832
+ WORD=True, # 匹配标点
785
833
  match_middle=True,
786
834
  ),
787
- WORD=False,
835
+ WORD=not text.endswith(" "),
788
836
  ).get_completions(document=document, complete_event=complete_event)
@@ -1,26 +1,11 @@
1
- import enum
2
1
  import json
3
2
  import os
4
- import sys
5
3
  from pathlib import Path
6
4
 
7
- from . import global_val
8
- from .easyrip_log import log
9
- from .easyrip_mlang import all_supported_lang_map, gettext
10
-
11
- PROJECT_NAME = global_val.PROJECT_NAME
12
- CONFIG_VERSION = "2.9.4"
13
-
14
-
15
- class Config_key(enum.Enum):
16
- language = "language"
17
- check_update = "check_update"
18
- check_dependent = "check_dependent"
19
- startup_directory = "startup_directory"
20
- force_log_file_path = "force_log_file_path"
21
- log_print_level = "log_print_level"
22
- log_write_level = "log_write_level"
23
-
5
+ from ..easyrip_log import log
6
+ from ..easyrip_mlang import all_supported_lang_map, gettext
7
+ from ..global_val import CONFIG_DIR
8
+ from .config_key import CONFIG_VERSION, Config_key
24
9
 
25
10
  CONFIG_DEFAULT_DICT: dict[Config_key, str | bool] = {
26
11
  Config_key.language: "auto",
@@ -30,6 +15,7 @@ CONFIG_DEFAULT_DICT: dict[Config_key, str | bool] = {
30
15
  Config_key.force_log_file_path: "",
31
16
  Config_key.log_print_level: log.LogLevel.send.name,
32
17
  Config_key.log_write_level: log.LogLevel.send.name,
18
+ Config_key.prompt_history_save_file: True,
33
19
  }
34
20
 
35
21
 
@@ -40,19 +26,8 @@ class config:
40
26
 
41
27
  @classmethod
42
28
  def init(cls) -> None:
43
- if sys.platform == "win32":
44
- # Windows: C:\Users\<用户名>\AppData\Roaming\<app_name>
45
- cls._config_dir = Path(os.getenv("APPDATA", ""))
46
- elif sys.platform == "darwin":
47
- # macOS: ~/Library/Application Support/<app_name>
48
- cls._config_dir = (
49
- Path(os.path.expanduser("~")) / "Library" / "Application Support"
50
- )
51
- else:
52
- # Linux: ~/.config/<app_name>
53
- cls._config_dir = Path(os.path.expanduser("~")) / ".config"
54
- cls._config_dir = Path(cls._config_dir) / PROJECT_NAME
55
- cls._config_file = Path(cls._config_dir) / "config.json"
29
+ cls._config_dir = CONFIG_DIR
30
+ cls._config_file = cls._config_dir / "config.json"
56
31
 
57
32
  if not cls._config_file.is_file():
58
33
  cls._config_dir.mkdir(exist_ok=True)
@@ -149,7 +124,11 @@ class config:
149
124
  return cls._write_config()
150
125
 
151
126
  @classmethod
152
- def get_user_profile(cls, key: str) -> str | int | float | bool | None:
127
+ def get_user_profile(
128
+ cls, config_key: Config_key | str
129
+ ) -> str | int | float | bool | None:
130
+ key = config_key.value if isinstance(config_key, Config_key) else config_key
131
+
153
132
  if cls._config is None:
154
133
  cls._read_config()
155
134
  if cls._config is None:
@@ -216,6 +195,10 @@ class config:
216
195
  CONFIG_DEFAULT_DICT[Config_key.log_write_level],
217
196
  ", ".join(log.LogLevel._member_names_),
218
197
  ),
198
+ Config_key.prompt_history_save_file.value: gettext(
199
+ "Save prompt history to config directory, otherwise save to memory. Take effect after reboot. Default: {}",
200
+ CONFIG_DEFAULT_DICT[Config_key.prompt_history_save_file],
201
+ ),
219
202
  }
220
203
  | (cls._config or {})
221
204
  ).get(key, "None about")
@@ -0,0 +1,14 @@
1
+ import enum
2
+
3
+ CONFIG_VERSION = "4.2.0"
4
+
5
+
6
+ class Config_key(enum.Enum):
7
+ language = "language"
8
+ check_update = "check_update"
9
+ check_dependent = "check_dependent"
10
+ startup_directory = "startup_directory"
11
+ force_log_file_path = "force_log_file_path"
12
+ log_print_level = "log_print_level"
13
+ log_write_level = "log_write_level"
14
+ prompt_history_save_file = "prompt_history_file"
easyrip/easyrip_main.py CHANGED
@@ -9,6 +9,7 @@ import subprocess
9
9
  import sys
10
10
  import threading
11
11
  import tkinter as tk
12
+ import tomllib
12
13
  from collections.abc import Callable
13
14
  from concurrent.futures import ThreadPoolExecutor
14
15
  from datetime import datetime
@@ -17,13 +18,11 @@ from pathlib import Path
17
18
  from threading import Thread
18
19
  from time import sleep
19
20
  from tkinter import filedialog
20
- from typing import Final
21
-
22
- import tomllib
21
+ from typing import Final, Literal
23
22
 
24
23
  from . import easyrip_mlang, easyrip_web, global_val
25
24
  from .easyrip_command import Cmd_type, Opt_type, get_help_doc
26
- from .easyrip_config import config
25
+ from .easyrip_config.config import Config_key, config
27
26
  from .easyrip_log import Event as LogEvent
28
27
  from .easyrip_log import log
29
28
  from .easyrip_mlang import (
@@ -34,6 +33,7 @@ from .easyrip_mlang import (
34
33
  gettext,
35
34
  translate_subtitles,
36
35
  )
36
+ from .easyrip_prompt import easyrip_prompt
37
37
  from .ripper import Media_info, Ripper
38
38
  from .ripper.ripper import DEFAULT_PRESET_PARAMS
39
39
  from .utils import change_title, check_ver, read_text
@@ -73,7 +73,7 @@ def check_env() -> None:
73
73
  try:
74
74
  change_title(f"{gettext('Check env...')} {PROJECT_TITLE}")
75
75
 
76
- if config.get_user_profile("check_dependent"):
76
+ if config.get_user_profile(Config_key.check_dependent):
77
77
  _url = "https://ffmpeg.org/download.html"
78
78
  for _name in ("FFmpeg", "FFprobe"):
79
79
  if not shutil.which(_name):
@@ -198,7 +198,7 @@ def check_env() -> None:
198
198
  # ).stdout:
199
199
  # log.error("The MediaInfo must be CLI ver")
200
200
 
201
- if config.get_user_profile("check_update"):
201
+ if config.get_user_profile(Config_key.check_update):
202
202
  log_new_ver(
203
203
  easyrip_web.github.get_latest_release_ver(
204
204
  global_val.PROJECT_RELEASE_API
@@ -369,6 +369,36 @@ def run_ripper_list(
369
369
  log.info("Run completed")
370
370
 
371
371
 
372
+ def get_web_server_params(
373
+ opt: str,
374
+ ) -> Literal[False] | tuple[str, int, str | None]:
375
+ """[<address>]:[<port>]@[<password>]"""
376
+ if easyrip_web.http_server.Event.is_run_command:
377
+ log.error("Can not start multiple services")
378
+ return False
379
+
380
+ if ":" not in opt:
381
+ log.error("{} param illegal", f"':' not in '{opt}':")
382
+ return False
383
+
384
+ opt_list: list[str] = opt.split(":")
385
+ opt_list = [opt_list[0], *opt_list[1].split("@")]
386
+
387
+ if len(opt_list) != 3:
388
+ log.error("{} param illegal", f"len('{opt}') != 3:")
389
+ return False
390
+
391
+ host, port, password = opt_list
392
+
393
+ port = port or "0"
394
+
395
+ if not port.isdigit():
396
+ log.error("{} param illegal", f"The port in '{opt}' not a digit:")
397
+ return False
398
+
399
+ return (host or "", int(port), password)
400
+
401
+
372
402
  def run_command(command: list[str] | str) -> bool:
373
403
  if isinstance(command, list):
374
404
  cmd_list = command
@@ -564,8 +594,12 @@ def run_command(command: list[str] | str) -> bool:
564
594
  log.error("list index out of range")
565
595
 
566
596
  case Cmd_type.run:
567
- is_run_exit = False
597
+ is_run_exit: bool = False
598
+
599
+ _web_server_params = None
600
+
568
601
  _enable_multithreading: bool = False
602
+
569
603
  _shutdown_sec_str: str | None = None
570
604
 
571
605
  _skip_run_param: int = 0
@@ -594,6 +628,13 @@ def run_command(command: list[str] | str) -> bool:
594
628
  log.error("{} need param", cmd)
595
629
  return False
596
630
 
631
+ case "server":
632
+ _skip_run_param += 1
633
+ if (
634
+ _web_server_params := get_web_server_params(cmd_list[2])
635
+ ) is False:
636
+ return False
637
+
597
638
  case "-multithreading":
598
639
  _skip_run_param += 1
599
640
  if i + 1 < len(cmd_list[1:]):
@@ -606,58 +647,32 @@ def run_command(command: list[str] | str) -> bool:
606
647
  log.error("Unsupported param: {}", param)
607
648
  return False
608
649
 
609
- run_ripper_list(
610
- is_exit_when_run_finished=is_run_exit,
611
- shutdow_sec_str=_shutdown_sec_str,
612
- enable_multithreading=_enable_multithreading,
613
- )
614
-
615
- case Cmd_type.server:
616
- if easyrip_web.http_server.Event.is_run_command:
617
- log.error("Can not start multiple services")
618
- return False
619
-
620
- address, password = None, None
621
-
622
- for i in range(1, len(cmd_list)):
623
- match cmd_list[i]:
624
- case "-a" | "-address":
625
- address = cmd_list[i + 1]
626
- case "-p" | "-password":
627
- password = cmd_list[i + 1]
628
- case _:
629
- if address is None:
630
- address = cmd_list[i]
631
- elif password is None:
632
- password = cmd_list[i]
633
- if address:
634
- res = re.match(
635
- r"^([a-zA-Z0-9.-]+)(:(\d+))?$",
636
- address,
650
+ if _web_server_params is None:
651
+ run_ripper_list(
652
+ is_exit_when_run_finished=is_run_exit,
653
+ shutdow_sec_str=_shutdown_sec_str,
654
+ enable_multithreading=_enable_multithreading,
637
655
  )
638
- if res:
639
- host = res.group(1)
640
- port = res.group(2)
641
- if port:
642
- port = int(port.lstrip(":"))
643
- elif host.isdigit():
644
- port = int(host)
645
- host = None
646
- else:
647
- port = None
648
- host = None
649
- else:
650
- host, port = "localhost", 0
651
656
  else:
652
- host, port = "localhost", 0
657
+ easyrip_web.run_server(
658
+ *_web_server_params,
659
+ after_start_server_hook=lambda: run_ripper_list(
660
+ is_exit_when_run_finished=is_run_exit,
661
+ shutdow_sec_str=_shutdown_sec_str,
662
+ enable_multithreading=_enable_multithreading,
663
+ ),
664
+ )
653
665
 
654
- easyrip_web.run_server(host=host or "", port=port or 0, password=password)
666
+ case Cmd_type.server:
667
+ if (_params := get_web_server_params(cmd_list[1])) is False:
668
+ return False
669
+ easyrip_web.run_server(*_params)
655
670
 
656
671
  case Cmd_type.config:
657
672
  match cmd_list[1]:
658
673
  case "list" | "":
659
674
  config.show_config_list()
660
- case "clear" | "clean" | "reset" | "regenerate":
675
+ case "regenerate" | "clear" | "clean":
661
676
  config.regenerate_config()
662
677
  init()
663
678
  case "open":
@@ -703,6 +718,17 @@ def run_command(command: list[str] | str) -> bool:
703
718
  log.error("Unsupported param: {}", param)
704
719
  return False
705
720
 
721
+ case Cmd_type.prompt:
722
+ match cmd_list[1]:
723
+ case "history":
724
+ with easyrip_prompt.PROMPT_HISTORY_FILE.open(
725
+ "rt", encoding="utf-8"
726
+ ) as f:
727
+ for line in f.read().splitlines():
728
+ log.send(line, is_format=False)
729
+ case "clear" | "clean":
730
+ easyrip_prompt.clear()
731
+
706
732
  case Cmd_type.translate:
707
733
  if not (_infix := cmd_list[1]):
708
734
  log.error("Need target infix")
@@ -761,6 +787,7 @@ def run_command(command: list[str] | str) -> bool:
761
787
  preset_name: str | Ripper.PresetName | None = None
762
788
  option_map: dict[str, str] = {}
763
789
  is_run: bool = False
790
+ web_server_params = None
764
791
  is_exit_when_run_finished: bool = False
765
792
  shutdown_sec_str: str | None = None
766
793
  enable_multithreading: bool = False
@@ -829,6 +856,13 @@ def run_command(command: list[str] | str) -> bool:
829
856
  case "shutdown":
830
857
  shutdown_sec_str = cmd_list[i + 2] or "60"
831
858
  _skip += 1
859
+ case "server":
860
+ web_server_params = get_web_server_params(
861
+ cmd_list[i + 2]
862
+ )
863
+ if web_server_params is False:
864
+ return False
865
+ _skip += 1
832
866
  case _:
833
867
  _skip -= 1
834
868
 
@@ -1023,11 +1057,21 @@ def run_command(command: list[str] | str) -> bool:
1023
1057
  return False
1024
1058
 
1025
1059
  if is_run:
1026
- run_ripper_list(
1027
- is_exit_when_run_finished=is_exit_when_run_finished,
1028
- shutdow_sec_str=shutdown_sec_str,
1029
- enable_multithreading=enable_multithreading,
1030
- )
1060
+ if web_server_params is None:
1061
+ run_ripper_list(
1062
+ is_exit_when_run_finished=is_exit_when_run_finished,
1063
+ shutdow_sec_str=shutdown_sec_str,
1064
+ enable_multithreading=enable_multithreading,
1065
+ )
1066
+ else:
1067
+ easyrip_web.run_server(
1068
+ *web_server_params,
1069
+ after_start_server_hook=lambda: run_ripper_list(
1070
+ is_exit_when_run_finished=is_exit_when_run_finished,
1071
+ shutdow_sec_str=shutdown_sec_str,
1072
+ enable_multithreading=enable_multithreading,
1073
+ ),
1074
+ )
1031
1075
 
1032
1076
  return True
1033
1077
 
@@ -1045,7 +1089,10 @@ def init(is_first_run: bool = False) -> None:
1045
1089
  # 设置语言
1046
1090
  _sys_lang = get_system_language()
1047
1091
  Global_lang_val.gettext_target_lang = _sys_lang
1048
- if (_lang_config := config.get_user_profile("language")) not in {"auto", None}:
1092
+ if (_lang_config := config.get_user_profile(Config_key.language)) not in {
1093
+ "auto",
1094
+ None,
1095
+ }:
1049
1096
  Global_lang_val.gettext_target_lang = Lang_tag.from_str(str(_lang_config))
1050
1097
 
1051
1098
  # 设置日志文件路径名
@@ -1056,10 +1103,10 @@ def init(is_first_run: bool = False) -> None:
1056
1103
  # 设置日志级别
1057
1104
  try:
1058
1105
  log.print_level = getattr(
1059
- log.LogLevel, str(config.get_user_profile("log_print_level"))
1106
+ log.LogLevel, str(config.get_user_profile(Config_key.log_print_level))
1060
1107
  )
1061
1108
  log.write_level = getattr(
1062
- log.LogLevel, str(config.get_user_profile("log_write_level"))
1109
+ log.LogLevel, str(config.get_user_profile(Config_key.log_write_level))
1063
1110
  )
1064
1111
  except Exception as e:
1065
1112
  log.error(f"{e!r} {e}", deep=True)
@@ -1067,7 +1114,7 @@ def init(is_first_run: bool = False) -> None:
1067
1114
  if is_first_run:
1068
1115
  # 设置启动目录
1069
1116
  try:
1070
- if _path := str(config.get_user_profile("startup_directory")):
1117
+ if _path := str(config.get_user_profile(Config_key.startup_directory)):
1071
1118
  os.chdir(_path)
1072
1119
  except Exception as e:
1073
1120
  log.error(f"{e!r} {e}", deep=True)
@@ -71,21 +71,24 @@ LANG_MAP: dict[str, str] = {
71
71
  "<int> <int>:\n"
72
72
  " 交换指定索引"
73
73
  ),
74
- Cmd_type.run.value.param: "<run 选项>",
74
+ Cmd_type.run.value.param: "[<run 选项>] [-multithreading <0 | 1>]",
75
75
  Cmd_type.run.value.description: (
76
76
  "执行 Ripper list 中的 Ripper\n"
77
- " \n"
77
+ "\n"
78
78
  "默认:\n"
79
79
  " 仅执行\n"
80
- " \n"
80
+ "\n"
81
81
  "exit:\n"
82
82
  " 执行后退出程序\n"
83
- " \n"
83
+ "\n"
84
84
  "shutdown [<秒数>]:\n"
85
85
  " 执行后关机\n"
86
- " 默认: 60"
86
+ " 默认: 60\n"
87
+ "\n"
88
+ "server [<地址>]:[<端口>]@[<密码>]:\n"
89
+ " 详见对应的 help"
87
90
  ),
88
- Cmd_type.server.value.param: "[[-a | -address] <地址>[:<端口>] [[-p | -password] <密码>]]",
91
+ Cmd_type.server.value.param: "[<地址>]:[<端口>]@[<密码>]",
89
92
  Cmd_type.server.value.description: (
90
93
  "启动 web 服务\n"
91
94
  "默认: server localhost:0\n"
@@ -103,6 +106,7 @@ LANG_MAP: dict[str, str] = {
103
106
  " 设置 config\n"
104
107
  " 例如 config set language en"
105
108
  ),
109
+ Cmd_type.prompt.value.param: "<prompt 选项>",
106
110
  Cmd_type.translate.value.param: "<中缀> <目标语言标签> [-overwrite]",
107
111
  Cmd_type.translate.value.description: (
108
112
  "翻译字幕文件\n"
@@ -238,16 +242,19 @@ LANG_MAP: dict[str, str] = {
238
242
  ),
239
243
  Opt_type._run.value.description: (
240
244
  "执行 Ripper list 中的 Ripper\n"
241
- " \n"
245
+ "\n"
242
246
  "默认:\n"
243
247
  " 仅执行\n"
244
- " \n"
248
+ "\n"
245
249
  "exit:\n"
246
250
  " 执行后退出程序\n"
247
- " \n"
251
+ "\n"
248
252
  "shutdown [<秒数>]:\n"
249
253
  " 执行后关机\n"
250
- " 默认: 60"
254
+ " 默认: 60\n"
255
+ "\n"
256
+ "server [<地址>]:[<端口>]@[<密码>]:\n"
257
+ " 详见对应的 help"
251
258
  ),
252
259
  Opt_type._ff_params_ff.value.description: (
253
260
  "设置 FFmpeg 的全局选项\n" # .
@@ -353,6 +360,7 @@ LANG_MAP: dict[str, str] = {
353
360
  "User profile is not a valid dictionary": "用户配置文件不是有效的字典",
354
361
  "User profile is not found": "用户配置文件不存在",
355
362
  "Key '{}' is not found in user profile": "用户配置文件中不存在 {}",
363
+ "Save prompt history to config directory, otherwise save to memory. Take effect after reboot. Default: {}": "将 prompt 历史保存到 config 目录,否则保存到内存。重启后生效。默认: {}",
356
364
  # config about
357
365
  "Easy Rip's language, support incomplete matching. Default: {}. Supported: {}": "Easy Rip 的语言, 支持不完整匹配。默认: {}。支持: {}",
358
366
  "Auto check the update of Easy Rip. Default: {}": "自动检测 Easy Rip 更新。默认: {}",
@@ -0,0 +1,17 @@
1
+ from prompt_toolkit.history import FileHistory
2
+
3
+ from .global_val import C_Z, CONFIG_DIR
4
+
5
+
6
+ class easyrip_prompt:
7
+ PROMPT_HISTORY_FILE = CONFIG_DIR / "prompt_history.txt"
8
+
9
+ @classmethod
10
+ def clear(cls) -> None:
11
+ cls.PROMPT_HISTORY_FILE.unlink(True)
12
+
13
+
14
+ class ConfigFileHistory(FileHistory):
15
+ def store_string(self, string: str) -> None:
16
+ if not string.startswith(C_Z):
17
+ super().store_string(string)
@@ -4,6 +4,7 @@ import os
4
4
  import secrets
5
5
  import signal
6
6
  from collections import deque
7
+ from collections.abc import Callable
7
8
  from http.server import BaseHTTPRequestHandler, HTTPServer
8
9
  from threading import Thread
9
10
  from time import sleep
@@ -204,7 +205,13 @@ class MainHTTPRequestHandler(BaseHTTPRequestHandler):
204
205
  )
205
206
 
206
207
 
207
- def run_server(host: str = "", port: int = 0, password: str | None = None) -> None:
208
+ def run_server(
209
+ host: str = "",
210
+ port: int = 0,
211
+ password: str | None = None,
212
+ *,
213
+ after_start_server_hook: Callable[[], None] = lambda: None,
214
+ ) -> None:
208
215
  from ..easyrip_log import log
209
216
 
210
217
  MainHTTPRequestHandler.token = secrets.token_urlsafe(16)
@@ -223,6 +230,15 @@ def run_server(host: str = "", port: int = 0, password: str | None = None) -> No
223
230
 
224
231
  protocol = "HTTP"
225
232
 
233
+ def _hook() -> None:
234
+ try:
235
+ after_start_server_hook()
236
+ finally:
237
+ Event.is_run_command = False
238
+
239
+ Event.is_run_command = True
240
+ Thread(target=_hook, daemon=True).start()
241
+
226
242
  log.info(
227
243
  "Starting {protocol} service on port {port}...",
228
244
  protocol=protocol,
easyrip/global_val.py CHANGED
@@ -1,5 +1,25 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
1
5
  PROJECT_NAME = "Easy Rip"
2
- PROJECT_VERSION = "4.1.0"
6
+ PROJECT_VERSION = "4.3.0"
3
7
  PROJECT_TITLE = f"{PROJECT_NAME} v{PROJECT_VERSION}"
4
8
  PROJECT_URL = "https://github.com/op200/EasyRip"
5
9
  PROJECT_RELEASE_API = "https://api.github.com/repos/op200/EasyRip/releases/latest"
10
+
11
+
12
+ if sys.platform == "win32":
13
+ # Windows: C:\Users\<用户名>\AppData\Roaming\<app_name>
14
+ __config_dir = Path(os.getenv("APPDATA", ""))
15
+ elif sys.platform == "darwin":
16
+ # macOS: ~/Library/Application Support/<app_name>
17
+ __config_dir = Path(os.path.expanduser("~")) / "Library" / "Application Support"
18
+ else:
19
+ # Linux: ~/.config/<app_name>
20
+ __config_dir = Path(os.path.expanduser("~")) / ".config"
21
+ CONFIG_DIR = Path(__config_dir) / PROJECT_NAME
22
+
23
+
24
+ C_D = "\x04"
25
+ C_Z = "\x1a"
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyrip
3
- Version: 4.1.0
4
- Project-URL: Repository, https://github.com/op200/EasyRip
3
+ Version: 4.3.0
4
+ Author: op200
5
+ License-Expression: AGPL-3.0-or-later
6
+ Project-URL: Homepage, https://github.com/op200/EasyRip
7
+ Project-URL: Source, https://github.com/op200/EasyRip
5
8
  Classifier: Topic :: Multimedia
6
9
  Classifier: Development Status :: 5 - Production/Stable
7
- Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
8
10
  Classifier: Programming Language :: Python :: 3.12
9
11
  Classifier: Programming Language :: Python :: 3.13
10
12
  Classifier: Programming Language :: Python :: 3.14
@@ -21,13 +23,7 @@ License-File: LICENSE
21
23
  Requires-Dist: prompt-toolkit>=3.0.52
22
24
  Requires-Dist: fonttools>=4.61.0
23
25
  Requires-Dist: pycryptodome>=3.21.0
24
- Dynamic: classifier
25
- Dynamic: description
26
- Dynamic: description-content-type
27
26
  Dynamic: license-file
28
- Dynamic: project-url
29
- Dynamic: requires-dist
30
- Dynamic: requires-python
31
27
 
32
28
  # Easy Rip
33
29
 
@@ -39,31 +35,17 @@ Easy Rip 网页版控制台](https://op200.github.io/EasyRip-WebPanel/)**
39
35
 
40
36
  ## Start
41
37
 
42
- * If you have Python environment
43
- 如果你有 Python 环境
38
+ 1. Install [Python](https://www.python.org/)
39
+ 安装 [Python](https://www.python.org/)
40
+ 2. Install Easy Rip using pip: `pip install -U easyrip`
41
+ 使用 pip 安装 Easy Rip: `pip install -U easyrip`
42
+ 3. The you can use Easy Rip directly, run the command `easyrip`
43
+ 然后你就可以直接 Easy Rip 了,运行命令 `easyrip`
44
44
 
45
- 1. You can `pip install -U git+https://github.com/op200/EasyRip.git` to pull the version from the repositoryand, or `pip install -U easyrip` to obtain the stable version.
46
- 你可以 `pip install -U git+https://github.com/op200/EasyRip.git` 获取仓库中的版本,或者 `pip install -U easyrip` 获取稳定版。
47
-
48
- 2. After installation, run `easyrip` on command.
49
- 安装后直接在命令行运行 `easyrip`。
50
-
51
- * Or if you want to download a standalone exe file (not recommended)
52
- 或者如果你想下载一个独立的可执行文件(不推荐)
53
-
54
- *
55
- Download exe in [Actions](https://github.com/op200/EasyRip/actions).
56
- Or download exe or bat script collection in [Releases](https://github.com/op200/EasyRip/releases).
57
-
58
- 在 [Actions](https://github.com/op200/EasyRip/actions) 中下载最新的 exe。
59
- 或者在 [Releases](https://github.com/op200/EasyRip/releases) 中下载 exe 或 bat 脚本包。
60
-
61
- *
62
- The file `BatchScriptPackage` in [Releases](https://github.com/op200/EasyRip/releases) is a bat script collection.
63
- It is used to facilitate ordinary users, it only has Chinese.
64
-
65
- [Releases](https://github.com/op200/EasyRip/releases) 中每隔一段时间发布一次名为 BatchScriptPackage 的 bat 脚本包
66
- 用于方便一般用户,其内只有中文
45
+ * (If you have special requirements and want to use a separate file, you can download it from [Github Actions](https://github.com/op200/EasyRip/actions))
46
+ (如果你有特殊需求,想使用独立文件,可以在 [Github Actions](https://github.com/op200/EasyRip/actions) 中下载)
47
+ * (The file `BatchScriptPackage` in [Releases](https://github.com/op200/EasyRip/releases) is a bat script collection. It is used to facilitate ordinary users, it only has Chinese.)
48
+ ([Releases](https://github.com/op200/EasyRip/releases) 中每隔一段时间发布一次名为 `BatchScriptPackage` bat 脚本包,用于方便一般用户,其内只有中文。)
67
49
 
68
50
  ## Usage
69
51
 
@@ -75,7 +57,7 @@ Run `easyrip`, input `help` to get help doc
75
57
 
76
58
  ## Dependency
77
59
 
78
- * ### Python 3.13 (must >=3.12)
60
+ * ### Python 3.14 (must >=3.12)
79
61
 
80
62
  If you want to develop, you need to install dependencies. If you just want to use them, you don't need to manually install dependencies.
81
63
  如果你想开发,需要安装依赖,如果你只是想使用,不需要手动安装依赖。
@@ -1,18 +1,20 @@
1
1
  easyrip/__init__.py,sha256=BlDIN9htmQXETt7M8pjYSOVrKevuuyfRn2ajQI4J7us,648
2
- easyrip/__main__.py,sha256=BWheIP5HEW4WuQeVNqyjFasSsTROgn1pTOP0XM3whWE,3892
3
- easyrip/easyrip_command.py,sha256=yncaSUzSbE5houTdd4qptSFw-jMSPo0ygzWKLm52fFw,25159
4
- easyrip/easyrip_config.py,sha256=CO7nTilnbhzGw9HCl2aiXCxtYs6eTfrAds8E-_qvCos,8360
2
+ easyrip/__main__.py,sha256=lwEV17Xi1cBWWHd7GXloKDjuWe2empflf7rMkMiLWGw,4375
3
+ easyrip/easyrip_command.py,sha256=KirlybDIkz3aYPlxqT_VCPsxfdCp-pi62KJc1P6Yc6g,26960
5
4
  easyrip/easyrip_log.py,sha256=0V5wBwbqWt56w_NHoLmQmkTHPnPLkYZCAbhDSYnLnDw,15611
6
- easyrip/easyrip_main.py,sha256=kPjP-yi4uSCnmlwpynUSnw22BnEtUIbQLwSi2ZUpozI,41738
7
- easyrip/global_val.py,sha256=ZqTz586C9eRs9dVsPbxySom6BxYyAi7wU7egqCMifdU,237
5
+ easyrip/easyrip_main.py,sha256=dEDUoBDF6flbFrgwdnR8x7yofBdprHNe8Nwkqp00J04,43497
6
+ easyrip/easyrip_prompt.py,sha256=cEWI3XFooUwwQcQ_C8dVfd5bP6TlqQSwRHtkj_ezZs4,432
7
+ easyrip/global_val.py,sha256=_8DiZFRvM97uzbKlD2ELPtJwV5WjRXd-sTUtJAOuL1E,773
8
8
  easyrip/utils.py,sha256=u1Qi5z0ZF31f_dQHtvsfcfeKk5PirlTeuHnlqL4mkrs,6452
9
+ easyrip/easyrip_config/config.py,sha256=9uskiGIpYLuS-vjzNASZlhdWLWXkEVxtSjODxcZU3pc,7935
10
+ easyrip/easyrip_config/config_key.py,sha256=wANVusz9kNqsYJUvXM7DUQn6k_G6EO2VWTHWh9dOifw,394
9
11
  easyrip/easyrip_mlang/__init__.py,sha256=0XYa0kcnUPH1aaRV3ceIPuYO1Oov8LBxgVjel-1slvI,3226
10
12
  easyrip/easyrip_mlang/global_lang_val.py,sha256=Un20KGMVVMQbOaV_7VaHg3E1dvK2Y7kI1cPzq_sFWKY,9984
11
13
  easyrip/easyrip_mlang/lang_en.py,sha256=heUSPeVtY1Nf9eDhrjPM28N3PLsu62op3llbXAfXvAs,140
12
- easyrip/easyrip_mlang/lang_zh_Hans_CN.py,sha256=opi0FAluT_kqomyEofxj2k69Gl6yIhdBeGLf3wyKo5M,18066
14
+ easyrip/easyrip_mlang/lang_zh_Hans_CN.py,sha256=0DtvBg7khE6NbFt1carSwgMNkigQbMx2e0LsDv4wFhY,18520
13
15
  easyrip/easyrip_mlang/translator.py,sha256=Vg0S0p2rpMNAy3LHTdK7qKFdkPEXlOoCCXcaH7PyAC4,5939
14
16
  easyrip/easyrip_web/__init__.py,sha256=tMyEeaSGeEJjND7MF0MBv9aDiDgaO3MOnppwxA70U2c,177
15
- easyrip/easyrip_web/http_server.py,sha256=Hi6XJuuSgpjN5EimzLBoMEKokNlE8BLy7UNKgrtR06A,8013
17
+ easyrip/easyrip_web/http_server.py,sha256=iyulCAFQrJlz86Lrr-Dm3fhOnNCf79Bp6fVHhr0ephY,8350
16
18
  easyrip/easyrip_web/third_party_api.py,sha256=umj-QsfOa0IM60Ic2pXigVfnGfAiiC95fJSXQ-WFpJA,4419
17
19
  easyrip/ripper/__init__.py,sha256=o5gj8QRvkuVc0S9jbRjVOWXpLAaQKC0-n-o6Zx9Dowo,171
18
20
  easyrip/ripper/media_info.py,sha256=U_XLhNrprIQXWWdxOkGqQGN5V1wla55XxYhf5z9hRI0,4986
@@ -21,9 +23,9 @@ easyrip/ripper/font_subset/__init__.py,sha256=a6UoRH1AU1l5FkFgQ-wQxOswLmovgVUUl3
21
23
  easyrip/ripper/font_subset/ass.py,sha256=Z1TYmF32pJoXhwsjjZKgScjcsbiayXjp9tnM533ULqs,25806
22
24
  easyrip/ripper/font_subset/font.py,sha256=9yhZmZc-8wvbk2NDN3B4oJMt-UVByzM_pukzMzd-du4,7114
23
25
  easyrip/ripper/font_subset/subset.py,sha256=yj5JYZg1XPO1U0zsZUHSsOX105wrmE_g-2jLpcrvqqc,17137
24
- easyrip-4.1.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
25
- easyrip-4.1.0.dist-info/METADATA,sha256=aZp0MtumO45vduMX8WLngISHEawZZTH4ZBZa1U5ZMm0,4035
26
- easyrip-4.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- easyrip-4.1.0.dist-info/entry_points.txt,sha256=D6GBMMTzZ-apgX76KyZ6jxMmIFqGYwU9neeLLni_qKI,49
28
- easyrip-4.1.0.dist-info/top_level.txt,sha256=kuEteBXm-Gf90jRQgH3-fTo-Z-Q6czSuUEqY158H4Ww,8
29
- easyrip-4.1.0.dist-info/RECORD,,
26
+ easyrip-4.3.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
27
+ easyrip-4.3.0.dist-info/METADATA,sha256=ynq-ebYL9XrnQDIZsnE279h1RXP1ABOpZ-VgjJUFreI,3506
28
+ easyrip-4.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ easyrip-4.3.0.dist-info/entry_points.txt,sha256=D6GBMMTzZ-apgX76KyZ6jxMmIFqGYwU9neeLLni_qKI,49
30
+ easyrip-4.3.0.dist-info/top_level.txt,sha256=kuEteBXm-Gf90jRQgH3-fTo-Z-Q6czSuUEqY158H4Ww,8
31
+ easyrip-4.3.0.dist-info/RECORD,,