bcmd 0.5.2__py3-none-any.whl → 0.5.4__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/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,201 +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}% / 忽略)')
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}% / 不处理)')
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)