easyrip 4.13.3__py3-none-any.whl → 4.15.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 +25 -11
- easyrip/easyrip_command.py +14 -2
- easyrip/easyrip_main.py +11 -2
- easyrip/easyrip_mlang/lang_zh_Hans_CN.py +1 -1
- easyrip/easyrip_prompt.py +108 -86
- easyrip/easyrip_web/third_party_api.py +1 -1
- easyrip/global_val.py +1 -1
- easyrip/ripper/media_info.py +4 -4
- easyrip/ripper/ripper.py +132 -8
- {easyrip-4.13.3.dist-info → easyrip-4.15.0.dist-info}/METADATA +2 -1
- {easyrip-4.13.3.dist-info → easyrip-4.15.0.dist-info}/RECORD +15 -15
- {easyrip-4.13.3.dist-info → easyrip-4.15.0.dist-info}/WHEEL +1 -1
- {easyrip-4.13.3.dist-info → easyrip-4.15.0.dist-info}/entry_points.txt +0 -0
- {easyrip-4.13.3.dist-info → easyrip-4.15.0.dist-info}/licenses/LICENSE +0 -0
- {easyrip-4.13.3.dist-info → easyrip-4.15.0.dist-info}/top_level.txt +0 -0
easyrip/__main__.py
CHANGED
|
@@ -6,6 +6,8 @@ import Crypto
|
|
|
6
6
|
import fontTools
|
|
7
7
|
import prompt_toolkit
|
|
8
8
|
from prompt_toolkit import ANSI, prompt
|
|
9
|
+
from prompt_toolkit.application import get_app
|
|
10
|
+
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
|
|
9
11
|
from prompt_toolkit.completion import merge_completers
|
|
10
12
|
from prompt_toolkit.history import InMemoryHistory
|
|
11
13
|
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
|
|
@@ -17,15 +19,18 @@ from .easyrip_command import (
|
|
|
17
19
|
Cmd_type,
|
|
18
20
|
Cmd_type_val,
|
|
19
21
|
CmdCompleter,
|
|
20
|
-
FuzzyCompleter,
|
|
21
|
-
NestedCompleter,
|
|
22
22
|
Opt_type,
|
|
23
23
|
OptCompleter,
|
|
24
24
|
nested_dict,
|
|
25
25
|
)
|
|
26
26
|
from .easyrip_config.config import Config_key, config
|
|
27
27
|
from .easyrip_main import Ripper, get_input_prompt, init, log, run_command
|
|
28
|
-
from .easyrip_prompt import
|
|
28
|
+
from .easyrip_prompt import (
|
|
29
|
+
ConfigFileHistory,
|
|
30
|
+
CustomPromptCompleter,
|
|
31
|
+
SmartPathCompleter,
|
|
32
|
+
easyrip_prompt,
|
|
33
|
+
)
|
|
29
34
|
from .global_val import C_D, C_Z
|
|
30
35
|
|
|
31
36
|
|
|
@@ -46,8 +51,22 @@ def run() -> NoReturn:
|
|
|
46
51
|
|
|
47
52
|
@key_bindings.add(Keys.ControlC)
|
|
48
53
|
def _(event: KeyPressEvent) -> None:
|
|
54
|
+
buffer = event.app.current_buffer
|
|
55
|
+
|
|
56
|
+
# 检查是否有选中的文本
|
|
57
|
+
if buffer.selection_state is not None:
|
|
58
|
+
get_app().clipboard.set_data(buffer.copy_selection())
|
|
59
|
+
return
|
|
60
|
+
|
|
49
61
|
event.app.exit(exception=KeyboardInterrupt, style="class:exiting")
|
|
50
62
|
|
|
63
|
+
@key_bindings.add(Keys.ControlA)
|
|
64
|
+
def _(event: KeyPressEvent) -> None:
|
|
65
|
+
buff = event.app.current_buffer
|
|
66
|
+
buff.cursor_position = 0
|
|
67
|
+
buff.start_selection()
|
|
68
|
+
buff.cursor_position = len(buff.text)
|
|
69
|
+
|
|
51
70
|
@key_bindings.add(Keys.ControlD)
|
|
52
71
|
def _(event: KeyPressEvent) -> None:
|
|
53
72
|
event.app.current_buffer.insert_text(C_D)
|
|
@@ -61,6 +80,7 @@ def run() -> NoReturn:
|
|
|
61
80
|
return named_commands.get_by_name("unix-word-rubout").handler(event)
|
|
62
81
|
|
|
63
82
|
path_completer = SmartPathCompleter()
|
|
83
|
+
clipboard = PyperclipClipboard()
|
|
64
84
|
|
|
65
85
|
def _ctv_to_nc(ctvs: Iterable[Cmd_type_val]) -> CmdCompleter:
|
|
66
86
|
return CmdCompleter(
|
|
@@ -130,18 +150,12 @@ def run() -> NoReturn:
|
|
|
130
150
|
(
|
|
131
151
|
_ctv_to_nc(cmd_ctv_tuple),
|
|
132
152
|
OptCompleter(opt_tree=_ctv_to_nd(ct.value for ct in Opt_type)),
|
|
133
|
-
|
|
134
|
-
NestedCompleter(
|
|
135
|
-
{
|
|
136
|
-
k: NestedCompleter({v: None})
|
|
137
|
-
for k, v in easyrip_prompt.get_custom_prompt().items()
|
|
138
|
-
}
|
|
139
|
-
),
|
|
140
|
-
),
|
|
153
|
+
CustomPromptCompleter(),
|
|
141
154
|
)
|
|
142
155
|
),
|
|
143
156
|
history=prompt_history,
|
|
144
157
|
complete_while_typing=True,
|
|
158
|
+
clipboard=clipboard,
|
|
145
159
|
)
|
|
146
160
|
if command.startswith(C_Z):
|
|
147
161
|
raise EOFError
|
easyrip/easyrip_command.py
CHANGED
|
@@ -253,19 +253,22 @@ class Cmd_type(enum.Enum):
|
|
|
253
253
|
description=(
|
|
254
254
|
"history\n" # .
|
|
255
255
|
" Show prompt history\n"
|
|
256
|
-
"
|
|
256
|
+
"history_clear\n"
|
|
257
257
|
" Delete history file\n"
|
|
258
258
|
"add <name:string> <cmd:string>\n"
|
|
259
259
|
" Add a custom prompt\n"
|
|
260
260
|
" e.g. prompt add myprompt echo my prompt\n"
|
|
261
261
|
"del <name:string>\n"
|
|
262
262
|
" Delete a custom prompt"
|
|
263
|
+
"show\n"
|
|
264
|
+
" Show custom prompt"
|
|
263
265
|
),
|
|
264
266
|
childs=(
|
|
265
267
|
Cmd_type_val(("history",)),
|
|
266
|
-
Cmd_type_val(("
|
|
268
|
+
Cmd_type_val(("history_clear",)),
|
|
267
269
|
Cmd_type_val(("add",)),
|
|
268
270
|
Cmd_type_val(("del",)),
|
|
271
|
+
Cmd_type_val(("show",)),
|
|
269
272
|
),
|
|
270
273
|
)
|
|
271
274
|
translate = Cmd_type_val(
|
|
@@ -661,6 +664,15 @@ class Opt_type(enum.Enum):
|
|
|
661
664
|
),
|
|
662
665
|
childs=(Cmd_type_val(("0", "1")),),
|
|
663
666
|
)
|
|
667
|
+
_quality_detection = Cmd_type_val(
|
|
668
|
+
("-quality-detection",),
|
|
669
|
+
param="<algorithm>[:<threshold>]",
|
|
670
|
+
description=(
|
|
671
|
+
"Comparison of quality between detection and source after encoding is completed\n"
|
|
672
|
+
"Algorithm: ssim psnr vmaf"
|
|
673
|
+
),
|
|
674
|
+
childs=(Cmd_type_val(("ssim", "psnr", "vmaf")),),
|
|
675
|
+
)
|
|
664
676
|
|
|
665
677
|
@classmethod
|
|
666
678
|
def from_str(cls, s: str) -> Self | None:
|
easyrip/easyrip_main.py
CHANGED
|
@@ -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}",)
|
|
@@ -776,7 +776,7 @@ def run_command(command: Iterable[str] | str) -> bool:
|
|
|
776
776
|
for line in f.read().splitlines():
|
|
777
777
|
log.send(line, is_format=False)
|
|
778
778
|
|
|
779
|
-
case "
|
|
779
|
+
case "history_clear":
|
|
780
780
|
easyrip_prompt.clear_history()
|
|
781
781
|
|
|
782
782
|
case "add":
|
|
@@ -797,6 +797,15 @@ def run_command(command: Iterable[str] | str) -> bool:
|
|
|
797
797
|
log.info("Del custom prompt {!r}", _name)
|
|
798
798
|
return easyrip_prompt.del_custom_prompt(_name)
|
|
799
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
|
+
)
|
|
808
|
+
|
|
800
809
|
case Cmd_type.translate:
|
|
801
810
|
if not (_infix := cmd_list[1]):
|
|
802
811
|
log.error("Need target infix")
|
|
@@ -328,7 +328,7 @@ LANG_MAP: dict[str, str] = {
|
|
|
328
328
|
"There have error in running": "执行时出错",
|
|
329
329
|
"{} param illegal": "{} 参数非法",
|
|
330
330
|
'The file "{}" already exists, skip translating it': '文件 "{}" 已存在, 跳过翻译',
|
|
331
|
-
"Subset
|
|
331
|
+
"Subset failed, cancel mux": "子集化失败, 取消混流",
|
|
332
332
|
"FFmpeg report: {}": "FFmpeg 报告: {}",
|
|
333
333
|
"{} not found. Skip it": "没找到 {}。默认跳过",
|
|
334
334
|
"{} not found. Skip it. Perhaps you want the {}": "没找到 {}。默认跳过。或许你想要的是 {}",
|
easyrip/easyrip_prompt.py
CHANGED
|
@@ -83,94 +83,95 @@ class ConfigFileHistory(FileHistory):
|
|
|
83
83
|
super().store_string(string)
|
|
84
84
|
|
|
85
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
|
+
|
|
86
171
|
class SmartPathCompleter(Completer):
|
|
87
172
|
def __init__(self) -> None:
|
|
88
173
|
pass
|
|
89
174
|
|
|
90
|
-
def _highlight_fuzzy_match(
|
|
91
|
-
self,
|
|
92
|
-
suggestion: str,
|
|
93
|
-
user_input: str,
|
|
94
|
-
style_config: dict | None = None,
|
|
95
|
-
) -> StyleAndTextTuples:
|
|
96
|
-
"""
|
|
97
|
-
高亮显示模糊匹配结果
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
suggestion: 建议的完整字符串
|
|
101
|
-
user_input: 用户输入的匹配字符
|
|
102
|
-
style_config: 样式配置字典
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
包含样式信息的格式化文本
|
|
106
|
-
|
|
107
|
-
"""
|
|
108
|
-
if style_config is None:
|
|
109
|
-
style_config = {
|
|
110
|
-
"match_char": "class:fuzzymatch.inside.character",
|
|
111
|
-
"match_section": "class:fuzzymatch.inside",
|
|
112
|
-
"non_match": "class:fuzzymatch.outside",
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if not user_input:
|
|
116
|
-
# 用户没有输入,返回原始字符串
|
|
117
|
-
return [(style_config["non_match"], suggestion)]
|
|
118
|
-
|
|
119
|
-
# 找到最佳匹配位置
|
|
120
|
-
result = []
|
|
121
|
-
|
|
122
|
-
# 简化的模糊匹配算法
|
|
123
|
-
pattern = ".*?".join(map(re.escape, user_input))
|
|
124
|
-
regex = re.compile(pattern, re.IGNORECASE)
|
|
125
|
-
|
|
126
|
-
match = regex.search(suggestion)
|
|
127
|
-
if not match:
|
|
128
|
-
# 没有匹配,返回原始字符串
|
|
129
|
-
return [(style_config["non_match"], suggestion)]
|
|
130
|
-
|
|
131
|
-
start, end = match.span()
|
|
132
|
-
match_text = suggestion[start:end]
|
|
133
|
-
|
|
134
|
-
# 匹配段之前的文本
|
|
135
|
-
if start > 0:
|
|
136
|
-
result.append((style_config["non_match"], suggestion[:start]))
|
|
137
|
-
|
|
138
|
-
# 匹配段内部的字符
|
|
139
|
-
input_chars = list(user_input)
|
|
140
|
-
for char in match_text:
|
|
141
|
-
if input_chars and char.lower() == input_chars[0].lower():
|
|
142
|
-
result.append((style_config["match_char"], char))
|
|
143
|
-
input_chars.pop(0)
|
|
144
|
-
else:
|
|
145
|
-
result.append((style_config["match_section"], char))
|
|
146
|
-
|
|
147
|
-
# 匹配段之后的文本
|
|
148
|
-
if end < len(suggestion):
|
|
149
|
-
result.append((style_config["non_match"], suggestion[end:]))
|
|
150
|
-
|
|
151
|
-
return result
|
|
152
|
-
|
|
153
|
-
def _fuzzy_filter_and_sort(self, filenames: list[str], match_str: str) -> list[str]:
|
|
154
|
-
"""模糊过滤和排序"""
|
|
155
|
-
if not match_str:
|
|
156
|
-
return sorted(filenames)
|
|
157
|
-
|
|
158
|
-
# 构建模糊匹配模式
|
|
159
|
-
pattern = ".*?".join(map(re.escape, match_str))
|
|
160
|
-
regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
|
|
161
|
-
|
|
162
|
-
matches = []
|
|
163
|
-
for filename in filenames:
|
|
164
|
-
regex_matches = list(regex.finditer(filename))
|
|
165
|
-
if regex_matches:
|
|
166
|
-
# 找到最佳匹配(最左、最短)
|
|
167
|
-
best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
|
|
168
|
-
matches.append((best.start(), len(best.group(1)), filename))
|
|
169
|
-
|
|
170
|
-
# 按匹配质量排序:先按匹配位置,再按匹配长度
|
|
171
|
-
matches.sort(key=lambda x: (x[0], x[1]))
|
|
172
|
-
return [item[2] for item in matches]
|
|
173
|
-
|
|
174
175
|
def get_completions(
|
|
175
176
|
self,
|
|
176
177
|
document: Document,
|
|
@@ -187,7 +188,7 @@ class SmartPathCompleter(Completer):
|
|
|
187
188
|
os.listdir(directory) if os.path.isdir(directory) else []
|
|
188
189
|
)
|
|
189
190
|
|
|
190
|
-
for filename in
|
|
191
|
+
for filename in _fuzzy_filter_and_sort(filenames, basename):
|
|
191
192
|
full_name = (
|
|
192
193
|
filename if directory == "." else os.path.join(directory, filename)
|
|
193
194
|
)
|
|
@@ -203,8 +204,29 @@ class SmartPathCompleter(Completer):
|
|
|
203
204
|
yield Completion(
|
|
204
205
|
text=completion,
|
|
205
206
|
start_position=-len(text),
|
|
206
|
-
display=
|
|
207
|
+
display=_highlight_fuzzy_match(filename, basename),
|
|
207
208
|
)
|
|
208
209
|
|
|
209
210
|
except OSError:
|
|
210
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
|
+
)
|
|
@@ -120,7 +120,7 @@ class mkvtoolnix:
|
|
|
120
120
|
log.debug(
|
|
121
121
|
"'{}' execution failed: {}",
|
|
122
122
|
f"{cls.__name__}.{cls.get_latest_release_ver.__name__}",
|
|
123
|
-
f"XML parse
|
|
123
|
+
f"XML parse failed: {xml.etree.ElementTree.tostring(xml_tree)}",
|
|
124
124
|
print_level=log.LogLevel._detail,
|
|
125
125
|
)
|
|
126
126
|
return None
|
easyrip/global_val.py
CHANGED
|
@@ -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.
|
|
7
|
+
PROJECT_VERSION = "4.15.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"
|
easyrip/ripper/media_info.py
CHANGED
|
@@ -120,7 +120,7 @@ class Media_info:
|
|
|
120
120
|
sample_fmt = _audio_info_dict.get("sample_fmt")
|
|
121
121
|
if sample_fmt is None:
|
|
122
122
|
log.debug(
|
|
123
|
-
'
|
|
123
|
+
'Failed to get audio info: {}. file "{}" track index {}',
|
|
124
124
|
"sample_fmt",
|
|
125
125
|
path,
|
|
126
126
|
index,
|
|
@@ -130,7 +130,7 @@ class Media_info:
|
|
|
130
130
|
sample_rate = _audio_info_dict.get("sample_rate")
|
|
131
131
|
if sample_rate is None:
|
|
132
132
|
log.debug(
|
|
133
|
-
'
|
|
133
|
+
'Failed to get audio info: {}. file "{}" track index {}',
|
|
134
134
|
"sample_rate",
|
|
135
135
|
path,
|
|
136
136
|
index,
|
|
@@ -140,7 +140,7 @@ class Media_info:
|
|
|
140
140
|
bits_per_sample = _audio_info_dict.get("bits_per_sample")
|
|
141
141
|
if bits_per_sample is None:
|
|
142
142
|
log.debug(
|
|
143
|
-
'
|
|
143
|
+
'Failed to get audio info: {}. file "{}" track index {}',
|
|
144
144
|
"bits_per_sample",
|
|
145
145
|
path,
|
|
146
146
|
index,
|
|
@@ -150,7 +150,7 @@ class Media_info:
|
|
|
150
150
|
bits_per_raw_sample = _audio_info_dict.get("bits_per_raw_sample")
|
|
151
151
|
if bits_per_raw_sample is None:
|
|
152
152
|
log.debug(
|
|
153
|
-
'
|
|
153
|
+
'Failed to get audio info: {}. file "{}" track index {}',
|
|
154
154
|
"bits_per_raw_sample",
|
|
155
155
|
path,
|
|
156
156
|
index,
|
easyrip/ripper/ripper.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import csv
|
|
1
2
|
import os
|
|
2
3
|
import re
|
|
3
4
|
import shutil
|
|
@@ -14,7 +15,7 @@ from typing import Final, Self, final
|
|
|
14
15
|
from .. import easyrip_web
|
|
15
16
|
from ..easyrip_log import log
|
|
16
17
|
from ..easyrip_mlang import Global_lang_val, gettext, translate_subtitles
|
|
17
|
-
from ..utils import get_base62_time
|
|
18
|
+
from ..utils import get_base62_time, read_text
|
|
18
19
|
from .media_info import Media_info, Stream_error
|
|
19
20
|
from .param import (
|
|
20
21
|
FONT_SUFFIX_SET,
|
|
@@ -1088,9 +1089,15 @@ class Ripper:
|
|
|
1088
1089
|
f'{gettext("Encoding speed")}: <span style="color:darkcyan;">{speed}</span><br>'
|
|
1089
1090
|
)
|
|
1090
1091
|
|
|
1092
|
+
# 获取 ffmpeg report 中的报错
|
|
1093
|
+
if FF_REPORT_LOG_FILE.is_file():
|
|
1094
|
+
with FF_REPORT_LOG_FILE.open("rt", encoding="utf-8") as file:
|
|
1095
|
+
for line in file.readlines()[2:]:
|
|
1096
|
+
log.warning("FFmpeg report: {}", line)
|
|
1097
|
+
|
|
1091
1098
|
if is_cmd_run_failed:
|
|
1092
1099
|
log.error("There have error in running")
|
|
1093
|
-
else: # 多文件合成
|
|
1100
|
+
else: # 多文件合成 or 后处理
|
|
1094
1101
|
# flac 音频轨合成
|
|
1095
1102
|
if (
|
|
1096
1103
|
self.preset_name != Ripper.Preset_name.flac
|
|
@@ -1245,16 +1252,133 @@ class Ripper:
|
|
|
1245
1252
|
).run() and os.path.exists(new_full_name):
|
|
1246
1253
|
os.remove(new_full_name)
|
|
1247
1254
|
else:
|
|
1248
|
-
log.error("Subset
|
|
1255
|
+
log.error("Subset failed, cancel mux")
|
|
1249
1256
|
|
|
1250
1257
|
# 清理临时文件
|
|
1251
1258
|
shutil.rmtree(subset_folder)
|
|
1252
1259
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1260
|
+
# 画质检测
|
|
1261
|
+
if quality_detection := self.option_map.get("quality-detection"):
|
|
1262
|
+
quality_detection = quality_detection.split(":")
|
|
1263
|
+
quality_detection_th: float
|
|
1264
|
+
quality_detection_filter: str
|
|
1265
|
+
while True:
|
|
1266
|
+
match quality_detection[0]:
|
|
1267
|
+
case "ssim":
|
|
1268
|
+
quality_detection_th = 0.9
|
|
1269
|
+
quality_detection_filter = "ssim=f="
|
|
1270
|
+
|
|
1271
|
+
def quality_detection_cmp(
|
|
1272
|
+
text: str, threshold: float
|
|
1273
|
+
) -> None:
|
|
1274
|
+
for line in text.splitlines():
|
|
1275
|
+
values = tuple(
|
|
1276
|
+
s.split(":")[1] for s in line.split()[:-1]
|
|
1277
|
+
)
|
|
1278
|
+
ssim_all = float(values[-1])
|
|
1279
|
+
n = values[0]
|
|
1280
|
+
log.debug(
|
|
1281
|
+
f"{n}: {ssim_all}",
|
|
1282
|
+
is_format=False,
|
|
1283
|
+
print_level=log.LogLevel._detail,
|
|
1284
|
+
)
|
|
1285
|
+
if ssim_all < threshold:
|
|
1286
|
+
log.error(
|
|
1287
|
+
"SSIM {} < threshold {} in frame {}",
|
|
1288
|
+
ssim_all,
|
|
1289
|
+
threshold,
|
|
1290
|
+
n,
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
break
|
|
1294
|
+
case "psnr":
|
|
1295
|
+
quality_detection_th = 30
|
|
1296
|
+
quality_detection_filter = "psnr=f="
|
|
1297
|
+
|
|
1298
|
+
def quality_detection_cmp(
|
|
1299
|
+
text: str, threshold: float
|
|
1300
|
+
) -> None:
|
|
1301
|
+
for line in text.splitlines():
|
|
1302
|
+
values = tuple(
|
|
1303
|
+
s.split(":")[1] for s in line.split()
|
|
1304
|
+
)
|
|
1305
|
+
psnr_avg_all = float(values[-4])
|
|
1306
|
+
n = values[0]
|
|
1307
|
+
log.debug(
|
|
1308
|
+
f"{n}: {psnr_avg_all}",
|
|
1309
|
+
is_format=False,
|
|
1310
|
+
print_level=log.LogLevel._detail,
|
|
1311
|
+
)
|
|
1312
|
+
if psnr_avg_all < threshold:
|
|
1313
|
+
log.error(
|
|
1314
|
+
"PSNR {} < threshold {} in frame {}",
|
|
1315
|
+
psnr_avg_all,
|
|
1316
|
+
threshold,
|
|
1317
|
+
n,
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
break
|
|
1321
|
+
case "vmaf":
|
|
1322
|
+
quality_detection_th = 80
|
|
1323
|
+
quality_detection_filter = "libvmaf=log_fmt=csv:log_path="
|
|
1324
|
+
|
|
1325
|
+
def quality_detection_cmp(
|
|
1326
|
+
text: str, threshold: float
|
|
1327
|
+
) -> None:
|
|
1328
|
+
for line in tuple(csv.reader(text))[1:]:
|
|
1329
|
+
vmaf = float(line[-1])
|
|
1330
|
+
n = int(line[0]) + 1
|
|
1331
|
+
log.debug(
|
|
1332
|
+
f"{n}: {vmaf}",
|
|
1333
|
+
is_format=False,
|
|
1334
|
+
print_level=log.LogLevel._detail,
|
|
1335
|
+
)
|
|
1336
|
+
if vmaf < threshold:
|
|
1337
|
+
log.error(
|
|
1338
|
+
"VMAF {} < threshold {} in frame {}",
|
|
1339
|
+
vmaf,
|
|
1340
|
+
threshold,
|
|
1341
|
+
n,
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
break
|
|
1345
|
+
case _:
|
|
1346
|
+
log.error(
|
|
1347
|
+
"Param error from '{}': {}",
|
|
1348
|
+
"-quality-detection",
|
|
1349
|
+
f"{quality_detection[0]} -> ssim",
|
|
1350
|
+
)
|
|
1351
|
+
quality_detection[0] = "ssim"
|
|
1352
|
+
|
|
1353
|
+
if len(quality_detection) > 1:
|
|
1354
|
+
try:
|
|
1355
|
+
quality_detection_th = float(quality_detection[1])
|
|
1356
|
+
except ValueError as e:
|
|
1357
|
+
log.error("Param error from '{}': {}", "-quality-detection", e)
|
|
1358
|
+
|
|
1359
|
+
quality_detection_data_file: Path = Path(
|
|
1360
|
+
self.output_dir, "quality_detection_data.log"
|
|
1361
|
+
)
|
|
1362
|
+
quality_detection_data_file_filter_str: str = (
|
|
1363
|
+
str(quality_detection_data_file)
|
|
1364
|
+
.replace("\\", "/")
|
|
1365
|
+
.replace(":", "\\\\:")
|
|
1366
|
+
)
|
|
1367
|
+
if os.system(
|
|
1368
|
+
f'ffmpeg -i "{self.input_path_list[0]}" -i "{os.path.join(self.output_dir, temp_name)}" -lavfi "{quality_detection_filter}{quality_detection_data_file_filter_str}" -f null -'
|
|
1369
|
+
):
|
|
1370
|
+
log.error("Run {} failed", "-quality-detection")
|
|
1371
|
+
else:
|
|
1372
|
+
log.debug(
|
|
1373
|
+
"'{}' start: {}",
|
|
1374
|
+
"-quality-detection",
|
|
1375
|
+
f"{quality_detection[0]}:{quality_detection_th}",
|
|
1376
|
+
)
|
|
1377
|
+
quality_detection_cmp(
|
|
1378
|
+
read_text(quality_detection_data_file), quality_detection_th
|
|
1379
|
+
)
|
|
1380
|
+
log.debug("'{}' end", "-quality-detection")
|
|
1381
|
+
quality_detection_data_file.unlink(missing_ok=True)
|
|
1258
1382
|
|
|
1259
1383
|
# 获取体积
|
|
1260
1384
|
temp_name_full = os.path.join(self.output_dir, temp_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyrip
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.15.0
|
|
4
4
|
Author: op200
|
|
5
5
|
License-Expression: AGPL-3.0-or-later
|
|
6
6
|
Project-URL: Homepage, https://github.com/op200/EasyRip
|
|
@@ -20,6 +20,7 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pyperclip>=1.11.0
|
|
23
24
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
24
25
|
Requires-Dist: fonttools>=4.61.1
|
|
25
26
|
Requires-Dist: pycryptodome>=3.23.0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
easyrip/__init__.py,sha256=DULQoFEAEHYk7dS8Zxky56so7qDPqHm7jUc_Zop1eXw,616
|
|
2
|
-
easyrip/__main__.py,sha256=
|
|
3
|
-
easyrip/easyrip_command.py,sha256=
|
|
2
|
+
easyrip/__main__.py,sha256=RmJ2_bFzJmyymKnJpTqrbQc64tjK-ziGEen8sGsMm8c,5791
|
|
3
|
+
easyrip/easyrip_command.py,sha256=ZEQ8v8FHaSB7tKKMv0HY0NXOftR1S3gRaPTslfxc9tk,30650
|
|
4
4
|
easyrip/easyrip_log.py,sha256=R-dM3CWUBFITtG7GSD1zy4X4MhZqxkoiBPjlIpI76cY,15573
|
|
5
|
-
easyrip/easyrip_main.py,sha256=
|
|
6
|
-
easyrip/easyrip_prompt.py,sha256=
|
|
7
|
-
easyrip/global_val.py,sha256=
|
|
5
|
+
easyrip/easyrip_main.py,sha256=9KMpKUal8ApWZr80AQ7UwO1n6HUSWeVo2SuJ4pMW1f8,48130
|
|
6
|
+
easyrip/easyrip_prompt.py,sha256=TOmRJDigGRz7wRWFNakJdfNI1tn9vGekq6FH5ypmQfA,7068
|
|
7
|
+
easyrip/global_val.py,sha256=Q6PBQ8iB8H7cJN5KRaLBAj8mHdGCj47EAiLzW62cSrg,866
|
|
8
8
|
easyrip/utils.py,sha256=N1rMF1MyoC-YFBgy10_u29cFoowfhR-5Viea93O7wQ4,8750
|
|
9
9
|
easyrip/easyrip_config/config.py,sha256=KWXZMEYxdXYUGLQ-MR0A7nnOwR6QZdVrWBopfb2QZSA,9869
|
|
10
10
|
easyrip/easyrip_config/config_key.py,sha256=_jjdKOunskUoG7UUWOz3QZK-s4LF_x6hmM9MKttyS2Q,766
|
|
@@ -12,21 +12,21 @@ easyrip/easyrip_mlang/__init__.py,sha256=QqnL0kgV_trGPeLF5gawP1qAlj0GXUadLNhMSdK
|
|
|
12
12
|
easyrip/easyrip_mlang/global_lang_val.py,sha256=pG9DxPl6vUOZoFHYQKCM-AM0TYWbd8L4S65CUQFPRh4,4998
|
|
13
13
|
easyrip/easyrip_mlang/lang_en.py,sha256=fTM9ejuPW6gEfSMbnMEW-LzlUfvj0YGfoUfmHZpRzms,121
|
|
14
14
|
easyrip/easyrip_mlang/lang_tag_val.py,sha256=Ec-U0XglpSYvmkHlcEBueSj8ocTLSTH3cacElAkmYVU,5184
|
|
15
|
-
easyrip/easyrip_mlang/lang_zh_Hans_CN.py,sha256
|
|
15
|
+
easyrip/easyrip_mlang/lang_zh_Hans_CN.py,sha256=-GyAtjhSHqfR2x1mDgW3CdMz7odd58uJzONK67wow8g,19032
|
|
16
16
|
easyrip/easyrip_mlang/translator.py,sha256=jlgZYSPHvwv1Pps3akKkSgVsGcLtV2psKaXyZH4QCbA,5870
|
|
17
17
|
easyrip/easyrip_web/__init__.py,sha256=tMyEeaSGeEJjND7MF0MBv9aDiDgaO3MOnppwxA70U2c,177
|
|
18
18
|
easyrip/easyrip_web/http_server.py,sha256=iyulCAFQrJlz86Lrr-Dm3fhOnNCf79Bp6fVHhr0ephY,8350
|
|
19
|
-
easyrip/easyrip_web/third_party_api.py,sha256=
|
|
20
|
-
easyrip/ripper/media_info.py,sha256=
|
|
19
|
+
easyrip/easyrip_web/third_party_api.py,sha256=E-60yoY6D0pPUfYW1VIh0763htyV5z6getzlLtLAdQc,4624
|
|
20
|
+
easyrip/ripper/media_info.py,sha256=KdSodS6nIp2BWEer5y4mD5xwyhP15_PgNRhz2fnHmw0,5082
|
|
21
21
|
easyrip/ripper/param.py,sha256=PfJzJz9LPCB5hAM9G4GjPxdn_EZRgAz-vxYzuHGQLp8,13084
|
|
22
|
-
easyrip/ripper/ripper.py,sha256=
|
|
22
|
+
easyrip/ripper/ripper.py,sha256=FkbOi1H_IQtcOmr1sMnRgZvPEnxkMvINV2EiFIjSIqI,58898
|
|
23
23
|
easyrip/ripper/sub_and_font/__init__.py,sha256=cBT7mxL7RRFaJXFPXuZ7RT-YK6FbnanaU5v6U9BOquw,153
|
|
24
24
|
easyrip/ripper/sub_and_font/ass.py,sha256=EhDkVY5JXU77euWPId7H2v85j444m8ZLm7wUid7TYd8,35307
|
|
25
25
|
easyrip/ripper/sub_and_font/font.py,sha256=X2dPcPzbwQf3fv_g_mxO-zY7puVAX9Nv-9QHn88q4oA,7745
|
|
26
26
|
easyrip/ripper/sub_and_font/subset.py,sha256=--rAA3VH1rm_jBOC3yMs3rOJpn3tPuvfXqkimbBtx3s,18653
|
|
27
|
-
easyrip-4.
|
|
28
|
-
easyrip-4.
|
|
29
|
-
easyrip-4.
|
|
30
|
-
easyrip-4.
|
|
31
|
-
easyrip-4.
|
|
32
|
-
easyrip-4.
|
|
27
|
+
easyrip-4.15.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
28
|
+
easyrip-4.15.0.dist-info/METADATA,sha256=EeTyI6jF0S972Q706cZggO-ffPg8Y7hUMUIF0uJWf00,3540
|
|
29
|
+
easyrip-4.15.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
30
|
+
easyrip-4.15.0.dist-info/entry_points.txt,sha256=D6GBMMTzZ-apgX76KyZ6jxMmIFqGYwU9neeLLni_qKI,49
|
|
31
|
+
easyrip-4.15.0.dist-info/top_level.txt,sha256=kuEteBXm-Gf90jRQgH3-fTo-Z-Q6czSuUEqY158H4Ww,8
|
|
32
|
+
easyrip-4.15.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|