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.
- easyrip/__init__.py +5 -1
- easyrip/__main__.py +124 -15
- easyrip/easyrip_command.py +457 -148
- easyrip/easyrip_config/config.py +269 -0
- easyrip/easyrip_config/config_key.py +28 -0
- easyrip/easyrip_log.py +120 -42
- easyrip/easyrip_main.py +509 -259
- easyrip/easyrip_mlang/__init__.py +20 -45
- easyrip/easyrip_mlang/global_lang_val.py +18 -16
- easyrip/easyrip_mlang/lang_en.py +1 -1
- easyrip/easyrip_mlang/lang_zh_Hans_CN.py +101 -77
- easyrip/easyrip_mlang/translator.py +12 -10
- easyrip/easyrip_prompt.py +73 -0
- easyrip/easyrip_web/__init__.py +2 -1
- easyrip/easyrip_web/http_server.py +56 -42
- easyrip/easyrip_web/third_party_api.py +60 -8
- easyrip/global_val.py +21 -1
- easyrip/ripper/media_info.py +10 -3
- easyrip/ripper/param.py +482 -0
- easyrip/ripper/ripper.py +260 -574
- easyrip/ripper/sub_and_font/__init__.py +10 -0
- easyrip/ripper/{font_subset → sub_and_font}/ass.py +95 -84
- easyrip/ripper/{font_subset → sub_and_font}/font.py +72 -79
- easyrip/ripper/{font_subset → sub_and_font}/subset.py +122 -81
- easyrip/utils.py +129 -27
- easyrip-4.9.1.dist-info/METADATA +92 -0
- easyrip-4.9.1.dist-info/RECORD +31 -0
- easyrip/easyrip_config.py +0 -198
- easyrip/ripper/__init__.py +0 -10
- easyrip/ripper/font_subset/__init__.py +0 -7
- easyrip-3.13.2.dist-info/METADATA +0 -89
- easyrip-3.13.2.dist-info/RECORD +0 -29
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/WHEEL +0 -0
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/entry_points.txt +0 -0
- {easyrip-3.13.2.dist-info → easyrip-4.9.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
easyrip/easyrip_web/__init__.py
CHANGED
|
@@ -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
|
|
12
|
-
from Crypto.Util.Padding import pad, unpad
|
|
12
|
+
from ..utils import AES
|
|
13
13
|
|
|
14
|
-
__all__ = ["
|
|
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
|
-
|
|
39
|
-
"""
|
|
21
|
+
"""用于防止 server 二次运行,以及告知客户端运行状态"""
|
|
22
|
+
|
|
40
23
|
progress: deque[dict[str, int | float]] = deque([{}])
|
|
41
24
|
|
|
42
|
-
@
|
|
43
|
-
def post_run_event(cmd: str):
|
|
44
|
-
|
|
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
|
|
72
|
-
|
|
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", "
|
|
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 #
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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("
|
|
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
|
|
65
|
+
except urllib.error.HTTPError:
|
|
64
66
|
sleep(0.5)
|
|
65
67
|
if retry_num == 4:
|
|
66
|
-
raise
|
|
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
|
-
@
|
|
75
|
-
def
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 = "
|
|
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"
|
easyrip/ripper/media_info.py
CHANGED
|
@@ -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
|
|
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 =
|
|
114
|
+
_audio_info_dict = {}
|
|
108
115
|
|
|
109
116
|
index = _audio_info_dict.get("index")
|
|
110
117
|
if index is None:
|