easyrip 4.13.2__tar.gz → 4.14.0__tar.gz

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 (38) hide show
  1. {easyrip-4.13.2 → easyrip-4.14.0}/PKG-INFO +1 -1
  2. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/__main__.py +7 -1
  3. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_command.py +13 -3
  4. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_main.py +31 -3
  5. easyrip-4.14.0/easyrip/easyrip_prompt.py +232 -0
  6. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/global_val.py +1 -1
  7. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/PKG-INFO +1 -1
  8. easyrip-4.13.2/easyrip/easyrip_prompt.py +0 -151
  9. {easyrip-4.13.2 → easyrip-4.14.0}/LICENSE +0 -0
  10. {easyrip-4.13.2 → easyrip-4.14.0}/README.md +0 -0
  11. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/__init__.py +0 -0
  12. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_config/config.py +0 -0
  13. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_config/config_key.py +0 -0
  14. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_log.py +0 -0
  15. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/__init__.py +0 -0
  16. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/global_lang_val.py +0 -0
  17. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/lang_en.py +0 -0
  18. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/lang_tag_val.py +0 -0
  19. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/lang_zh_Hans_CN.py +0 -0
  20. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/translator.py +0 -0
  21. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_web/__init__.py +0 -0
  22. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_web/http_server.py +0 -0
  23. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_web/third_party_api.py +0 -0
  24. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/media_info.py +0 -0
  25. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/param.py +0 -0
  26. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/ripper.py +0 -0
  27. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/__init__.py +0 -0
  28. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/ass.py +0 -0
  29. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/font.py +0 -0
  30. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/subset.py +0 -0
  31. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/utils.py +0 -0
  32. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/SOURCES.txt +0 -0
  33. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/dependency_links.txt +0 -0
  34. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/entry_points.txt +0 -0
  35. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/requires.txt +0 -0
  36. {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/top_level.txt +0 -0
  37. {easyrip-4.13.2 → easyrip-4.14.0}/pyproject.toml +0 -0
  38. {easyrip-4.13.2 → easyrip-4.14.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyrip
3
- Version: 4.13.2
3
+ Version: 4.14.0
4
4
  Author: op200
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Project-URL: Homepage, https://github.com/op200/EasyRip
@@ -23,7 +23,12 @@ from .easyrip_command import (
23
23
  )
24
24
  from .easyrip_config.config import Config_key, config
25
25
  from .easyrip_main import Ripper, get_input_prompt, init, log, run_command
26
- from .easyrip_prompt import ConfigFileHistory, SmartPathCompleter, easyrip_prompt
26
+ from .easyrip_prompt import (
27
+ ConfigFileHistory,
28
+ CustomPromptCompleter,
29
+ SmartPathCompleter,
30
+ easyrip_prompt,
31
+ )
27
32
  from .global_val import C_D, C_Z
28
33
 
29
34
 
@@ -128,6 +133,7 @@ def run() -> NoReturn:
128
133
  (
129
134
  _ctv_to_nc(cmd_ctv_tuple),
130
135
  OptCompleter(opt_tree=_ctv_to_nd(ct.value for ct in Opt_type)),
136
+ CustomPromptCompleter(),
131
137
  )
132
138
  ),
133
139
  history=prompt_history,
@@ -253,12 +253,22 @@ class Cmd_type(enum.Enum):
253
253
  description=(
254
254
  "history\n" # .
255
255
  " Show prompt history\n"
256
- "clear | clean\n"
257
- " Delete history file"
256
+ "history_clear\n"
257
+ " Delete history file\n"
258
+ "add <name:string> <cmd:string>\n"
259
+ " Add a custom prompt\n"
260
+ " e.g. prompt add myprompt echo my prompt\n"
261
+ "del <name:string>\n"
262
+ " Delete a custom prompt"
263
+ "show\n"
264
+ " Show custom prompt"
258
265
  ),
259
266
  childs=(
260
267
  Cmd_type_val(("history",)),
261
- Cmd_type_val(("clear", "clean")),
268
+ Cmd_type_val(("history_clear",)),
269
+ Cmd_type_val(("add",)),
270
+ Cmd_type_val(("del",)),
271
+ Cmd_type_val(("show",)),
262
272
  ),
263
273
  )
