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
@@ -0,0 +1,73 @@
1
+ import os
2
+ from collections.abc import Iterable
3
+
4
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
5
+ from prompt_toolkit.document import Document
6
+ from prompt_toolkit.history import FileHistory
7
+
8
+ from .global_val import C_Z, CONFIG_DIR
9
+
10
+
11
+ class easyrip_prompt:
12
+ PROMPT_HISTORY_FILE = CONFIG_DIR / "prompt_history.txt"
13
+
14
+ @classmethod
15
+ def clear(cls) -> None:
16
+ cls.PROMPT_HISTORY_FILE.unlink(True)
17
+
18
+
19
+ class ConfigFileHistory(FileHistory):
20
+ def store_string(self, string: str) -> None:
21
+ if not string.startswith(C_Z):
22
+ super().store_string(string)
23
+
24
+
25
+ class SmartPathCompleter(Completer):
26
+ def __init__(self) -> None:
27
+ pass
28
+
29
+ def get_completions(
30
+ self,
31
+ document: Document,
32
+ complete_event: CompleteEvent, # noqa: ARG002
33
+ ) -> Iterable[Completion]:
34
+ text = document.text_before_cursor.strip("\"'")
35
+
36
+ try:
37
+ directory = (
38
+ os.path.dirname(os.path.join(".", text))
39
+ if os.path.dirname(text)
40
+ else "."
41
+ )
42
+
43
+ prefix = os.path.basename(text)
44
+
45
+ filenames: list[tuple[str, str]] = (
46
+ [
47
+ (directory, filename)
48
+ for filename in os.listdir(directory)
49
+ if filename.startswith(prefix)
50
+ ]
51
+ if os.path.isdir(directory)
52
+ else []
53
+ )
54
+
55
+ for directory, filename in sorted(filenames, key=lambda k: k[1]):
56
+ completion = filename[len(prefix) :]
57
+ full_name = os.path.join(directory, filename)
58
+
59
+ if os.path.isdir(full_name):
60
+ filename += "/"
61
+
62
+ yield Completion(
63
+ text=(
64
+ f'{"" if any(c in text for c in "\\/") else '"'}{completion}"'
65
+ if any(c in r"""!$%&()*:;<=>?[]^`{|}~""" for c in completion)
66
+ else completion
67
+ ),
68
+ start_position=0,
69
+ display=filename,
70
+ )
71
+
72
+ except OSError:
73
+ pass
@@ -1,8 +1,9 @@
1
1
  from .http_server import run_server
2
- from .third_party_api import github, zhconvert
2
+ from .third_party_api import github, mkvtoolnix, zhconvert
3
3
 
4
4
  __all__ = [
5
5
  "github",
6
+ "mkvtoolnix",
6
7
  "run_server",
7
8
  "zhconvert",
8
9
  ]
@@ -4,44 +4,32 @@ import os
4
4
  import secrets
5
5
  import signal
6
6
  from collections import deque
7
+ from collections.abc import Callable
7
8
  from http.server import BaseHTTPRequestHandler, HTTPServer
8
9
  from threading import Thread
9
10
  from time import sleep
10
11
 
11
- from Crypto.Cipher import AES as CryptoAES
12
- from Crypto.Util.Padding import pad, unpad
12
+ from ..utils import AES
13
13
 
14
- __all__ = ["Event", "run_server"]
15
-
16
-
17
- class AES:
18
- @staticmethod
19
- def encrypt(plaintext: bytes, key: bytes) -> bytes:
20
- cipher = CryptoAES.new(key, CryptoAES.MODE_CBC) # 使用 CBC 模式
21
- ciphertext = cipher.encrypt(pad(plaintext, CryptoAES.block_size)) # 加密并填充
22
- return bytes(cipher.iv) + ciphertext # 返回 IV 和密文
23
-
24
- @staticmethod
25
- def decrypt(ciphertext: bytes, key: bytes) -> bytes:
26
- iv = ciphertext[:16] # 提取 IV
27
- cipher = CryptoAES.new(key, CryptoAES.MODE_CBC, iv=iv)
28
- plaintext = unpad(
29
- cipher.decrypt(ciphertext[16:]), CryptoAES.block_size
30
- ) # 解密并去除填充
31
- return plaintext
14
+ __all__ = ["run_server"]
32
15
 
