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
@@ -1,16 +1,48 @@
1
1
  import enum
2
+ import itertools
2
3
  import textwrap
4
+ from collections.abc import Iterable
3
5
  from dataclasses import dataclass
4
- from typing import Self
6
+ from typing import Final, Self, final
7
+
8
+ from prompt_toolkit.completion import (
9
+ Completer,
10
+ DeduplicateCompleter,
11
+ FuzzyCompleter,
12
+ FuzzyWordCompleter,
13
+ NestedCompleter,
14
+ WordCompleter,
15
+ merge_completers,
16
+ )
17
+ from prompt_toolkit.completion.base import CompleteEvent, Completion
18
+ from prompt_toolkit.document import Document
5
19
 
6
20
  from . import global_val
21
+ from .easyrip_config.config_key import Config_key
22
+ from .ripper.param import Audio_codec, Preset_name
7
23
 
8
24
 
25
+ @final
9
26
  @dataclass(slots=True, init=False, eq=False)
10
27
  class Cmd_type_val:
11
- name: str
12
- opt_str: str
28
+ names: tuple[str, ...]
29
+ _param: str
13
30
  _description: str
31
+ childs: tuple["Cmd_type_val", ...]
32
+
33
+ @property
34
+ def param(self) -> str:
35
+ try:
36
+ from .easyrip_mlang import gettext
37
+
38
+ return gettext(self._param, is_format=False)
39
+
40
+ except ImportError: # 启动时,原字符串导入翻译文件
41
+ return self._param
42
+
43
+ @param.setter
44
+ def param(self, val: str) -> None:
45
+ self._param = val
14
46
 
15
47
  @property
16
48
  def description(self) -> str:
@@ -23,56 +55,58 @@ class Cmd_type_val:
23
55
  return self._description
24
56
 
25
57
  @description.setter
26
- def description(self, val: str):
58
+ def description(self, val: str) -> None:
27
59
  self._description = val
28
60
 
29
61
  def __init__(
30
62
  self,
31
- name: str,
63
+ names: tuple[str, ...],
32
64
  *,
33
- opt_str: str,
65
+ param: str = "",
34
66
  description: str = "",
67
+ childs: tuple["Cmd_type_val", ...] = (),
35
68
  ) -> None:
36
- self.name = name
37
- self.opt_str = opt_str
69
+ self.names = names
70
+ self.param = param
38
71
  self.description = description
72
+ self.childs = childs
39
73
 
40
74
  def __eq__(self, other: object) -> bool:
41
75
  if isinstance(other, Cmd_type_val):
42
- return self.name == other.name
76
+ return self.names == other.names
43
77
  return False
44
78
 
45
79
  def __hash__(self) -> int:
46
- return hash(self.name)
80
+ return hash(self.names)
47
81
 
48
82
  def to_doc(self) -> str:
49
- return (
50
- f"{self.opt_str}\n{textwrap.indent(self.description, ' │', lambda _: True)}"
51
- )
83
+ return f"{' / '.join(self.names)} {self.param}\n{textwrap.indent(self.description, ' │ ', lambda _: True)}"
52
84
 
53
85
 
54
86
  class Cmd_type(enum.Enum):
55
87
  help = h = Cmd_type_val(
56
- "help",
57
- opt_str="h / help [<cmd>]",
58
- description="Show full help or show the <cmd> help",
88
+ ("h", "help"),
89
+ param="[<cmd> [<cmd param>]]",
90
+ description=(
91
+ "Show full help or show the <cmd> help.\n"
92
+ "e.g. help list\n" # .
93
+ "e.g. h -p x265slow"
94
+ ),
59
95
  )
60
96
  version = v = ver = Cmd_type_val(
61
- "version",
62
- opt_str="v / ver / version",
97
+ ("v", "ver", "version"),
63
98
  description="Show version info",
64
99
  )
65
100
  init = Cmd_type_val(
66
- "init",
67
- opt_str="init",
101
+ ("init",),
68
102
  description=(
69
103
  "Execute initialization function\n"
70
104
  "e.g. you can execute it after modifying the dynamic translation file"
71
105
  ),
72
106
  )
73
107
  log = Cmd_type_val(
74
- "log",
75
- opt_str="log [<LogLevel>] <string>",
108
+ ("log",),
109
+ param="[<LogLevel>] <string>",
76
110
  description=(
77
111
  "Output custom log\n"
78
112
  "log level:\n"
@@ -83,10 +117,17 @@ class Cmd_type(enum.Enum):
83
117
  " debug\n"
84
118
  " Default: info"
85
119
  ),
120
+ childs=(
121
+ Cmd_type_val(("info",)),
122
+ Cmd_type_val(("warning", "warn")),
123
+ Cmd_type_val(("error", "err")),
124
+ Cmd_type_val(("send",)),
125
+ Cmd_type_val(("debug",)),
126
+ ),
86
127
  )
