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 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 ConfigFileHistory, SmartPathCompleter, easyrip_prompt
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
- FuzzyCompleter(
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
@@ -253,19 +253,22 @@ class Cmd_type(enum.Enum):
253
253
  description=(
254
254
  "history\n" # .
255
255
  " Show prompt history\n"
256
- "clear | clean\n"
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(("clear", "clean")),
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 "clear" | "clean":
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 faild, cancel mux": "子集化失败, 取消混流",
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 self._fuzzy_filter_and_sort(filenames, basename):
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=self._highlight_fuzzy_match(filename, basename),
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 faild: {xml.etree.ElementTree.tostring(xml_tree)}",
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.13.3"
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"
@@ -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
- 'Faild to get audio info: {}. file "{}" track index {}',
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
- 'Faild to get audio info: {}. file "{}" track index {}',
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
- 'Faild to get audio info: {}. file "{}" track index {}',
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
- 'Faild to get audio info: {}. file "{}" track index {}',
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 faild, cancel mux")
1255
+ log.error("Subset failed, cancel mux")
1249
1256
 
1250
1257
  # 清理临时文件
1251
1258
  shutil.rmtree(subset_folder)
1252
1259
 
1253
- # 获取 ffmpeg report 中的报错
1254
- if FF_REPORT_LOG_FILE.is_file():
1255
- with FF_REPORT_LOG_FILE.open("rt", encoding="utf-8") as file:
1256
- for line in file.readlines()[2:]:
1257
- log.warning("FFmpeg report: {}", line)
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.13.3
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=QkzhmJPZUf79J-9ugJbks46fyYNG6dPn1K29nzfFVvU,5474
3
- easyrip/easyrip_command.py,sha256=g6gOJUrtQrSH2Hh6np2JgDqnWKVIWcV8SpMlkxApNgw,30210
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=jadrI1qyjEw_x-RSx6TCxXoqwTIjQbamsen8lA71Ewc,47757
6
- easyrip/easyrip_prompt.py,sha256=he5gBUDwoDTPyriizFnwY39SX3UHV3eX6cHrCM7OdvI,6644
7
- easyrip/global_val.py,sha256=q5DwBxWzBDjF2w-1TYxm9YeZmz65ttkgytFuJjGuCsI,866
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=uW6esedtSYi6Y4R4MA4Xkd2y3f-2O5-ywJFgogk1O7Y,19031
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=GhP6LmR1sVMeLLbnj82r-QYjoUdSnyaU9xp0LRnRLsw,4623
20
- easyrip/ripper/media_info.py,sha256=mQq_vbQ7S9fWpb39HLkoZlAL-pqNfwxewv6X776Nf50,5078
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=bymJVnEqQa3lTEiLJwRQpmfVctvP-2tLprCQ2XfH9Lk,52756
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.13.3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
28
- easyrip-4.13.3.dist-info/METADATA,sha256=fgp3xx0u6tSa8VIrAxNWMUYHSjotJOsDVF6eILHJZ20,3507
29
- easyrip-4.13.3.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
30
- easyrip-4.13.3.dist-info/entry_points.txt,sha256=D6GBMMTzZ-apgX76KyZ6jxMmIFqGYwU9neeLLni_qKI,49
31
- easyrip-4.13.3.dist-info/top_level.txt,sha256=kuEteBXm-Gf90jRQgH3-fTo-Z-Q6czSuUEqY158H4Ww,8
32
- easyrip-4.13.3.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5