tinycomp-amadeus 0.1.0__tar.gz
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.
- tinycomp_amadeus-0.1.0/LICENSE +21 -0
- tinycomp_amadeus-0.1.0/PKG-INFO +139 -0
- tinycomp_amadeus-0.1.0/README.md +113 -0
- tinycomp_amadeus-0.1.0/setup.cfg +4 -0
- tinycomp_amadeus-0.1.0/setup.py +41 -0
- tinycomp_amadeus-0.1.0/tinycomp/__init__.py +11 -0
- tinycomp_amadeus-0.1.0/tinycomp/api_manager.py +318 -0
- tinycomp_amadeus-0.1.0/tinycomp/cli.py +149 -0
- tinycomp_amadeus-0.1.0/tinycomp/compressor.py +195 -0
- tinycomp_amadeus-0.1.0/tinycomp/tests/__init__.py +3 -0
- tinycomp_amadeus-0.1.0/tinycomp/tests/test_compressor.py +101 -0
- tinycomp_amadeus-0.1.0/tinycomp_amadeus.egg-info/PKG-INFO +139 -0
- tinycomp_amadeus-0.1.0/tinycomp_amadeus.egg-info/SOURCES.txt +15 -0
- tinycomp_amadeus-0.1.0/tinycomp_amadeus.egg-info/dependency_links.txt +1 -0
- tinycomp_amadeus-0.1.0/tinycomp_amadeus.egg-info/entry_points.txt +2 -0
- tinycomp_amadeus-0.1.0/tinycomp_amadeus.egg-info/requires.txt +6 -0
- tinycomp_amadeus-0.1.0/tinycomp_amadeus.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TinyComp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tinycomp-amadeus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package for compressing images using TinyPNG API
|
|
5
|
+
Home-page: https://github.com/Amadeus9029/tinycomp
|
|
6
|
+
Author: Amadeus9029
|
|
7
|
+
Author-email: 965720890@qq.com
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: tinify
|
|
21
|
+
Requires-Dist: requests
|
|
22
|
+
Requires-Dist: tqdm
|
|
23
|
+
Requires-Dist: beautifulsoup4
|
|
24
|
+
Requires-Dist: selenium
|
|
25
|
+
Requires-Dist: fake-useragent
|
|
26
|
+
|
|
27
|
+
# TinyComp
|
|
28
|
+
|
|
29
|
+
TinyComp is a Python package that helps you compress images using the TinyPNG API. It provides both a command-line interface and a Python API for easy integration into your projects.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Automatic API key management and rotation
|
|
34
|
+
- Automatic API key acquisition when needed
|
|
35
|
+
- Batch image compression
|
|
36
|
+
- Support for multiple image formats (PNG, JPG, JPEG, SVG, GIF)
|
|
37
|
+
- Progress bar for tracking compression status
|
|
38
|
+
- Multi-threaded compression for better performance
|
|
39
|
+
- Automatic handling of API usage limits
|
|
40
|
+
- Automatic API key update through web automation
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install tinycomp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Command Line Interface
|
|
51
|
+
|
|
52
|
+
#### Compressing Images
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Basic compression
|
|
56
|
+
tinycomp compress --source ./images --target ./compressed
|
|
57
|
+
|
|
58
|
+
# With custom API key
|
|
59
|
+
tinycomp compress --source ./images --target ./compressed --api-key YOUR_API_KEY
|
|
60
|
+
|
|
61
|
+
# Set number of threads
|
|
62
|
+
tinycomp compress --source ./images --target ./compressed --threads 4
|
|
63
|
+
|
|
64
|
+
# Enable automatic API key updates when needed
|
|
65
|
+
tinycomp compress --source ./images --target ./compressed --auto-update-key
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Managing API Keys
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Update API key (checks current key first)
|
|
72
|
+
tinycomp update-key
|
|
73
|
+
|
|
74
|
+
# Force update API key even if current one is valid
|
|
75
|
+
tinycomp update-key --force
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Python API
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from tinycomp import TinyCompressor
|
|
82
|
+
|
|
83
|
+
# Initialize compressor
|
|
84
|
+
compressor = TinyCompressor(api_key="YOUR_API_KEY") # API key is optional
|
|
85
|
+
|
|
86
|
+
# Enable automatic API key updates
|
|
87
|
+
compressor = TinyCompressor(auto_update_key=True)
|
|
88
|
+
|
|
89
|
+
# Compress a single image
|
|
90
|
+
compressor.compress_image("input.png", "output.png")
|
|
91
|
+
|
|
92
|
+
# Compress multiple images
|
|
93
|
+
compressor.compress_directory("./images", "./compressed")
|
|
94
|
+
|
|
95
|
+
# Update API key programmatically
|
|
96
|
+
from tinycomp.api_manager import APIKeyManager
|
|
97
|
+
api_manager = APIKeyManager()
|
|
98
|
+
new_key = api_manager.get_new_api_key()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
You can configure TinyComp using environment variables:
|
|
104
|
+
|
|
105
|
+
- `TINYCOMP_API_KEY`: Your TinyPNG API key
|
|
106
|
+
- `TINYCOMP_MAX_THREADS`: Maximum number of compression threads (default: 4)
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- Python 3.6 or higher
|
|
111
|
+
- Chrome/Chromium browser (for automatic API key updates)
|
|
112
|
+
- ChromeDriver matching your Chrome version
|
|
113
|
+
|
|
114
|
+
## API Key Management
|
|
115
|
+
|
|
116
|
+
TinyComp includes an automatic API key management system that:
|
|
117
|
+
|
|
118
|
+
1. Automatically rotates between multiple API keys
|
|
119
|
+
2. Monitors remaining API usage
|
|
120
|
+
3. Can automatically obtain new API keys when needed
|
|
121
|
+
4. Saves API keys for future use
|
|
122
|
+
|
|
123
|
+
The package offers two modes for handling API key depletion:
|
|
124
|
+
|
|
125
|
+
1. Default mode: Stops compression and notifies when the API key runs out
|
|
126
|
+
2. Auto-update mode: Automatically obtains new API keys when needed (use `--auto-update-key` flag)
|
|
127
|
+
|
|
128
|
+
The automatic key update feature requires:
|
|
129
|
+
- Chrome/Chromium browser installed
|
|
130
|
+
- ChromeDriver in your system PATH or in the working directory
|
|
131
|
+
- Internet connection for accessing TinyPNG website
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# TinyComp
|
|
2
|
+
|
|
3
|
+
TinyComp is a Python package that helps you compress images using the TinyPNG API. It provides both a command-line interface and a Python API for easy integration into your projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Automatic API key management and rotation
|
|
8
|
+
- Automatic API key acquisition when needed
|
|
9
|
+
- Batch image compression
|
|
10
|
+
- Support for multiple image formats (PNG, JPG, JPEG, SVG, GIF)
|
|
11
|
+
- Progress bar for tracking compression status
|
|
12
|
+
- Multi-threaded compression for better performance
|
|
13
|
+
- Automatic handling of API usage limits
|
|
14
|
+
- Automatic API key update through web automation
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tinycomp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Command Line Interface
|
|
25
|
+
|
|
26
|
+
#### Compressing Images
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Basic compression
|
|
30
|
+
tinycomp compress --source ./images --target ./compressed
|
|
31
|
+
|
|
32
|
+
# With custom API key
|
|
33
|
+
tinycomp compress --source ./images --target ./compressed --api-key YOUR_API_KEY
|
|
34
|
+
|
|
35
|
+
# Set number of threads
|
|
36
|
+
tinycomp compress --source ./images --target ./compressed --threads 4
|
|
37
|
+
|
|
38
|
+
# Enable automatic API key updates when needed
|
|
39
|
+
tinycomp compress --source ./images --target ./compressed --auto-update-key
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Managing API Keys
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Update API key (checks current key first)
|
|
46
|
+
tinycomp update-key
|
|
47
|
+
|
|
48
|
+
# Force update API key even if current one is valid
|
|
49
|
+
tinycomp update-key --force
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Python API
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from tinycomp import TinyCompressor
|
|
56
|
+
|
|
57
|
+
# Initialize compressor
|
|
58
|
+
compressor = TinyCompressor(api_key="YOUR_API_KEY") # API key is optional
|
|
59
|
+
|
|
60
|
+
# Enable automatic API key updates
|
|
61
|
+
compressor = TinyCompressor(auto_update_key=True)
|
|
62
|
+
|
|
63
|
+
# Compress a single image
|
|
64
|
+
compressor.compress_image("input.png", "output.png")
|
|
65
|
+
|
|
66
|
+
# Compress multiple images
|
|
67
|
+
compressor.compress_directory("./images", "./compressed")
|
|
68
|
+
|
|
69
|
+
# Update API key programmatically
|
|
70
|
+
from tinycomp.api_manager import APIKeyManager
|
|
71
|
+
api_manager = APIKeyManager()
|
|
72
|
+
new_key = api_manager.get_new_api_key()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
You can configure TinyComp using environment variables:
|
|
78
|
+
|
|
79
|
+
- `TINYCOMP_API_KEY`: Your TinyPNG API key
|
|
80
|
+
- `TINYCOMP_MAX_THREADS`: Maximum number of compression threads (default: 4)
|
|
81
|
+
|
|
82
|
+
## Requirements
|
|
83
|
+
|
|
84
|
+
- Python 3.6 or higher
|
|
85
|
+
- Chrome/Chromium browser (for automatic API key updates)
|
|
86
|
+
- ChromeDriver matching your Chrome version
|
|
87
|
+
|
|
88
|
+
## API Key Management
|
|
89
|
+
|
|
90
|
+
TinyComp includes an automatic API key management system that:
|
|
91
|
+
|
|
92
|
+
1. Automatically rotates between multiple API keys
|
|
93
|
+
2. Monitors remaining API usage
|
|
94
|
+
3. Can automatically obtain new API keys when needed
|
|
95
|
+
4. Saves API keys for future use
|
|
96
|
+
|
|
97
|
+
The package offers two modes for handling API key depletion:
|
|
98
|
+
|
|
99
|
+
1. Default mode: Stops compression and notifies when the API key runs out
|
|
100
|
+
2. Auto-update mode: Automatically obtains new API keys when needed (use `--auto-update-key` flag)
|
|
101
|
+
|
|
102
|
+
The automatic key update feature requires:
|
|
103
|
+
- Chrome/Chromium browser installed
|
|
104
|
+
- ChromeDriver in your system PATH or in the working directory
|
|
105
|
+
- Internet connection for accessing TinyPNG website
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
110
|
+
|
|
111
|
+
## Contributing
|
|
112
|
+
|
|
113
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="tinycomp-amadeus",
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
author="Amadeus9029",
|
|
10
|
+
author_email="965720890@qq.com",
|
|
11
|
+
description="A Python package for compressing images using TinyPNG API",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/Amadeus9029/tinycomp",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
classifiers=[
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.6",
|
|
23
|
+
"Programming Language :: Python :: 3.7",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9",
|
|
26
|
+
],
|
|
27
|
+
python_requires=">=3.6",
|
|
28
|
+
install_requires=[
|
|
29
|
+
"tinify",
|
|
30
|
+
"requests",
|
|
31
|
+
"tqdm",
|
|
32
|
+
"beautifulsoup4",
|
|
33
|
+
"selenium",
|
|
34
|
+
"fake-useragent"
|
|
35
|
+
],
|
|
36
|
+
entry_points={
|
|
37
|
+
'console_scripts': [
|
|
38
|
+
'tinycomp=tinycomp.cli:main',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
)
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API key management for TinyPNG
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import random
|
|
9
|
+
import string
|
|
10
|
+
from typing import List, Dict, Optional, Tuple
|
|
11
|
+
import tinify
|
|
12
|
+
from selenium import webdriver
|
|
13
|
+
from selenium.webdriver.chrome.options import Options
|
|
14
|
+
from selenium.webdriver.chrome.service import Service
|
|
15
|
+
from selenium.webdriver.common.by import By
|
|
16
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
17
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
18
|
+
from fake_useragent import UserAgent
|
|
19
|
+
|
|
20
|
+
class APIKeyManager:
|
|
21
|
+
"""Manages TinyPNG API keys, including loading, saving, and validation."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the API key manager.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
api_key (str, optional): Initial API key to use. If not provided,
|
|
29
|
+
will try to get from environment or saved keys.
|
|
30
|
+
"""
|
|
31
|
+
self.api_keys_file = "tinypng_api_keys.json"
|
|
32
|
+
self.current_key = api_key or os.getenv("TINYCOMP_API_KEY")
|
|
33
|
+
|
|
34
|
+
if not self.current_key:
|
|
35
|
+
self.current_key = self._get_valid_api_key()
|
|
36
|
+
|
|
37
|
+
def _load_api_keys(self) -> List[str]:
|
|
38
|
+
"""Load saved API keys from file."""
|
|
39
|
+
if os.path.exists(self.api_keys_file):
|
|
40
|
+
try:
|
|
41
|
+
with open(self.api_keys_file, 'r') as f:
|
|
42
|
+
data = json.load(f)
|
|
43
|
+
return data.get("api_keys", [])
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"Error loading API keys file: {str(e)}")
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
def _save_api_keys(self, api_keys: List[str]) -> None:
|
|
49
|
+
"""Save API keys to file."""
|
|
50
|
+
try:
|
|
51
|
+
data = {"api_keys": api_keys}
|
|
52
|
+
with open(self.api_keys_file, 'w') as f:
|
|
53
|
+
json.dump(data, f, indent=2)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"Error saving API keys to file: {str(e)}")
|
|
56
|
+
|
|
57
|
+
def _get_compression_count(self, api_key: Optional[str] = None) -> Dict[str, any]:
|
|
58
|
+
"""
|
|
59
|
+
Get the compression count for an API key.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
api_key (str, optional): API key to check. If None, uses current key.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
dict: Contains compression count information and status.
|
|
66
|
+
"""
|
|
67
|
+
result = {
|
|
68
|
+
'compression_count': 0,
|
|
69
|
+
'remaining': 500,
|
|
70
|
+
'success': False,
|
|
71
|
+
'error': None
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# If provided new API key, temporarily set it
|
|
75
|
+
old_key = None
|
|
76
|
+
if api_key:
|
|
77
|
+
old_key = tinify.key
|
|
78
|
+
tinify.key = api_key
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Create a tiny PNG image for validation
|
|
82
|
+
tiny_png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
|
|
83
|
+
|
|
84
|
+
# Send request to activate compression_count
|
|
85
|
+
source = tinify.from_buffer(tiny_png)
|
|
86
|
+
|
|
87
|
+
# Get compression count
|
|
88
|
+
compression_count = getattr(tinify, 'compression_count', 0)
|
|
89
|
+
if compression_count is None:
|
|
90
|
+
compression_count = 0
|
|
91
|
+
|
|
92
|
+
# Calculate remaining
|
|
93
|
+
remaining = 500 - compression_count
|
|
94
|
+
|
|
95
|
+
result.update({
|
|
96
|
+
'compression_count': compression_count,
|
|
97
|
+
'remaining': remaining,
|
|
98
|
+
'success': True
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
except tinify.Error as e:
|
|
102
|
+
result['error'] = str(e)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
result['error'] = f"Unknown error: {str(e)}"
|
|
105
|
+
|
|
106
|
+
# Restore original API key
|
|
107
|
+
if old_key:
|
|
108
|
+
tinify.key = old_key
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def _get_valid_api_key(self) -> Optional[str]:
|
|
113
|
+
"""Get a valid API key from saved keys or environment."""
|
|
114
|
+
# Load saved API keys
|
|
115
|
+
api_keys = self._load_api_keys()
|
|
116
|
+
|
|
117
|
+
# Check each saved key
|
|
118
|
+
for key in api_keys:
|
|
119
|
+
tinify.key = key
|
|
120
|
+
try:
|
|
121
|
+
result = self._get_compression_count(key)
|
|
122
|
+
if result['success'] and result['remaining'] > 0:
|
|
123
|
+
return key
|
|
124
|
+
except:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def _generate_random_name(self) -> str:
|
|
130
|
+
"""Generate random name for registration."""
|
|
131
|
+
first_names = ['Zhang', 'Li', 'Wang', 'Liu', 'Chen', 'Yang', 'Huang', 'Zhao', 'Wu', 'Zhou']
|
|
132
|
+
last_names = ['Wei', 'Min', 'Jie', 'Fang', 'Ying', 'Hai', 'Jun', 'Xin', 'Feng', 'Yu']
|
|
133
|
+
return f"{random.choice(first_names)} {random.choice(last_names)}"
|
|
134
|
+
|
|
135
|
+
def _configure_chrome_options(self) -> Options:
|
|
136
|
+
"""Configure Chrome options with random fingerprint."""
|
|
137
|
+
chrome_options = Options()
|
|
138
|
+
chrome_options.add_argument('--headless')
|
|
139
|
+
chrome_options.add_argument('--no-sandbox')
|
|
140
|
+
chrome_options.add_argument('--disable-dev-shm-usage')
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
ua = UserAgent().chrome
|
|
144
|
+
except:
|
|
145
|
+
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
|
146
|
+
|
|
147
|
+
chrome_options.add_argument(f'--user-agent={ua}')
|
|
148
|
+
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
|
|
149
|
+
chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])
|
|
150
|
+
chrome_options.add_experimental_option('useAutomationExtension', False)
|
|
151
|
+
|
|
152
|
+
return chrome_options
|
|
153
|
+
|
|
154
|
+
def _get_temp_email(self) -> Tuple[Optional[str], Optional[webdriver.Chrome]]:
|
|
155
|
+
"""Get temporary email address from temporary email service."""
|
|
156
|
+
print("Getting temporary email...")
|
|
157
|
+
|
|
158
|
+
chrome_options = self._configure_chrome_options()
|
|
159
|
+
driver_path = os.path.join(os.getcwd(), "chromedriver.exe")
|
|
160
|
+
driver = webdriver.Chrome(service=Service(driver_path), options=chrome_options)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
driver.get("https://www.nimail.cn/index.html")
|
|
164
|
+
|
|
165
|
+
random_email_btn = WebDriverWait(driver, 10).until(
|
|
166
|
+
EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), '随机邮箱')]"))
|
|
167
|
+
)
|
|
168
|
+
random_email_btn.click()
|
|
169
|
+
|
|
170
|
+
time.sleep(2)
|
|
171
|
+
|
|
172
|
+
email_username_element = WebDriverWait(driver, 10).until(
|
|
173
|
+
EC.presence_of_element_located((By.XPATH, '//*[@id="mailuser"]'))
|
|
174
|
+
)
|
|
175
|
+
email_username = email_username_element.get_attribute("value")
|
|
176
|
+
email = f"{email_username}@nimail.cn"
|
|
177
|
+
|
|
178
|
+
apply_email_btn = WebDriverWait(driver, 10).until(
|
|
179
|
+
EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), '申请邮箱')]"))
|
|
180
|
+
)
|
|
181
|
+
apply_email_btn.click()
|
|
182
|
+
|
|
183
|
+
time.sleep(3)
|
|
184
|
+
print(f"Temporary email activated: {email}")
|
|
185
|
+
|
|
186
|
+
return email, driver
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"Failed to get temporary email: {str(e)}")
|
|
189
|
+
if driver:
|
|
190
|
+
driver.quit()
|
|
191
|
+
return None, None
|
|
192
|
+
|
|
193
|
+
def _request_new_api_key(self, email: str, driver: webdriver.Chrome) -> Optional[str]:
|
|
194
|
+
"""Request new TinyPNG API key using temporary email."""
|
|
195
|
+
print(f"Requesting new TinyPNG API key using email: {email}")
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
original_window = driver.current_window_handle
|
|
199
|
+
driver.execute_script("window.open('https://tinify.com/developers', '_blank');")
|
|
200
|
+
time.sleep(2)
|
|
201
|
+
driver.switch_to.window(driver.window_handles[-1])
|
|
202
|
+
|
|
203
|
+
WebDriverWait(driver, 20).until(
|
|
204
|
+
EC.presence_of_element_located((By.NAME, "name"))
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
name_input = driver.find_element(By.NAME, "name")
|
|
208
|
+
name_input.send_keys(self._generate_random_name())
|
|
209
|
+
|
|
210
|
+
email_input = driver.find_element(By.NAME, "email")
|
|
211
|
+
email_input.send_keys(email)
|
|
212
|
+
|
|
213
|
+
submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
|
|
214
|
+
submit_button.click()
|
|
215
|
+
|
|
216
|
+
driver.switch_to.window(original_window)
|
|
217
|
+
|
|
218
|
+
max_attempts = 15
|
|
219
|
+
for attempt in range(max_attempts):
|
|
220
|
+
print(f"Waiting for confirmation email... ({attempt+1}/{max_attempts})")
|
|
221
|
+
time.sleep(10)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
tinypng_email = WebDriverWait(driver, 5).until(
|
|
225
|
+
EC.presence_of_element_located((By.XPATH, '//*[@id="inbox"]/tr[2]'))
|
|
226
|
+
)
|
|
227
|
+
tinypng_email.click()
|
|
228
|
+
time.sleep(3)
|
|
229
|
+
|
|
230
|
+
new_window = driver.window_handles[-1]
|
|
231
|
+
driver.switch_to.window(new_window)
|
|
232
|
+
|
|
233
|
+
dashboard_link = WebDriverWait(driver, 5).until(
|
|
234
|
+
EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), 'Visit your dashboard') or contains(@href, 'dashboard')]"))
|
|
235
|
+
)
|
|
236
|
+
dashboard_url = dashboard_link.get_attribute("href")
|
|
237
|
+
driver.execute_script(f"window.open('{dashboard_url}', '_blank');")
|
|
238
|
+
time.sleep(3)
|
|
239
|
+
|
|
240
|
+
driver.switch_to.window(driver.window_handles[-1])
|
|
241
|
+
time.sleep(5)
|
|
242
|
+
|
|
243
|
+
api_key_element = WebDriverWait(driver, 10).until(
|
|
244
|
+
EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div/main/section/div/div/section/div[2]/div[1]/div/div[3]/strong/p"))
|
|
245
|
+
)
|
|
246
|
+
key_text = api_key_element.text.strip()
|
|
247
|
+
|
|
248
|
+
if key_text and len(key_text) > 20:
|
|
249
|
+
print(f"Successfully obtained new API key")
|
|
250
|
+
return key_text
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"Attempt {attempt+1} failed: {str(e)}")
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
print("Timeout waiting for API key")
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
print(f"Failed to request new API key: {str(e)}")
|
|
261
|
+
return None
|
|
262
|
+
finally:
|
|
263
|
+
if driver:
|
|
264
|
+
driver.quit()
|
|
265
|
+
|
|
266
|
+
def get_new_api_key(self) -> Optional[str]:
|
|
267
|
+
"""Get new API key and save it."""
|
|
268
|
+
email, driver = self._get_temp_email()
|
|
269
|
+
if not email or not driver:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
new_key = self._request_new_api_key(email, driver)
|
|
274
|
+
if new_key:
|
|
275
|
+
# Add new key to saved keys
|
|
276
|
+
api_keys = self._load_api_keys()
|
|
277
|
+
if new_key not in api_keys:
|
|
278
|
+
api_keys.append(new_key)
|
|
279
|
+
self._save_api_keys(api_keys)
|
|
280
|
+
return new_key
|
|
281
|
+
return None
|
|
282
|
+
finally:
|
|
283
|
+
if driver:
|
|
284
|
+
driver.quit()
|
|
285
|
+
|
|
286
|
+
def check_and_update_api_key(self) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Check current API key and update if necessary.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
bool: True if a valid API key is available, False otherwise.
|
|
292
|
+
"""
|
|
293
|
+
if not self.current_key:
|
|
294
|
+
self.current_key = self._get_valid_api_key()
|
|
295
|
+
if not self.current_key:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
tinify.key = self.current_key
|
|
299
|
+
|
|
300
|
+
# Get API key usage
|
|
301
|
+
result = self._get_compression_count()
|
|
302
|
+
|
|
303
|
+
if result['success']:
|
|
304
|
+
if result['remaining'] <= 50: # If less than 50 compressions remaining
|
|
305
|
+
# Try to get a new key
|
|
306
|
+
new_key = self._get_valid_api_key()
|
|
307
|
+
if new_key:
|
|
308
|
+
self.current_key = new_key
|
|
309
|
+
tinify.key = new_key
|
|
310
|
+
return True
|
|
311
|
+
else:
|
|
312
|
+
# Current key is invalid, try to get a new one
|
|
313
|
+
new_key = self._get_valid_api_key()
|
|
314
|
+
if new_key:
|
|
315
|
+
self.current_key = new_key
|
|
316
|
+
tinify.key = new_key
|
|
317
|
+
return True
|
|
318
|
+
return False
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for TinyComp
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import argparse
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from .compressor import TinyCompressor
|
|
9
|
+
from .api_manager import APIKeyManager
|
|
10
|
+
|
|
11
|
+
def parse_args():
|
|
12
|
+
"""Parse command line arguments."""
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="Compress images using TinyPNG API",
|
|
15
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
|
19
|
+
|
|
20
|
+
# Compress command
|
|
21
|
+
compress_parser = subparsers.add_parser('compress', help='Compress images')
|
|
22
|
+
compress_parser.add_argument(
|
|
23
|
+
"--source",
|
|
24
|
+
"-s",
|
|
25
|
+
required=True,
|
|
26
|
+
help="Source directory or file path"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
compress_parser.add_argument(
|
|
30
|
+
"--target",
|
|
31
|
+
"-t",
|
|
32
|
+
required=True,
|
|
33
|
+
help="Target directory or file path"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
compress_parser.add_argument(
|
|
37
|
+
"--api-key",
|
|
38
|
+
"-k",
|
|
39
|
+
help="TinyPNG API key (optional)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
compress_parser.add_argument(
|
|
43
|
+
"--threads",
|
|
44
|
+
"-n",
|
|
45
|
+
type=int,
|
|
46
|
+
default=4,
|
|
47
|
+
help="Number of compression threads"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
compress_parser.add_argument(
|
|
51
|
+
"--skip-existing",
|
|
52
|
+
"-x",
|
|
53
|
+
action="store_true",
|
|
54
|
+
default=True,
|
|
55
|
+
help="Skip existing files in target directory"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
compress_parser.add_argument(
|
|
59
|
+
"--auto-update-key",
|
|
60
|
+
"-a",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Automatically get new API keys when current one runs out"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Update API key command
|
|
66
|
+
update_parser = subparsers.add_parser('update-key', help='Update TinyPNG API key')
|
|
67
|
+
update_parser.add_argument(
|
|
68
|
+
"--force",
|
|
69
|
+
"-f",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Force update even if current key is still valid"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return parser.parse_args()
|
|
75
|
+
|
|
76
|
+
def compress_images(args):
|
|
77
|
+
"""Handle image compression command."""
|
|
78
|
+
# Initialize compressor
|
|
79
|
+
compressor = TinyCompressor(
|
|
80
|
+
api_key=args.api_key,
|
|
81
|
+
max_workers=args.threads,
|
|
82
|
+
auto_update_key=args.auto_update_key
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Check if source is a directory or file
|
|
86
|
+
if os.path.isdir(args.source):
|
|
87
|
+
# Compress directory
|
|
88
|
+
print(f"Compressing directory: {args.source}")
|
|
89
|
+
stats = compressor.compress_directory(
|
|
90
|
+
args.source,
|
|
91
|
+
args.target,
|
|
92
|
+
skip_existing=args.skip_existing
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Print results
|
|
96
|
+
print("\nCompression completed!")
|
|
97
|
+
print(f"Total files: {stats['total']}")
|
|
98
|
+
print(f"Successfully compressed: {stats['success']}")
|
|
99
|
+
print(f"Failed: {stats['failed']}")
|
|
100
|
+
print(f"Success rate: {stats['percent']:.1f}%")
|
|
101
|
+
if stats.get('keys_used'):
|
|
102
|
+
print(f"API keys used: {stats['keys_used']}")
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
# Compress single file
|
|
106
|
+
print(f"Compressing file: {args.source}")
|
|
107
|
+
result = compressor.compress_image(args.source, args.target)
|
|
108
|
+
|
|
109
|
+
if result['status'] == 'success':
|
|
110
|
+
print("File compressed successfully!")
|
|
111
|
+
else:
|
|
112
|
+
print(f"Compression failed: {result['message']}")
|
|
113
|
+
|
|
114
|
+
def update_api_key(args):
|
|
115
|
+
"""Handle API key update command."""
|
|
116
|
+
api_manager = APIKeyManager()
|
|
117
|
+
|
|
118
|
+
if not args.force:
|
|
119
|
+
# Check if current key is still valid
|
|
120
|
+
if api_manager.current_key:
|
|
121
|
+
result = api_manager._get_compression_count()
|
|
122
|
+
if result['success'] and result['remaining'] > 50:
|
|
123
|
+
print(f"Current API key is still valid (remaining: {result['remaining']} compressions)")
|
|
124
|
+
print("Use --force to update anyway")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
print("Requesting new API key...")
|
|
128
|
+
new_key = api_manager.get_new_api_key()
|
|
129
|
+
|
|
130
|
+
if new_key:
|
|
131
|
+
print("Successfully obtained and saved new API key")
|
|
132
|
+
print(f"Remaining compressions: {api_manager._get_compression_count(new_key)['remaining']}")
|
|
133
|
+
else:
|
|
134
|
+
print("Failed to obtain new API key")
|
|
135
|
+
|
|
136
|
+
def main():
|
|
137
|
+
"""Main entry point for the CLI."""
|
|
138
|
+
args = parse_args()
|
|
139
|
+
|
|
140
|
+
if args.command == 'compress':
|
|
141
|
+
compress_images(args)
|
|
142
|
+
elif args.command == 'update-key':
|
|
143
|
+
update_api_key(args)
|
|
144
|
+
else:
|
|
145
|
+
print("Please specify a command: compress or update-key")
|
|
146
|
+
print("Use -h or --help for more information")
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TinyCompressor class for handling image compression using TinyPNG API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from typing import List, Dict, Optional, Union
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
import tinify
|
|
12
|
+
from .api_manager import APIKeyManager
|
|
13
|
+
|
|
14
|
+
class TinyCompressor:
|
|
15
|
+
"""
|
|
16
|
+
A class for compressing images using the TinyPNG API.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
SUPPORTED_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.svg', '.gif']
|
|
20
|
+
|
|
21
|
+
def __init__(self, api_key: Optional[str] = None, max_workers: int = 4, auto_update_key: bool = False):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the TinyCompressor.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key (str, optional): TinyPNG API key. If not provided, will try to get from environment
|
|
27
|
+
or manage automatically.
|
|
28
|
+
max_workers (int): Maximum number of concurrent compression threads.
|
|
29
|
+
auto_update_key (bool): Whether to automatically get new API keys when current one runs out.
|
|
30
|
+
"""
|
|
31
|
+
self.max_workers = max_workers
|
|
32
|
+
self.auto_update_key = auto_update_key
|
|
33
|
+
self.api_manager = APIKeyManager(api_key)
|
|
34
|
+
self.keys_used = set() # Track used API keys
|
|
35
|
+
|
|
36
|
+
def compress_image(self, source_path: str, target_path: str) -> Dict[str, str]:
|
|
37
|
+
"""
|
|
38
|
+
Compress a single image.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
source_path (str): Path to the source image.
|
|
42
|
+
target_path (str): Path where the compressed image will be saved.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Compression result containing status and message.
|
|
46
|
+
"""
|
|
47
|
+
# Ensure the API key is valid
|
|
48
|
+
if not self.api_manager.check_and_update_api_key():
|
|
49
|
+
if self.auto_update_key:
|
|
50
|
+
print("Current API key is invalid or depleted, requesting new key...")
|
|
51
|
+
new_key = self.api_manager.get_new_api_key()
|
|
52
|
+
if not new_key:
|
|
53
|
+
return {
|
|
54
|
+
'status': 'failed',
|
|
55
|
+
'message': 'Failed to obtain new API key'
|
|
56
|
+
}
|
|
57
|
+
self.api_manager.current_key = new_key
|
|
58
|
+
tinify.key = new_key
|
|
59
|
+
else:
|
|
60
|
+
return {
|
|
61
|
+
'status': 'failed',
|
|
62
|
+
'message': 'No valid API key available. Use --auto-update-key to automatically get new keys.'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Add current key to used keys set
|
|
66
|
+
if self.api_manager.current_key:
|
|
67
|
+
self.keys_used.add(self.api_manager.current_key)
|
|
68
|
+
|
|
69
|
+
# Create target directory if it doesn't exist
|
|
70
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Compress the image
|
|
74
|
+
source = tinify.from_file(source_path)
|
|
75
|
+
source.to_file(target_path)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
'status': 'success',
|
|
79
|
+
'message': 'Image compressed successfully'
|
|
80
|
+
}
|
|
81
|
+
except tinify.AccountError as e:
|
|
82
|
+
if self.auto_update_key:
|
|
83
|
+
print("API key limit reached, requesting new key...")
|
|
84
|
+
new_key = self.api_manager.get_new_api_key()
|
|
85
|
+
if new_key:
|
|
86
|
+
self.api_manager.current_key = new_key
|
|
87
|
+
tinify.key = new_key
|
|
88
|
+
# Retry compression with new key
|
|
89
|
+
return self.compress_image(source_path, target_path)
|
|
90
|
+
return {
|
|
91
|
+
'status': 'failed',
|
|
92
|
+
'message': f'API key error: {str(e)}. Use --auto-update-key to automatically get new keys.'
|
|
93
|
+
}
|
|
94
|
+
except tinify.Error as e:
|
|
95
|
+
return {
|
|
96
|
+
'status': 'failed',
|
|
97
|
+
'message': f'Compression failed: {str(e)}'
|
|
98
|
+
}
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return {
|
|
101
|
+
'status': 'failed',
|
|
102
|
+
'message': f'Processing error: {str(e)}'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def compress_directory(self, source_dir: str, target_dir: str,
|
|
106
|
+
skip_existing: bool = True) -> Dict[str, Union[int, float]]:
|
|
107
|
+
"""
|
|
108
|
+
Compress all supported images in a directory.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
source_dir (str): Source directory containing images to compress.
|
|
112
|
+
target_dir (str): Target directory for compressed images.
|
|
113
|
+
skip_existing (bool): Whether to skip files that already exist in target directory.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
dict: Compression statistics.
|
|
117
|
+
"""
|
|
118
|
+
# Get list of files to process
|
|
119
|
+
image_files = self._get_image_files(source_dir)
|
|
120
|
+
|
|
121
|
+
if skip_existing:
|
|
122
|
+
image_files = [f for f in image_files if self._should_compress(f, source_dir, target_dir)]
|
|
123
|
+
|
|
124
|
+
total_files = len(image_files)
|
|
125
|
+
if total_files == 0:
|
|
126
|
+
return {
|
|
127
|
+
'total': 0,
|
|
128
|
+
'processed': 0,
|
|
129
|
+
'success': 0,
|
|
130
|
+
'failed': 0,
|
|
131
|
+
'percent': 100.0,
|
|
132
|
+
'keys_used': 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
stats = {
|
|
136
|
+
'total': total_files,
|
|
137
|
+
'processed': 0,
|
|
138
|
+
'success': 0,
|
|
139
|
+
'failed': 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Process files with progress bar
|
|
143
|
+
with tqdm(total=total_files, unit="file", desc="Compressing images") as progress_bar:
|
|
144
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
145
|
+
future_to_file = {
|
|
146
|
+
executor.submit(
|
|
147
|
+
self._process_single_file,
|
|
148
|
+
file_path,
|
|
149
|
+
source_dir,
|
|
150
|
+
target_dir
|
|
151
|
+
): file_path for file_path in image_files
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for future in as_completed(future_to_file):
|
|
155
|
+
result = future.result()
|
|
156
|
+
stats['processed'] += 1
|
|
157
|
+
|
|
158
|
+
if result['status'] == 'success':
|
|
159
|
+
stats['success'] += 1
|
|
160
|
+
else:
|
|
161
|
+
stats['failed'] += 1
|
|
162
|
+
print(f"\nFailed to compress {future_to_file[future]}: {result['message']}")
|
|
163
|
+
|
|
164
|
+
progress_bar.update(1)
|
|
165
|
+
|
|
166
|
+
stats['percent'] = (stats['success'] / stats['total']) * 100
|
|
167
|
+
stats['keys_used'] = len(self.keys_used)
|
|
168
|
+
return stats
|
|
169
|
+
|
|
170
|
+
def _get_image_files(self, directory: str) -> List[str]:
|
|
171
|
+
"""Get all supported image files in the directory."""
|
|
172
|
+
image_files = []
|
|
173
|
+
|
|
174
|
+
for root, _, files in os.walk(directory):
|
|
175
|
+
for name in files:
|
|
176
|
+
file_path = os.path.join(root, name)
|
|
177
|
+
_, file_ext = os.path.splitext(name)
|
|
178
|
+
|
|
179
|
+
if file_ext.lower() in self.SUPPORTED_EXTENSIONS:
|
|
180
|
+
image_files.append(file_path)
|
|
181
|
+
|
|
182
|
+
return image_files
|
|
183
|
+
|
|
184
|
+
def _should_compress(self, file_path: str, source_dir: str, target_dir: str) -> bool:
|
|
185
|
+
"""Check if the file should be compressed (skip if target exists)."""
|
|
186
|
+
relative_path = os.path.relpath(file_path, source_dir)
|
|
187
|
+
target_path = os.path.join(target_dir, relative_path)
|
|
188
|
+
return not os.path.exists(target_path)
|
|
189
|
+
|
|
190
|
+
def _process_single_file(self, file_path: str, source_dir: str, target_dir: str) -> Dict[str, str]:
|
|
191
|
+
"""Process a single file for compression."""
|
|
192
|
+
relative_path = os.path.relpath(file_path, source_dir)
|
|
193
|
+
target_path = os.path.join(target_dir, relative_path)
|
|
194
|
+
|
|
195
|
+
return self.compress_image(file_path, target_path)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the TinyCompressor class
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
from tinycomp import TinyCompressor
|
|
9
|
+
|
|
10
|
+
class TestTinyCompressor(unittest.TestCase):
|
|
11
|
+
"""Test cases for TinyCompressor class."""
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
"""Set up test fixtures."""
|
|
15
|
+
self.compressor = TinyCompressor(api_key="YYKcY8Z99BDJnjvZhgRWJQqqvNFKhlcL")
|
|
16
|
+
self.test_image = "test.png"
|
|
17
|
+
self.test_output = "output.png"
|
|
18
|
+
|
|
19
|
+
@patch('tinycomp.compressor.tinify.from_file')
|
|
20
|
+
def test_compress_image_success(self, mock_from_file):
|
|
21
|
+
"""Test successful image compression."""
|
|
22
|
+
# Mock the tinify.from_file() call
|
|
23
|
+
mock_source = MagicMock()
|
|
24
|
+
mock_from_file.return_value = mock_source
|
|
25
|
+
|
|
26
|
+
# Mock successful compression
|
|
27
|
+
result = self.compressor.compress_image(self.test_image, self.test_output)
|
|
28
|
+
|
|
29
|
+
# 验证返回值
|
|
30
|
+
self.assertEqual(result['status'], 'success')
|
|
31
|
+
self.assertEqual(result['message'], 'Image compressed successfully')
|
|
32
|
+
|
|
33
|
+
# 验证调用
|
|
34
|
+
mock_from_file.assert_called_once_with(self.test_image)
|
|
35
|
+
mock_source.to_file.assert_called_once_with(self.test_output)
|
|
36
|
+
|
|
37
|
+
def test_get_image_files(self):
|
|
38
|
+
"""Test getting supported image files from directory."""
|
|
39
|
+
# Create temporary test directory with some files
|
|
40
|
+
test_dir = "test_dir"
|
|
41
|
+
os.makedirs(test_dir, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
# Create test files
|
|
44
|
+
test_files = [
|
|
45
|
+
"test1.png",
|
|
46
|
+
"test2.jpg",
|
|
47
|
+
"test3.txt", # Unsupported extension
|
|
48
|
+
"test4.jpeg"
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for file in test_files:
|
|
52
|
+
with open(os.path.join(test_dir, file), 'w') as f:
|
|
53
|
+
f.write("test")
|
|
54
|
+
|
|
55
|
+
# Get image files
|
|
56
|
+
image_files = self.compressor._get_image_files(test_dir)
|
|
57
|
+
|
|
58
|
+
# Verify results
|
|
59
|
+
self.assertEqual(len(image_files), 3) # Should find 3 supported images
|
|
60
|
+
|
|
61
|
+
# Clean up
|
|
62
|
+
for file in test_files:
|
|
63
|
+
os.remove(os.path.join(test_dir, file))
|
|
64
|
+
os.rmdir(test_dir)
|
|
65
|
+
|
|
66
|
+
def test_should_compress(self):
|
|
67
|
+
"""Test should_compress method."""
|
|
68
|
+
# Create test directories
|
|
69
|
+
source_dir = "test_source"
|
|
70
|
+
target_dir = "test_target"
|
|
71
|
+
os.makedirs(source_dir, exist_ok=True)
|
|
72
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# Create test file
|
|
75
|
+
test_file = os.path.join(source_dir, "test.png")
|
|
76
|
+
with open(test_file, 'w') as f:
|
|
77
|
+
f.write("test")
|
|
78
|
+
|
|
79
|
+
# Test when target doesn't exist
|
|
80
|
+
self.assertTrue(
|
|
81
|
+
self.compressor._should_compress(test_file, source_dir, target_dir)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Create target file
|
|
85
|
+
target_file = os.path.join(target_dir, "test.png")
|
|
86
|
+
with open(target_file, 'w') as f:
|
|
87
|
+
f.write("test")
|
|
88
|
+
|
|
89
|
+
# Test when target exists
|
|
90
|
+
self.assertFalse(
|
|
91
|
+
self.compressor._should_compress(test_file, source_dir, target_dir)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Clean up
|
|
95
|
+
os.remove(test_file)
|
|
96
|
+
os.remove(target_file)
|
|
97
|
+
os.rmdir(source_dir)
|
|
98
|
+
os.rmdir(target_dir)
|
|
99
|
+
|
|
100
|
+
if __name__ == '__main__':
|
|
101
|
+
unittest.main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tinycomp-amadeus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package for compressing images using TinyPNG API
|
|
5
|
+
Home-page: https://github.com/Amadeus9029/tinycomp
|
|
6
|
+
Author: Amadeus9029
|
|
7
|
+
Author-email: 965720890@qq.com
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: tinify
|
|
21
|
+
Requires-Dist: requests
|
|
22
|
+
Requires-Dist: tqdm
|
|
23
|
+
Requires-Dist: beautifulsoup4
|
|
24
|
+
Requires-Dist: selenium
|
|
25
|
+
Requires-Dist: fake-useragent
|
|
26
|
+
|
|
27
|
+
# TinyComp
|
|
28
|
+
|
|
29
|
+
TinyComp is a Python package that helps you compress images using the TinyPNG API. It provides both a command-line interface and a Python API for easy integration into your projects.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Automatic API key management and rotation
|
|
34
|
+
- Automatic API key acquisition when needed
|
|
35
|
+
- Batch image compression
|
|
36
|
+
- Support for multiple image formats (PNG, JPG, JPEG, SVG, GIF)
|
|
37
|
+
- Progress bar for tracking compression status
|
|
38
|
+
- Multi-threaded compression for better performance
|
|
39
|
+
- Automatic handling of API usage limits
|
|
40
|
+
- Automatic API key update through web automation
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install tinycomp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Command Line Interface
|
|
51
|
+
|
|
52
|
+
#### Compressing Images
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Basic compression
|
|
56
|
+
tinycomp compress --source ./images --target ./compressed
|
|
57
|
+
|
|
58
|
+
# With custom API key
|
|
59
|
+
tinycomp compress --source ./images --target ./compressed --api-key YOUR_API_KEY
|
|
60
|
+
|
|
61
|
+
# Set number of threads
|
|
62
|
+
tinycomp compress --source ./images --target ./compressed --threads 4
|
|
63
|
+
|
|
64
|
+
# Enable automatic API key updates when needed
|
|
65
|
+
tinycomp compress --source ./images --target ./compressed --auto-update-key
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Managing API Keys
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Update API key (checks current key first)
|
|
72
|
+
tinycomp update-key
|
|
73
|
+
|
|
74
|
+
# Force update API key even if current one is valid
|
|
75
|
+
tinycomp update-key --force
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Python API
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from tinycomp import TinyCompressor
|
|
82
|
+
|
|
83
|
+
# Initialize compressor
|
|
84
|
+
compressor = TinyCompressor(api_key="YOUR_API_KEY") # API key is optional
|
|
85
|
+
|
|
86
|
+
# Enable automatic API key updates
|
|
87
|
+
compressor = TinyCompressor(auto_update_key=True)
|
|
88
|
+
|
|
89
|
+
# Compress a single image
|
|
90
|
+
compressor.compress_image("input.png", "output.png")
|
|
91
|
+
|
|
92
|
+
# Compress multiple images
|
|
93
|
+
compressor.compress_directory("./images", "./compressed")
|
|
94
|
+
|
|
95
|
+
# Update API key programmatically
|
|
96
|
+
from tinycomp.api_manager import APIKeyManager
|
|
97
|
+
api_manager = APIKeyManager()
|
|
98
|
+
new_key = api_manager.get_new_api_key()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
You can configure TinyComp using environment variables:
|
|
104
|
+
|
|
105
|
+
- `TINYCOMP_API_KEY`: Your TinyPNG API key
|
|
106
|
+
- `TINYCOMP_MAX_THREADS`: Maximum number of compression threads (default: 4)
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- Python 3.6 or higher
|
|
111
|
+
- Chrome/Chromium browser (for automatic API key updates)
|
|
112
|
+
- ChromeDriver matching your Chrome version
|
|
113
|
+
|
|
114
|
+
## API Key Management
|
|
115
|
+
|
|
116
|
+
TinyComp includes an automatic API key management system that:
|
|
117
|
+
|
|
118
|
+
1. Automatically rotates between multiple API keys
|
|
119
|
+
2. Monitors remaining API usage
|
|
120
|
+
3. Can automatically obtain new API keys when needed
|
|
121
|
+
4. Saves API keys for future use
|
|
122
|
+
|
|
123
|
+
The package offers two modes for handling API key depletion:
|
|
124
|
+
|
|
125
|
+
1. Default mode: Stops compression and notifies when the API key runs out
|
|
126
|
+
2. Auto-update mode: Automatically obtains new API keys when needed (use `--auto-update-key` flag)
|
|
127
|
+
|
|
128
|
+
The automatic key update feature requires:
|
|
129
|
+
- Chrome/Chromium browser installed
|
|
130
|
+
- ChromeDriver in your system PATH or in the working directory
|
|
131
|
+
- Internet connection for accessing TinyPNG website
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
tinycomp/__init__.py
|
|
5
|
+
tinycomp/api_manager.py
|
|
6
|
+
tinycomp/cli.py
|
|
7
|
+
tinycomp/compressor.py
|
|
8
|
+
tinycomp/tests/__init__.py
|
|
9
|
+
tinycomp/tests/test_compressor.py
|
|
10
|
+
tinycomp_amadeus.egg-info/PKG-INFO
|
|
11
|
+
tinycomp_amadeus.egg-info/SOURCES.txt
|
|
12
|
+
tinycomp_amadeus.egg-info/dependency_links.txt
|
|
13
|
+
tinycomp_amadeus.egg-info/entry_points.txt
|
|
14
|
+
tinycomp_amadeus.egg-info/requires.txt
|
|
15
|
+
tinycomp_amadeus.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tinycomp
|