bcmd 0.0.67__py3-none-any.whl → 0.5.2__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 -0
- bcmd/main.py +1 -1
- bcmd/resources/project/.gitignore +3 -0
- bcmd/resources/project/.vscode/launch.json +15 -0
- bcmd/resources/project/.vscode/settings.json +11 -0
- bcmd/resources/project/.vscode/tasks.json +68 -0
- bcmd/resources/project/main.py +1 -0
- bcmd/tasks/__init__.py +3 -3
- bcmd/tasks/bin.py +12 -15
- bcmd/tasks/code.py +96 -0
- bcmd/tasks/crypto.py +3 -0
- bcmd/tasks/download.py +47 -12
- bcmd/tasks/image.py +201 -0
- bcmd/tasks/lib.py +50 -44
- bcmd/tasks/math.py +2 -49
- bcmd/tasks/mirror.py +29 -38
- bcmd/tasks/project.py +34 -0
- bcmd/tasks/proxy.py +35 -8
- bcmd/tasks/time.py +12 -12
- bcmd/tasks/venv.py +164 -65
- bcmd-0.5.2.dist-info/METADATA +18 -0
- bcmd-0.5.2.dist-info/RECORD +30 -0
- {bcmd-0.0.67.dist-info → bcmd-0.5.2.dist-info}/WHEEL +1 -1
- bcmd/tasks/jwt.py +0 -87
- bcmd/tasks/task.py +0 -144
- bcmd/tasks/temp.py +0 -31
- bcmd-0.0.67.dist-info/METADATA +0 -11
- bcmd-0.0.67.dist-info/RECORD +0 -24
- {bcmd-0.0.67.dist-info → bcmd-0.5.2.dist-info}/entry_points.txt +0 -0
- {bcmd-0.0.67.dist-info → bcmd-0.5.2.dist-info}/top_level.txt +0 -0
bcmd/common/func.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import importlib.resources
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from beni import btask
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def checkFileOrNotExists(file: Path):
|
|
9
|
+
btask.assertTrue(file.is_file() or not file.exists(), f'必须是文件 {file}')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def checkPathOrNotExists(folder: Path):
|
|
13
|
+
btask.assertTrue(folder.is_dir() or not folder.exists(), f'必须是目录 {folder}')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def useResources(name: str):
|
|
18
|
+
with importlib.resources.path('bcmd.resources', name) as target:
|
|
19
|
+
yield target
|
bcmd/main.py
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
|
3
|
+
// Hover to view descriptions of existing attributes.
|
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"configurations": [
|
|
7
|
+
{
|
|
8
|
+
"name": "Python: main.py",
|
|
9
|
+
"type": "debugpy",
|
|
10
|
+
"request": "launch",
|
|
11
|
+
// "console": "internalConsole",
|
|
12
|
+
"program": "${workspaceFolder}/main.py"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"tasks": [
|
|
4
|
+
{
|
|
5
|
+
"label": "git commit",
|
|
6
|
+
"problemMatcher": [],
|
|
7
|
+
"command": "TortoiseGitProc.exe",
|
|
8
|
+
"args": [
|
|
9
|
+
"/command:commit",
|
|
10
|
+
"/path:${workspaceFolder}/",
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"label": "git commit file",
|
|
15
|
+
"problemMatcher": [],
|
|
16
|
+
"command": "TortoiseGitProc.exe",
|
|
17
|
+
"args": [
|
|
18
|
+
"/command:commit",
|
|
19
|
+
"/path:${file}",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"label": "git revert",
|
|
24
|
+
"problemMatcher": [],
|
|
25
|
+
"command": "TortoiseGitProc.exe",
|
|
26
|
+
"args": [
|
|
27
|
+
"/command:revert",
|
|
28
|
+
"/path:${workspaceFolder}/",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"label": "git revert file",
|
|
33
|
+
"problemMatcher": [],
|
|
34
|
+
"command": "TortoiseGitProc.exe",
|
|
35
|
+
"args": [
|
|
36
|
+
"/command:revert",
|
|
37
|
+
"/path:${file}",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"label": "git sync",
|
|
42
|
+
"problemMatcher": [],
|
|
43
|
+
"command": "TortoiseGitProc.exe",
|
|
44
|
+
"args": [
|
|
45
|
+
"/command:sync",
|
|
46
|
+
"/path:${workspaceFolder}/",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"label": "git log",
|
|
51
|
+
"problemMatcher": [],
|
|
52
|
+
"command": "TortoiseGitProc.exe",
|
|
53
|
+
"args": [
|
|
54
|
+
"/command:log",
|
|
55
|
+
"/path:${workspaceFolder}/",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"label": "git log file",
|
|
60
|
+
"problemMatcher": [],
|
|
61
|
+
"command": "TortoiseGitProc.exe",
|
|
62
|
+
"args": [
|
|
63
|
+
"/command:log",
|
|
64
|
+
"/path:${file}",
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
print('OK')
|
bcmd/tasks/__init__.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# type: ignore
|
|
2
2
|
from . import bin
|
|
3
|
+
from . import code
|
|
3
4
|
from . import crypto
|
|
4
5
|
from . import debian
|
|
5
6
|
from . import download
|
|
7
|
+
from . import image
|
|
6
8
|
from . import json
|
|
7
|
-
from . import jwt
|
|
8
9
|
from . import lib
|
|
9
10
|
from . import math
|
|
10
11
|
from . import mirror
|
|
12
|
+
from . import project
|
|
11
13
|
from . import proxy
|
|
12
|
-
from . import task
|
|
13
|
-
from . import temp
|
|
14
14
|
from . import time
|
|
15
15
|
from . import venv
|
bcmd/tasks/bin.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from datetime import datetime
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
from typing import Final
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
from beni import bcolor, bfile, bpath, btask, bzip
|
|
8
|
-
from beni.bfunc import syncCall
|
|
7
|
+
from beni.bfunc import syncCall, textToAry
|
|
9
8
|
from beni.bqiniu import QiniuBucket
|
|
10
9
|
from beni.btype import Null
|
|
11
10
|
from prettytable import PrettyTable
|
|
12
11
|
|
|
13
|
-
from
|
|
12
|
+
from ..common import password
|
|
14
13
|
|
|
15
14
|
app: Final = btask.newSubApp('bin 工具')
|
|
16
15
|
|
|
@@ -20,24 +19,18 @@ _PREFIX = 'bin/'
|
|
|
20
19
|
@app.command()
|
|
21
20
|
@syncCall
|
|
22
21
|
async def download(
|
|
23
|
-
names: str = typer.Argument(None, help="
|
|
22
|
+
names: list[str] = typer.Argument(None, help="支持多个"),
|
|
24
23
|
file: Path = typer.Option(None, '--file', '-f', help="文件形式指定参数,行为单位"),
|
|
25
|
-
output: Path = typer.Option(
|
|
24
|
+
output: Path = typer.Option(Path.cwd(), '--output', '-o', help="本地保存路径"),
|
|
26
25
|
):
|
|
27
26
|
'从七牛云下载执行文件'
|
|
28
27
|
bucket: QiniuBucket = Null
|
|
29
|
-
if not output:
|
|
30
|
-
output = Path(os.curdir)
|
|
31
|
-
output = output.resolve()
|
|
32
|
-
targetList: list[str] = []
|
|
33
|
-
if names:
|
|
34
|
-
targetList.extend(names.split(','))
|
|
35
28
|
if file:
|
|
36
29
|
content = await bfile.readText(Path(file))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for target in
|
|
30
|
+
names.extend(
|
|
31
|
+
textToAry(content)
|
|
32
|
+
)
|
|
33
|
+
for target in names:
|
|
41
34
|
binFile = output / target
|
|
42
35
|
if binFile.exists():
|
|
43
36
|
bcolor.printYellow(f'已存在 {binFile}')
|
|
@@ -55,6 +48,7 @@ async def getList():
|
|
|
55
48
|
bucket = await _getBucket()
|
|
56
49
|
datas = (await bucket.getFileList(_PREFIX, limit=1000))[0]
|
|
57
50
|
datas = [x for x in datas if x.key != _PREFIX and x.key.endswith('.zip')]
|
|
51
|
+
datas.sort(key=lambda x: x.time, reverse=True)
|
|
58
52
|
table = PrettyTable()
|
|
59
53
|
table.add_column(
|
|
60
54
|
bcolor.yellow('文件名称'),
|
|
@@ -97,6 +91,9 @@ async def remove(
|
|
|
97
91
|
bcolor.printGreen('OK')
|
|
98
92
|
|
|
99
93
|
|
|
94
|
+
# ------------------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
100
97
|
async def _getBucket():
|
|
101
98
|
ak, sk = await password.getQiniu()
|
|
102
99
|
return QiniuBucket(
|
bcmd/tasks/code.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from beni import bfile, bpath, btask
|
|
6
|
+
from beni.bcolor import printGreen, printMagenta, printYellow
|
|
7
|
+
from beni.bfunc import syncCall
|
|
8
|
+
|
|
9
|
+
app: Final = btask.newSubApp('code 工具')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
@syncCall
|
|
14
|
+
async def tidy_tasks(
|
|
15
|
+
tasks_path: Path = typer.Argument(Path.cwd(), help="tasks 路径"),
|
|
16
|
+
):
|
|
17
|
+
'整理 task 项目中的 tasks/__init__.py'
|
|
18
|
+
|
|
19
|
+
initFile = tasks_path / '__init__.py'
|
|
20
|
+
btask.assertTrue(initFile.is_file(), '文件不存在', initFile)
|
|
21
|
+
files = bpath.listFile(tasks_path)
|
|
22
|
+
files = [x for x in files if not x.name.startswith('_')]
|
|
23
|
+
contents = [f'from . import {x.stem}' for x in files]
|
|
24
|
+
contents.insert(0, '# type: ignore')
|
|
25
|
+
contents.append('')
|
|
26
|
+
content = '\n'.join(contents)
|
|
27
|
+
oldContent = await bfile.readText(initFile)
|
|
28
|
+
if oldContent != content:
|
|
29
|
+
await bfile.writeText(
|
|
30
|
+
initFile,
|
|
31
|
+
content,
|
|
32
|
+
)
|
|
33
|
+
printYellow(initFile)
|
|
34
|
+
printMagenta(content)
|
|
35
|
+
printGreen('OK')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
@syncCall
|
|
40
|
+
async def tidy_modules(
|
|
41
|
+
modules_path: Path = typer.Argument(Path.cwd(), help="modules_path 路径"),
|
|
42
|
+
):
|
|
43
|
+
'整理 fastapi 项目中的 modules/__init__.py'
|
|
44
|
+
|
|
45
|
+
importContents: list[str] = []
|
|
46
|
+
managerContents: list[str] = []
|
|
47
|
+
|
|
48
|
+
xxdict: dict[str, set[Path]] = {}
|
|
49
|
+
for file in sorted(modules_path.glob('**/*Manager.py')):
|
|
50
|
+
if file.parent == modules_path:
|
|
51
|
+
subName = '.'
|
|
52
|
+
elif file.parent.parent == modules_path:
|
|
53
|
+
subName = f'.{file.parent.stem}'
|
|
54
|
+
else:
|
|
55
|
+
continue
|
|
56
|
+
xxdict.setdefault(subName, set()).add(file)
|
|
57
|
+
for subName in sorted(xxdict.keys()):
|
|
58
|
+
files = sorted(xxdict[subName])
|
|
59
|
+
importContents.append(f'from {subName} import {", ".join([x.stem for x in files])}')
|
|
60
|
+
managerContents.extend([f' {x.stem},' for x in sorted([y for x in xxdict.values() for y in x])])
|
|
61
|
+
|
|
62
|
+
managerContents = [x for x in managerContents if x]
|
|
63
|
+
contents = [
|
|
64
|
+
'\n'.join(importContents),
|
|
65
|
+
'managers = [\n' + '\n'.join(managerContents) + '\n]',
|
|
66
|
+
]
|
|
67
|
+
content = '\n\n'.join(contents) + '\n'
|
|
68
|
+
file = modules_path / '__init__.py'
|
|
69
|
+
printYellow(str(file))
|
|
70
|
+
printMagenta(content)
|
|
71
|
+
await bfile.writeText(file, content)
|
|
72
|
+
printGreen('OK')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command()
|
|
76
|
+
@syncCall
|
|
77
|
+
async def gen_init_py(
|
|
78
|
+
workspace_path: Path = typer.Argument(Path.cwd(), help='workspace 路径'),
|
|
79
|
+
):
|
|
80
|
+
'递归生成 __init__.py 文件'
|
|
81
|
+
|
|
82
|
+
async def makeInitFiles(p: Path):
|
|
83
|
+
if p.name == '__pycache__':
|
|
84
|
+
return
|
|
85
|
+
if p.name.startswith('.'):
|
|
86
|
+
return
|
|
87
|
+
if workspace_path != p:
|
|
88
|
+
initFile = p / '__init__.py'
|
|
89
|
+
if not initFile.exists():
|
|
90
|
+
printYellow(initFile)
|
|
91
|
+
await bfile.writeText(initFile, '')
|
|
92
|
+
for x in bpath.listDir(p):
|
|
93
|
+
await makeInitFiles(x)
|
|
94
|
+
|
|
95
|
+
await makeInitFiles(workspace_path)
|
|
96
|
+
printGreen('OK')
|
bcmd/tasks/crypto.py
CHANGED
bcmd/tasks/download.py
CHANGED
|
@@ -2,38 +2,73 @@ import asyncio
|
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Final
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
from uuid import uuid4
|
|
5
7
|
|
|
6
8
|
import pyperclip
|
|
7
9
|
import typer
|
|
8
|
-
from beni import bcolor, bhttp, binput, bpath, btask
|
|
9
|
-
from beni.bfunc import syncCall
|
|
10
|
+
from beni import bcolor, bfile, bhttp, binput, bpath, btask
|
|
11
|
+
from beni.bfunc import syncCall, textToAry
|
|
10
12
|
|
|
11
|
-
app: Final = btask.
|
|
13
|
+
app: Final = btask.app
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@app.command()
|
|
15
17
|
@syncCall
|
|
16
|
-
async def
|
|
17
|
-
|
|
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='保持原始目录结构,默认不保持'),
|
|
18
22
|
):
|
|
19
|
-
'
|
|
20
|
-
|
|
21
|
-
|
|
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))
|
|
23
33
|
|
|
24
34
|
for i, url in enumerate(urlSet):
|
|
25
35
|
print(f'{i + 1}. {url}')
|
|
26
|
-
print(f'输出目录:{
|
|
36
|
+
print(f'输出目录:{save_path}')
|
|
27
37
|
await binput.confirm('是否确认?')
|
|
28
38
|
|
|
39
|
+
fileSet: set[Path] = set()
|
|
40
|
+
retryUrlSet: set[str] = set()
|
|
41
|
+
|
|
29
42
|
async def download(url: str):
|
|
30
|
-
|
|
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)
|
|
31
51
|
try:
|
|
32
52
|
bcolor.printGreen(url)
|
|
33
53
|
await bhttp.download(url, file)
|
|
34
54
|
except:
|
|
55
|
+
retryUrlSet.add(url)
|
|
35
56
|
bcolor.printRed(url)
|
|
36
57
|
|
|
37
58
|
await asyncio.gather(*[download(x) for x in urlSet])
|
|
38
59
|
|
|
39
|
-
|
|
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
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
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}% / 忽略)')
|
|
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
|
+
async def updateInfo(self):
|
|
144
|
+
fileBytes = await bfile.readBytes(self._file)
|
|
145
|
+
self._compression = 0.0
|
|
146
|
+
self._isTiny = False
|
|
147
|
+
blockAry = fileBytes.split(self._sep)
|
|
148
|
+
if len(blockAry) > 1:
|
|
149
|
+
info = BytesReader(self._endian, blockAry[1])
|
|
150
|
+
size = info.readUint()
|
|
151
|
+
if size == len(blockAry[0]):
|
|
152
|
+
self._compression = round(info.readFloat(), 2)
|
|
153
|
+
self._isTiny = info.readBool()
|
|
154
|
+
|
|
155
|
+
async def _flushInfo(self, compression: float, isTiny: bool):
|
|
156
|
+
self._compression = compression
|
|
157
|
+
self._isTiny = isTiny
|
|
158
|
+
content = await self._getPureContent()
|
|
159
|
+
info = (
|
|
160
|
+
BytesWriter(self._endian)
|
|
161
|
+
.writeUint(len(content))
|
|
162
|
+
.writeFloat(compression)
|
|
163
|
+
.writeBool(isTiny)
|
|
164
|
+
.toBytes()
|
|
165
|
+
)
|
|
166
|
+
content += self._sep + info
|
|
167
|
+
await bfile.writeBytes(self._file, content)
|
|
168
|
+
|
|
169
|
+
async def _getPureContent(self):
|
|
170
|
+
content = await bfile.readBytes(self._file)
|
|
171
|
+
content = content.split(self._sep)[0]
|
|
172
|
+
return content
|
|
173
|
+
|
|
174
|
+
@block.limit(1)
|
|
175
|
+
async def runTiny(self, key: str, compression: float, isKeepOriginal: bool):
|
|
176
|
+
content = await self._getPureContent()
|
|
177
|
+
async with httpx.AsyncClient() as client:
|
|
178
|
+
response = await client.post(
|
|
179
|
+
'https://api.tinify.com/shrink',
|
|
180
|
+
auth=('api', key),
|
|
181
|
+
content=content,
|
|
182
|
+
timeout=30,
|
|
183
|
+
)
|
|
184
|
+
response.raise_for_status()
|
|
185
|
+
result = response.json()
|
|
186
|
+
outputCompression = round(result['output']['ratio'] * 100, 2)
|
|
187
|
+
if outputCompression < compression:
|
|
188
|
+
# 下载文件
|
|
189
|
+
url = result['output']['url']
|
|
190
|
+
with bpath.useTempFile() as tempFile:
|
|
191
|
+
await bhttp.download(url, tempFile)
|
|
192
|
+
await _TinyFile(tempFile)._flushInfo(outputCompression, True)
|
|
193
|
+
outputFile = bpath.get(self._file)
|
|
194
|
+
if isKeepOriginal:
|
|
195
|
+
outputFile = outputFile.with_stem(f'{outputFile.stem}_tiny')
|
|
196
|
+
bpath.move(tempFile, outputFile, True)
|
|
197
|
+
bcolor.printGreen(f'{outputFile}({outputCompression}% / 已压缩)')
|
|
198
|
+
else:
|
|
199
|
+
# 不进行压缩
|
|
200
|
+
await self._flushInfo(outputCompression, False)
|
|
201
|
+
bcolor.printMagenta(f'{self._file} ({outputCompression}% / 不处理)')
|