264
274
  translate = Cmd_type_val(
@@ -345,7 +345,7 @@ def run_ripper_list(
345
345
  log.info("Execute shutdown in {}s", shutdown_sec)
346
346
  if os.name == "nt":
347
347
  _cmd = (
348
- f'shutdown /s /t {shutdown_sec} /c "{gettext("{} run completed, shutdown in {}s", PROJECT_TITLE, shutdown_sec)}"',
348
+ f'shutdown /s /t {shutdown_sec} /d p:4:0 /c "{gettext("{} run completed, shutdown in {}s", PROJECT_TITLE, shutdown_sec)}"',
349
349
  )
350
350
  elif os.name == "posix":
351
351
  _cmd = (f"shutdown -h +{shutdown_sec // 60}",)
@@ -775,8 +775,36 @@ def run_command(command: Iterable[str] | str) -> bool:
775
775
  ) as f:
776
776
  for line in f.read().splitlines():
777
777
  log.send(line, is_format=False)
778
- case "clear" | "clean":
779
- easyrip_prompt.clear()
778
+
779
+ case "history_clear":
780
+ easyrip_prompt.clear_history()
781
+
782
+ case "add":
783
+ _name = cmd_list[2]
784
+ _cmd = (
785
+ command.split(maxsplit=3)[-1]
786
+ if isinstance(command, str)
787
+ else " ".join(cmd_list[3:])
788
+ )
789
+ if " " in _name:
790
+ log.error("The name must not contain spaces")
791
+ return False
792
+ log.info("Add custom prompt {!r} = {!r}", _name, _cmd)
793
+ return easyrip_prompt.add_custom_prompt(_name, _cmd)
794
+
795
+ case "del":
796
+ _name = cmd_list[2]
797
+ log.info("Del custom prompt {!r}", _name)
798
+ return easyrip_prompt.del_custom_prompt(_name)
799
+
800
+ case "show":
801
+ if _custom_prompt := easyrip_prompt.get_custom_prompt():
802
+ log.send(
803
+ "\n".join(
804
+ f"{k!r} = {v!r}" for k, v in _custom_prompt.items()
805
+ ),
806
+ is_format=False,
807
+ )
780
808
 
781
809
  case Cmd_type.translate:
782
810
  if not (_infix := cmd_list[1]):
@@ -0,0 +1,232 @@
1
+ import os
2
+ import re
3
+ import tomllib
4
+ from collections.abc import Iterable
5
+
6
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
7
+ from prompt_toolkit.document import Document
8
+ from prompt_toolkit.formatted_text import StyleAndTextTuples
9
+ from prompt_toolkit.history import FileHistory
10
+
11
+ from .global_val import C_Z, get_CONFIG_DIR
12
+ from .utils import type_match
13
+
14
+
15
+ class easyrip_prompt:
16
+ PROMPT_HISTORY_FILE = get_CONFIG_DIR() / "prompt_history.txt"
17
+ PROMPT_CUSTOM_FILE = get_CONFIG_DIR() / "prompt_custom.toml"
18
+
19
+ __prompt_custom_data: dict[str, str] | None = None
20
+
21
+ @classmethod
22
+ def clear_history(cls) -> None:
23
+ cls.PROMPT_HISTORY_FILE.unlink(True)
24
+
25
+ @classmethod
26
+ def get_custom_prompt(cls) -> dict[str, str]:
27
+ if cls.__prompt_custom_data is not None:
28
+ return cls.__prompt_custom_data
29
+
30
+ cls.PROMPT_CUSTOM_FILE.touch()
31
+ with cls.PROMPT_CUSTOM_FILE.open("rb") as f:
32
+ data = tomllib.load(f)
33
+ assert type_match(data, dict[str, str])
34
+ cls.__prompt_custom_data = data
35
+ return data
36
+
37
+ @classmethod
38
+ def update_custom_prompt(cls, data: dict[str, str]) -> bool:
39
+ cls.PROMPT_CUSTOM_FILE.touch()
40
+ with cls.PROMPT_CUSTOM_FILE.open("wt", encoding="utf-8", newline="\n") as f:
41
+ f.writelines(f"{k!r} = {v!r}\n" for k, v in data.items())
42
+ cls.__prompt_custom_data = None
43
+ return True
44
+
45
+ @classmethod
46
+ def add_custom_prompt(cls, name: str, cmd: str) -> bool:
47
+ data: dict[str, str] = cls.get_custom_prompt()
48
+ assert type_match(data, dict[str, str])
49
+
50
+ if name in data:
51
+ from .easyrip_log import log
52
+
53
+ log.error("The name {!r} is already in custom prompt", name)
54
+ return False
55
+
56
+ data[name] = cmd
57
+
58
+ cls.update_custom_prompt(data)
59
+
60
+ return True
61
+
62
+ @classmethod
63
+ def del_custom_prompt(cls, name: str) -> bool:
64
+ data: dict[str, str] = cls.get_custom_prompt()
65
+
66
+ pop_res: bool
67
+ if name in data:
68
+ data.pop(name)
69
+ pop_res = True
70
+ cls.update_custom_prompt(data)
71
+ else:
72
+ from .easyrip_log import log
73
+
74
+ log.warning("The name {!r} not in custom prompt", name)
75
+ pop_res = False
76
+
77
+ return pop_res
78
+
79
+
80
+ class ConfigFileHistory(FileHistory):
81
+ def store_string(self, string: str) -> None:
82
+ if not string.startswith(C_Z):
83
+ super().store_string(string)
84
+
85
+
86
+ def _highlight_fuzzy_match(
87
+ suggestion: str,
88
+ user_input: str,
89
+ style_config: dict | None = None,
90
+ ) -> StyleAndTextTuples:
91
+ """
92
+ 高亮显示模糊匹配结果
93
+
94
+ Args:
95
+ suggestion: 建议的完整字符串
96
+ user_input: 用户输入的匹配字符
97
+ style_config: 样式配置字典
98
+
99
+ Returns:
100
+ 包含样式信息的格式化文本
101
+
102
+ """
103
+ if style_config is None:
104
+ style_config = {
105
+ "match_char": "class:fuzzymatch.inside.character",
106
+ "match_section": "class:fuzzymatch.inside",
107
+ "non_match": "class:fuzzymatch.outside",
108
+ }
109
+
110
+ if not user_input:
111
+ # 用户没有输入,返回原始字符串
112
+ return [(style_config["non_match"], suggestion)]
113
+
114
+ # 找到最佳匹配位置
115
+ result = []
116
+
117
+ # 简化的模糊匹配算法
118
+ pattern = ".*?".join(map(re.escape, user_input))
119
+ regex = re.compile(pattern, re.IGNORECASE)
120
+
121
+ match = regex.search(suggestion)
122
+ if not match:
123
+ # 没有匹配,返回原始字符串
124
+ return [(style_config["non_match"], suggestion)]
125
+
126
+ start, end = match.span()
127
+ match_text = suggestion[start:end]
128
+
129
+ # 匹配段之前的文本
130
+ if start > 0:
131
+ result.append((style_config["non_match"], suggestion[:start]))
132
+
133
+ # 匹配段内部的字符
134
+ input_chars = list(user_input)
135
+ for char in match_text:
136
+ if input_chars and char.lower() == input_chars[0].lower():
137
+ result.append((style_config["match_char"], char))
138
+ input_chars.pop(0)
139
+ else:
140
+ result.append((style_config["match_section"], char))
141
+
142
+ # 匹配段之后的文本
143
+ if end < len(suggestion):
144
+ result.append((style_config["non_match"], suggestion[end:]))
145
+
146
+ return result
147
+
148
+
149
+ def _fuzzy_filter_and_sort(names: list[str], match_str: str) -> list[str]:
150
+ """模糊过滤和排序"""
151
+ if not match_str:
152
+ return sorted(names)
153
+
154
+ # 构建模糊匹配模式
155
+ pattern = ".*?".join(map(re.escape, match_str))
156
+ regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
157
+
158
+ matches = []
159
+ for filename in names:
160
+ regex_matches = list(regex.finditer(filename))
161
+ if regex_matches:
162
+ # 找到最佳匹配(最左、最短)
163
+ best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
164
+ matches.append((best.start(), len(best.group(1)), filename))
165
+
166
+ # 按匹配质量排序:先按匹配位置,再按匹配长度
167
+ matches.sort(key=lambda x: (x[0], x[1]))
168
+ return [item[2] for item in matches]
169
+
170
+
171
+ class SmartPathCompleter(Completer):
172
+ def __init__(self) -> None:
173
+ pass
174
+
175
+ def get_completions(
176
+ self,
177
+ document: Document,
178
+ complete_event: CompleteEvent, # noqa: ARG002
179
+ ) -> Iterable[Completion]:
180
+ text = document.text_before_cursor
181
+ input_path = text.strip("\"'")
182
+
183
+ try:
184
+ directory = os.path.dirname(input_path) or "."
185
+ basename = os.path.basename(input_path)
186
+
187
+ filenames: list[str] = (
188
+ os.listdir(directory) if os.path.isdir(directory) else []
189
+ )
190
+
191
+ for filename in _fuzzy_filter_and_sort(filenames, basename):
192
+ full_name = (
193
+ filename if directory == "." else os.path.join(directory, filename)
194
+ )
195
+
196
+ if os.path.isdir(full_name):
197
+ filename += "/"
198
+
199
+ completion = full_name
200
+
201
+ if any(c in r""" !$%&()*:;<=>?[]^`{|}~""" for c in completion):
202
+ completion = f'"{completion}"'
203
+
204
+ yield Completion(
205
+ text=completion,
206
+ start_position=-len(text),
207
+ display=_highlight_fuzzy_match(filename, basename),
208
+ )
209
+
210
+ except OSError:
211
+ pass
212
+
213
+
214
+ class CustomPromptCompleter(Completer):
215
+ def get_completions(
216
+ self,
217
+ document: Document,
218
+ complete_event: CompleteEvent, # noqa: ARG002
219
+ ) -> Iterable[Completion]:
220
+ text = document.text_before_cursor
221
+ words = text.split()
222
+
223
+ custom_prompt = easyrip_prompt.get_custom_prompt()
224
+ for word in words[-1:]:
225
+ for name in _fuzzy_filter_and_sort(list(custom_prompt), word):
226
+ target_cmd = custom_prompt[name]
227
+ yield Completion(
228
+ text=target_cmd,
229
+ start_position=-len(word),
230
+ display=_highlight_fuzzy_match(name, word),
231
+ display_meta=target_cmd,
232
+ )
@@ -4,7 +4,7 @@ from functools import cache
4
4
  from pathlib import Path
5
5
 
6
6
  PROJECT_NAME = "Easy Rip"
7
- PROJECT_VERSION = "4.13.2"
7
+ PROJECT_VERSION = "4.14.0"
8
8
  PROJECT_TITLE = f"{PROJECT_NAME} v{PROJECT_VERSION}"
9
9
  PROJECT_URL = "https://github.com/op200/EasyRip"
10
10
  PROJECT_RELEASE_API = "https://api.github.com/repos/op200/EasyRip/releases/latest"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyrip
3
- Version: 4.13.2
3
+ Version: 4.14.0
4
4
  Author: op200
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Project-URL: Homepage, https://github.com/op200/EasyRip
@@ -1,151 +0,0 @@
1
- import os
2
- import re
3
- from collections.abc import Iterable
4
-
5
- from prompt_toolkit.completion import CompleteEvent, Completer, Completion
6
- from prompt_toolkit.document import Document
7
- from prompt_toolkit.formatted_text import StyleAndTextTuples
8
- from prompt_toolkit.history import FileHistory
9
-
10
- from .global_val import C_Z, get_CONFIG_DIR
11
-
12
-
13
- class easyrip_prompt:
14
- PROMPT_HISTORY_FILE = get_CONFIG_DIR() / "prompt_history.txt"
15
-
16
- @classmethod
17
- def clear(cls) -> None:
18
- cls.PROMPT_HISTORY_FILE.unlink(True)
19
-
20
-
21
- class ConfigFileHistory(FileHistory):
22
- def store_string(self, string: str) -> None:
23
- if not string.startswith(C_Z):
24
- super().store_string(string)
25
-
26
-
27
- class SmartPathCompleter(Completer):
28
- def __init__(self) -> None:
29
- pass
30
-
31
- def _highlight_fuzzy_match(
32
- self,
33
- suggestion: str,
34
- user_input: str,
35
- style_config: dict | None = None,
36
- ) -> StyleAndTextTuples:
37
- """
38
- 高亮显示模糊匹配结果
39
-
40
- Args:
41
- suggestion: 建议的完整字符串
42
- user_input: 用户输入的匹配字符
43
- style_config: 样式配置字典
44
-
45
- Returns:
46
- 包含样式信息的格式化文本
47
-
48
- """
49
- if style_config is None:
50
- style_config = {
51
- "match_char": "class:fuzzymatch.inside.character",
52
- "match_section": "class:fuzzymatch.inside",
53
- "non_match": "class:fuzzymatch.outside",
54
- }
55
-
56
- if not user_input:
57
- # 用户没有输入,返回原始字符串
58
- return [(style_config["non_match"], suggestion)]
59
-
60
- # 找到最佳匹配位置
61
- result = []
62
-
63
- # 简化的模糊匹配算法
64
- pattern = ".*?".join(map(re.escape, user_input))
65
- regex = re.compile(pattern, re.IGNORECASE)
66
-
67
- match = regex.search(suggestion)
68
- if not match:
69
- # 没有匹配,返回原始字符串
70
- return [(style_config["non_match"], suggestion)]
71
-
72
- start, end = match.span()
73
- match_text = suggestion[start:end]
74
-
75
- # 匹配段之前的文本
76
- if start > 0:
77
- result.append((style_config["non_match"], suggestion[:start]))
78
-
79
- # 匹配段内部的字符
80
- input_chars = list(user_input)
81
- for char in match_text:
82
- if input_chars and char.lower() == input_chars[0].lower():
83
- result.append((style_config["match_char"], char))
84
- input_chars.pop(0)
85
- else:
86
- result.append((style_config["match_section"], char))
87
-
88
- # 匹配段之后的文本
89
- if end < len(suggestion):
90
- result.append((style_config["non_match"], suggestion[end:]))
91
-
92
- return result
93
-
94
- def _fuzzy_filter_and_sort(self, filenames: list[str], match_str: str) -> list[str]:
95
- """模糊过滤和排序"""
96
- if not match_str:
97
- return sorted(filenames)
98
-
99
- # 构建模糊匹配模式
100
- pattern = ".*?".join(map(re.escape, match_str))
101
- regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
102
-
103
- matches = []
104
- for filename in filenames:
105
- regex_matches = list(regex.finditer(filename))
106
- if regex_matches:
107
- # 找到最佳匹配(最左、最短)
108
- best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
109
- matches.append((best.start(), len(best.group(1)), filename))
110
-
111
- # 按匹配质量排序:先按匹配位置,再按匹配长度
112
- matches.sort(key=lambda x: (x[0], x[1]))
113
- return [item[2] for item in matches]
114
-
115
- def get_completions(
116
- self,
117
- document: Document,
118
- complete_event: CompleteEvent, # noqa: ARG002
119
- ) -> Iterable[Completion]:
120
- text = document.text_before_cursor
121
- input_path = text.strip("\"'")
122
-
123
- try:
124
- directory = os.path.dirname(input_path) or "."
125
- basename = os.path.basename(input_path)
126
-
127
- filenames: list[str] = (
128
- os.listdir(directory) if os.path.isdir(directory) else []
129
- )
130
-
131
- for filename in self._fuzzy_filter_and_sort(filenames, basename):
132
- full_name = (
133
- filename if directory == "." else os.path.join(directory, filename)
134
- )
135
-
136
- if os.path.isdir(full_name):
137
- filename += "/"
138
-
139
- completion = full_name
140
-
141
- if any(c in r""" !$%&()*:;<=>?[]^`{|}~""" for c in completion):
142
- completion = f'"{completion}"'
143
-
144
- yield Completion(
145
- text=completion,
146
- start_position=-len(text),
147
- display=self._highlight_fuzzy_match(filename, basename),
148
- )
149
-
150
- except OSError:
151
- pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes