├── .gitignore ├── LICENSE ├── README.md ├── ScreenShot └── 3.0.png ├── pyproject.toml ├── requirements.txt ├── src └── pixivd │ ├── AESCipher.py │ ├── api.py │ ├── i18n.py │ ├── locale │ ├── en_US │ │ └── LC_MESSAGES │ │ │ ├── messages.mo │ │ │ └── messages.po │ └── zh_CN │ │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po │ ├── model.py │ └── pixivd.py └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | illustrations/ 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 3 | 4 | *.iml 5 | 6 | ## Directory-based project format: 7 | .idea/ 8 | # if you remove the above rule, at least ignore the following: 9 | 10 | # User-specific stuff: 11 | # .idea/workspace.xml 12 | # .idea/tasks.xml 13 | # .idea/dictionaries 14 | 15 | # Sensitive or high-churn files: 16 | # .idea/dataSources.ids 17 | # .idea/dataSources.xml 18 | # .idea/sqlDataSources.xml 19 | # .idea/dynamic.xml 20 | # .idea/uiDesigner.xml 21 | 22 | # Gradle: 23 | # .idea/gradle.xml 24 | # .idea/libraries 25 | 26 | # Mongo Explorer plugin: 27 | # .idea/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.ipr 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | 49 | 50 | # Byte-compiled / optimized / DLL files 51 | __pycache__/ 52 | *.py[cod] 53 | 54 | # C extensions 55 | *.so 56 | 57 | # Distribution / packaging 58 | .Python 59 | env/ 60 | build/ 61 | develop-eggs/ 62 | dist/ 63 | downloads/ 64 | eggs/ 65 | lib/ 66 | lib64/ 67 | parts/ 68 | sdist/ 69 | var/ 70 | *.egg-info/ 71 | .installed.cfg 72 | *.egg 73 | 74 | # PyInstaller 75 | # Usually these files are written by a python script from a template 76 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 77 | *.manifest 78 | *.spec 79 | 80 | # Installer logs 81 | pip-log.txt 82 | pip-delete-this-directory.txt 83 | 84 | # Unit test / coverage reports 85 | htmlcov/ 86 | .tox/ 87 | .coverage 88 | .cache 89 | nosetests.xml 90 | coverage.xml 91 | 92 | # Translations 93 | *.pot 94 | 95 | # Django stuff: 96 | *.log 97 | 98 | # Sphinx documentation 99 | docs/_build/ 100 | 101 | # PyBuilder 102 | target/ 103 | 104 | 105 | # Windows image file caches 106 | Thumbs.db 107 | ehthumbs.db 108 | 109 | # Folder config file 110 | Desktop.ini 111 | 112 | # Recycle Bin used on file shares 113 | $RECYCLE.BIN/ 114 | 115 | # Windows Installer files 116 | *.cab 117 | *.msi 118 | *.msm 119 | *.msp 120 | 121 | # Windows shortcuts 122 | *.lnk 123 | 124 | # ========================= 125 | # Operating System Files 126 | # ========================= 127 | 128 | # OSX 129 | # ========================= 130 | 131 | .DS_Store 132 | .AppleDouble 133 | .LSOverride 134 | 135 | # Thumbnails 136 | ._* 137 | 138 | # Files that might appear on external disk 139 | .Spotlight-V100 140 | .Trashes 141 | 142 | # Directories potentially created on remote AFP share 143 | .AppleDB 144 | .AppleDesktop 145 | Network Trash Folder 146 | Temporary Items 147 | .apdisk 148 | 149 | test.py 150 | session 151 | illustrations 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2020 KK bebound@gmail.com 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PixivD 2 | 3 | [![PyPI version](https://badge.fury.io/py/pixivd.svg)](https://badge.fury.io/py/pixivd) 4 | 5 | A simple tool to download illustrations from Pixiv. 6 | 7 | Download illustrations by **uers\_id**, **daily ranking** or **history ranking**. 8 | 9 | ## Features 10 | - [x] Keep login sessions 11 | - [x] Local storage 12 | - [x] Secure storage (not memory safe) 13 | - [x] Update downloaded artists 14 | - [x] Refresh downloaded artists 15 | - [x] Mutil-Language 16 | - [x] Command-line interface 17 | 18 | 19 | ## Installation 20 | `pip install pixivd` 21 | 22 | ## Usage 23 | ``` 24 | pixivd 25 | pixivd ... 26 | pixivd -r [-d | --date=] 27 | pixivd -u 28 | 29 | Arguments: 30 | user_ids 31 | 32 | Options: 33 | -r Download by ranking 34 | -d --date Target date 35 | -u Update exist folder 36 | -h --help Show this screen 37 | -v --version Show version 38 | 39 | Examples: 40 | pixivd 7210261 1980643 41 | pixivd -r -d 2016-09-24 42 | ``` 43 | 44 | The illusts will be downloaded to `illustrations` folder. 45 | 46 | ## Screenshot 47 | 48 | 49 | ![img](https://raw.github.com/bebound/Pixiv/master/ScreenShot/3.0.png) 50 | 51 | 52 | ## Credits 53 | - [Pixiv-API](https://github.com/twopon/Pixiv-API) 54 | - [PixivPy](https://github.com/upbit/pixivpy) 55 | - [pixiv api](https://danbooru.donmai.us/wiki_pages/58938) 56 | - [Pixiv OAuth Flow](https://gist.github.com/ZipFile/c9ebedb224406f4f11845ab700124362) 57 | 58 | -------------------------------------------------------------------------------- /ScreenShot/3.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bebound/pixivd/64c2da889caf39b03b524d179e24691363696199/ScreenShot/3.0.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pixivd" 7 | dynamic = ['version'] 8 | description = "A simple tool to download illustrations from Pixiv." 9 | readme = "README.md" 10 | license = "MIT" 11 | license-files = ["LICENSE"] 12 | authors = [ 13 | {name = "KK", email = "bebound@gmail.com"} 14 | ] 15 | requires-python = ">=3.9" 16 | classifiers = [ 17 | "Programming Language :: Python :: 3", 18 | "Operating System :: OS Independent" 19 | ] 20 | dependencies = [ 21 | "requests>=2.4.3", 22 | "pyaes>=1.6.1", 23 | "docopt>=0.6.2", 24 | "tqdm", 25 | "PixivPy>=3.6.0" 26 | ] 27 | 28 | [project.urls] 29 | homepage = "https://github.com/bebound/pixiv" 30 | Source = "https://github.com/bebound/pixiv" 31 | 32 | [tool.setuptools.packages.find] 33 | where = ["src"] 34 | 35 | [tool.setuptools.package-data] 36 | pixivd = ['src/pixivd/local/*'] 37 | 38 | [dependency-groups] 39 | dev = [ 40 | "ruff>=0.11.8", 41 | ] 42 | 43 | [tool.setuptools] 44 | include-package-data = true 45 | 46 | [tool.setuptools.dynamic] 47 | version = {attr = "pixivd.pixivd.__version__"} 48 | 49 | [project.scripts] 50 | pixivd = "pixivd.pixivd:main" 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.4.3 2 | pyaes>=1.6.1 3 | docopt>=0.6.2 4 | tqdm 5 | PixivPy>=3.6.0 6 | -------------------------------------------------------------------------------- /src/pixivd/AESCipher.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import uuid 3 | 4 | import pyaes 5 | 6 | class AESCipher: 7 | def __init__(self, key=uuid.UUID(int=uuid.getnode()).hex): 8 | self.key = key.encode('utf-8') 9 | 10 | def encrypt(self, raw): 11 | cipher = pyaes.AESModeOfOperationCTR(self.key) 12 | return base64.b64encode(cipher.encrypt(raw)) 13 | 14 | def decrypt(self, enc): 15 | enc = base64.b64decode(enc) 16 | cipher = pyaes.AESModeOfOperationCTR(self.key) 17 | return cipher.decrypt(enc).decode('utf-8') 18 | 19 | -------------------------------------------------------------------------------- /src/pixivd/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | from base64 import urlsafe_b64encode 4 | from hashlib import sha256 5 | from pathlib import Path 6 | from secrets import token_urlsafe 7 | from urllib.parse import urlencode 8 | 9 | import requests 10 | from pixivpy3 import * 11 | 12 | from .AESCipher import AESCipher 13 | from .i18n import i18n as _ 14 | 15 | 16 | class Pixiv_Get_Error(Exception): 17 | def __init__(self, url, Err=None): 18 | self.url = url 19 | self.error = Err 20 | 21 | def __str__(self): 22 | return 'Failed to get data: ' + self.url 23 | 24 | 25 | class PixivApi: 26 | """ 27 | Attribution: 28 | access_token 29 | user_id: str, login user id 30 | User_Agent: str, the version of pixiv app 31 | """ 32 | aapi = AppPixivAPI() 33 | user_agent = 'PixivIOSApp/6.4.0' 34 | hash_secret = '28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c' 35 | session = '' 36 | user_id = '' 37 | image_sizes = ','.join(['px_128x128', 'px_480mw', 'small', 'medium', 'large']) 38 | profile_image_sizes = ','.join(['px_170x170', 'px_50x50']) 39 | timeout = 20 40 | access_token = '' 41 | refresh_token = '' 42 | client_id = 'MOBrBDS8blbauoSck0ZfDbtuzpyT' 43 | client_secret = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj' 44 | auth_token_url = 'https://oauth.secure.pixiv.net/auth/token' 45 | session_path = Path(__file__).parent / 'data' / 'session' 46 | 47 | def __init__(self): 48 | self.ensure_session_dir() 49 | self.login_required() 50 | 51 | def ensure_session_dir(self): 52 | if not Path(self.session_path).parent.exists(): 53 | self.session_path.parent.mkdir() 54 | 55 | def load_session(self): 56 | cipher = AESCipher() 57 | with open(self.session_path, 'rb') as f: 58 | enc = f.read() 59 | try: 60 | plain = cipher.decrypt(enc) 61 | loaded_session = json.loads(str(plain)) 62 | self.access_token = loaded_session['access_token'] 63 | self.refresh_token = loaded_session['refresh_token'] 64 | return True 65 | except: 66 | print("error when load session, please delete session file and try again.") 67 | 68 | def save_session(self): 69 | data = { 70 | 'access_token': self.access_token, 71 | 'refresh_token': self.refresh_token 72 | } 73 | cipher = AESCipher() 74 | enc = cipher.encrypt(json.dumps(data)) 75 | with open(self.session_path, 'wb') as f: 76 | f.write(enc) 77 | 78 | def parse_token(self, data): 79 | return data["access_token"], data["refresh_token"] 80 | 81 | def login(self): 82 | """ 83 | logging to Pixiv 84 | doc: https://gist.github.com/ZipFile/c9ebedb224406f4f11845ab700124362 85 | 86 | Return: 87 | a requests session object 88 | 89 | """ 90 | REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback" 91 | LOGIN_URL = "https://app-api.pixiv.net/web/v1/login" 92 | 93 | def s256(data): 94 | """S256 transformation method.""" 95 | return urlsafe_b64encode(sha256(data).digest()).rstrip(b"=").decode("ascii") 96 | 97 | def oauth_pkce(transform): 98 | """Proof Key for Code Exchange by OAuth Public Clients (RFC7636).""" 99 | 100 | code_verifier = token_urlsafe(32) 101 | code_challenge = transform(code_verifier.encode("ascii")) 102 | 103 | return code_verifier, code_challenge 104 | 105 | code_verifier, code_challenge = oauth_pkce(s256) 106 | login_params = { 107 | "code_challenge": code_challenge, 108 | "code_challenge_method": "S256", 109 | "client": "pixiv-android", 110 | } 111 | 112 | print( 113 | f"Please open {LOGIN_URL}?{urlencode(login_params)}\n, and following steps in https://gist.github.com/ZipFile/c9ebedb224406f4f11845ab700124362 to get code") 114 | 115 | try: 116 | code = input("Please input code: ").strip() 117 | except (EOFError, KeyboardInterrupt): 118 | return 119 | 120 | r = requests.post( 121 | self.auth_token_url, 122 | data={ 123 | "client_id": self.client_id, 124 | "client_secret": self.client_secret, 125 | "code": code, 126 | "code_verifier": code_verifier, 127 | "grant_type": "authorization_code", 128 | "include_policy": "true", 129 | "redirect_uri": REDIRECT_URI, 130 | }, 131 | headers={"User-Agent": self.user_agent}, 132 | ) 133 | self.access_token, self.refresh_token = self.parse_token(r.json()) 134 | print(f'refresh token: {self.refresh_token}') 135 | self.refresh() 136 | 137 | def refresh(self): 138 | """Use refresh token to get new access token""" 139 | # self.aapi.auth(refresh_token='cKwCinT4vVbRy4kOitoQTA7Q1lhjBr69tGy44fI-3Ho') 140 | self.aapi.auth(refresh_token=self.refresh_token) 141 | self.access_token, self.refresh_token = self.aapi.access_token, self.aapi.refresh_token 142 | self.save_session() 143 | 144 | def login_required(self): 145 | if self.session_path.exists(): 146 | if self.load_session(): 147 | self.refresh() 148 | 149 | if not self.access_token: 150 | print(_('Please login')) 151 | self.login() 152 | 153 | def get_all_user_illustrations(self, user_id, offset=0, size=-1): 154 | """ 155 | 156 | :param user_id: str 157 | :param offset: int 158 | :param size: int, max result length, if < 0, return all 159 | :return: 160 | [ 161 | { 162 | "id": 92990893, 163 | "title": "-", 164 | "type": "illust", 165 | "image_urls": { 166 | "square_medium": "https://i.pximg.net/c/360x360_70/img-master/img/2021/09/25/00/05/35/92990893_p0_square1200.jpg", 167 | "medium": "https://i.pximg.net/c/540x540_70/img-master/img/2021/09/25/00/05/35/92990893_p0_master1200.jpg", 168 | "large": "https://i.pximg.net/c/600x1200_90/img-master/img/2021/09/25/00/05/35/92990893_p0_master1200.jpg" 169 | }, 170 | "caption": "", 171 | "restrict": 0, 172 | "user": { 173 | "id": 22124330, 174 | "name": "\u8d85\u51f6\u306e\u72c4\u7490\u5361", 175 | "account": "swd3e22", 176 | "profile_image_urls": { 177 | "medium": "https://i.pximg.net/user-profile/img/2017/01/10/13/28/42/11988991_bae951a38d31d217fa1eceedc0aafdbe_170.jpg" 178 | }, 179 | "is_followed": true 180 | }, 181 | "tags": [ 182 | { 183 | "name": "\u5973\u306e\u5b50", 184 | "translated_name": "girl" 185 | }, 186 | { 187 | "name": "\u843d\u66f8", 188 | "translated_name": "doodle" 189 | }, 190 | { 191 | "name": "closers", 192 | "translated_name": null 193 | }, 194 | { 195 | "name": "\u30b7\u30e7\u30fc\u30c8\u30d1\u30f3\u30c4", 196 | "translated_name": "short pants" 197 | }, 198 | { 199 | "name": "\u9b45\u60d1\u306e\u9854", 200 | "translated_name": "alluring face" 201 | }, 202 | { 203 | "name": "Mirae", 204 | "translated_name": null 205 | }, 206 | { 207 | "name": "\u80f8\u30dd\u30c1", 208 | "translated_name": "hanging breasts" 209 | }, 210 | { 211 | "name": "\u898b\u305b\u30cf\u30a4\u30ec\u30b0\u30d1\u30f3\u30c4/\u30c1\u30e7\u30fc\u30ab\u30fc/\u30a2\u30fc\u30e0\u30ab\u30d0\u30fc/\u30cf\u30fc\u30cd\u30b9", 212 | "translated_name": null 213 | }, 214 | { 215 | "name": "\u7740\u8863\u5de8\u4e73/\u80f8\u306b\u624b/\u30a2\u30e1\u30ea\u30ab\u30f3\u30b9\u30ea\u30fc\u30d6/\u3078\u305d\u51fa\u3057/\u307a\u305f\u3093\u5ea7\u308a", 216 | "translated_name": null 217 | }, 218 | { 219 | "name": "\u30c1\u30e3\u30c3\u30af\u4e0b\u308d\u3057/\u6c57/\u64ab\u3067\u56de\u3057\u305f\u3044\u304a\u8179/\u3082\u3082\u3077\u304f", 220 | "translated_name": null 221 | } 222 | ], 223 | "tools": [], 224 | "create_date": "2021-09-25T00:05:35+09:00", 225 | "page_count": 1, 226 | "width": 1329, 227 | "height": 1919, 228 | "sanity_level": 4, 229 | "x_restrict": 0, 230 | "series": null, 231 | "meta_single_page": { 232 | "original_image_url": "https://i.pximg.net/img-original/img/2021/09/25/00/05/35/92990893_p0.jpg" 233 | }, 234 | "meta_pages": [], 235 | "total_view": 45615, 236 | "total_bookmarks": 8179, 237 | "is_bookmarked": false, 238 | "visible": true, 239 | "is_muted": false, 240 | "total_comments": 28 241 | } 242 | ] 243 | 244 | """ 245 | 246 | r = [] 247 | done = False 248 | cur_size = 0 249 | 250 | while not done: 251 | data = self.aapi.user_illusts(user_id, offset=offset) 252 | try: 253 | r.extend(data['illusts']) 254 | except: 255 | print(data) 256 | offset += 30 257 | cur_size += 30 258 | if not data['next_url'] or (0 <= size <= cur_size): 259 | done = True 260 | return r[:size] if size >= 0 else r 261 | 262 | def get_illustration(self, illustration_id): 263 | """ 264 | get illustration data 265 | 266 | Return: list of illustration instances 267 | """ 268 | self.login_required() 269 | return [self.aapi.illust_detail(illustration_id)['illust']] 270 | 271 | def get_ranking_illustrations(self, mode='day', date='', total_page=3): 272 | """ 273 | fetch illustrations by ranking 274 | 275 | Args: 276 | mode: [day, week, month, day_male, day_female, week_original, week_rookie, day_manga, 277 | day_r18, day_male_r18, day_female_r18, week_r18, week_r18g] 278 | date: '2015-04-01' 279 | per_page: int 280 | page: int 281 | 282 | Return: list of illustration data, same as `get_all_user_illustrations()` 283 | """ 284 | self.login_required() 285 | r = [] 286 | page = 0 287 | offset = 0 288 | while page <= total_page: 289 | data = self.aapi.illust_ranking(mode, date=date, offset=offset) 290 | r.extend(data['illusts']) 291 | offset += 30 292 | page += 1 293 | for index, i in enumerate(r): 294 | i['rank'] = index + 1 295 | return r 296 | 297 | def set_timeout(self, timeout): 298 | self.timeout = timeout 299 | -------------------------------------------------------------------------------- /src/pixivd/i18n.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import os 3 | import sys 4 | import locale 5 | 6 | languages = [locale.getdefaultlocale()[0], 'en_US'] 7 | 8 | current_path = os.path.dirname(os.path.abspath(__file__)) 9 | t = gettext.translation('messages', os.path.join(current_path, "locale"), languages=languages) 10 | i18n = t.gettext 11 | -------------------------------------------------------------------------------- /src/pixivd/locale/en_US/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bebound/pixivd/64c2da889caf39b03b524d179e24691363696199/src/pixivd/locale/en_US/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /src/pixivd/locale/en_US/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-04-02 15:39-0400\n" 11 | "PO-Revision-Date: 2017-04-02 15:39-0400\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: en_US\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 1.8.7.1\n" 19 | 20 | #: api.py:97 21 | msgid "Unknown Method:" 22 | msgstr "Unknown Method:" 23 | 24 | #: api.py:102 api.py:135 api.py:170 25 | msgid "[ERROR] connection failed!" 26 | msgstr "[ERROR] connection failed!" 27 | 28 | #: api.py:139 29 | msgid "Please login" 30 | msgstr "Please login" 31 | 32 | #: api.py:140 33 | msgid "username:" 34 | msgstr "username:" 35 | 36 | #: api.py:141 37 | msgid "password:" 38 | msgstr "password:" 39 | 40 | #: pixiv.py:119 41 | #, python-format 42 | msgid "Connection error: %s" 43 | msgstr "Connection error: %s" 44 | 45 | #: pixiv.py:141 46 | #, python-format 47 | msgid "\r%s => %s download error, retry" 48 | msgstr "\r%s => %s download error, retry" 49 | 50 | #: pixiv.py:225 51 | msgid "Start download, total illustrations " 52 | msgstr "Start download, total illustrations " 53 | 54 | #: pixiv.py:233 55 | msgid "There is no new illustration need to download" 56 | msgstr "There is no new illustration need to download" 57 | 58 | #: pixiv.py:239 59 | msgid "Input the artist's id:(separate with space)" 60 | msgstr "Input the artist's id:(separate with space)" 61 | 62 | #: pixiv.py:241 63 | #, python-format 64 | msgid "Artists %s\n" 65 | msgstr "Artists %s\n" 66 | 67 | #: pixiv.py:255 68 | msgid "Input the date:(eg:2015-07-10)" 69 | msgstr "Input the date:(eg:2015-07-10)" 70 | 71 | #: pixiv.py:257 72 | msgid "[invalid]" 73 | msgstr "[invalid]" 74 | 75 | #: pixiv.py:284 76 | #, python-format 77 | msgid "Artists %s [%s]" 78 | msgstr "Artists %s [%s]" 79 | 80 | #: pixiv.py:286 81 | #, python-format 82 | msgid "Artists %s ?? [%s]" 83 | msgstr "Artists %s ?? [%s]" 84 | 85 | #: pixiv.py:314 86 | msgid "Dangerous Action: continue?(y/n)" 87 | msgstr "Dangerous Action: continue?(y/n)" 88 | 89 | #: pixiv.py:331 90 | msgid " Pixiv Downloader 2.4 " 91 | msgstr " Pixiv Downloader 2.4 " 92 | 93 | #: pixiv.py:359 94 | msgid "Which do you want to:" 95 | msgstr "Which do you want to:" 96 | 97 | #: pixiv.py:362 98 | msgid "exit" 99 | msgstr "exit" 100 | 101 | #: pixiv.py:366 102 | msgid " finished " 103 | msgstr " finished " 104 | 105 | #: pixiv.py:370 106 | msgid "Wrong input!" 107 | msgstr "Wrong input!" 108 | 109 | #~ msgid "process cancelled" 110 | #~ msgstr "process cancelled" 111 | 112 | #~ msgid "%s => %s download failed" 113 | #~ msgstr "%s => %s download failed" 114 | 115 | #~ msgid "%s => %s download error, retry" 116 | #~ msgstr "%s => %s download error, retry" 117 | 118 | #~ msgid "Artists %s ??\n" 119 | #~ msgstr "Artists %s ??\n" 120 | 121 | #~ msgid " Pixiv Downloader 2.2 " 122 | #~ msgstr " Pixiv Downloader 2.2 " 123 | 124 | #~ msgid "download_by_user_id" 125 | #~ msgstr "download_by_user_id" 126 | 127 | #~ msgid "download_by_ranking" 128 | #~ msgstr "download_by_ranking" 129 | 130 | #~ msgid "download_by_history_ranking" 131 | #~ msgstr "download_by_history_ranking" 132 | 133 | #~ msgid "update_exist" 134 | #~ msgstr "update_exist" 135 | 136 | #~ msgid "refresh_exist" 137 | #~ msgstr "refresh_exist" 138 | 139 | #~ msgid "Checking session" 140 | #~ msgstr "Checking session" 141 | 142 | #~ msgid " [VALID]" 143 | #~ msgstr " [VALID]" 144 | 145 | #~ msgid " [EXPIRED]" 146 | #~ msgstr " [EXPIRED]" 147 | -------------------------------------------------------------------------------- /src/pixivd/locale/zh_CN/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bebound/pixivd/64c2da889caf39b03b524d179e24691363696199/src/pixivd/locale/zh_CN/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /src/pixivd/locale/zh_CN/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-04-02 15:36-0400\n" 11 | "PO-Revision-Date: 2017-04-02 15:38-0400\n" 12 | "Last-Translator: \n" 13 | "Language-Team: \n" 14 | "Language: zh_CN\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 1.8.7.1\n" 19 | 20 | #: api.py:97 21 | msgid "Unknown Method:" 22 | msgstr "未知方法:" 23 | 24 | #: api.py:102 api.py:135 api.py:170 25 | msgid "[ERROR] connection failed!" 26 | msgstr "[错误] 连接失败!" 27 | 28 | #: api.py:139 29 | msgid "Please login" 30 | msgstr "请登录" 31 | 32 | #: api.py:140 33 | msgid "username:" 34 | msgstr "用户名:" 35 | 36 | #: api.py:141 37 | msgid "password:" 38 | msgstr "密码:" 39 | 40 | #: pixiv.py:119 41 | #, python-format 42 | msgid "Connection error: %s" 43 | msgstr "连接错误: %s" 44 | 45 | #: pixiv.py:141 46 | #, python-format 47 | msgid "\r%s => %s download error, retry" 48 | msgstr "%s => %s 下载错误,重试" 49 | 50 | #: pixiv.py:225 51 | msgid "Start download, total illustrations " 52 | msgstr "开始下载,插画总数 " 53 | 54 | #: pixiv.py:233 55 | msgid "There is no new illustration need to download" 56 | msgstr "没有新的插画需要下载" 57 | 58 | #: pixiv.py:239 59 | msgid "Input the artist's id:(separate with space)" 60 | msgstr "输入艺术家的id:(用空格分隔)" 61 | 62 | #: pixiv.py:241 63 | #, python-format 64 | msgid "Artists %s\n" 65 | msgstr "艺术家 %s\n" 66 | 67 | #: pixiv.py:255 68 | msgid "Input the date:(eg:2015-07-10)" 69 | msgstr "输入日期:(例:2015-07-10)" 70 | 71 | #: pixiv.py:257 72 | msgid "[invalid]" 73 | msgstr "[失效]" 74 | 75 | #: pixiv.py:284 76 | #, python-format 77 | msgid "Artists %s [%s]" 78 | msgstr "艺术家 %s [%s]" 79 | 80 | #: pixiv.py:286 81 | #, python-format 82 | msgid "Artists %s ?? [%s]" 83 | msgstr "艺术家 %s ?? [%s]" 84 | 85 | #: pixiv.py:314 86 | msgid "Dangerous Action: continue?(y/n)" 87 | msgstr "危险操作,继续?(y/n)" 88 | 89 | #: pixiv.py:331 90 | msgid " Pixiv Downloader 2.4 " 91 | msgstr " Pixiv下载器 2.4 " 92 | 93 | #: pixiv.py:359 94 | msgid "Which do you want to:" 95 | msgstr "功能选择:" 96 | 97 | #: pixiv.py:362 98 | msgid "exit" 99 | msgstr "退出" 100 | 101 | #: pixiv.py:366 102 | msgid " finished " 103 | msgstr " 完成" 104 | 105 | #: pixiv.py:370 106 | msgid "Wrong input!" 107 | msgstr "输入错误" 108 | 109 | #~ msgid "process cancelled" 110 | #~ msgstr "过程已中断" 111 | 112 | #~ msgid "%s => %s download failed" 113 | #~ msgstr "%s => %s 下载失败" 114 | 115 | #~ msgid "%s => %s download error, retry" 116 | #~ msgstr "%s => %s 下载错误,重试" 117 | 118 | #~ msgid "Artists %s ??\n" 119 | #~ msgstr "艺术家 %s ??\n" 120 | 121 | #~ msgid " Pixiv Downloader 2.2 " 122 | #~ msgstr " Pixiv 下载器 2.2 " 123 | 124 | msgid "download_by_user_id" 125 | msgstr "下载指定艺术家作品" 126 | 127 | msgid "download_by_ranking" 128 | msgstr "下载今日排行榜" 129 | 130 | msgid "download_by_history_ranking" 131 | msgstr "下载历史排行榜" 132 | 133 | msgid "update_exist" 134 | msgstr "更新已存在艺术家作品" 135 | 136 | msgid "refresh_exist" 137 | msgstr "刷新已存在艺术家作品" 138 | 139 | #~ msgid "Checking session" 140 | #~ msgstr "验证会话" 141 | 142 | #~ msgid " [VALID]" 143 | #~ msgstr " [有效]" 144 | 145 | #~ msgid " [EXPIRED]" 146 | #~ msgstr " [无效]" 147 | -------------------------------------------------------------------------------- /src/pixivd/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from abc import abstractmethod, ABCMeta 3 | 4 | 5 | class PixivModel(object): 6 | """ 7 | store illust/novel data 8 | 9 | Attribute 10 | """ 11 | __metaclass__ = ABCMeta 12 | 13 | @abstractmethod 14 | def from_data(self, data): 15 | """parse the data to a dict""" 16 | pass 17 | 18 | 19 | class PixivIllustModel(PixivModel): 20 | @classmethod 21 | def create_illust_from_data(cls, data): 22 | illust = cls() 23 | for k, v in data.items(): 24 | setattr(illust, k, v) 25 | illust.user_id = str(illust.user['id']) 26 | illust.user_name = illust.user['name'] 27 | if data['meta_single_page']: 28 | illust.image_urls = [data['meta_single_page']['original_image_url']] 29 | if data['type'] == 'ugoira': 30 | illust.image_urls = [ 31 | illust.image_urls[0].replace('img-original', 'img-zip-ugoira') 32 | .replace('ugoira0.jpg', 'ugoira600x600.zip') 33 | .replace('ugoira0.png', 'ugoira600x600.zip')] 34 | elif data['meta_pages']: 35 | illust.image_urls = [i['image_urls']['original'] for i in data['meta_pages']] 36 | return illust 37 | 38 | @classmethod 39 | def from_data(cls, data_list): 40 | """parse data to dict contains illust information 41 | 42 | Return: 43 | result: a list of instance contains illust information 44 | """ 45 | illusts = [] 46 | for data in list(data_list): 47 | illust = cls.create_illust_from_data(data) 48 | illusts.append(illust) 49 | return illusts 50 | -------------------------------------------------------------------------------- /src/pixivd/pixivd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | pixiv 4 | 5 | Usage: 6 | pixivd 7 | pixivd ... 8 | pixivd -r [-d | --date=] 9 | pixivd -u 10 | pixivd --version 11 | pixivd -h | --help 12 | 13 | Arguments: 14 | Pixiv user id 15 | 16 | Options: 17 | -r Download by ranking 18 | -d --date Target date 19 | -u Update exist folder 20 | -h --help Show this screen 21 | --version Show version 22 | 23 | Examples: 24 | pixivd 7210261 1980643 25 | pixivd -r -d 2016-09-24 26 | """ 27 | import datetime 28 | import math 29 | import os 30 | import queue 31 | import re 32 | import sys 33 | import threading 34 | import time 35 | import traceback 36 | 37 | import requests 38 | from docopt import docopt 39 | from tqdm import tqdm 40 | 41 | from .api import PixivApi 42 | from .i18n import i18n as _ 43 | from .model import PixivIllustModel 44 | 45 | _THREADING_NUMBER = 10 46 | _finished_download = 0 47 | _CREATE_FOLDER_LOCK = threading.Lock() 48 | _PROGRESS_LOCK = threading.Lock() 49 | _SPEED_LOCK = threading.Lock() 50 | _Global_Download = 0 51 | _error_count = {} 52 | _ILLUST_PER_PAGE = 30 53 | _MAX_ERROR_COUNT = 5 54 | 55 | __version__ = '3.1.2' 56 | 57 | 58 | def get_default_save_path(): 59 | current_path = os.getcwd() 60 | filepath = os.path.join(current_path, 'illustrations') 61 | if not os.path.exists(filepath): 62 | with _CREATE_FOLDER_LOCK: 63 | if not os.path.exists(os.path.dirname(filepath)): 64 | os.makedirs(os.path.dirname(filepath)) 65 | os.makedirs(filepath) 66 | return filepath 67 | 68 | 69 | def get_speed(elapsed): 70 | """Get current download speed""" 71 | with _SPEED_LOCK: 72 | global _Global_Download 73 | down = _Global_Download 74 | _Global_Download = 0 75 | speed = down / elapsed 76 | if speed == 0: 77 | return '%8.2f /s' % 0 78 | units = [' B', 'KB', 'MB', 'GB', 'TB', 'PB'] 79 | unit = math.floor(math.log(speed, 1024.0)) 80 | speed /= math.pow(1024.0, unit) 81 | return '%6.2f %s/s' % (speed, units[unit]) 82 | 83 | 84 | def print_progress(max_size): 85 | global _finished_download 86 | pbar = tqdm(total=max_size) 87 | 88 | last = 0 89 | while _finished_download != max_size: 90 | pbar.update(_finished_download - last) 91 | last = _finished_download 92 | time.sleep(0.5) 93 | pbar.update(_finished_download - last) 94 | pbar.close() 95 | 96 | 97 | def download_file(url, filepath): 98 | headers = {'Referer': 'https://www.pixiv.net/', 'User-Agent': PixivApi.user_agent} 99 | r = requests.get(url, headers=headers, stream=True, timeout=PixivApi.timeout) 100 | if r.status_code == requests.codes.ok: 101 | total_length = r.headers.get('content-length') 102 | if total_length: 103 | data = [] 104 | for chunk in r.iter_content(1024 * 16): 105 | data.append(chunk) 106 | with _SPEED_LOCK: 107 | global _Global_Download 108 | _Global_Download += len(chunk) 109 | with open(filepath, 'wb') as f: 110 | list(map(f.write, data)) 111 | else: 112 | raise ConnectionError('\r', _('Connection error: %s') % r.status_code) 113 | 114 | 115 | def download_threading(download_queue): 116 | global _finished_download 117 | while not download_queue.empty(): 118 | illustration = download_queue.get() 119 | filepath = illustration['path'] 120 | filename = illustration['file'] 121 | url = illustration['url'] 122 | count = _error_count.get(url, 0) 123 | if count < _MAX_ERROR_COUNT: 124 | if not os.path.exists(filepath): 125 | with _CREATE_FOLDER_LOCK: 126 | if not os.path.exists(os.path.dirname(filepath)): 127 | os.makedirs(os.path.dirname(filepath)) 128 | try: 129 | download_file(url, filepath) 130 | with _PROGRESS_LOCK: 131 | _finished_download += 1 132 | except Exception as e: 133 | if count < _MAX_ERROR_COUNT: 134 | print(_('%s => %s download error, retry') % (e, filename)) 135 | download_queue.put(illustration) 136 | _error_count[url] = count + 1 137 | else: 138 | print(url, 'reach max retries, canceled') 139 | with _PROGRESS_LOCK: 140 | _finished_download += 1 141 | download_queue.task_done() 142 | 143 | 144 | def start_and_wait_download_threading(download_queue, count): 145 | """start download threading and wait till complete""" 146 | progress_t = threading.Thread(target=print_progress, args=(count,)) 147 | progress_t.daemon = True 148 | progress_t.start() 149 | for i in range(_THREADING_NUMBER): 150 | download_t = threading.Thread(target=download_threading, args=(download_queue,)) 151 | download_t.daemon = True 152 | download_t.start() 153 | 154 | progress_t.join() 155 | download_queue.join() 156 | 157 | 158 | def get_filepath(url, illustration, save_path='.', add_user_folder=False, add_rank=False): 159 | """return (filename,filepath)""" 160 | 161 | if add_user_folder: 162 | user_id = illustration.user_id 163 | user_name = illustration.user_name 164 | current_path = get_default_save_path() 165 | cur_dirs = list(filter(os.path.isdir, [os.path.join(current_path, i) for i in os.listdir(current_path)])) 166 | cur_user_ids = [os.path.basename(cur_dir).split()[0] for cur_dir in cur_dirs] 167 | if user_id not in cur_user_ids: 168 | dir_name = re.sub(r'[<>:"/\\|\?\*]', ' ', user_id + ' ' + user_name) 169 | else: 170 | dir_name = list(i for i in cur_dirs if os.path.basename(i).split()[0] == user_id)[0] 171 | save_path = os.path.join(save_path, dir_name) 172 | 173 | filename = url.split('/')[-1] 174 | if add_rank: 175 | filename = f'{illustration.rank} - {filename}' 176 | filepath = os.path.join(save_path, filename) 177 | return filename, filepath 178 | 179 | 180 | def check_files(illustrations, save_path='.', add_user_folder=False, add_rank=False): 181 | download_queue = queue.Queue() 182 | index_list = [] 183 | count = 0 184 | if illustrations: 185 | last_i = -1 186 | for index, illustration in enumerate(illustrations): 187 | if not illustration.image_urls: 188 | continue 189 | else: 190 | for url in illustration.image_urls: 191 | filename, filepath = get_filepath(url, illustration, save_path, add_user_folder, add_rank) 192 | if os.path.exists(filepath): 193 | continue 194 | else: 195 | if last_i != index: 196 | last_i = index 197 | index_list.append(index) 198 | download_queue.put({'url': url, 'file': filename, 'path': filepath}) 199 | count += 1 200 | return download_queue, count, index_list 201 | 202 | 203 | def count_illustrations(illustrations): 204 | return sum(len(i.image_urls) for i in illustrations) 205 | 206 | 207 | def is_manga(illustrate): 208 | return True if illustrate.is_manga or illustrate.type == 'manga' else False 209 | 210 | 211 | def download_illustrations(api, data_list, save_path='.', add_user_folder=False, add_rank=False, skip_manga=False): 212 | """Download illustratons 213 | 214 | Args: 215 | api: PixivApi() 216 | data_list: json 217 | save_path: str, download path of the illustrations 218 | add_user_folder: bool, whether put the illustration into user folder 219 | add_rank: bool, add illustration rank at the beginning of filename 220 | """ 221 | illustrations = PixivIllustModel.from_data(data_list) 222 | if skip_manga: 223 | manga_number = sum([is_manga(i) for i in illustrations]) 224 | if manga_number: 225 | print('skip', manga_number, 'manga') 226 | illustrations = list(filter(lambda x: not is_manga(x), illustrations)) 227 | download_queue, count = check_files(illustrations, save_path, add_user_folder, add_rank)[0:2] 228 | if count > 0: 229 | print(_('Start download, total illustrations '), count) 230 | global _finished_download, _Global_Download 231 | _finished_download = 0 232 | _Global_Download = 0 233 | start_and_wait_download_threading(download_queue, count) 234 | print() 235 | else: 236 | print(_('There is no new illustration need to download')) 237 | 238 | 239 | def download_by_user_id(api, user_ids=None): 240 | save_path = get_default_save_path() 241 | if not user_ids: 242 | user_ids = input(_('Input the artist\'s id:(separate with space)')).strip().split(' ') 243 | for user_id in user_ids: 244 | print(_('Artists %s') % user_id) 245 | data_list = api.get_all_user_illustrations(user_id) 246 | download_illustrations(api, data_list, save_path, add_user_folder=True) 247 | 248 | 249 | def download_by_ranking(api): 250 | today = str(datetime.date.today()) 251 | save_path = os.path.join(get_default_save_path(), today + ' ranking') 252 | data_list = api.get_ranking_illustrations() 253 | download_illustrations(api, data_list, save_path, add_rank=True) 254 | 255 | 256 | def download_by_history_ranking(api, date=''): 257 | if not date: 258 | date = input(_('Input the date:(eg:2015-07-10)')) 259 | if not (re.search(r"^\d{4}-\d{2}-\d{2}", date)): 260 | print(_('[invalid date format]')) 261 | date = str(datetime.date.today() - datetime.timedelta(days=1)) 262 | save_path = os.path.join(get_default_save_path(), date + ' ranking') 263 | data_list = api.get_ranking_illustrations(date=date) 264 | download_illustrations(api, data_list, save_path, add_rank=True) 265 | 266 | 267 | def artist_folder_scanner(api, user_id_list, save_path, final_list, fast): 268 | while not user_id_list.empty(): 269 | user_info = user_id_list.get() 270 | user_id = user_info['id'] 271 | folder = user_info['folder'] 272 | try: 273 | if fast: 274 | data_list = [] 275 | offset = 0 276 | page_result = api.get_all_user_illustrations(user_id, offset, _ILLUST_PER_PAGE) 277 | if len(page_result) > 0: 278 | data_list.extend(page_result) 279 | file_path = os.path.join(save_path, folder, data_list[-1]['image_urls']['large'].split('/')[-1]) 280 | while not os.path.exists(file_path) and len(page_result) == _ILLUST_PER_PAGE: 281 | offset += _ILLUST_PER_PAGE 282 | page_result = api.get_all_user_illustrations(user_id, offset, _ILLUST_PER_PAGE) 283 | data_list.extend(page_result) 284 | file_path = os.path.join(save_path, folder, data_list[-1]['image_urls']['large'].split('/')[-1]) 285 | # prevent rate limit 286 | time.sleep(1) 287 | else: 288 | data_list = api.get_all_user_illustrations(user_id) 289 | illustrations = PixivIllustModel.from_data(data_list) 290 | count, checked_list = check_files(illustrations, save_path, add_user_folder=True, add_rank=False)[1:3] 291 | if len(sys.argv) < 2 or count: 292 | try: 293 | print(_('Artists %s [%s]') % (folder, count)) 294 | except UnicodeError: 295 | print(_('Artists %s ?? [%s]') % (user_id, count)) 296 | with _PROGRESS_LOCK: 297 | for index in checked_list: 298 | final_list.append(data_list[index]) 299 | except Exception: 300 | traceback.print_exc() 301 | user_id_list.task_done() 302 | 303 | 304 | def update_exist(api, fast=True): 305 | current_path = get_default_save_path() 306 | final_list = [] 307 | user_id_list = queue.Queue() 308 | for folder in os.listdir(current_path): 309 | if os.path.isdir(os.path.join(current_path, folder)): 310 | user_id = re.search(r'^(\d+) ', folder) 311 | if user_id: 312 | user_id = user_id.group(1) 313 | user_id_list.put({'id': user_id, 'folder': folder}) 314 | for i in range(1): 315 | # use one thread to prevent Rate Limit in new App API 316 | scan_t = threading.Thread(target=artist_folder_scanner, 317 | args=(api, user_id_list, current_path, final_list, fast,)) 318 | scan_t.daemon = True 319 | scan_t.start() 320 | user_id_list.join() 321 | download_illustrations(api, final_list, current_path, add_user_folder=True) 322 | 323 | 324 | def remove_repeat(_): 325 | """Delete xxxxx.img if xxxxx_p0.img exist""" 326 | choice = input(_('Dangerous Action: continue?(y/n)')) 327 | if choice == 'y': 328 | illust_path = get_default_save_path() 329 | for folder in os.listdir(illust_path): 330 | if os.path.isdir(os.path.join(illust_path, folder)): 331 | if re.search(r'^(\d+) ', folder): 332 | path = os.path.join(illust_path, folder) 333 | for file_name in os.listdir(path): 334 | illustration_id = re.search(r'^\d+\.', file_name) 335 | if illustration_id: 336 | if os.path.isfile(os.path.join(path 337 | , illustration_id.string.replace('.', '_p0.'))): 338 | os.remove(os.path.join(path, file_name)) 339 | print('Delete', os.path.join(path, file_name)) 340 | 341 | 342 | def main(): 343 | arguments = docopt(__doc__) 344 | api = PixivApi() 345 | if len(sys.argv) > 1: 346 | ids = arguments[''] 347 | is_rank = arguments['-r'] 348 | date = arguments['--date'] 349 | is_update = arguments['-u'] 350 | show_version = arguments['--version'] 351 | if show_version: 352 | print(__version__) 353 | return 354 | print(datetime.datetime.now().strftime('%X %x')) 355 | if ids: 356 | download_by_user_id(api, ids) 357 | elif is_rank: 358 | if date: 359 | date = date[0] 360 | download_by_history_ranking(api, date) 361 | else: 362 | download_by_ranking(api) 363 | elif is_update: 364 | update_exist(api) 365 | print(datetime.datetime.now().strftime('%X %x')) 366 | else: 367 | print(_(f' Pixiv Downloader {__version__}').center(77, '#')) 368 | options = { 369 | '1': download_by_user_id, 370 | '2': download_by_ranking, 371 | '3': download_by_history_ranking, 372 | '4': update_exist, 373 | '5': remove_repeat 374 | } 375 | while True: 376 | print(_('Which do you want to:')) 377 | for i in sorted(options.keys()): 378 | print('\t %s %s' % (i, _(options[i].__name__).replace('_', ' '))) 379 | choose = input('\t e %s \n:' % _('exit')) 380 | if choose in [str(i) for i in range(1, len(options) + 1)]: 381 | print((' ' + _(options[choose].__name__).replace('_', ' ') + ' ').center(60, '#') + '\n') 382 | if choose == 4: 383 | options[choose](api, False) 384 | else: 385 | options[choose](api) 386 | print('\n' + (' ' + _(options[choose].__name__).replace('_', ' ') + _(' finished ')).center(60, 387 | '#') + '\n') 388 | elif choose == 'e': 389 | break 390 | else: 391 | print(_('Wrong input!')) 392 | 393 | 394 | if __name__ == '__main__': 395 | sys.exit(main()) 396 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.9" 4 | 5 | [[package]] 6 | name = "certifi" 7 | version = "2025.4.26" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, 12 | ] 13 | 14 | [[package]] 15 | name = "charset-normalizer" 16 | version = "3.4.1" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, 21 | { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, 22 | { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, 23 | { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, 24 | { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, 25 | { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, 26 | { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, 27 | { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, 28 | { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, 29 | { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, 30 | { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, 31 | { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, 32 | { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, 33 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, 34 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, 35 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, 36 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, 37 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, 38 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, 39 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, 40 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, 41 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, 42 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, 43 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, 44 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, 45 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, 46 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 47 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 48 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 49 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 50 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 51 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 52 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 53 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 54 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 55 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 56 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 57 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 58 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 59 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 60 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 61 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 62 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 63 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 64 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 65 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 66 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 67 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 68 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 69 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 70 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 71 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 72 | { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 }, 73 | { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 }, 74 | { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 }, 75 | { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 }, 76 | { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 }, 77 | { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 }, 78 | { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 }, 79 | { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 }, 80 | { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 }, 81 | { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 }, 82 | { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 }, 83 | { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 }, 84 | { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 }, 85 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 86 | ] 87 | 88 | [[package]] 89 | name = "cloudscraper" 90 | version = "1.2.48" 91 | source = { registry = "https://pypi.org/simple" } 92 | dependencies = [ 93 | { name = "pyparsing" }, 94 | { name = "requests" }, 95 | { name = "requests-toolbelt" }, 96 | ] 97 | sdist = { url = "https://files.pythonhosted.org/packages/9a/2c/2139cdd276f54d0dbf6397950e65b53f617082cde7832c47b4afea327e4d/cloudscraper-1.2.48.tar.gz", hash = "sha256:bb6be1c2d12720c9fcde80f1965a2250444821f64a900e5bddf9aef2c1fa5d62", size = 90167 } 98 | wheels = [ 99 | { url = "https://files.pythonhosted.org/packages/83/e4/f1d3872ce822f52f4133cc04960185f64676ca5ec87f05a5350fc0c0a92f/cloudscraper-1.2.48-py2.py3-none-any.whl", hash = "sha256:c09bd6c283e6b0918cff12ad829857337431c486409abb1916d50abfed6ef118", size = 94577 }, 100 | ] 101 | 102 | [[package]] 103 | name = "colorama" 104 | version = "0.4.6" 105 | source = { registry = "https://pypi.org/simple" } 106 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 107 | wheels = [ 108 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 109 | ] 110 | 111 | [[package]] 112 | name = "docopt" 113 | version = "0.6.2" 114 | source = { registry = "https://pypi.org/simple" } 115 | sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901 } 116 | 117 | [[package]] 118 | name = "idna" 119 | version = "3.10" 120 | source = { registry = "https://pypi.org/simple" } 121 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 122 | wheels = [ 123 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 124 | ] 125 | 126 | [[package]] 127 | name = "pixivd" 128 | source = { editable = "." } 129 | dependencies = [ 130 | { name = "docopt" }, 131 | { name = "pixivpy" }, 132 | { name = "pyaes" }, 133 | { name = "requests" }, 134 | { name = "tqdm" }, 135 | ] 136 | 137 | [package.dev-dependencies] 138 | dev = [ 139 | { name = "ruff" }, 140 | ] 141 | 142 | [package.metadata] 143 | requires-dist = [ 144 | { name = "docopt", specifier = ">=0.6.2" }, 145 | { name = "pixivpy", specifier = ">=3.6.0" }, 146 | { name = "pyaes", specifier = ">=1.6.1" }, 147 | { name = "requests", specifier = ">=2.4.3" }, 148 | { name = "tqdm" }, 149 | ] 150 | 151 | [package.metadata.requires-dev] 152 | dev = [{ name = "ruff", specifier = ">=0.11.8" }] 153 | 154 | [[package]] 155 | name = "pixivpy" 156 | version = "3.7.0" 157 | source = { registry = "https://pypi.org/simple" } 158 | dependencies = [ 159 | { name = "cloudscraper" }, 160 | { name = "requests" }, 161 | { name = "requests-toolbelt" }, 162 | ] 163 | sdist = { url = "https://files.pythonhosted.org/packages/53/cb/8bb8eb5e6423d74f0a6dbbbd2bbdc534f858e54fdb2916ef37a1d3e1b869/PixivPy-3.7.0.tar.gz", hash = "sha256:f31833b3d012473ca18f4aac6ab4b9ed77900cac63be7a908df9b25bd8e795b4", size = 12630 } 164 | wheels = [ 165 | { url = "https://files.pythonhosted.org/packages/b3/4f/de61f64db66ddba64c8a7a3deb1179097392f5f9fd189a34087df000e6c0/PixivPy-3.7.0-py3-none-any.whl", hash = "sha256:93d5cb51ca8cf769e5155aecdf96df58b5a0f26ca438bbe09478619e73cafb9e", size = 17001 }, 166 | ] 167 | 168 | [[package]] 169 | name = "pyaes" 170 | version = "1.6.1" 171 | source = { registry = "https://pypi.org/simple" } 172 | sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536 } 173 | 174 | [[package]] 175 | name = "pyparsing" 176 | version = "3.2.3" 177 | source = { registry = "https://pypi.org/simple" } 178 | sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } 179 | wheels = [ 180 | { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, 181 | ] 182 | 183 | [[package]] 184 | name = "requests" 185 | version = "2.32.3" 186 | source = { registry = "https://pypi.org/simple" } 187 | dependencies = [ 188 | { name = "certifi" }, 189 | { name = "charset-normalizer" }, 190 | { name = "idna" }, 191 | { name = "urllib3" }, 192 | ] 193 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 196 | ] 197 | 198 | [[package]] 199 | name = "requests-toolbelt" 200 | version = "1.0.0" 201 | source = { registry = "https://pypi.org/simple" } 202 | dependencies = [ 203 | { name = "requests" }, 204 | ] 205 | sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } 206 | wheels = [ 207 | { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, 208 | ] 209 | 210 | [[package]] 211 | name = "ruff" 212 | version = "0.11.8" 213 | source = { registry = "https://pypi.org/simple" } 214 | sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399 } 215 | wheels = [ 216 | { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473 }, 217 | { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862 }, 218 | { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273 }, 219 | { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330 }, 220 | { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223 }, 221 | { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353 }, 222 | { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936 }, 223 | { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083 }, 224 | { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834 }, 225 | { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713 }, 226 | { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182 }, 227 | { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027 }, 228 | { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298 }, 229 | { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884 }, 230 | { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102 }, 231 | { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410 }, 232 | { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129 }, 233 | ] 234 | 235 | [[package]] 236 | name = "tqdm" 237 | version = "4.67.1" 238 | source = { registry = "https://pypi.org/simple" } 239 | dependencies = [ 240 | { name = "colorama", marker = "sys_platform == 'win32'" }, 241 | ] 242 | sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } 243 | wheels = [ 244 | { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, 245 | ] 246 | 247 | [[package]] 248 | name = "urllib3" 249 | version = "2.4.0" 250 | source = { registry = "https://pypi.org/simple" } 251 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } 252 | wheels = [ 253 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, 254 | ] 255 | --------------------------------------------------------------------------------