87
128
  _run_any = Cmd_type_val(
88
- "$",
89
- opt_str="$ <code>",
129
+ ("$",),
130
+ param="<code>",
90
131
  description=(
91
132
  "Run code directly from the internal environment.\n"
92
133
  "Execute the code string directly after the '$'.\n"
@@ -94,33 +135,35 @@ class Cmd_type(enum.Enum):
94
135
  ),
95
136
  )
96
137
  exit = Cmd_type_val(
97
- "exit",
98
- opt_str="exit",
138
+ ("exit",),
139
+ param="exit",
99
140
  description="Exit this program",
100
141
  )
101
142
  cd = Cmd_type_val(
102
- "cd",
103
- opt_str="cd <string>",
143
+ ("cd",),
144
+ param="<<path> | 'fd' | 'cfd'>",
104
145
  description="Change current working directory",
146
+ childs=(
147
+ Cmd_type_val(("fd",)),
148
+ Cmd_type_val(("cfd",)),
149
+ ),
105
150
  )
106
- dir = Cmd_type_val(
107
- "dir",
108
- opt_str="dir",
151
+ dir = ls = Cmd_type_val(
152
+ ("dir", "ls"),
109
153
  description="Print files and folders' name in the current working directory",
110
154
  )
111
155
  mkdir = makedir = Cmd_type_val(
112
- "mkdir",
113
- opt_str="mkdir / makedir <string>",
156
+ ("mkdir", "makedir"),
157
+ param="<string>",
114
158
  description="Create a new path",
115
159
  )
116
160
  cls = clear = Cmd_type_val(
117
- "cls",
118
- opt_str="cls / clear",
161
+ ("cls", "clear"),
119
162
  description="Clear screen",
120
163
  )
121
164
  list = Cmd_type_val(
122
- "list",
123
- opt_str="list <list option>",
165
+ ("list",),
166
+ param="<list option>",
124
167
  description=(
125
168
  "Operate Ripper list\n"
126
169
  " \n"
@@ -141,27 +184,40 @@ class Cmd_type(enum.Enum):
141
184
  "<int> <int>:\n"
142
185
  " Exchange specified index"
143
186
  ),
187
+ childs=(
188
+ Cmd_type_val(("clear", "clean")),
189
+ Cmd_type_val(("del", "pop")),
190
+ Cmd_type_val(("sort",), childs=(Cmd_type_val(("n", "r", "nr")),)),
191
+ ),
144
192
  )
145
193
  run = Cmd_type_val(
146
- "run",
147
- opt_str="run [<run option>]",
194
+ ("run",),
195
+ param="[<run option>] [-multithreading <0 | 1>]",
148
196
  description=(
149
197
  "Run the Ripper in the Ripper list\n"
150
- " \n"
198
+ "\n"
151
199
  "Default:\n"
152
200
  " Only run\n"
153
- " \n"
201
+ "\n"
154
202
  "exit:\n"
155
203
  " Close program when run finished\n"
156
- " \n"
204
+ "\n"
157
205
  "shutdown [<sec>]:\n"
158
206
  " Shutdown when run finished\n"
159
- " Default: 60"
207
+ " Default: 60\n"
208
+ "\n"
209
+ "server [<address>]:[<port>]@[<password>]:\n"
210
+ " See the corresponding help for details"
211
+ ),
212
+ childs=(
213
+ Cmd_type_val(("exit",)),
214
+ Cmd_type_val(("shutdown",)),
215
+ Cmd_type_val(("server",)),
160
216
  ),
161
217
  )
162
218
  server = Cmd_type_val(
163
- "server",
164
- opt_str="server [[-a | -address] <address>[:<port>] [[-p | -password] <password>]]",
219
+ ("server",),
220
+ param="[<address>]:[<port>]@[<password>]",
165
221
  description=(
166
222
  "Boot web service\n"
167
223
  "Default: server localhost:0\n"
@@ -169,10 +225,10 @@ class Cmd_type(enum.Enum):
169
225
  ),
170
226
  )
171
227
  config = Cmd_type_val(
172
- "config",
173
- opt_str="config <config option>",
228
+ ("config",),
229
+ param="<config option>",
174
230
  description=(
175
- "regenerate | clear | clean | reset\n"
231
+ "regenerate | clear | clean\n"
176
232
  " Regenerate config file\n"
177
233
  "open\n"
178
234
  " Open the directory where the config file is located\n"
@@ -182,23 +238,60 @@ class Cmd_type(enum.Enum):
182
238
  " Set config\n"
183
239
  " e.g. config set language zh"
184
240
  ),
241
+ childs=(
242
+ Cmd_type_val(("regenerate", "clear", "clean")),
243
+ Cmd_type_val(("open",)),
244
+ Cmd_type_val(("list",)),
245
+ Cmd_type_val(
246
+ ("set",),
247
+ childs=tuple(Cmd_type_val((k,)) for k in Config_key._member_map_),
248
+ ),
249
+ ),
250
+ )
251
+ prompt = Cmd_type_val(
252
+ ("prompt",),
253
+ param="<prompt option>",
254
+ description=(
255
+ "history\n" # .
256
+ " Show prompt history\n"
257
+ "clear | clean\n"
258
+ " Delete history file"
259
+ ),
260
+ childs=(
261
+ Cmd_type_val(("history",)),
262
+ Cmd_type_val(("clear", "clean")),
263
+ ),
185
264
  )
186
265
  translate = Cmd_type_val(
187
- "translate",
188
- opt_str="translate <files' infix> <target lang tag> [-overwrite]",
266
+ ("translate",),
267
+ param="<files' infix> <target lang tag> [-overwrite]",
189
268
  description=(
190
269
  "Translate subtitle files\n"
191
270
  "e.g. 'translate zh-Hans zh-Hant' will translate all '*.zh-Hans.ass' files into zh-Hant"
192
271
  ),
272
+ childs=(Cmd_type_val(("-overwrite",)),),
193
273
  )
194
274
  mediainfo = Cmd_type_val(
195
- "mediainfo",
196
- opt_str="mediainfo <path>",
275
+ ("mediainfo",),
276
+ param="<<path> | 'fd' | 'cfd'>",
197
277
  description="Get the media info by the Media_info class",
278
+ childs=(
279
+ Cmd_type_val(("fd",)),
280
+ Cmd_type_val(("cfd",)),
281
+ ),
282
+ )
283
+ fontinfo = Cmd_type_val(
284
+ ("fontinfo",),
285
+ param="<<path> | 'fd' | 'cfd'>",
286
+ description="Get the font info by the Font class",
287
+ childs=(
288
+ Cmd_type_val(("fd",)),
289
+ Cmd_type_val(("cfd",)),
290
+ ),
198
291
  )
199
292
  Option = Cmd_type_val(
200
- "Option",
201
- opt_str="<Option> ...",
293
+ ("Option",),
294
+ param="...",
202
295
  description=(
203
296
  "-i <input> -p <preset name> [-o <output>] [-o:dir <dir>] [-pipe <vpy pathname> -crf <val> -psy-rd <val> ...] [-sub <subtitle pathname>] [-c:a <audio encoder> -b:a <audio bitrate>] [-muxer <muxer> [-r <fps>]] [-run [<run option>]] [...]\n"
204
297
  " \n"
@@ -209,7 +302,7 @@ class Cmd_type(enum.Enum):
209
302
  @classmethod
210
303
  def from_str(cls, s: str) -> Self | None:
211
304
  guess_str = s.replace("-", "_").replace(":", "_")
212
- if guess_str in cls._member_map_.keys():
305
+ if guess_str in cls._member_map_:
213
306
  return cls[guess_str]
214
307
  return None
215
308
 
@@ -220,21 +313,25 @@ class Cmd_type(enum.Enum):
220
313
 
221
314
  class Opt_type(enum.Enum):
222
315
  _i = Cmd_type_val(
223
- "-i",
224
- opt_str="-i <string[::string[?string...]...] | 'fd' | 'cfd'>",
316
+ ("-i",),
317
+ param="<<path>[::<path>[?<path>...]...] | 'fd' | 'cfd'>",
225
318
  description=(
226
319
  "Input files' pathname or enter 'fd' to use file dialog, 'cfd' to open from the current directory\n"
227
320
  "In some cases, it is allowed to use '?' as a delimiter to input multiple into a Ripper, for example, 'preset subset' allows multiple ASS inputs"
228
321
  ),
322
+ childs=(
323
+ Cmd_type_val(("fd",)),
324
+ Cmd_type_val(("cfd",)),
325
+ ),
229
326
  )
230
327
  _o_dir = Cmd_type_val(
231
- "-o:dir",
232
- opt_str="-o:dir <string>",
328
+ ("-o:dir",),
329
+ param="<path>",
233
330
  description="Destination directory of the output file",
234
331
  )
235
332
  _o = Cmd_type_val(
236
- "-o",
237
- opt_str="-o <string>",
333
+ ("-o",),
334
+ param="<path>",
238
335
  description=(
239
336
  "Output file basename's prefix\n"
240
337
  "Allow iterators and time formatting for multiple inputs\n"
@@ -242,58 +339,52 @@ class Opt_type(enum.Enum):
242
339
  ),
243
340
  )
244
341
  _auto_infix = Cmd_type_val(
245
- "-auto-infix",
246
- opt_str="-auto-infix <0 | 1>",
342
+ ("-auto-infix",),
343
+ param="<0 | 1>",
247
344
  description=(
248
345
  "If enable, output file name will add auto infix:\n"
249
346
  " no audio: '.v'\n"
250
347
  " with audio: '.va'\n"
251
348
  "Default: 1"
252
349
  ),
350
+ childs=(Cmd_type_val(("0", "1")),),
253
351
  )
254
352
  _preset = _p = Cmd_type_val(
255
- "-preset",
256
- opt_str="-p / -preset <string>",
353
+ ("-p", "-preset"),
354
+ param="<string>",
257
355
  description=(
258
356
  "Setting preset\n"
259
- "Preset name:\n"
260
- " custom\n"
261
- " subset\n"
262
- " copy\n"
263
- " flac\n"
264
- " x264fast x264slow\n"
265
- " x265fast4 x265fast3 x265fast2 x265fast x265slow x265full\n"
266
- " h264_amf h264_nvenc h264_qsv\n"
267
- " hevc_amf hevc_nvenc hevc_qsv\n"
268
- " av1_amf av1_nvenc av1_qsv"
357
+ "Preset name:\n" # .
358
+ f"{Preset_name.to_help_string(' ')}"
269
359
  ),
360
+ childs=(Cmd_type_val(tuple(Preset_name._value2member_map_)),),
270
361
  )
271
362
  _pipe = Cmd_type_val(
272
- "-pipe",
273
- opt_str="-pipe <string>",
363
+ ("-pipe",),
364
+ param="<string>",
274
365
  description=(
275
366
  "Select a vpy file as pipe to input, this vpy must have input global val\n"
276
367
  "The input in vspipe: vspipe -a input=<input> filter.vpy"
277
368
  ),
278
369
  )
279
370
  _pipe_gvar = Cmd_type_val(
280
- "-pipe",
281
- opt_str="-pipe:gvar <key>=<val>[:...]",
371
+ ("-pipe:gvar",),
372
+ param="<key>=<val>[:...]",
282
373
  description=(
283
374
  "Customize the global variables passed to vspipe, and use ':' intervals for multiple variables\n"
284
375
  ' e.g. -pipe:gvar "a=1 2 3:b=abc" -> vspipe -a "a=1 2 3" -a "b=abc"'
285
376
  ),
286
377
  )
287
378
  _vf = Cmd_type_val(
288
- "-vf",
289
- opt_str="-vf <string>",
379
+ ("-vf",),
380
+ param="<string>",
290
381
  description=(
291
382
  "Customize FFmpeg's -vf\nUsing it together with -sub is undefined behavior"
292
383
  ),
293
384
  )
294
385
  _sub = Cmd_type_val(
295
- "-sub",
296
- opt_str="-sub <string | 'auto' | 'auto:...'>",
386
+ ("-sub",),
387
+ param="<<path> | 'auto' | 'auto:...'>",
297
388
  description=(
298
389
  "It use libass to make hard subtitle, input a subtitle pathname when you need hard subtitle\n"
299
390
  'It can add multiple subtitles by "::"\n'
@@ -302,44 +393,51 @@ class Opt_type(enum.Enum):
302
393
  "'auto:...' can only select which match infix.\n"
303
394
  " e.g. 'auto:zh-Hans:zh-Hant'"
304
395
  ),
396
+ childs=(Cmd_type_val(("auto",)),),
305
397
  )
306
398
  _only_mux_sub_path = Cmd_type_val(
307
- "-only-mux-sub-path",
308
- opt_str="-only-mux-sub-path <string>",
399
+ ("-only-mux-sub-path",),
400
+ param="<path>",
309
401
  description="All subtitles and fonts in this path will be muxed",
310
402
  )
311
403
  _soft_sub = Cmd_type_val(
312
- "-soft-sub",
313
- opt_str="-soft-sub <string[?string...] | 'auto' | 'auto:...'>",
314
- description="Mux ASS subtitles in MKV with subset",
404
+ ("-soft-sub",),
405
+ param="<<path>[?<path>...] | 'auto' | 'auto:...'>",
406
+ description=(
407
+ "Mux ASS subtitles in MKV with subset\n" # .
408
+ "The usage of 'auto' is detailed in '-sub'"
409
+ ),
410
+ childs=(Cmd_type_val(("auto",)),),
315
411
  )
316
412
  _subset_font_dir = Cmd_type_val(
317
- "-subset-font-dir",
318
- opt_str="-subset-font-dir <string[?string...]>",
413
+ ("-subset-font-dir",),
414
+ param="<<path>[?<path>...]>",
319
415
  description=(
320
416
  "The fonts directory when subset\n"
321
417
  'Default: Prioritize the current directory, followed by folders containing "font" (case-insensitive) within the current directory'
322
418
  ),
323
419
  )
324
420
  _subset_font_in_sub = Cmd_type_val(
325
- "-subset-font-in-sub",
326
- opt_str="-subset-font-in-sub <0 | 1>",
421
+ ("-subset-font-in-sub",),
422
+ param="<0 | 1>",
327
423
  description=(
328
424
  "Encode fonts into ASS file instead of standalone files\n" # .
329
425
  "Default: 0"
330
426
  ),
427
+ childs=(Cmd_type_val(("0", "1")),),
331
428
  )
332
429
  _subset_use_win_font = Cmd_type_val(
333
- "-subset-use-win-font",
334
- opt_str="-subset-use-win-font <0 | 1>",
430
+ ("-subset-use-win-font",),
431
+ param="<0 | 1>",
335
432
  description=(
336
433
  "Use Windows fonts when can not find font in subset-font-dir\n" # .
337
434
  "Default: 0"
338
435
  ),
436
+ childs=(Cmd_type_val(("0", "1")),),
339
437
  )
340
438
  _subset_use_libass_spec = Cmd_type_val(
341
- "-subset-use-libass-spec",
342
- opt_str="-subset-use-libass-spec <0 | 1>",
439
+ ("-subset-use-libass-spec",),
440
+ param="<0 | 1>",
343
441
  description=(
344
442
  "Use libass specification when subset\n"
345
443
  'e.g. "11\\{22}33" ->\n'
@@ -347,59 +445,61 @@ class Opt_type(enum.Enum):
347
445
  ' "11{22}33" (libass)\n'
348
446
  "Default: 0"
349
447
  ),
448
+ childs=(Cmd_type_val(("0", "1")),),
350
449
  )
351
450
  _subset_drop_non_render = Cmd_type_val(
352
- "-subset-drop-non-render",
353
- opt_str="-subset-use-libass-spec <0 | 1>",
451
+ ("-subset-drop-non-render",),
452
+ param="<0 | 1>",
354
453
  description=(
355
454
  "Drop non rendered content such as Comment lines, Name, Effect, etc. in ASS\n"
356
455
  "Default: 1"
357
456
  ),
457
+ childs=(Cmd_type_val(("0", "1")),),
358
458
  )
359
459
  _subset_drop_unkow_data = Cmd_type_val(
360
- "-subset-drop-unkow-data",
361
- opt_str="-subset-drop-unkow-data <0 | 1>",
460
+ ("-subset-drop-unkow-data",),
461
+ param="<0 | 1>",
362
462
  description=(
363
463
  "Drop lines that are not in {[Script Info], [V4+ Styles], [Events]} in ASS\n"
364
464
  "Default: 1"
365
465
  ),
466
+ childs=(Cmd_type_val(("0", "1")),),
366
467
  )
367
468
  _subset_strict = Cmd_type_val(
368
- "-subset-strict",
369
- opt_str="-subset-strict <0 | 1>",
469
+ ("-subset-strict",),
470
+ param="<0 | 1>",
370
471
  description=(
371
472
  "Some error will interrupt subset\n" # .
372
473
  "Default: 0"
373
474
  ),
475
+ childs=(Cmd_type_val(("0", "1")),),
374
476
  )
375
477
  _translate_sub = Cmd_type_val(
376
- "-translate-sub",
377
- opt_str="-translate-sub <infix>:<language-tag>",
478
+ ("-translate-sub",),
479
+ param="<infix>:<language-tag>",
378
480
  description=(
379
481
  "Temporary generation of subtitle translation files\n"
380
482
  "e.g. 'zh-Hans:zh-Hant' will temporary generation of Traditional Chinese subtitles"
381
483
  ),
382
484
  )
383
485
  _c_a = Cmd_type_val(
384
- "-c:a",
385
- opt_str="-c:a <string>",
486
+ ("-c:a",),
487
+ param="<string>",
386
488
  description=(
387
489
  "Setting audio encoder\n"
388
- " \n" # .
389
- "Audio encoder:\n"
390
- " copy\n"
391
- " libopus\n"
392
- " flac"
490
+ "Audio encoder:\n" # .
491
+ f"{Audio_codec.to_help_string(' ')}"
393
492
  ),
493
+ childs=(Cmd_type_val(tuple(Audio_codec._value2member_map_)),),
394
494
  )
395
495
  _b_a = Cmd_type_val(
396
- "-b:a",
397
- opt_str="-b:a <string>",
496
+ ("-b:a",),
497
+ param="<string>",
398
498
  description="Setting audio bitrate. Default '160k'",
399
499
  )
400
500
  _muxer = Cmd_type_val(
401
- "-muxer",
402
- opt_str="-muxer <string>",
501
+ ("-muxer",),
502
+ param="<string>",
403
503
  description=(
404
504
  "Setting muxer\n"
405
505
  " \n" # .
@@ -409,24 +509,25 @@ class Opt_type(enum.Enum):
409
509
  ),
410
510
  )
411
511
  _r = _fps = Cmd_type_val(
412
- "-r",
413
- opt_str="-r / -fps <string | 'auto'>",
512
+ ("-r", "-fps"),
513
+ param="<string | 'auto'>",
414
514
  description=(
415
515
  "Setting FPS when muxing\n"
416
516
  "When using auto, the frame rate is automatically obtained from the input video and adsorbed to the nearest preset point"
417
517
  ),
518
+ childs=(Cmd_type_val(("auto",)),),
418
519
  )
419
520
  _chapters = Cmd_type_val(
420
- "-chapters",
421
- opt_str="-chapters <string>",
521
+ ("-chapters",),
522
+ param="<path>",
422
523
  description=(
423
524
  "Specify the chapters file to add\n"
424
525
  "Supports the same iteration syntax as '-o'"
425
526
  ),
426
527
  )
427
528
  _custom_template = _custom = _custom_format = Cmd_type_val(
428
- "-custom:format",
429
- opt_str="-custom / -custom:format / -custom:template <string>",
529
+ ("-custom", "-custom:format", "-custom:tempate"),
530
+ param="<string>",
430
531
  description=(
431
532
  "When -preset custom, this option will run\n"
432
533
  "String escape: \\34/ -> \", \\39/ -> ', '' -> \"\n"
@@ -434,89 +535,106 @@ class Opt_type(enum.Enum):
434
535
  ),
435
536
  )
436
537
  _custom_suffix = Cmd_type_val(
437
- "-custom:suffix",
438
- opt_str="-custom:suffix <string>",
538
+ ("-custom:suffix",),
539
+ param="<string>",
439
540
  description=(
440
541
  "When -preset custom, this option will be used as a suffix for the output file\n"
441
542
  'Default: ""'
442
543
  ),
443
544
  )
444
545
  _run = Cmd_type_val(
445
- "-run",
446
- opt_str="-run [<string>]",
546
+ ("-run",),
547
+ param="[<string>]",
447
548
  description=(
448
549
  "Run the Ripper from the Ripper list\n"
449
- " \n"
550
+ "\n"
450
551
  "Default:\n"
451
552
  " Only run\n"
452
- " \n"
553
+ "\n"
453
554
  "exit:\n"
454
555
  " Close program when run finished\n"
455
- " \n"
556
+ "\n"
456
557
  "shutdown [<sec>]:\n"
457
558
  " Shutdown when run finished\n"
458
559
  " Default: 60\n"
560
+ "\n"
561
+ "server [<address>]:[<port>]@[<password>]:\n"
562
+ " See the corresponding help for details"
563
+ ),
564
+ childs=(
565
+ Cmd_type_val(("exit",)),
566
+ Cmd_type_val(("shutdown",)),
567
+ Cmd_type_val(("server",)),
459
568
  ),
460
569
  )
461
570
  _ff_params_ff = _ff_params = Cmd_type_val(
462
- "-ff-params:ff",
463
- opt_str="-ff-params / -ff-params:ff <string>",
571
+ ("-ff-params", "-ff-params:ff"),
572
+ param="<string>",
464
573
  description=(
465
574
  "Set FFmpeg global options\n" # .
466
575
  "Same as ffmpeg <option> ... -i ..."
467
576
  ),
468
577
  )
469
578
  _ff_params_in = Cmd_type_val(
470
- "-ff-params:in",
471
- opt_str="-ff-params:in <string>",
579
+ ("-ff-params:in",),
580
+ param="<string>",
472
581
  description=(
473
582
  "Set FFmpeg input options\n" # .
474
583
  "Same as ffmpeg ... <option> -i ..."
475
584
  ),
476
585
  )
477
586
  _ff_params_out = Cmd_type_val(
478
- "-ff-params:out",
479
- opt_str="-ff-params:out <string>",
587
+ ("-ff-params:out",),
588
+ param="<string>",
480
589
  description=(
481
590
  "Set FFmpeg output options\n" # .
482
591
  "Same as ffmpeg -i ... <option> ..."
483
592
  ),
484
593
  )
485
594
  _hwaccel = Cmd_type_val(
486
- "-hwaccel",
487
- opt_str="-hwaccel <string>",
595
+ ("-hwaccel",),
596
+ param="<string>",
488
597
  description="Use FFmpeg hwaccel (See 'ffmpeg -hwaccels' for details)",
489
598
  )
490
599
  _ss = Cmd_type_val(
491
- "-ss",
492
- opt_str="-ss <time>",
600
+ ("-ss",),
601
+ param="<time>",
493
602
  description=(
494
603
  "Set FFmpeg input file start time\n" # .
495
604
  "Same as ffmpeg -ss <time> -i ..."
496
605
  ),
497
606
  )
498
607
  _t = Cmd_type_val(
499
- "-t",
500
- opt_str="-t <time>",
608
+ ("-t",),
609
+ param="<time>",
501
610
  description=(
502
611
  "Set FFmpeg output file duration\n" # .
503
612
  "Same as ffmpeg -i ... -t <time> ..."
504
613
  ),
505
614
  )
506
615
  _hevc_strict = Cmd_type_val(
507
- "-hevc-strict",
508
- opt_str="-hevc-strict <0 | 1>",
616
+ ("-hevc-strict",),
617
+ param="<0 | 1>",
509
618
  description=(
510
- "Auto reduce the --ref\n" # .
511
- "When the resolution >= 4k, close HME\n"
619
+ "When the resolution >= 4K, close HME, and auto reduce the -ref\n" # .
512
620
  "Default: 1"
513
621
  ),
622
+ childs=(Cmd_type_val(("0", "1")),),
623
+ )
624
+ _multithreading = Cmd_type_val(
625
+ ("-multithreading",),
626
+ param="<0 | 1>",
627
+ description=(
628
+ "Use multi-threading to run Ripper list, suitable for situations with low performance occupancy\n"
629
+ "e.g. -p subset or -p copy"
630
+ ),
631
+ childs=(Cmd_type_val(("0", "1")),),
514
632
  )
515
633
 
516
634
  @classmethod
517
635
  def from_str(cls, s: str) -> Self | None:
518
636
  guess_str = s.replace("-", "_").replace(":", "_")
519
- if guess_str in cls._member_map_.keys():
637
+ if guess_str in cls._member_map_:
520
638
  return cls[guess_str]
521
639
  return None
522
640
 
@@ -525,6 +643,11 @@ class Opt_type(enum.Enum):
525
643
  return "\n\n".join(ct.value.to_doc() for ct in cls)
526
644
 
527
645
 
646
+ Cmd_type.help.value.childs = tuple(
647
+ ct.value for ct in itertools.chain(Cmd_type, Opt_type) if ct is not Cmd_type.help
648
+ )
649
+
650
+
528
651
  def get_help_doc() -> str:
529
652
  from .easyrip_mlang import gettext
530
653
 
@@ -534,15 +657,201 @@ def get_help_doc() -> str:
534
657
  "\n"
535
658
  f"{gettext('Help')}:\n"
536
659
  "\n"
537
- f" {gettext('You can input command or use command-line arguments to run.')}\n"
660
+ f"{textwrap.indent(gettext("Enter '<cmd> [<param> ...]' to execute Easy Rip commands or any commands that exist in environment.\nOr enter '<option> <param> [<option> <param> ...]' to add Ripper."), ' ')}\n"
538
661
  "\n"
539
662
  "\n"
540
- f"{gettext('Commands')}:\n"
663
+ f"{gettext('Easy Rip Commands')}:\n"
541
664
  "\n"
542
- f"{textwrap.indent(Cmd_type.to_doc(), ' ')}"
665
+ f"{textwrap.indent(Cmd_type.to_doc(), ' ')}\n"
543
666
  "\n"
544
667
  "\n"
545
668
  f"{gettext('Ripper options')}:\n"
546
669
  "\n"
547
670
  f"{textwrap.indent(Opt_type.to_doc(), ' ')}"
548
671
  )
672
+
673
+
674
+ type nested_dict = dict[str, "nested_dict | Completer"]
675
+ META_DICT_OPT_TYPE = {
676
+ name: lambda opt=opt: opt.value.param
677
+ for opt in Opt_type
678
+ for name in opt.value.names
679
+ }
680
+ META_DICT_CMD_TYPE = {
681
+ name: lambda opt=opt: opt.value.param
682
+ for opt in Cmd_type
683
+ for name in opt.value.names
684
+ }
685
+
686
+
687
+ def _nested_dict_to_nc(n_dict: nested_dict) -> NestedCompleter:
688
+ return NestedCompleter(
689
+ {
690
+ k: (v if isinstance(v, Completer) else _nested_dict_to_nc(v) if v else None)
691
+ for k, v in n_dict.items()
692
+ }
693
+ )
694
+
695
+
696
+ class CmdCompleter(NestedCompleter):
697
+ def get_completions(
698
+ self, document: Document, complete_event: CompleteEvent
699
+ ) -> Iterable[Completion]:
700
+ # Split document.
701
+ text = document.text_before_cursor.lstrip()
702
+ words = text.split()
703
+ stripped_len = len(document.text_before_cursor) - len(text)
704
+
705
+ # If there is a space, check for the first term, and use a
706
+ # subcompleter.
707
+ if " " in text:
708
+ first_term = text.split()[0]
709
+ completer = self.options.get(first_term)
710
+
711
+ # If we have a sub completer, use this for the completions.
712
+ if completer is not None:
713
+ remaining_text = text[len(first_term) :].lstrip()
714
+ move_cursor = len(text) - len(remaining_text) + stripped_len
715
+
716
+ new_document = Document(
717
+ remaining_text,
718
+ cursor_position=document.cursor_position - move_cursor,
719
+ )
720
+
721
+ yield from completer.get_completions(new_document, complete_event)
722
+
723
+ elif words and (_cmd := Cmd_type.from_str(words[-1])) is not None:
724
+ yield from (
725
+ Completion(
726
+ text=words[-1],
727
+ start_position=-len(words[-1]),
728
+ display_meta=META_DICT_CMD_TYPE.get(words[-1], ""),
729
+ ),
730
+ Completion(
731
+ text="",
732
+ display="✔",
733
+ display_meta=(
734
+ f"{_desc_list[0]}..."
735
+ if len(_desc_list := _cmd.value.description.split("\n")) > 1
736
+ else _desc_list[0]
737
+ ),
738
+ ),
739
+ )
740
+
741
+ # No space in the input: behave exactly like `WordCompleter`.
742
+ else:
743
+ # custom
744
+ completer = FuzzyWordCompleter(
745
+ tuple(self.options),
746
+ meta_dict=META_DICT_CMD_TYPE, # pyright: ignore[reportArgumentType]
747
+ WORD=True,
748
+ )
749
+ yield from completer.get_completions(document, complete_event)
750
+
751
+
752
+ class OptCompleter(Completer):
753
+ def __init__(self, *, opt_tree: nested_dict) -> None:
754
+ self.opt_tree: Final[nested_dict] = opt_tree
755
+
756
+ def get_completions(
757
+ self, document: Document, complete_event: CompleteEvent
758
+ ) -> Iterable[Completion]:
759
+ text = document.text_before_cursor.lstrip()
760
+
761
+ words = text.split()
762
+
763
+ if len(words) >= 1 and not text.startswith("-"):
764
+ return
765
+
766
+ opt_tree_pos_list: list[nested_dict | Completer] = [self.opt_tree]
767
+
768
+ for word in words:
769
+ if isinstance(opt_tree_pos_list[-1], Completer):
770
+ opt_tree_pos_list.append(self.opt_tree.get(word, self.opt_tree))
771
+ else:
772
+ opt_tree_pos_list.append(
773
+ opt_tree_pos_list[-1].get(
774
+ word, self.opt_tree.get(word, self.opt_tree)
775
+ )
776
+ )
777
+
778
+ if opt_tree_pos_list[-1] is not self.opt_tree and not text.endswith(" "):
779
+ yield from (
780
+ Completion(
781
+ text=words[-1],
782
+ start_position=-len(words[-1]),
783
+ display_meta=META_DICT_OPT_TYPE.get(words[-1], ""),
784
+ ),
785
+ Completion(
786
+ text="",
787
+ display="✔",
788
+ display_meta=(
789
+ ""
790
+ if (_opt := Opt_type.from_str(text)) is None
791
+ else f"{_desc_list[0]}..."
792
+ if len(_desc_list := _opt.value.description.split("\n")) > 1
793
+ else _desc_list[0]
794
+ ),
795
+ ),
796
+ )
797
+
798
+ elif isinstance(opt_tree_pos_list[-1], Completer):
799
+ # 直接使用 PathCompleter 会因为上下文问题失效,所以将上文套进 NestedCompleter
800
+ new_nd: nested_dict = {}
801
+ new_nd_pos: nested_dict = new_nd
802
+ for word in words[:-1]:
803
+ new_nd_pos[word] = new_nd_pos = {}
804
+ new_nd_pos[words[-1]] = opt_tree_pos_list[-1]
805
+
806
+ yield from _nested_dict_to_nc(new_nd).get_completions(
807
+ document=document, complete_event=complete_event
808
+ )
809
+
810
+ elif len(words) >= 2 and isinstance(opt_tree_pos_list[-2], Completer):
811
+ new_nd: nested_dict = {}
812
+ new_nd_pos: nested_dict = new_nd
813
+ for word in words[:-2]:
814
+ new_nd_pos[word] = new_nd_pos = {}
815
+ new_nd_pos[words[-2]] = opt_tree_pos_list[-2]
816
+
817
+ yield from merge_completers(
818
+ (
819
+ DeduplicateCompleter(
820
+ merge_completers(
821
+ (
822
+ _nested_dict_to_nc(new_nd),
823
+ FuzzyCompleter(_nested_dict_to_nc(new_nd), WORD=True),
824
+ )
825
+ )
826
+ ),
827
+ FuzzyCompleter(
828
+ WordCompleter(
829
+ words=tuple(opt_tree_pos_list[-1]),
830
+ meta_dict=META_DICT_OPT_TYPE,
831
+ WORD=True, # 匹配标点
832
+ match_middle=True,
833
+ ),
834
+ WORD=False,
835
+ ),
836
+ )
837
+ ).get_completions(document=document, complete_event=complete_event)
838
+
839
+ else:
840
+ yield from FuzzyCompleter(
841
+ WordCompleter(
842
+ words=tuple(
843
+ opt_tree_pos_list[-1]
844
+ | (
845
+ {}
846
+ if text.endswith(" ")
847
+ or len(words) <= 1
848
+ or isinstance(opt_tree_pos_list[-2], Completer)
849
+ else opt_tree_pos_list[-2]
850
+ )
851
+ ),
852
+ meta_dict=META_DICT_OPT_TYPE,
853
+ WORD=True, # 匹配标点
854
+ match_middle=True,
855
+ ),
856
+ WORD=not text.endswith(" "),
857
+ ).get_completions(document=document, complete_event=complete_event)