33
16
 
34
17
  class Event:
35
18
  log_queue: deque[tuple[str, str, str]] = deque()
19
+
36
20
  is_run_command: bool = False
37
- """
38
- 用于防止 server 二次运行,以及告知客户端运行状态
39
- """
21
+ """用于防止 server 二次运行,以及告知客户端运行状态"""
22
+
40
23
  progress: deque[dict[str, int | float]] = deque([{}])
41
24
 
42
- @staticmethod
43
- def post_run_event(cmd: str):
44
- pass
25
+ @classmethod
26
+ def post_run_event(cls, cmd: str) -> None:
27
+ from ..easyrip_main import run_command
28
+
29
+ try:
30
+ run_command(cmd)
31
+ finally:
32
+ cls.is_run_command = False
45
33
 
46
34
 
47
35
  class MainHTTPRequestHandler(BaseHTTPRequestHandler):
@@ -68,14 +56,19 @@ class MainHTTPRequestHandler(BaseHTTPRequestHandler):
68
56
  .strip('"')
69
57
  )
70
58
 
71
- def do_OPTIONS(self):
72
- self.send_response(200)
59
+ def _send_cors_headers(self) -> None:
60
+ """统一设置 CORS 头"""
73
61
  self.send_header("Access-Control-Allow-Origin", "*")
74
- self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
62
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
75
63
  self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
64
+ # self.send_header("Access-Control-Allow-Credentials", "true")
65
+
66
+ def do_OPTIONS(self) -> None:
67
+ self.send_response(200)
68
+ self._send_cors_headers()
76
69
  self.end_headers()
77
70
 
78
- def do_POST(self):
71
+ def do_POST(self) -> None:
79
72
  from ..easyrip_log import log
80
73
 
81
74
  # 获取请求体的长度
@@ -111,7 +104,7 @@ class MainHTTPRequestHandler(BaseHTTPRequestHandler):
111
104
 
112
105
  # 设置标志请求关闭服务
113
106
  if data.get("shutdown") == "shutdown":
114
- self.server.shutdown_requested = True # type: ignore
107
+ self.server.shutdown_requested = True # pyright: ignore[reportAttributeAccessIssue]
115
108
 
116
109
  # 通过 token 判断一致性
