bcmd 0.5.4__py3-none-any.whl → 0.5.5__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.
Potentially problematic release.
This version of bcmd might be problematic. Click here for more details.
- bcmd/common/func.py +19 -19
- bcmd/common/password.py +34 -34
- bcmd/main.py +10 -10
- bcmd/resources/project/.gitignore +3 -3
- bcmd/resources/project/.vscode/launch.json +14 -14
- bcmd/resources/project/.vscode/settings.json +10 -10
- bcmd/resources/project/.vscode/tasks.json +67 -67
- bcmd/tasks/__init__.py +15 -15
- bcmd/tasks/bin.py +104 -104
- bcmd/tasks/code.py +127 -96
- bcmd/tasks/crypto.py +116 -116
- bcmd/tasks/debian.py +78 -78
- bcmd/tasks/download.py +74 -74
- bcmd/tasks/image.py +205 -205
- bcmd/tasks/json.py +25 -25
- bcmd/tasks/lib.py +118 -118
- bcmd/tasks/math.py +97 -97
- bcmd/tasks/mirror.py +46 -46
- bcmd/tasks/project.py +59 -34
- bcmd/tasks/proxy.py +55 -55
- bcmd/tasks/time.py +81 -81
- bcmd/tasks/venv.py +226 -217
- {bcmd-0.5.4.dist-info → bcmd-0.5.5.dist-info}/METADATA +1 -1
- bcmd-0.5.5.dist-info/RECORD +30 -0
- bcmd-0.5.4.dist-info/RECORD +0 -30
- {bcmd-0.5.4.dist-info → bcmd-0.5.5.dist-info}/WHEEL +0 -0
- {bcmd-0.5.4.dist-info → bcmd-0.5.5.dist-info}/entry_points.txt +0 -0
- {bcmd-0.5.4.dist-info → bcmd-0.5.5.dist-info}/top_level.txt +0 -0
bcmd/tasks/download.py
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Final
|
|
5
|
-
from urllib.parse import urlparse
|
|
6
|
-
from uuid import uuid4
|
|
7
|
-
|
|
8
|
-
import pyperclip
|
|
9
|
-
import typer
|
|
10
|
-
from beni import bcolor, bfile, bhttp, binput, bpath, btask
|
|
11
|
-
from beni.bfunc import syncCall, textToAry
|
|
12
|
-
|
|
13
|
-
app: Final = btask.app
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@app.command()
|
|
17
|
-
@syncCall
|
|
18
|
-
async def download(
|
|
19
|
-
url_file: Path = typer.Option(None, '--file', '-f', help='需要下载的url文件路径,默认使用剪贴板内容'),
|
|
20
|
-
save_path: Path = typer.Option(None, '--path', '-p', help='下载存放目录,默认当前目录'),
|
|
21
|
-
keep_directory: bool = typer.Option(False, '--keep', '-k', help='保持原始目录结构,默认不保持'),
|
|
22
|
-
):
|
|
23
|
-
'下载资源资源文件'
|
|
24
|
-
save_path = save_path or Path(os.getcwd())
|
|
25
|
-
|
|
26
|
-
if url_file:
|
|
27
|
-
if not url_file.exists():
|
|
28
|
-
btask.abort('指定文件不存在', url_file)
|
|
29
|
-
content = await bfile.readText(url_file)
|
|
30
|
-
else:
|
|
31
|
-
content = pyperclip.paste()
|
|
32
|
-
urlSet = set(textToAry(content))
|
|
33
|
-
|
|
34
|
-
for i, url in enumerate(urlSet):
|
|
35
|
-
print(f'{i + 1}. {url}')
|
|
36
|
-
print(f'输出目录:{save_path}')
|
|
37
|
-
await binput.confirm('是否确认?')
|
|
38
|
-
|
|
39
|
-
fileSet: set[Path] = set()
|
|
40
|
-
retryUrlSet: set[str] = set()
|
|
41
|
-
|
|
42
|
-
async def download(url: str):
|
|
43
|
-
urlPath = urlparse(url).path
|
|
44
|
-
if keep_directory:
|
|
45
|
-
file = bpath.get(save_path, '/'.join([x for x in urlPath.split('/') if x]))
|
|
46
|
-
else:
|
|
47
|
-
file = save_path / Path(urlPath).name
|
|
48
|
-
if file in fileSet:
|
|
49
|
-
file = file.with_stem(f'{file.stem}--{uuid4()}')
|
|
50
|
-
fileSet.add(file)
|
|
51
|
-
try:
|
|
52
|
-
bcolor.printGreen(url)
|
|
53
|
-
await bhttp.download(url, file)
|
|
54
|
-
except:
|
|
55
|
-
retryUrlSet.add(url)
|
|
56
|
-
bcolor.printRed(url)
|
|
57
|
-
|
|
58
|
-
await asyncio.gather(*[download(x) for x in urlSet])
|
|
59
|
-
|
|
60
|
-
for i in range(4):
|
|
61
|
-
if i > 0:
|
|
62
|
-
print(f'等待重试第 {i} 次')
|
|
63
|
-
await asyncio.sleep(3)
|
|
64
|
-
await asyncio.gather(*[download(x) for x in urlSet])
|
|
65
|
-
if not retryUrlSet:
|
|
66
|
-
break
|
|
67
|
-
urlSet = set(retryUrlSet)
|
|
68
|
-
retryUrlSet.clear()
|
|
69
|
-
|
|
70
|
-
if retryUrlSet:
|
|
71
|
-
pyperclip.copy('\n'.join(retryUrlSet))
|
|
72
|
-
bcolor.printYellow('部分下载失败,失败部分已复制到剪贴板')
|
|
73
|
-
else:
|
|
74
|
-
bcolor.printGreen('OK')
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Final
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
import pyperclip
|
|
9
|
+
import typer
|
|
10
|
+
from beni import bcolor, bfile, bhttp, binput, bpath, btask
|
|
11
|
+
from beni.bfunc import syncCall, textToAry
|
|
12
|
+
|
|
13
|
+
app: Final = btask.app
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command()
|
|
17
|
+
@syncCall
|
|
18
|
+
async def download(
|
|
19
|
+
url_file: Path = typer.Option(None, '--file', '-f', help='需要下载的url文件路径,默认使用剪贴板内容'),
|
|
20
|
+
save_path: Path = typer.Option(None, '--path', '-p', help='下载存放目录,默认当前目录'),
|
|
21
|
+
keep_directory: bool = typer.Option(False, '--keep', '-k', help='保持原始目录结构,默认不保持'),
|
|
22
|
+
):
|
|
23
|
+
'下载资源资源文件'
|
|
24
|
+
save_path = save_path or Path(os.getcwd())
|
|
25
|
+
|
|
26
|
+
if url_file:
|
|
27
|
+
if not url_file.exists():
|
|
28
|
+
btask.abort('指定文件不存在', url_file)
|
|
29
|
+
content = await bfile.readText(url_file)
|
|
30
|
+
else:
|
|
31
|
+
content = pyperclip.paste()
|
|
32
|
+
urlSet = set(textToAry(content))
|
|
33
|
+
|
|
34
|
+
for i, url in enumerate(urlSet):
|
|
35
|
+
print(f'{i + 1}. {url}')
|
|
36
|
+
print(f'输出目录:{save_path}')
|
|
37
|
+
await binput.confirm('是否确认?')
|
|
38
|
+
|
|
39
|
+
fileSet: set[Path] = set()
|
|
40
|
+
retryUrlSet: set[str] = set()
|
|
41
|
+
|
|
42
|
+
async def download(url: str):
|
|
43
|
+
urlPath = urlparse(url).path
|
|
44
|
+
if keep_directory:
|
|
45
|
+
file = bpath.get(save_path, '/'.join([x for x in urlPath.split('/') if x]))
|
|
46
|
+
else:
|
|
47
|
+
file = save_path / Path(urlPath).name
|
|
48
|
+
if file in fileSet:
|
|
49
|
+
file = file.with_stem(f'{file.stem}--{uuid4()}')
|
|
50
|
+
fileSet.add(file)
|
|
51
|
+
try:
|
|
52
|
+
bcolor.printGreen(url)
|
|
53
|
+
await bhttp.download(url, file)
|
|
54
|
+
except:
|
|
55
|
+
retryUrlSet.add(url)
|
|
56
|
+
bcolor.printRed(url)
|
|
57
|
+
|
|
58
|
+
await asyncio.gather(*[download(x) for x in urlSet])
|
|
59
|
+
|
|
60
|
+
for i in range(4):
|
|
61
|
+
if i > 0:
|
|
62
|
+
print(f'等待重试第 {i} 次')
|
|
63
|
+
await asyncio.sleep(3)
|
|
64
|
+
await asyncio.gather(*[download(x) for x in urlSet])
|
|
65
|
+
if not retryUrlSet:
|
|
66
|
+
break
|
|
67
|
+
urlSet = set(retryUrlSet)
|
|
68
|
+
retryUrlSet.clear()
|
|
69
|
+
|
|
70
|
+
if retryUrlSet:
|
|
71
|
+
pyperclip.copy('\n'.join(retryUrlSet))
|
|
72
|
+
bcolor.printYellow('部分下载失败,失败部分已复制到剪贴板')
|
|
73
|
+
else:
|
|
74
|
+
bcolor.printGreen('OK')
|
bcmd/tasks/image.py
CHANGED
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
import random
|
|
4
|
-
from enum import StrEnum
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Final
|
|
7
|
-
|
|
8
|
-
import httpx
|
|
9
|
-
import typer
|
|
10
|
-
from beni import bcolor, bfile, bhttp, block, bpath, btask
|
|
11
|
-
from beni.bbyte import BytesReader, BytesWriter
|
|
12
|
-
from beni.bfunc import syncCall
|
|
13
|
-
from beni.btype import Null, XPath
|
|
14
|
-
from PIL import Image
|
|
15
|
-
|
|
16
|
-
app: Final = btask.newSubApp('图片工具集')
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class _OutputType(StrEnum):
|
|
20
|
-
'输出类型'
|
|
21
|
-
normal = '0'
|
|
22
|
-
replace_ = '1'
|
|
23
|
-
crc_replace = '2'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@app.command()
|
|
27
|
-
@syncCall
|
|
28
|
-
async def convert(
|
|
29
|
-
path: Path = typer.Option(None, '--path', '-p', help='指定目录或具体图片文件,默认当前目录'),
|
|
30
|
-
src_format: str = typer.Option('jpg|jpeg|png', '--src-format', '-s', help='如果path是目录,指定源格式,可以指定多个,默认值:jpg|jpeg|png'),
|
|
31
|
-
dst_format: str = typer.Option('webp', '--dst-format', '-d', help='目标格式,只能是单个'),
|
|
32
|
-
rgb: bool = typer.Option(False, '--rgb', help='转换为RGB格式'),
|
|
33
|
-
quality: int = typer.Option(85, '--quality', '-q', help='图片质量,0-100,默认85'),
|
|
34
|
-
output_type: _OutputType = typer.Option(_OutputType.normal, '--output-type', help='输出类型,0:普通输出,1:删除源文件,2:输出文件使用CRC32命名并删除源文件'),
|
|
35
|
-
):
|
|
36
|
-
'图片格式转换'
|
|
37
|
-
path = path or Path(os.getcwd())
|
|
38
|
-
fileList: list[Path] = []
|
|
39
|
-
if path.is_file():
|
|
40
|
-
fileList.append(path)
|
|
41
|
-
elif path.is_dir():
|
|
42
|
-
extNameList = [x for x in src_format.strip().split('|')]
|
|
43
|
-
fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in extNameList]
|
|
44
|
-
if not fileList:
|
|
45
|
-
return bcolor.printRed(f'未找到图片文件({path})')
|
|
46
|
-
for file in fileList:
|
|
47
|
-
with Image.open(file) as img:
|
|
48
|
-
if rgb:
|
|
49
|
-
img = img.convert('RGB')
|
|
50
|
-
with bpath.useTempFile() as tempFile:
|
|
51
|
-
img.save(tempFile, format=dst_format, quality=quality)
|
|
52
|
-
outputFile = file.with_suffix(f'.{dst_format}')
|
|
53
|
-
if output_type == _OutputType.crc_replace:
|
|
54
|
-
outputFile = outputFile.with_stem(await bfile.crc(tempFile))
|
|
55
|
-
bpath.copy(tempFile, outputFile)
|
|
56
|
-
if output_type in [_OutputType.replace_, _OutputType.crc_replace]:
|
|
57
|
-
if outputFile != file:
|
|
58
|
-
bpath.remove(file)
|
|
59
|
-
bcolor.printGreen(f'{file} -> {outputFile}')
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# ------------------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
@app.command()
|
|
65
|
-
@syncCall
|
|
66
|
-
async def tiny(
|
|
67
|
-
path: Path = typer.Option(None, '--path', help='指定目录或具体图片文件,默认当前目录'),
|
|
68
|
-
compression: int = typer.Option(75, '--compression', help='如果压缩小于指定数值则使用原来的图片,单位:%,默认75'),
|
|
69
|
-
isKeepOriginal: bool = typer.Option(False, '--keep-original', help='保留原始图片'),
|
|
70
|
-
):
|
|
71
|
-
|
|
72
|
-
keyList = [
|
|
73
|
-
'MB3QmtvZ8HKRkXcDnxhWCNTXzvx6cNF3',
|
|
74
|
-
'7L7X2CJ35GM1bChSHdT14yZPLx7FlpNk',
|
|
75
|
-
'q8YLcvrXVW2NYcr5mMyzQhsSHF4j7gny',
|
|
76
|
-
]
|
|
77
|
-
random.shuffle(keyList)
|
|
78
|
-
await block.setLimit(_TinyFile.runTiny, len(keyList))
|
|
79
|
-
|
|
80
|
-
# 整理文件列表
|
|
81
|
-
fileList = []
|
|
82
|
-
path = path or Path(os.getcwd())
|
|
83
|
-
if path.is_file():
|
|
84
|
-
fileList.append(path)
|
|
85
|
-
elif path.is_dir():
|
|
86
|
-
fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in ['jpg', 'jpeg', 'png']]
|
|
87
|
-
else:
|
|
88
|
-
btask.abort('未找到图片文件', path)
|
|
89
|
-
fileList.sort()
|
|
90
|
-
|
|
91
|
-
# 将文件列表整理成 _TinyFile 对象
|
|
92
|
-
fileList = [_TinyFile(x) for x in fileList]
|
|
93
|
-
await asyncio.gather(*[x.updateInfo() for x in fileList])
|
|
94
|
-
|
|
95
|
-
# 过滤掉已经处理过的图片
|
|
96
|
-
for i in range(len(fileList)):
|
|
97
|
-
file = fileList[i]
|
|
98
|
-
if file.compression == 0:
|
|
99
|
-
# 未处理过的图片,进行图片的压缩处理
|
|
100
|
-
pass
|
|
101
|
-
elif not file.isTiny and file.compression < compression:
|
|
102
|
-
# 之前测试的压缩率不符合要求,不过现在符合了,进行图片的压缩处理
|
|
103
|
-
pass
|
|
104
|
-
else:
|
|
105
|
-
# 要忽略掉的文件
|
|
106
|
-
bcolor.printYellow(f'{file.file}({file.compression - 100:.2f}% / 已压缩 / {file.getSizeDisplay()})')
|
|
107
|
-
fileList[i] = Null
|
|
108
|
-
fileList = [x for x in fileList if x]
|
|
109
|
-
|
|
110
|
-
# 开始压缩
|
|
111
|
-
taskList = [
|
|
112
|
-
x.runTiny(keyList[i % len(keyList)], compression, isKeepOriginal)
|
|
113
|
-
for i, x in enumerate(fileList)
|
|
114
|
-
]
|
|
115
|
-
await asyncio.gather(*taskList)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# ------------------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class _TinyFile:
|
|
122
|
-
|
|
123
|
-
_endian: Final = '>'
|
|
124
|
-
_sep = BytesWriter(_endian).writeStr('tiny').writeUint(9527).writeUint(709394).toBytes()
|
|
125
|
-
|
|
126
|
-
@property
|
|
127
|
-
def compression(self):
|
|
128
|
-
return self._compression
|
|
129
|
-
|
|
130
|
-
@property
|
|
131
|
-
def isTiny(self):
|
|
132
|
-
return self._isTiny
|
|
133
|
-
|
|
134
|
-
@property
|
|
135
|
-
def file(self):
|
|
136
|
-
return self._file
|
|
137
|
-
|
|
138
|
-
def __init__(self, file: XPath):
|
|
139
|
-
self._file = file
|
|
140
|
-
self._compression: float = 0.0
|
|
141
|
-
self._isTiny: bool = False
|
|
142
|
-
|
|
143
|
-
def getSizeDisplay(self):
|
|
144
|
-
size = bpath.get(self._file).stat().st_size / 1024
|
|
145
|
-
return f'{size:,.2f}KB'
|
|
146
|
-
|
|
147
|
-
async def updateInfo(self):
|
|
148
|
-
fileBytes = await bfile.readBytes(self._file)
|
|
149
|
-
self._compression = 0.0
|
|
150
|
-
self._isTiny = False
|
|
151
|
-
blockAry = fileBytes.split(self._sep)
|
|
152
|
-
if len(blockAry) > 1:
|
|
153
|
-
info = BytesReader(self._endian, blockAry[1])
|
|
154
|
-
size = info.readUint()
|
|
155
|
-
if size == len(blockAry[0]):
|
|
156
|
-
self._compression = round(info.readFloat(), 2)
|
|
157
|
-
self._isTiny = info.readBool()
|
|
158
|
-
|
|
159
|
-
async def _flushInfo(self, compression: float, isTiny: bool):
|
|
160
|
-
self._compression = compression
|
|
161
|
-
self._isTiny = isTiny
|
|
162
|
-
content = await self._getPureContent()
|
|
163
|
-
info = (
|
|
164
|
-
BytesWriter(self._endian)
|
|
165
|
-
.writeUint(len(content))
|
|
166
|
-
.writeFloat(compression)
|
|
167
|
-
.writeBool(isTiny)
|
|
168
|
-
.toBytes()
|
|
169
|
-
)
|
|
170
|
-
content += self._sep + info
|
|
171
|
-
await bfile.writeBytes(self._file, content)
|
|
172
|
-
|
|
173
|
-
async def _getPureContent(self):
|
|
174
|
-
content = await bfile.readBytes(self._file)
|
|
175
|
-
content = content.split(self._sep)[0]
|
|
176
|
-
return content
|
|
177
|
-
|
|
178
|
-
@block.limit(1)
|
|
179
|
-
async def runTiny(self, key: str, compression: float, isKeepOriginal: bool):
|
|
180
|
-
content = await self._getPureContent()
|
|
181
|
-
async with httpx.AsyncClient() as client:
|
|
182
|
-
response = await client.post(
|
|
183
|
-
'https://api.tinify.com/shrink',
|
|
184
|
-
auth=('api', key),
|
|
185
|
-
content=content,
|
|
186
|
-
timeout=30,
|
|
187
|
-
)
|
|
188
|
-
response.raise_for_status()
|
|
189
|
-
result = response.json()
|
|
190
|
-
outputCompression = round(result['output']['ratio'] * 100, 2)
|
|
191
|
-
if outputCompression < compression:
|
|
192
|
-
# 下载文件
|
|
193
|
-
url = result['output']['url']
|
|
194
|
-
with bpath.useTempFile() as tempFile:
|
|
195
|
-
await bhttp.download(url, tempFile)
|
|
196
|
-
await _TinyFile(tempFile)._flushInfo(outputCompression, True)
|
|
197
|
-
outputFile = bpath.get(self._file)
|
|
198
|
-
if isKeepOriginal:
|
|
199
|
-
outputFile = outputFile.with_stem(f'{outputFile.stem}_tiny')
|
|
200
|
-
bpath.move(tempFile, outputFile, True)
|
|
201
|
-
bcolor.printGreen(f'{outputFile}({outputCompression - 100:.2f}% / 压缩 / {self.getSizeDisplay()})')
|
|
202
|
-
else:
|
|
203
|
-
# 不进行压缩
|
|
204
|
-
await self._flushInfo(outputCompression, False)
|
|
205
|
-
bcolor.printMagenta(f'{self._file} ({outputCompression - 100:.2f}% / 不处理 / {self.getSizeDisplay()})')
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import typer
|
|
10
|
+
from beni import bcolor, bfile, bhttp, block, bpath, btask
|
|
11
|
+
from beni.bbyte import BytesReader, BytesWriter
|
|
12
|
+
from beni.bfunc import syncCall
|
|
13
|
+
from beni.btype import Null, XPath
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
app: Final = btask.newSubApp('图片工具集')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _OutputType(StrEnum):
|
|
20
|
+
'输出类型'
|
|
21
|
+
normal = '0'
|
|
22
|
+
replace_ = '1'
|
|
23
|
+
crc_replace = '2'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
@syncCall
|
|
28
|
+
async def convert(
|
|
29
|
+
path: Path = typer.Option(None, '--path', '-p', help='指定目录或具体图片文件,默认当前目录'),
|
|
30
|
+
src_format: str = typer.Option('jpg|jpeg|png', '--src-format', '-s', help='如果path是目录,指定源格式,可以指定多个,默认值:jpg|jpeg|png'),
|
|
31
|
+
dst_format: str = typer.Option('webp', '--dst-format', '-d', help='目标格式,只能是单个'),
|
|
32
|
+
rgb: bool = typer.Option(False, '--rgb', help='转换为RGB格式'),
|
|
33
|
+
quality: int = typer.Option(85, '--quality', '-q', help='图片质量,0-100,默认85'),
|
|
34
|
+
output_type: _OutputType = typer.Option(_OutputType.normal, '--output-type', help='输出类型,0:普通输出,1:删除源文件,2:输出文件使用CRC32命名并删除源文件'),
|
|
35
|
+
):
|
|
36
|
+
'图片格式转换'
|
|
37
|
+
path = path or Path(os.getcwd())
|
|
38
|
+
fileList: list[Path] = []
|
|
39
|
+
if path.is_file():
|
|
40
|
+
fileList.append(path)
|
|
41
|
+
elif path.is_dir():
|
|
42
|
+
extNameList = [x for x in src_format.strip().split('|')]
|
|
43
|
+
fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in extNameList]
|
|
44
|
+
if not fileList:
|
|
45
|
+
return bcolor.printRed(f'未找到图片文件({path})')
|
|
46
|
+
for file in fileList:
|
|
47
|
+
with Image.open(file) as img:
|
|
48
|
+
if rgb:
|
|
49
|
+
img = img.convert('RGB')
|
|
50
|
+
with bpath.useTempFile() as tempFile:
|
|
51
|
+
img.save(tempFile, format=dst_format, quality=quality)
|
|
52
|
+
outputFile = file.with_suffix(f'.{dst_format}')
|
|
53
|
+
if output_type == _OutputType.crc_replace:
|
|
54
|
+
outputFile = outputFile.with_stem(await bfile.crc(tempFile))
|
|
55
|
+
bpath.copy(tempFile, outputFile)
|
|
56
|
+
if output_type in [_OutputType.replace_, _OutputType.crc_replace]:
|
|
57
|
+
if outputFile != file:
|
|
58
|
+
bpath.remove(file)
|
|
59
|
+
bcolor.printGreen(f'{file} -> {outputFile}')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
@syncCall
|
|
66
|
+
async def tiny(
|
|
67
|
+
path: Path = typer.Option(None, '--path', help='指定目录或具体图片文件,默认当前目录'),
|
|
68
|
+
compression: int = typer.Option(75, '--compression', help='如果压缩小于指定数值则使用原来的图片,单位:%,默认75'),
|
|
69
|
+
isKeepOriginal: bool = typer.Option(False, '--keep-original', help='保留原始图片'),
|
|
70
|
+
):
|
|
71
|
+
|
|
72
|
+
keyList = [
|
|
73
|
+
'MB3QmtvZ8HKRkXcDnxhWCNTXzvx6cNF3',
|
|
74
|
+
'7L7X2CJ35GM1bChSHdT14yZPLx7FlpNk',
|
|
75
|
+
'q8YLcvrXVW2NYcr5mMyzQhsSHF4j7gny',
|
|
76
|
+
]
|
|
77
|
+
random.shuffle(keyList)
|
|
78
|
+
await block.setLimit(_TinyFile.runTiny, len(keyList))
|
|
79
|
+
|
|
80
|
+
# 整理文件列表
|
|
81
|
+
fileList = []
|
|
82
|
+
path = path or Path(os.getcwd())
|
|
83
|
+
if path.is_file():
|
|
84
|
+
fileList.append(path)
|
|
85
|
+
elif path.is_dir():
|
|
86
|
+
fileList = [x for x in bpath.listFile(path, True) if x.suffix[1:].lower() in ['jpg', 'jpeg', 'png']]
|
|
87
|
+
else:
|
|
88
|
+
btask.abort('未找到图片文件', path)
|
|
89
|
+
fileList.sort()
|
|
90
|
+
|
|
91
|
+
# 将文件列表整理成 _TinyFile 对象
|
|
92
|
+
fileList = [_TinyFile(x) for x in fileList]
|
|
93
|
+
await asyncio.gather(*[x.updateInfo() for x in fileList])
|
|
94
|
+
|
|
95
|
+
# 过滤掉已经处理过的图片
|
|
96
|
+
for i in range(len(fileList)):
|
|
97
|
+
file = fileList[i]
|
|
98
|
+
if file.compression == 0:
|
|
99
|
+
# 未处理过的图片,进行图片的压缩处理
|
|
100
|
+
pass
|
|
101
|
+
elif not file.isTiny and file.compression < compression:
|
|
102
|
+
# 之前测试的压缩率不符合要求,不过现在符合了,进行图片的压缩处理
|
|
103
|
+
pass
|
|
104
|
+
else:
|
|
105
|
+
# 要忽略掉的文件
|
|
106
|
+
bcolor.printYellow(f'{file.file}({file.compression - 100:.2f}% / 已压缩 / {file.getSizeDisplay()})')
|
|
107
|
+
fileList[i] = Null
|
|
108
|
+
fileList = [x for x in fileList if x]
|
|
109
|
+
|
|
110
|
+
# 开始压缩
|
|
111
|
+
taskList = [
|
|
112
|
+
x.runTiny(keyList[i % len(keyList)], compression, isKeepOriginal)
|
|
113
|
+
for i, x in enumerate(fileList)
|
|
114
|
+
]
|
|
115
|
+
await asyncio.gather(*taskList)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _TinyFile:
|
|
122
|
+
|
|
123
|
+
_endian: Final = '>'
|
|
124
|
+
_sep = BytesWriter(_endian).writeStr('tiny').writeUint(9527).writeUint(709394).toBytes()
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def compression(self):
|
|
128
|
+
return self._compression
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def isTiny(self):
|
|
132
|
+
return self._isTiny
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def file(self):
|
|
136
|
+
return self._file
|
|
137
|
+
|
|
138
|
+
def __init__(self, file: XPath):
|
|
139
|
+
self._file = file
|
|
140
|
+
self._compression: float = 0.0
|
|
141
|
+
self._isTiny: bool = False
|
|
142
|
+
|
|
143
|
+
def getSizeDisplay(self):
|
|
144
|
+
size = bpath.get(self._file).stat().st_size / 1024
|
|
145
|
+
return f'{size:,.2f}KB'
|
|
146
|
+
|
|
147
|
+
async def updateInfo(self):
|
|
148
|
+
fileBytes = await bfile.readBytes(self._file)
|
|
149
|
+
self._compression = 0.0
|
|
150
|
+
self._isTiny = False
|
|
151
|
+
blockAry = fileBytes.split(self._sep)
|
|
152
|
+
if len(blockAry) > 1:
|
|
153
|
+
info = BytesReader(self._endian, blockAry[1])
|
|
154
|
+
size = info.readUint()
|
|
155
|
+
if size == len(blockAry[0]):
|
|
156
|
+
self._compression = round(info.readFloat(), 2)
|
|
157
|
+
self._isTiny = info.readBool()
|
|
158
|
+
|
|
159
|
+
async def _flushInfo(self, compression: float, isTiny: bool):
|
|
160
|
+
self._compression = compression
|
|
161
|
+
self._isTiny = isTiny
|
|
162
|
+
content = await self._getPureContent()
|
|
163
|
+
info = (
|
|
164
|
+
BytesWriter(self._endian)
|
|
165
|
+
.writeUint(len(content))
|
|
166
|
+
.writeFloat(compression)
|
|
167
|
+
.writeBool(isTiny)
|
|
168
|
+
.toBytes()
|
|
169
|
+
)
|
|
170
|
+
content += self._sep + info
|
|
171
|
+
await bfile.writeBytes(self._file, content)
|
|
172
|
+
|
|
173
|
+
async def _getPureContent(self):
|
|
174
|
+
content = await bfile.readBytes(self._file)
|
|
175
|
+
content = content.split(self._sep)[0]
|
|
176
|
+
return content
|
|
177
|
+
|
|
178
|
+
@block.limit(1)
|
|
179
|
+
async def runTiny(self, key: str, compression: float, isKeepOriginal: bool):
|
|
180
|
+
content = await self._getPureContent()
|
|
181
|
+
async with httpx.AsyncClient() as client:
|
|
182
|
+
response = await client.post(
|
|
183
|
+
'https://api.tinify.com/shrink',
|
|
184
|
+
auth=('api', key),
|
|
185
|
+
content=content,
|
|
186
|
+
timeout=30,
|
|
187
|
+
)
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
result = response.json()
|
|
190
|
+
outputCompression = round(result['output']['ratio'] * 100, 2)
|
|
191
|
+
if outputCompression < compression:
|
|
192
|
+
# 下载文件
|
|
193
|
+
url = result['output']['url']
|
|
194
|
+
with bpath.useTempFile() as tempFile:
|
|
195
|
+
await bhttp.download(url, tempFile)
|
|
196
|
+
await _TinyFile(tempFile)._flushInfo(outputCompression, True)
|
|
197
|
+
outputFile = bpath.get(self._file)
|
|
198
|
+
if isKeepOriginal:
|
|
199
|
+
outputFile = outputFile.with_stem(f'{outputFile.stem}_tiny')
|
|
200
|
+
bpath.move(tempFile, outputFile, True)
|
|
201
|
+
bcolor.printGreen(f'{outputFile}({outputCompression - 100:.2f}% / 压缩 / {self.getSizeDisplay()})')
|
|
202
|
+
else:
|
|
203
|
+
# 不进行压缩
|
|
204
|
+
await self._flushInfo(outputCompression, False)
|
|
205
|
+
bcolor.printMagenta(f'{self._file} ({outputCompression - 100:.2f}% / 不处理 / {self.getSizeDisplay()})')
|
bcmd/tasks/json.py
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Final
|
|
3
|
-
|
|
4
|
-
import pyperclip
|
|
5
|
-
from beni import bcolor, btask
|
|
6
|
-
from beni.bfunc import syncCall
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
|
|
9
|
-
app: Final = btask.app
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@app.command('json')
|
|
13
|
-
@syncCall
|
|
14
|
-
async def format_json():
|
|
15
|
-
'格式化 JSON (使用复制文本)'
|
|
16
|
-
content = pyperclip.paste()
|
|
17
|
-
try:
|
|
18
|
-
Console().print_json(content, indent=4, ensure_ascii=False, sort_keys=True)
|
|
19
|
-
data = json.loads(content)
|
|
20
|
-
pyperclip.copy(
|
|
21
|
-
json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
|
|
22
|
-
)
|
|
23
|
-
except:
|
|
24
|
-
bcolor.printRed('无效的 JSON')
|
|
25
|
-
bcolor.printRed(content)
|
|
1
|
+
import json
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
import pyperclip
|
|
5
|
+
from beni import bcolor, btask
|
|
6
|
+
from beni.bfunc import syncCall
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
app: Final = btask.app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command('json')
|
|
13
|
+
@syncCall
|
|
14
|
+
async def format_json():
|
|
15
|
+
'格式化 JSON (使用复制文本)'
|
|
16
|
+
content = pyperclip.paste()
|
|
17
|
+
try:
|
|
18
|
+
Console().print_json(content, indent=4, ensure_ascii=False, sort_keys=True)
|
|
19
|
+
data = json.loads(content)
|
|
20
|
+
pyperclip.copy(
|
|
21
|
+
json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True)
|
|
22
|
+
)
|
|
23
|
+
except:
|
|
24
|
+
bcolor.printRed('无效的 JSON')
|
|
25
|
+
bcolor.printRed(content)
|