117
110
  if (
@@ -151,11 +144,6 @@ class MainHTTPRequestHandler(BaseHTTPRequestHandler):
151
144
  sleep(1)
152
145
  except KeyboardInterrupt:
153
146
  log.error("Manually force exit")
154
- # Event.is_run_command.append(False)
155
- # Event.is_run_command.popleft()
156
- # sleep(1)
157
- # Event.progress.append({})
158
- # Event.progress.popleft()
159
147
 
160
148
  elif Event.is_run_command is True:
161
149
  log.warning("There is a running command, terminate this request")
@@ -187,15 +175,15 @@ class MainHTTPRequestHandler(BaseHTTPRequestHandler):
187
175
  header = ("Content-type", "text/html")
188
176
 
189
177
  self.send_response(status_code)
190
- self.send_header("Access-Control-Allow-Origin", "*")
178
+ self._send_cors_headers()
191
179
  self.send_header(*header)
192
180
  self.send_header("Content-Length", str(len(response)))
193
181
  self.end_headers()
194
182
  self.wfile.write(response.encode(encoding="utf-8"))
195
183
 
196
- def do_GET(self):
184
+ def do_GET(self) -> None:
197
185
  self.send_response(200)
198
- self.send_header("Access-Control-Allow-Origin", "*")
186
+ self._send_cors_headers()
199
187
  self.send_header("Content-Type", "application/json")
200
188
  self.end_headers()
201
189
  self.wfile.write(
@@ -217,7 +205,13 @@ class MainHTTPRequestHandler(BaseHTTPRequestHandler):
217
205
  )
218
206
 
219
207
 
220
- def run_server(host: str = "", port: int = 0, password: str | None = None):
208
+ def run_server(
209
+ host: str = "",
210
+ port: int = 0,
211
+ password: str | None = None,
212
+ *,
213
+ after_start_server_hook: Callable[[], None] = lambda: None,
214
+ ) -> None:
221
215
  from ..easyrip_log import log
222
216
 
223
217
  MainHTTPRequestHandler.token = secrets.token_urlsafe(16)
@@ -226,11 +220,31 @@ def run_server(host: str = "", port: int = 0, password: str | None = None):
226
220
  _pw_sha3_512 = hashlib.sha3_512(MainHTTPRequestHandler.password.encode())
227
221
  MainHTTPRequestHandler.password_sha3_512_last8 = _pw_sha3_512.hexdigest()[-8:]
228
222
  MainHTTPRequestHandler.aes_key = _pw_sha3_512.digest()[:16]
223
+ else:
224
+ MainHTTPRequestHandler.password = None
225
+ MainHTTPRequestHandler.password_sha3_512_last8 = None
226
+ MainHTTPRequestHandler.aes_key = None
229
227
 
230
228
  server_address = (host, port)
231
229
  httpd = HTTPServer(server_address, MainHTTPRequestHandler)
232
- log.info("Starting HTTP service on port {}...", httpd.server_port)
230
+
231
+ protocol = "HTTP"
232
+
233
+ def _hook() -> None:
234
+ try:
235
+ after_start_server_hook()
236
+ finally:
237
+ Event.is_run_command = False
238
+
239
+ Event.is_run_command = True
240
+ Thread(target=_hook, daemon=True).start()
241
+
242
+ log.info(
243
+ "Starting {protocol} service on port {port}...",
244
+ protocol=protocol,
245
+ port=httpd.server_port,
246
+ )
233
247
  try:
234
248
  httpd.serve_forever()
235
249
  except KeyboardInterrupt:
236
- log.info("HTTP service stopped by ^C")
250
+ log.info("{} service stopped by ^C", protocol)
@@ -3,6 +3,7 @@ import json
3
3
  import urllib.error
4
4
  import urllib.parse
5
5
  import urllib.request
6
+ import xml.etree.ElementTree
6
7
  from time import sleep
7
8
 
8
9
 
@@ -27,6 +28,7 @@ class zhconvert:
27
28
  org_text: str,
28
29
  target_lang: Target_lang,
29
30
  ) -> str:
31
+ """失败抛出异常"""
30
32
  from ..easyrip_log import log
31
33
 
32
34
  log.info(
@@ -60,10 +62,10 @@ class zhconvert:
60
62
  return text
61
63
 
62
64
  raise Exception(f"HTTP error: {response.getcode()}")
63
- except urllib.error.HTTPError as e:
65
+ except urllib.error.HTTPError:
64
66
  sleep(0.5)
65
67
  if retry_num == 4:
66
- raise e
68
+ raise
67
69
  log.debug("Attempt to reconnect")
68
70
  continue
69
71
 
@@ -71,15 +73,65 @@ class zhconvert:
71
73
 
72
74
 
73
75
  class github:
74
- @staticmethod
75
- def get_release_ver(release_api_url: str) -> str | None:
76
+ @classmethod
77
+ def get_latest_release_ver(cls, release_api_url: str) -> str | None:
78
+ """失败返回 None"""
79
+ from ..easyrip_log import log
80
+
76
81
  req = urllib.request.Request(release_api_url)
77
82
 
78
83
  try:
79
84
  with urllib.request.urlopen(req) as response:
80
- data = json.loads(response.read().decode("utf-8"))
81
- return data.get("tag_name")
82
- except Exception:
83
- pass
85
+ data: dict = json.loads(response.read().decode("utf-8"))
86
+ ver = data.get("tag_name")
87
+ if ver is None:
88
+ return None
89
+ if isinstance(ver, str):
90
+ return ver.lstrip("v")
91
+ raise ValueError(f"ver = {ver!r}")
92
+ except Exception as e:
93
+ log.debug(
94
+ "'{}' execution failed: {}",
95
+ f"{cls.__name__}.{cls.get_latest_release_ver.__name__}",
96
+ e,
97
+ print_level=log.LogLevel._detail,
98
+ )
99
+
100
+ return None
101
+
102
+
103
+ class mkvtoolnix:
104
+ __latest_release_ver_cache: str | None = None
105
+
106
+ @classmethod
107
+ def get_latest_release_ver(cls, *, flush_cache: bool = False) -> str | None:
108
+ """失败返回 None"""
109
+ if flush_cache is False and cls.__latest_release_ver_cache is not None:
110
+ return cls.__latest_release_ver_cache
111
+
112
+ from ..easyrip_log import log
113
+
114
+ req = urllib.request.Request("https://mkvtoolnix.download/latest-release.xml")
115
+
116
+ try:
117
+ with urllib.request.urlopen(req) as response:
118
+ xml_tree = xml.etree.ElementTree.XML(response.read().decode("utf-8"))
119
+ if (ver := xml_tree.find("latest-source/version")) is None:
120
+ log.debug(
121
+ "'{}' execution failed: {}",
122
+ f"{cls.__name__}.{cls.get_latest_release_ver.__name__}",
123
+ f"XML parse faild: {xml.etree.ElementTree.tostring(xml_tree)}",
124
+ print_level=log.LogLevel._detail,
125
+ )
126
+ return None
127
+ cls.__latest_release_ver_cache = ver.text
128
+ return ver.text
129
+ except Exception as e:
130
+ log.debug(
131
+ "'{}' execution failed: {}",
132
+ f"{cls.__name__}.{cls.get_latest_release_ver.__name__}",
133
+ e,
134
+ print_level=log.LogLevel._detail,
135
+ )
84
136
 
85
137
  return None
easyrip/global_val.py CHANGED
@@ -1,5 +1,25 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
1
5
  PROJECT_NAME = "Easy Rip"
2
- PROJECT_VERSION = "3.13.2"
6
+ PROJECT_VERSION = "4.9.1"
3
7
  PROJECT_TITLE = f"{PROJECT_NAME} v{PROJECT_VERSION}"
4
8
  PROJECT_URL = "https://github.com/op200/EasyRip"
5
9
  PROJECT_RELEASE_API = "https://api.github.com/repos/op200/EasyRip/releases/latest"
10
+
11
+
12
+ if sys.platform == "win32":
13
+ # Windows: C:\Users\<用户名>\AppData\Roaming\<app_name>
14
+ __config_dir = Path(os.getenv("APPDATA", ""))
15
+ elif sys.platform == "darwin":
16
+ # macOS: ~/Library/Application Support/<app_name>
17
+ __config_dir = Path(os.path.expanduser("~")) / "Library" / "Application Support"
18
+ else:
19
+ # Linux: ~/.config/<app_name>
20
+ __config_dir = Path(os.path.expanduser("~")) / ".config"
21
+ CONFIG_DIR = Path(__config_dir) / PROJECT_NAME
22
+
23
+
24
+ C_D = "\x04"
25
+ C_Z = "\x1a"
@@ -2,12 +2,18 @@ import json
2
2
  import subprocess
3
3
  from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
- from typing import Self
5
+ from typing import Self, final
6
6
 
7
7
  from ..easyrip_log import log
8
+ from ..easyrip_mlang import Mlang_exception
8
9
  from ..utils import time_str_to_sec
9
10
 
10
11
 
12
+ class Stream_error(Mlang_exception):
13
+ pass
14
+
15
+
16
+ @final
11
17
  @dataclass(slots=True)
12
18
  class Audio_info:
13
19
  index: int
@@ -17,6 +23,7 @@ class Audio_info:
17
23
  bits_per_raw_sample: int = 0
18
24
 
19
25
 
26
+ @final
20
27
  @dataclass(slots=True)
21
28
  class Media_info:
22
29
  width: int = 0
@@ -59,7 +66,7 @@ class Media_info:
59
66
  )
60
67
  _info_list: list = _info.get("streams", [])
61
68
 
62
- _video_info_dict: dict = _info_list[0] if _info_list else dict()
69
+ _video_info_dict: dict = _info_list[0] if _info_list else {}
63
70
 
64
71
  media_info.width = int(_video_info_dict.get("width", "0"))
65
72
  media_info.height = int(_video_info_dict.get("height", "0"))
@@ -104,7 +111,7 @@ class Media_info:
104
111
 
105
112
  for _audio_info_dict in _info_list:
106
113
  if not isinstance(_audio_info_dict, dict):
107
- _audio_info_dict = dict()
114
+ _audio_info_dict = {}
108
115
 
109
116
  index = _audio_info_dict.get("index")
110
117
  if index is None: