├── .gitattributes ├── .github └── workflows │ └── pkg.yml ├── .gitignore ├── .pkg ├── py-pkger.py ├── r_cloudscraper.py ├── r_pixiv.ico └── r_pixivpy.py ├── LICENSE ├── README.md ├── altfe ├── bridge.py ├── handle.py └── interface │ └── root.py ├── app ├── config │ ├── biu_default.yml │ ├── biu_en.yml │ ├── biu_es.yml │ ├── biu_ja.yml │ └── language │ │ ├── en.yml │ │ ├── es.yml │ │ └── zh.yml ├── lib │ ├── common │ │ └── login_helper │ │ │ ├── main.py │ │ │ └── token.py │ ├── core │ │ ├── biu.py │ │ └── dl │ │ │ ├── main.py │ │ │ └── model │ │ │ ├── dler.py │ │ │ ├── dler_aria2.py │ │ │ ├── dler_dl.py │ │ │ └── dler_dl_single.py │ ├── ins │ │ ├── conf │ │ │ ├── main.py │ │ │ └── wrapper.py │ │ └── i18n.py │ └── static │ │ ├── arg.py │ │ ├── file.py │ │ ├── msg.py │ │ └── util.py ├── plugin │ ├── do │ │ ├── dl.py │ │ ├── dl_stop.py │ │ ├── follow.py │ │ ├── mark.py │ │ ├── unfollow.py │ │ ├── unmark.py │ │ └── update_token.py │ ├── get │ │ ├── idfollowing.py │ │ ├── idmarks.py │ │ ├── idworks.py │ │ ├── newtome.py │ │ ├── onework.py │ │ ├── rank.py │ │ └── recommend.py │ ├── search │ │ ├── images.py │ │ ├── users.py │ │ └── works.py │ └── sys │ │ ├── language.py │ │ ├── outdated.py │ │ └── status.py └── v2 │ └── utils │ └── sprint.py ├── config.yml ├── docs ├── README_EN.md ├── README_ES.md └── README_JA.md ├── main.py ├── requirements.txt └── usr ├── static ├── browsers.json ├── multiverse │ ├── LICENSE.txt │ ├── README.txt │ ├── assets │ │ ├── css │ │ │ ├── fontawesome-all.min.css │ │ │ ├── images │ │ │ │ ├── arrow.svg │ │ │ │ ├── close.svg │ │ │ │ └── spinner.svg │ │ │ ├── main.css │ │ │ ├── n.css │ │ │ ├── noscript.css │ │ │ ├── nprogress.css │ │ │ ├── tooltipster.bundle.min.css │ │ │ └── tooltipster.theme.min.css │ │ ├── js │ │ │ ├── axios.min.js │ │ │ ├── biu │ │ │ │ ├── actions.js │ │ │ │ ├── blocks │ │ │ │ │ └── blockMain.js │ │ │ │ ├── functions.js │ │ │ │ ├── settings.js │ │ │ │ └── statusMsg.js │ │ │ ├── breakpoints.min.js │ │ │ ├── browser.min.js │ │ │ ├── jquery.min.js │ │ │ ├── jquery.popmenu.min.js │ │ │ ├── jquery.poptrox.min.js │ │ │ ├── js.cookie.min.js │ │ │ ├── main.js │ │ │ ├── nprogress.js │ │ │ ├── tooltipster.bundle.min.js │ │ │ ├── util.js │ │ │ └── vue.min.js │ │ └── webfonts │ │ │ ├── fa-brands-400.eot │ │ │ ├── fa-brands-400.svg │ │ │ ├── fa-brands-400.ttf │ │ │ ├── fa-brands-400.woff │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.eot │ │ │ ├── fa-regular-400.svg │ │ │ ├── fa-regular-400.ttf │ │ │ ├── fa-regular-400.woff │ │ │ ├── fa-regular-400.woff2 │ │ │ ├── fa-solid-900.eot │ │ │ ├── fa-solid-900.svg │ │ │ ├── fa-solid-900.ttf │ │ │ ├── fa-solid-900.woff │ │ │ └── fa-solid-900.woff2 │ └── images │ │ ├── tea.jpg │ │ └── thumbs │ │ ├── 01.jpg │ │ ├── 02.jpg │ │ ├── 03.jpg │ │ ├── 04.jpg │ │ ├── 05.jpg │ │ ├── 06.jpg │ │ ├── 07.jpg │ │ ├── 08.jpg │ │ ├── 09.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ └── 12.jpg └── pixiv.png └── templates └── multiverse └── index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | usr/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/pkg.yml: -------------------------------------------------------------------------------- 1 | name: 📦 Package 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the master branch 5 | # push: 6 | # branches: [ master ] 7 | # pull_request: 8 | # branches: [ master ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | inputs: 13 | releaseTag: 14 | description: "Release Tag" 15 | required: true 16 | default: "v2.x.x[a-z]" 17 | releaseName: 18 | description: "Release Name" 19 | required: true 20 | default: "2.x.x" 21 | 22 | jobs: 23 | build-on-platform: 24 | name: Package on ${{ matrix.version }} 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | include: 29 | - os: windows-2022 30 | version: win_x64 31 | pythonArch: x64 32 | - os: windows-2022 33 | version: win_x86 34 | pythonArch: x86 35 | - os: macos-14 36 | version: mac_apple 37 | pythonArch: arm64 38 | - os: macos-12 39 | version: mac_intel 40 | pythonArch: x64 41 | - os: ubuntu-20.04 42 | version: ubuntu_x64 43 | pythonArch: x64 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Classify files, prepare to be packaged 49 | run: | 50 | mkdir ./.pkg/code 51 | mkdir ./.pkg/public 52 | cp -r ./altfe/ ./.pkg/code/altfe/ 53 | cp -r ./app/ ./.pkg/code/app/ 54 | cp ./main.py ./.pkg/code/ 55 | cp -r ./usr/ ./.pkg/public/usr/ 56 | cp ./app/config/biu_default.yml ./.pkg/public/config.yml 57 | cp ./LICENSE ./.pkg/public/ 58 | cp ./README.md ./.pkg/public/ 59 | 60 | - name: Setup Python 61 | uses: actions/setup-python@v5 62 | with: 63 | python-version: "3.12" 64 | architecture: ${{ matrix.pythonArch }} 65 | 66 | - name: Install Requirements 67 | run: | 68 | pip install -r ./requirements.txt 69 | pip install pyinstaller 70 | 71 | - name: Run py-pkger.py 72 | run: | 73 | cd ./.pkg/ 74 | python ./py-pkger.py auto 75 | 76 | - name: Compress to ZIP (win) 77 | if: ${{ contains(matrix.version, 'win') }} 78 | run: | 79 | cd ./.pkg/dist/ 80 | mv ./main.exe ./PixivBiu.exe 81 | Compress-Archive * ../../${{ matrix.version }}.zip 82 | 83 | - name: Compress to ZIP (unix-like) 84 | if: ${{ contains(matrix.version, 'mac') || contains(matrix.version, 'ubuntu') }} 85 | run: | 86 | cd ./.pkg/dist/ 87 | mv ./main ./PixivBiu 88 | zip -r ../../${{ matrix.version }}.zip * 89 | 90 | - name: Upload to Artifact 91 | uses: actions/upload-artifact@v3 92 | with: 93 | name: pixivbiuArt 94 | path: ./${{ matrix.version }}.zip 95 | 96 | Release: 97 | needs: [build-on-platform] 98 | runs-on: ubuntu-22.04 99 | steps: 100 | - uses: actions/checkout@v4 101 | 102 | - name: Download from Artifact 103 | uses: actions/download-artifact@v3 104 | with: 105 | name: pixivbiuArt 106 | 107 | - name: Rename 108 | run: | 109 | mv ./win_x64.zip ./PixivBiu_${{ github.event.inputs.releaseTag }}_win_x64.zip 110 | mv ./win_x86.zip ./PixivBiu_${{ github.event.inputs.releaseTag }}_win_x86.zip 111 | mv ./mac_apple.zip ./PixivBiu_${{ github.event.inputs.releaseTag }}_mac_apple.zip 112 | mv ./mac_intel.zip ./PixivBiu_${{ github.event.inputs.releaseTag }}_mac_intel.zip 113 | mv ./ubuntu_x64.zip ./PixivBiu_${{ github.event.inputs.releaseTag }}_ubuntu_x64.zip 114 | 115 | - name: Release and Done 116 | uses: ncipollo/release-action@v1 117 | with: 118 | artifacts: "PixivBiu_${{ github.event.inputs.releaseTag }}_win_x64.zip, PixivBiu_${{ github.event.inputs.releaseTag }}_win_x86.zip, PixivBiu_${{ github.event.inputs.releaseTag }}_mac_apple.zip, PixivBiu_${{ github.event.inputs.releaseTag }}_mac_intel.zip, PixivBiu_${{ github.event.inputs.releaseTag }}_ubuntu_x64.zip" 119 | tag: ${{ github.event.inputs.releaseTag }} 120 | name: ${{ github.event.inputs.releaseName }} 121 | token: ${{ secrets.GITHUB_TOKEN }} 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/linux,macos,python,windows 2 | 3 | ### Biu ### 4 | usr/.token 5 | usr/cache/ 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # temporary files which can be created if a process still has a handle open of a deleted file 11 | .fuse_hidden* 12 | 13 | # KDE directory preferences 14 | .directory 15 | 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | 19 | # .nfs files are created when an open file is removed but is still being accessed 20 | .nfs* 21 | 22 | ### macOS ### 23 | # General 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### Python ### 51 | # Byte-compiled / optimized / DLL files 52 | __pycache__/ 53 | *.py[cod] 54 | *$py.class 55 | 56 | # C extensions 57 | *.so 58 | 59 | # Distribution / packaging 60 | .Python 61 | build/ 62 | develop-eggs/ 63 | dist/ 64 | downloads/ 65 | eggs/ 66 | .eggs/ 67 | lib64/ 68 | parts/ 69 | sdist/ 70 | var/ 71 | wheels/ 72 | pip-wheel-metadata/ 73 | share/python-wheels/ 74 | *.egg-info/ 75 | .installed.cfg 76 | *.egg 77 | MANIFEST 78 | 79 | # PyInstaller 80 | # Usually these files are written by a python script from a template 81 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 82 | *.manifest 83 | *.spec 84 | 85 | # Installer logs 86 | pip-log.txt 87 | pip-delete-this-directory.txt 88 | 89 | # Unit test / coverage reports 90 | htmlcov/ 91 | .tox/ 92 | .nox/ 93 | .coverage 94 | .coverage.* 95 | .cache 96 | nosetests.xml 97 | coverage.xml 98 | *.cover 99 | .hypothesis/ 100 | .pytest_cache/ 101 | 102 | # Translations 103 | *.mo 104 | *.pot 105 | 106 | # Scrapy stuff: 107 | .scrapy 108 | 109 | # Sphinx documentation 110 | docs/_build/ 111 | 112 | # PyBuilder 113 | target/ 114 | 115 | # pyenv 116 | .python-version 117 | 118 | # pipenv 119 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 120 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 121 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 122 | # install all needed dependencies. 123 | #Pipfile.lock 124 | 125 | # celery beat schedule file 126 | celerybeat-schedule 127 | 128 | # SageMath parsed files 129 | *.sage.py 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # Mr Developer 139 | .mr.developer.cfg 140 | .project 141 | .pydevproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | ### Windows ### 155 | # Windows thumbnail cache files 156 | Thumbs.db 157 | Thumbs.db:encryptable 158 | ehthumbs.db 159 | ehthumbs_vista.db 160 | 161 | # Dump file 162 | *.stackdump 163 | 164 | # Folder config file 165 | [Dd]esktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msix 174 | *.msm 175 | *.msp 176 | 177 | # Windows shortcuts 178 | *.lnk 179 | 180 | .vscode/ 181 | .venv/ 182 | -------------------------------------------------------------------------------- /.pkg/py-pkger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import warnings 5 | from pathlib import Path 6 | 7 | import cloudscraper 8 | import pixivpy3 9 | 10 | with warnings.catch_warnings(): 11 | warnings.simplefilter("ignore", DeprecationWarning) 12 | 13 | ROOT_PATH = os.path.split(os.path.realpath(sys.argv[0]))[0] 14 | CODE_PATH = os.path.join(ROOT_PATH, "code") 15 | PUBLIC_PATH = os.path.join(ROOT_PATH, "public") 16 | TMP_PATH = os.path.join(ROOT_PATH, "tmp") 17 | DIST_PATH = os.path.join(ROOT_PATH, "dist") 18 | 19 | 20 | # 复制文件夹 21 | def copyDIR(src, dst, cover=True, ignore=[]): 22 | if not os.path.exists(dst): 23 | os.makedirs(dst) 24 | for item in os.listdir(src): 25 | s = os.path.join(src, item) 26 | d = os.path.join(dst, item) 27 | if item in ignore: 28 | print("[Ignored] " + s) 29 | continue 30 | if os.path.isdir(s): 31 | copyDIR(s, d, cover, ignore) 32 | else: 33 | if cover is True or ( 34 | not os.path.exists(d) or os.stat(s).st_mtime - os.stat(d).st_mtime > 1 35 | ): 36 | shutil.copy2(s, d) 37 | print("[Copied] %s -> %s" % (s, d)) 38 | 39 | 40 | # 清空文件夹 41 | def deleteDIR(folder): 42 | if not os.path.exists(folder): 43 | return 44 | for filename in os.listdir(folder): 45 | file_path = os.path.join(folder, filename) 46 | try: 47 | if os.path.isfile(file_path) or os.path.islink(file_path): 48 | os.unlink(file_path) 49 | elif os.path.isdir(file_path): 50 | shutil.rmtree(file_path) 51 | except Exception as e: 52 | print("Failed to delete %s. Reason: %s" % (file_path, e)) 53 | 54 | 55 | # 替换文件 56 | def replaceFile(ori, dst): 57 | if not os.path.exists(ori) or not os.path.exists(dst): 58 | return False 59 | print("[Replaced] %s -> %s" % (ori, dst)) 60 | with open(ori, "r", encoding="utf-8") as f: 61 | data = f.read() 62 | with open(dst, "w", encoding="utf-8") as f: 63 | f.write(data) 64 | 65 | 66 | # 列出路径下所有文件 67 | def files(path, frmt="*", OTH=["", "pyc", "DS_Store"]): 68 | tmp = [] 69 | r = [] 70 | for x in list(Path(path).glob("*")): 71 | if os.path.isdir(x): 72 | tmp += files(x, frmt) 73 | else: 74 | if frmt == "*" or len(str(x).split(frmt)) > 1: 75 | tmp.append([str(x), x.stem, x.suffix[1:]]) 76 | for x in tmp: 77 | if x[2] in OTH: 78 | continue 79 | r.append(x) 80 | return r 81 | 82 | 83 | if __name__ == "__main__": 84 | silent = False 85 | if len(sys.argv) == 2: 86 | if sys.argv[1] == "auto": 87 | silent = True 88 | args = { 89 | "-F": "", 90 | "--distpath": DIST_PATH, 91 | "--workpath": os.path.join(TMP_PATH, "build"), 92 | "--specpath": CODE_PATH, 93 | } 94 | oargs = [] 95 | BET = ";" if os.name == "nt" else ":" 96 | SPT = "\\" if os.name == "nt" else "/" 97 | 98 | # CloudScraper 99 | if silent or input("是否替换 CloudScraper/user_agent/__init__.py 文件?") == "y": 100 | cdsr = os.path.dirname(cloudscraper.__file__) 101 | replaceFile( 102 | f"{ROOT_PATH}{SPT}r_cloudscraper.py", 103 | f"{cdsr}{SPT}user_agent{SPT}__init__.py", 104 | ) 105 | 106 | # pixivpy 107 | if silent or input("是否替换 pixivpy3/bapi.py 文件?") == "y": 108 | pxpy = os.path.dirname(pixivpy3.__file__) 109 | replaceFile(f"{ROOT_PATH}{SPT}r_pixivpy.py", f"{pxpy}{SPT}bapi.py") 110 | 111 | # 导入动态加载的文件、模块 112 | allImportLines = [] 113 | for x in files(os.path.join(CODE_PATH, "app")): 114 | print(x) 115 | ori = x[0].replace(CODE_PATH, "") 116 | dest = "/".join(ori.split(SPT)[:-1]) 117 | oargs.append(f"--add-data {ori[1:]}{BET}{dest[1:]}") 118 | # 分析动态加载文件中所使用的包 119 | if x[2] == "py": 120 | with open(x[0], "r", encoding="UTF-8") as f: 121 | lines = f.readlines() 122 | for line in lines: 123 | if ( 124 | line[:4] == "from" or line[:6] == "import" 125 | ) and line not in allImportLines: 126 | allImportLines.append(line) 127 | with open(os.path.join(CODE_PATH, "main.py"), "r+", encoding="UTF-8") as f: 128 | content = f.read() 129 | f.seek(0, 0) 130 | f.write("\n".join(allImportLines) + content) 131 | 132 | # 图标加载 133 | if os.name == "nt": 134 | args["--icon"] = os.path.join(ROOT_PATH, "r_pixiv.ico") 135 | 136 | # 参数拼接 137 | forarg = "" 138 | for x in args: 139 | forarg += " " + x + (" " if args[x] != "" else "") + args[x] 140 | for x in oargs: 141 | forarg += " " + x 142 | 143 | # 清空 DIST 文件夹 144 | if silent or input("是否清空 DIST 生成文件夹?") == "y": 145 | deleteDIR(DIST_PATH) 146 | 147 | # 复制 PUBLIC 文件 148 | copyDIR( 149 | PUBLIC_PATH, 150 | DIST_PATH, 151 | True, 152 | ["cache", "__pycache__", ".token", ".DS_Store"], 153 | ) 154 | 155 | # PyInstaller 打包 156 | os.system("pyinstaller%s %s" % (forarg, os.path.join(CODE_PATH, "main.py"))) 157 | 158 | # 清空 TMP 文件夹 159 | if silent or input("是否清空 TMP 缓存文件夹?") == "y": 160 | deleteDIR(TMP_PATH) 161 | -------------------------------------------------------------------------------- /.pkg/r_cloudscraper.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | import re 5 | import sys 6 | import ssl 7 | 8 | from collections import OrderedDict 9 | 10 | # ------------------------------------------------------------------------------- # 11 | 12 | 13 | class User_Agent(): 14 | 15 | # ------------------------------------------------------------------------------- # 16 | 17 | def __init__(self, *args, **kwargs): 18 | self.headers = None 19 | self.cipherSuite = [] 20 | self.loadUserAgent(*args, **kwargs) 21 | 22 | # ------------------------------------------------------------------------------- # 23 | 24 | def filterAgents(self, user_agents): 25 | filtered = {} 26 | 27 | if self.mobile: 28 | if self.platform in user_agents['mobile'] and user_agents['mobile'][self.platform]: 29 | filtered.update(user_agents['mobile'][self.platform]) 30 | 31 | if self.desktop: 32 | if self.platform in user_agents['desktop'] and user_agents['desktop'][self.platform]: 33 | filtered.update(user_agents['desktop'][self.platform]) 34 | 35 | return filtered 36 | 37 | # ------------------------------------------------------------------------------- # 38 | 39 | def tryMatchCustom(self, user_agents): 40 | for device_type in user_agents['user_agents']: 41 | for platform in user_agents['user_agents'][device_type]: 42 | for browser in user_agents['user_agents'][device_type][platform]: 43 | if re.search(re.escape(self.custom), ' '.join(user_agents['user_agents'][device_type][platform][browser])): 44 | self.headers = user_agents['headers'][browser] 45 | self.headers['User-Agent'] = self.custom 46 | self.cipherSuite = user_agents['cipherSuite'][browser] 47 | return True 48 | return False 49 | 50 | # ------------------------------------------------------------------------------- # 51 | 52 | def loadUserAgent(self, *args, **kwargs): 53 | self.browser = kwargs.pop('browser', None) 54 | 55 | self.platforms = ['linux', 'windows', 'darwin', 'android', 'ios'] 56 | self.browsers = ['chrome', 'firefox'] 57 | 58 | if isinstance(self.browser, dict): 59 | self.custom = self.browser.get('custom', None) 60 | self.platform = self.browser.get('platform', None) 61 | self.desktop = self.browser.get('desktop', True) 62 | self.mobile = self.browser.get('mobile', True) 63 | self.browser = self.browser.get('browser', None) 64 | else: 65 | self.custom = kwargs.pop('custom', None) 66 | self.platform = kwargs.pop('platform', None) 67 | self.desktop = kwargs.pop('desktop', True) 68 | self.mobile = kwargs.pop('mobile', True) 69 | 70 | if not self.desktop and not self.mobile: 71 | sys.tracebacklimit = 0 72 | raise RuntimeError("Sorry you can't have mobile and desktop disabled at the same time.") 73 | 74 | try: 75 | with open(os.path.join(os.path.split(os.path.realpath(sys.argv[0]))[0], 'usr/static/browsers.json'), 'r') as fp: 76 | user_agents = json.load( 77 | fp, 78 | object_pairs_hook=OrderedDict 79 | ) 80 | except: 81 | with open(os.path.join(os.path.dirname(__file__), 'browsers.json'), 'r') as fp: 82 | user_agents = json.load( 83 | fp, 84 | object_pairs_hook=OrderedDict 85 | ) 86 | 87 | if self.custom: 88 | if not self.tryMatchCustom(user_agents): 89 | self.cipherSuite = [ 90 | ssl._DEFAULT_CIPHERS, 91 | '!AES128-SHA', 92 | '!ECDHE-RSA-AES256-SHA', 93 | ] 94 | self.headers = OrderedDict([ 95 | ('User-Agent', self.custom), 96 | ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'), 97 | ('Accept-Language', 'en-US,en;q=0.9'), 98 | ('Accept-Encoding', 'gzip, deflate, br') 99 | ]) 100 | else: 101 | if self.browser and self.browser not in self.browsers: 102 | sys.tracebacklimit = 0 103 | raise RuntimeError(f'Sorry "{self.browser}" browser is not valid, valid browsers are [{", ".join(self.browsers)}].') 104 | 105 | if not self.platform: 106 | self.platform = random.SystemRandom().choice(self.platforms) 107 | 108 | if self.platform not in self.platforms: 109 | sys.tracebacklimit = 0 110 | raise RuntimeError(f'Sorry the platform "{self.platform}" is not valid, valid platforms are [{", ".join(self.platforms)}]') 111 | 112 | filteredAgents = self.filterAgents(user_agents['user_agents']) 113 | 114 | if not self.browser: 115 | # has to be at least one in there... 116 | while not filteredAgents.get(self.browser): 117 | self.browser = random.SystemRandom().choice(list(filteredAgents.keys())) 118 | 119 | if not filteredAgents[self.browser]: 120 | sys.tracebacklimit = 0 121 | raise RuntimeError(f'Sorry "{self.browser}" browser was not found with a platform of "{self.platform}".') 122 | 123 | self.cipherSuite = user_agents['cipherSuite'][self.browser] 124 | self.headers = user_agents['headers'][self.browser] 125 | 126 | self.headers['User-Agent'] = random.SystemRandom().choice(filteredAgents[self.browser]) 127 | 128 | if not kwargs.get('allow_brotli', False) and 'br' in self.headers['Accept-Encoding']: 129 | self.headers['Accept-Encoding'] = ','.join([ 130 | encoding for encoding in self.headers['Accept-Encoding'].split(',') if encoding.strip() != 'br' 131 | ]).strip() 132 | -------------------------------------------------------------------------------- /.pkg/r_pixiv.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/.pkg/r_pixiv.ico -------------------------------------------------------------------------------- /.pkg/r_pixivpy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import requests 4 | from requests_toolbelt.adapters import host_header_ssl 5 | 6 | from .aapi import AppPixivAPI 7 | 8 | 9 | class ByPassSniApi(AppPixivAPI): 10 | def __init__(self, **requests_kwargs): 11 | """initialize requests kwargs if need be""" 12 | super(AppPixivAPI, self).__init__(**requests_kwargs) 13 | session = requests.Session() 14 | session.mount("https://", host_header_ssl.HostHeaderSSLAdapter()) 15 | self.requests = session 16 | 17 | def require_appapi_hosts(self, hostname="app-api.pixiv.net", timeout=3): 18 | """ 19 | 通过 DoH 服务请求真实的 IP 地址。 20 | """ 21 | URLS = ( 22 | "https://1.0.0.1/dns-query", 23 | "https://1.1.1.1/dns-query", 24 | "https://doh.dns.sb/dns-query", 25 | "https://cloudflare-dns.com/dns-query", 26 | ) 27 | headers = {"Accept": "application/dns-json"} 28 | params = { 29 | "name": hostname, 30 | "type": "A", 31 | "do": "false", 32 | "cd": "false", 33 | } 34 | 35 | for url in URLS: 36 | try: 37 | response = requests.get( 38 | url, headers=headers, params=params, timeout=timeout 39 | ) 40 | self.hosts = "https://" + str(response.json()["Answer"][0]["data"]) 41 | return self.hosts 42 | except Exception: 43 | pass 44 | 45 | return False 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Trii Hsia 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PixivBiu 2 | 3 | PixivBiu,一款不错的 Pixiv **辅助**工具。 4 | 5 | - [中文](/README.md) 6 | - [English](/docs/README_EN.md) 7 | - [日本語](/docs/README_JA.md) 8 | - [Español](/docs/README_ES.md) 9 | 10 | ## 基础功能 11 | 12 | * Pixiv 搜索,可免会员按收藏数、人气、日期排序 13 | * 下载原始图片,包括插画、漫画、动图 14 | * 多种下载模式,单、多线程模式以及 aria2 支持 15 | * 获取用户的作品、收藏夹、关注列表、推荐等 16 | * 筛选图片的宽高、类型、标签等 17 | 18 | ## 使用 19 | 20 | ### 源码 21 | 22 | * 安装依赖,执行 `pip install -r requirements.txt` 23 | + [Flask](https://github.com/pallets/flask)、[requests](https://github.com/psf/requests)、[PyYAML](https://github.com/yaml/pyyaml)、[Pillow](https://github.com/python-pillow/Pillow)、[PixivPy](https://github.com/upbit/pixivpy)、[PySocks](https://github.com/Anorov/PySocks) 24 | * 修改 `./config.yml` 相关配置项,具体可参考[默认配置文件](/app/config/biu_default.yml) 25 | * 执行 `python main.py` 26 | * 访问运行地址,默认为 `http://127.0.0.1:4001/` 27 | 28 | ### 已编译程序 29 | 30 | 此项目基于 `Python@3.10(+)` 编写,使用 `PyInstaller` 构建编译版本。 31 | 32 | 这里提供 Windows、macOS 和 Ubuntu 的编译版本,如有其他需求请自行编译。 33 | 34 | 具体可在 [GitHub Releases](https://github.com/txperl/PixivBiu/releases) 中下载,或者[在这](https://biu.tls.moe/#/lib/dl)下载。 35 | 36 | ### Docker 37 | 38 | - [Docker_Buildx_PixivBiu](https://github.com/zzcabc/Docker_Buildx_PixivBiu) by [zzcabc](https://github.com/zzcabc) 39 | 40 | ## 贡献维护 41 | 42 | 如果你想参与此项目的开发,欢迎查看[开发文档](https://biu.tls.moe/#/develop/quickin)。 43 | 44 | ## 其他 45 | 46 | ### 感谢 47 | 48 | * [pixivpy](https://github.com/upbit/pixivpy) API 支持 49 | * [pixiv.cat](https://pixiv.cat/) 反代服务器支持 50 | * [HTML5 UP](https://html5up.net/) 前端代码支持 51 | 52 | ### 条款 53 | 54 | * 本程序(PixivBiu)仅供学习交流,最初目的达成后请自行删除 55 | * 使用后任何不可知事件都与原作者无关,原作者不承担任何后果 56 | * [MIT License](https://choosealicense.com/licenses/mit/) 57 | -------------------------------------------------------------------------------- /altfe/bridge.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | import os 3 | import random 4 | import string 5 | 6 | from altfe.interface.root import classRoot 7 | 8 | 9 | class bridgeInit(classRoot): 10 | """ 11 | Altfe 模块加载与初始化核心。 12 | """ 13 | 14 | def __init__(self): 15 | self.rootPath = self.getENV("rootPathFrozen") 16 | self.APP_PATH = { 17 | "ins": self.rootPath + "app/lib/ins/", 18 | "static": self.rootPath + "app/lib/static/", 19 | "common": self.rootPath + "app/lib/common/", 20 | "core": self.rootPath + "app/lib/core/", 21 | "plugin": self.rootPath + "app/plugin/" 22 | } 23 | 24 | def run(self, hint=False): 25 | if hint: 26 | print("[Altfe] ;)") 27 | bridgeInit.load_all(self.read_all_modules()) 28 | classRoot.mount(["LIB_STATIC"]) 29 | classRoot.instantiate(["LIB_INS"]) 30 | classRoot.mount(["LIB_INS", "LIB_COMMON"]) 31 | classRoot.instantiate(["LIB_CORE", "PRE"]) 32 | classRoot.mount(["LIB_CORE", "PRE", "PLUGIN"]) 33 | 34 | def read_all_modules(self): 35 | r = [] 36 | conf = {} 37 | for moduleType in self.APP_PATH: 38 | rootModulePath = self.APP_PATH[moduleType] 39 | files = os.listdir(rootModulePath) 40 | files.sort() 41 | for fileName in files: 42 | # skip 43 | if not bridgeInit.is_load(conf, moduleType, fileName): 44 | continue 45 | # read 46 | moduleName = "%s_%s_%s" % (moduleType, "".join( 47 | random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(5)), fileName) 48 | filePath = rootModulePath + fileName 49 | # dir handle 50 | if os.path.isdir(filePath): 51 | filePath += "/" 52 | tmp = os.listdir(filePath) 53 | if "main.py" in tmp: 54 | r.append([moduleName, filePath + "main.py"]) 55 | else: 56 | for x in tmp: 57 | if x[-3:] == ".py" and bridgeInit.is_load(conf, moduleType, x): 58 | r.append([moduleName, filePath + x]) 59 | elif fileName[-3:] == ".py": 60 | r.append([moduleName, filePath]) 61 | return r 62 | 63 | @staticmethod 64 | def is_load(conf, moduleType, fileName): 65 | if moduleType in conf and conf[moduleType] is not None and fileName in conf[moduleType]: 66 | if not conf[moduleType][fileName]: 67 | return False 68 | return True 69 | 70 | @staticmethod 71 | def load_all(modules): 72 | for x in modules: 73 | bridgeInit.load_single(*x) 74 | 75 | @staticmethod 76 | def load_single(name, path): 77 | spec = importlib.util.spec_from_file_location(name, path) 78 | cls = importlib.util.module_from_spec(spec) 79 | spec.loader.exec_module(cls) 80 | -------------------------------------------------------------------------------- /altfe/handle.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from altfe.interface.root import classRoot 4 | 5 | 6 | class handleRoute(classRoot): 7 | """ 8 | Altfe 指令处理核心。 9 | """ 10 | 11 | @classmethod 12 | def do(cls, cmd): 13 | # 指令选择 14 | rCMD = cmd 15 | if cmd not in cls.AVALS["PLUGIN"]: 16 | rCMD = None 17 | for x in cls.AVALS["PLUGIN"]: 18 | if x == cmd[:len(x)]: 19 | rCMD = x 20 | break 21 | if rCMD is None: 22 | return {"code": 0, "msg": "no method"} 23 | 24 | # 执行预处理函数 25 | # preFuns = cls.osGet("PRE") 26 | # for name in preFuns: 27 | # if not preFuns[name].run(rCMD): 28 | # return {"code": 403, "msg": f"[PRE] Forbidden by {name}"} 29 | 30 | # 执行指令并返回 31 | try: 32 | r = cls.osGet("PLUGIN", rCMD)().run(cmd.split(rCMD)[1]) 33 | return r 34 | except: 35 | cls.STATIC.localMsger.error(traceback.format_exc()) 36 | 37 | return {"code": 500, "msg": "plugin error"} 38 | -------------------------------------------------------------------------------- /altfe/interface/root.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import yaml 5 | 6 | 7 | class classEmpty(object): 8 | pass 9 | 10 | 11 | class classRoot(object): 12 | """ 13 | 根类,存储环境变量以及所有模块。 14 | """ 15 | __ENV = {} 16 | __MODULE = { 17 | "LIB_INS": {}, 18 | "LIB_STATIC": {}, 19 | "LIB_COMMON": {}, 20 | "LIB_CORE": {}, 21 | "PRE": {}, 22 | "PLUGIN": {} 23 | } 24 | AVALS = { 25 | "PRE": [], 26 | "PLUGIN": [] 27 | } 28 | 29 | @classmethod 30 | def setENV(cls, key, val): 31 | cls.__ENV[key] = val 32 | 33 | @classmethod 34 | def getENV(cls, key): 35 | if key in cls.__ENV: 36 | return cls.__ENV[key] 37 | return None 38 | 39 | @classmethod 40 | def osGet(cls, key, name=None): 41 | if key in cls.__MODULE: 42 | if name is None: 43 | return cls.__MODULE[key] 44 | elif name in cls.__MODULE[key]: 45 | return cls.__MODULE[key][name] 46 | return None 47 | 48 | @classmethod 49 | def instantiate(cls, keys=None): 50 | if keys is None: 51 | keys = cls.__MODULE.keys() 52 | for key in keys: 53 | if key not in cls.__MODULE: 54 | continue 55 | for name in cls.__MODULE[key]: 56 | cls.__MODULE[key][name] = cls.__MODULE[key][name]() 57 | 58 | @classmethod 59 | def mount(cls, keys=None): 60 | if keys is None: 61 | keys = cls.__MODULE.keys() 62 | for key in keys: 63 | if key not in cls.__MODULE: 64 | continue 65 | if "LIB_" in key: 66 | obj = classEmpty() 67 | for name in cls.__MODULE[key]: 68 | setattr(obj, name, cls.__MODULE[key][name]) 69 | setattr(cls, key[4:], obj) 70 | elif key in ("PRE", "PLUGIN"): 71 | cls.AVALS[key] = cls.__MODULE[key].keys() 72 | 73 | @classmethod 74 | def bind(cls, moduleName, key): 75 | if key not in cls.__MODULE: 76 | return 77 | 78 | def wrapper(module): 79 | cls.__MODULE[key].update({moduleName: module}) 80 | 81 | return wrapper 82 | 83 | @staticmethod 84 | def loadConfig(uri, default=False): 85 | if not os.path.exists(uri): 86 | return default 87 | with open(uri, "r", encoding="UTF-8") as f: 88 | try: 89 | sfx = uri.split(".")[-1] 90 | if sfx == "json": 91 | final = json.load(f) 92 | elif sfx == "yml" or sfx == "yaml": 93 | final = yaml.safe_load(f) 94 | else: 95 | final = f.read() 96 | if final is None: 97 | return default 98 | return final 99 | except: 100 | return default 101 | 102 | 103 | class interRoot(classRoot): 104 | """ 105 | 通用接口根类。 106 | """ 107 | 108 | @classmethod 109 | def osGet(cls): 110 | return 111 | 112 | @classmethod 113 | def instantiate(cls): 114 | return 115 | 116 | @classmethod 117 | def mount(cls): 118 | return 119 | -------------------------------------------------------------------------------- /app/config/biu_default.yml: -------------------------------------------------------------------------------- 1 | # You can find the default config files 2 | # of multiple languages (中文, English, 日本語, Español) 3 | # at https://github.com/txperl/PixivBiu/tree/master/app/config 4 | 5 | ## 系统相关 ## 6 | sys.host: "127.0.0.1:4001" 7 | # 程序运行的地址,如果不是很懂请不要修改 8 | # 不可带有 http:// 前缀 9 | 10 | sys.debug: false 11 | # 调试模式 12 | # true 为开启;false 为关闭 13 | 14 | sys.apiRoute: "direct" 15 | # 默认 API 线路 16 | # 如果能直接访问 Pixiv,请选择 "direct" 17 | # direct: 稳定 18 | # bypassSNI: 稳定性差,不推荐 19 | 20 | sys.proxy: "" 21 | # 本地代理服务监听地址 22 | # 如 http://127.0.0.1:1080/ 23 | # 留空则程序会自动检测系统代理设置(仅 Windows、macOS) 24 | # 填入 no 则不使用任何代理 25 | 26 | sys.language: "" 27 | # 语言设置,留空为程序自动判断 28 | # 可选:zh、en、es 29 | 30 | sys.theme: "multiverse" 31 | # 默认主题 32 | # 暂无其他,请保持默认为 multiverse 33 | 34 | sys.autoOpen: true 35 | # 启动后是否自动打开程序运行网址 36 | # true 为开启;false 为关闭 37 | 38 | 39 | ## 搜索相关 ## 40 | biu.search.maxThreads: 8 41 | # 搜索池最大线程数 42 | 43 | biu.search.loadCacheFirst: true 44 | # 搜索时优先加载本地缓存 45 | # true 为开启;false 为关闭 46 | 47 | biu.search.maxCacheSizeMiB: 512 48 | # 搜索缓存的最大容量(MB),达到后程序会自动清理 49 | 50 | 51 | ## 下载相关 ## 52 | biu.download.mode: "dl-single" 53 | # 下载模式 54 | # dl-single: 程序单线程下载 55 | # aria2: 使用 aria2 下载,如果启用此项,还需填写下方的 aria2Host、aria2Secret 56 | 57 | biu.download.aria2Host: "" 58 | # aria2 RPC 监听地址,如 localhost:6800 59 | # 下载时 dir, out, all-proxy, max-tries, check-certificate 配置项会由程序决定 60 | # 其余均为默认或自定义内容 61 | 62 | biu.download.aria2Secret: "" 63 | # aria2 RPC 密钥,如未设置请留空 64 | 65 | biu.download.deterPaths: true 66 | # 程序动态决定文件名 67 | # 启用后,程序会先以随机名称保存图片,后更改为下方自定义的 {saveFileName} 文件名 68 | # 此过程中,如果存在文件名相同的文件,则程序会自动判断它们是否为同一张图片 69 | # 若是,则只保留一份 70 | # 若不是,则自动将新图片重命名为 {saveFileName}_{hash} 或 {saveFileName}_{hash}_{time} 格式 71 | # 停用后,程序会直接将图片保存为 {saveFileName} 文件名 72 | # 若 {saveFileName} 格式存在唯一性,或欲使用 aria2 的重名文件策略,可将此功能关闭以提升性能 73 | # 若 aria2 服务地址并非 localhost 或 127.0.0.1 时,程序会自动禁用此项 74 | # true 为开启;false 为关闭 75 | 76 | biu.download.maxDownloading: 8 77 | # 最大同时下载任务数 78 | 79 | biu.download.saveURI: "{ROOTPATH}/downloads/{date_today}/" 80 | # 下载文件夹路径,以 / 结尾 81 | # 不可使用 \ 符号,请将其替换为 / 或 \\ 82 | # 可选变量 83 | # - {ROOTPATH}: 程序根目录 84 | # - {HOMEPATH}: 用户主目录,可能为 /Users/user/、C:/Users/user/、/home/user/ 85 | # - {KT}: 搜索关键词 86 | # - {title}: 作品标题 87 | # - {work_id}: 作品 ID 88 | # - {user_name}: 作者名称 89 | # - {user_id}: 作者 ID 90 | # - {type}: 作品类型 91 | # - {date_image}: 作品日期 92 | # - {date_today}: 当天日期 93 | 94 | biu.download.saveFileName: "{title}" 95 | # 下载图片的标题 96 | # 不可使用 \ 符号,请将其替换为 \\ 97 | # 可选变量 98 | # - {title}: 作品标题 99 | # - {work_id}: 作品 ID 100 | # - {user_name}: 作者名称 101 | # - {user_id}: 作者 ID 102 | # - {type}: 作品类型 103 | # - {date_image}: 作品日期 104 | # - {date_today}: 当天日期 105 | 106 | biu.download.autoArchive: true 107 | # 自动将拥有多张图片的作品归档(放入一个文件夹中) 108 | # true 为开启;false 为关闭 109 | 110 | biu.download.whatsUgoira: "webp" 111 | # 将动图转换为指定格式 112 | # 可选 webp, gif 113 | 114 | biu.download.imageHost: "" 115 | # 后端程序下载时使用的 Pixiv 图片服务器 116 | # 留空则程序自动判断 117 | # 可参考地址 118 | # - https://i.pximg.net 官方图片服务器(需代理) 119 | # - https://i.pixiv.cat 第三方反代图片服务器(需代理) 120 | # - https://i.pixiv.re 第三方反代图片服务器(无需代理) 121 | 122 | 123 | ## 私密 ## 124 | secret.key.apiSauceNAO: "" 125 | # SauceNAO 服务的 API Key 126 | # 若填写则可使用图片搜索功能 127 | # https://saucenao.com/user.php?page=search-api 128 | # 进入以上网址登录后,即可在页面上找到「api key」内容 129 | -------------------------------------------------------------------------------- /app/config/biu_en.yml: -------------------------------------------------------------------------------- 1 | # You can find the default config files 2 | # of multiple languages (中文, English, 日本語, Español) 3 | # at https://github.com/txperl/PixivBiu/tree/master/app/config 4 | 5 | ## System ## 6 | sys.host: "127.0.0.1:4001" 7 | # Address that program runs in 8 | # No need for "http://" prefix 9 | 10 | sys.debug: false 11 | # Enable for debug mode 12 | # Option: true, false 13 | 14 | sys.apiRoute: "direct" 15 | # Default API route mode 16 | # If you can visit Pixiv directly, just choose "direct" 17 | # Option: 18 | # - direct: stable 19 | # - bypassSNI: low-stable, not recommended 20 | 21 | sys.proxy: "" 22 | # Local proxy address 23 | # E.g. http://127.0.0.1:1080/ 24 | # Leave it blank, and program will detect automatically(Only for macOS, Windows) 25 | # Leave it "no", and no proxy will be used 26 | 27 | sys.language: "" 28 | # Language of program 29 | # Leave it blank, and program will detect automatically 30 | # Option: zh, en, es 31 | 32 | sys.theme: "multiverse" 33 | # Theme of webpage 34 | # Just keep it as multiverse 35 | 36 | sys.autoOpen: true 37 | # Whether to automatically open the program running URL after startup 38 | # Option: true, false 39 | 40 | 41 | ## Search ## 42 | biu.search.maxThreads: 8 43 | # Maximum number of threads in the search pool 44 | 45 | biu.search.loadCacheFirst: true 46 | # Whether to load the local cache first while searching 47 | # Option: true, false 48 | 49 | biu.search.maxCacheSizeMiB: 512 50 | # Maximum size of the search cache (MB) 51 | # Program will automatically clear it when reached 52 | 53 | 54 | ## Download ## 55 | biu.download.mode: "dl-single" 56 | # Download mode 57 | # Option: 58 | # - dl-single: default mode inside program 59 | # - aria2: if chosen, you also need to fill in the aria2Host and aria2Secret below 60 | 61 | biu.download.aria2Host: "" 62 | # Address of aria2 RPC, e.g. localhost:6800 63 | # While downloading 64 | # Setting items of "dir, out, all-proxy, max-tries, check-certificate" will be taken charge by program 65 | # The rest are default or custom ones 66 | 67 | biu.download.aria2Secret: "" 68 | # Secret of aria2 RPC 69 | 70 | biu.download.deterPaths: true 71 | # Dynamical Filename 72 | # If enabled, program will save images with a random name first, and then change it to {saveFileName} below 73 | # In this progress 74 | # If there're multiple images with the same final name in the download folder 75 | # Program will automatically determine whether they are the same image 76 | # If they are, only one copy will be kept 77 | # If not, the new image will be renamed to ones like {saveFileName}_{hash}, {saveFileName}_{hash}_{time} 78 | # If disabled, the image will be saved as {saveFileName} directly 79 | # If the {saveFileName} format is unique, or you want to use aria2's duplicate file name strategy, you can turn it off to improve performance 80 | # If the aria2 address is not "localhost" or "127.0.0.1", this option will be always disabled 81 | # Option: true, false 82 | 83 | biu.download.maxDownloading: 8 84 | # Maximum number of simultaneous download tasks 85 | 86 | biu.download.saveURI: "{ROOTPATH}/downloads/{date_today}/" 87 | # Download directory, a folder, ending with "/" 88 | # Don't use the "\" symbol, replace it with "/" or "\\" 89 | # Variables: 90 | # - {ROOTPATH}: Program directory 91 | # - {HOMEPATH}: User home directory,it might be "/Users/user/", "C:/Users/user/", "/home/user/"" 92 | # - {KT}: Search keywords 93 | # - {title}: Work title 94 | # - {work_id}: Work ID 95 | # - {user_name}: Author name 96 | # - {user_id}: Author ID 97 | # - {type}: Work type 98 | # - {date_image}: Date of work 99 | # - {date_today}: Date of today 100 | 101 | biu.download.saveFileName: "{title}" 102 | # Download filename 103 | # Don't use the "\" symbol, replace it with "/" or "\\" 104 | # Variables: 105 | # - {title}: Work title 106 | # - {work_id}: Work ID 107 | # - {user_name}: Author name 108 | # - {user_id}: Author ID 109 | # - {type}: Work type 110 | # - {date_image}: Date of work 111 | # - {date_today}: Date of today 112 | 113 | biu.download.autoArchive: true 114 | # Archive multiple images (into a folder) automatically 115 | # Option: true, false 116 | 117 | biu.download.whatsUgoira: "webp" 118 | # Convert the live image to the specified format 119 | # Option: webp, gif 120 | 121 | biu.download.imageHost: "" 122 | # Pixiv image address used by backend program 123 | # Leave it blank, and program will detect automatically 124 | # E.g. 125 | # - https://i.pximg.net (Official image address) 126 | # - https://i.pixiv.cat 127 | # - https://i.pixiv.re 128 | 129 | 130 | ## Secrets ## 131 | secret.key.apiSauceNAO: "" 132 | # The API key of SauceNAO 133 | # If filled in, image search feature will be available 134 | # https://saucenao.com/user.php?page=search-api 135 | # After logging in to the URL above, you can find the "api key" directly 136 | -------------------------------------------------------------------------------- /app/config/biu_es.yml: -------------------------------------------------------------------------------- 1 | # You can find the default config files 2 | # of multiple languages (中文, English, 日本語, Español) 3 | # at https://github.com/txperl/PixivBiu/tree/master/app/config 4 | 5 | ## Relacionado con el sistema ## 6 | sys.host: "127.0.0.1:4001" 7 | # La dirección de ejecución del programa; si no entiendes bien, por favor no la modifiques. 8 | # No debe llevar el prefijo http:// 9 | 10 | sys.debug: false 11 | # Modo de depuración 12 | # true para activar; false para desactivar 13 | 14 | sys.apiRoute: "direct" 15 | # Línea de API predeterminada 16 | # direct: estable, requiere proxy 17 | # bypassSNI: estabilidad moderada, no requiere proxy 18 | 19 | sys.proxy: "" 20 | # Dirección de escucha del servicio de proxy local 21 | # Ejemplo: http://127.0.0.1:1080/ 22 | # Si se deja en blanco, el programa detectará automáticamente la configuración de proxy del sistema (solo en Windows y macOS) 23 | # Si se ingresa "no", no se usará ningún proxy 24 | 25 | sys.language: "" 26 | # Configuración de idioma, dejar en blanco para detección automática 27 | # Opciones: zh, en, es 28 | 29 | sys.theme: "multiverse" 30 | # Tema predeterminado 31 | # No hay otros disponibles por ahora, mantener como multiverse 32 | 33 | sys.autoOpen: true 34 | # ¿Abrir automáticamente la URL de ejecución del programa al iniciar? 35 | # true para activar; false para desactivar 36 | 37 | 38 | ## Relacionado con la búsqueda ## 39 | biu.search.maxThreads: 8 40 | # Número máximo de hilos en el grupo de búsqueda 41 | 42 | biu.search.loadCacheFirst: true 43 | # Priorizar la carga del caché local al buscar 44 | # true para activar; false para desactivar 45 | 46 | biu.search.maxCacheSizeMiB: 512 47 | # Capacidad máxima de la caché de búsqueda (MB); al alcanzarla, el programa se limpiará automáticamente. 48 | 49 | ## Relacionado con la descarga ## 50 | biu.download.mode: "dl-single" 51 | # Modo de descarga 52 | # dl-single: descarga en un solo hilo 53 | # aria2: usar aria2 para descargar; si se activa esta opción, también se deben completar aria2Host y aria2Secret a continuación 54 | 55 | biu.download.aria2Host: "" 56 | # Dirección de escucha de RPC de aria2, como localhost:6800 57 | # Durante la descarga, los parámetros dir, out, all-proxy, max-tries y check-certificate serán decididos por el programa 58 | # El resto serán contenidos predeterminados o personalizados 59 | 60 | biu.download.aria2Secret: "" 61 | # Clave RPC de aria2; si no está configurada, déjala en blanco 62 | 63 | biu.download.deterPaths: true 64 | # El programa decide dinámicamente el nombre del archivo 65 | # Al activarse, el programa primero guardará la imagen con un nombre aleatorio y luego lo cambiará al nombre personalizado {saveFileName} 66 | # Durante este proceso, se comprobará automáticamente si las imágenes con el mismo nombre en el directorio de descarga son la misma obra 67 | # Si lo son, solo se conservará una; si no, la nueva imagen se renombrará automáticamente 68 | # El nuevo nombre de archivo tendrá el formato {saveFileName}_{hash} o {saveFileName}_{hash}_{time} 69 | # Al desactivarse, el programa guardará la imagen directamente con el nombre de archivo personalizado {saveFileName} 70 | # Si el formato de {saveFileName} es único o deseas usar la estrategia de archivos con nombre duplicado de aria2, puedes desactivar esta función para mejorar el rendimiento 71 | # Si la dirección del servicio de aria2 no es localhost o 127.0.0.1, esta opción se desactivará automáticamente 72 | # true para activar; false para desactivar 73 | 74 | biu.download.maxDownloading: 8 75 | # Número máximo de tareas de descarga simultáneas 76 | 77 | biu.download.saveURI: "{ROOTPATH}/downloads/{date_today}/" 78 | # Ruta de guardado de descargas, debe terminar con / 79 | # No se puede usar el símbolo \; por favor, reemplázalo por / o \\ 80 | # Variables opcionales 81 | # - {ROOTPATH}: directorio raíz del programa 82 | # - {HOMEPATH}: directorio principal del usuario, puede ser /Users/user/, C:/Users/user/, /home/user/ 83 | # - {KT}: palabra clave de búsqueda 84 | # - {title}: título de la obra 85 | # - {work_id}: ID de la obra 86 | # - {user_name}: nombre del autor 87 | # - {user_id}: ID del autor 88 | # - {type}: tipo de obra 89 | # - {date_image}: fecha de la obra 90 | # - {date_today}: fecha de hoy 91 | 92 | biu.download.saveFileName: "{title}" 93 | # Título de la imagen descargada 94 | # No se puede usar el símbolo \; por favor, reemplázalo por \\ 95 | # Variables opcionales 96 | # - {title}: título de la obra 97 | # - {work_id}: ID de la obra 98 | # - {user_name}: nombre del autor 99 | # - {user_id}: ID del autor 100 | # - {type}: tipo de obra 101 | # - {date_image}: fecha de la obra 102 | # - {date_today}: fecha de hoy 103 | 104 | biu.download.autoArchive: true 105 | # Archivar automáticamente obras con múltiples imágenes (colocarlas en una carpeta) 106 | # true para activar; false para desactivar 107 | 108 | biu.download.whatsUgoira: "webp" 109 | # Convertir animaciones a un formato específico 110 | # Opciones: webp, gif 111 | 112 | biu.download.imageHost: "" 113 | # Servidor de imágenes de Pixiv utilizado por el programa backend durante la descarga 114 | # Dejar en blanco para que el programa lo determine automáticamente 115 | # Direcciones de referencia 116 | # - https://i.pximg.net: servidor oficial de imágenes (requiere proxy) 117 | # - https://i.pixiv.cat: servidor de imágenes de terceros (requiere proxy) 118 | # - https://i.pixiv.re: servidor de imágenes de terceros (no requiere proxy) 119 | 120 | ## Privado ## 121 | secret.key.apiSauceNAO: "" 122 | # Clave API del servicio SauceNAO; si se completa, se podrá usar la función de búsqueda de imágenes 123 | # https://saucenao.com/user.php?page=search-api 124 | # Al ingresar a la URL anterior e iniciar sesión, podrás encontrar el contenido de "api key" en la página -------------------------------------------------------------------------------- /app/config/biu_ja.yml: -------------------------------------------------------------------------------- 1 | # You can find the default config files 2 | # of multiple languages (中文, English, 日本語, Español) 3 | # at https://github.com/txperl/PixivBiu/tree/master/app/config 4 | 5 | ## システム ## 6 | sys.host: "127.0.0.1:4001" 7 | # プログラムが実行されるアドレス 8 | # "http://" プレフィックスは不要 9 | 10 | sys.debug: false 11 | # デバッグモードを有効にする 12 | # オプション: true, false 13 | 14 | sys.apiRoute: "direct" 15 | # デフォルトのAPIルートモード 16 | # Pixivに直接アクセスできる場合は "direct" を選択 17 | # オプション: 18 | # - direct: 安定 19 | # - bypassSNI: 低安定性、非推奨 20 | 21 | sys.proxy: "" 22 | # ローカルプロキシアドレス 23 | # 例: http://127.0.0.1:1080/ 24 | # 空白の場合、自動検出します(macOS、Windowsのみ) 25 | # "no" の場合、プロキシを使用しません 26 | 27 | sys.language: "" 28 | # プログラムの言語 29 | # 空白の場合、自動検出します 30 | # オプション: zh, en, es 31 | 32 | sys.theme: "multiverse" 33 | # ウェブページのテーマ 34 | # multiverseのままにしてください 35 | 36 | sys.autoOpen: true 37 | # 起動時にプログラムの実行URLを自動で開くかどうか 38 | # オプション: true, false 39 | 40 | 41 | ## 検索 ## 42 | biu.search.maxThreads: 8 43 | # 検索プールの最大スレッド数 44 | 45 | biu.search.loadCacheFirst: true 46 | # 検索時にローカルキャッシュを優先的に読み込むかどうか 47 | # オプション: true, false 48 | 49 | biu.search.maxCacheSizeMiB: 512 50 | # 検索キャッシュの最大サイズ(MB) 51 | # 到達時に自動的にクリアされます 52 | 53 | 54 | ## ダウンロード ## 55 | biu.download.mode: "dl-single" 56 | # ダウンロードモード 57 | # オプション: 58 | # - dl-single: プログラム内のデフォルトモード 59 | # - aria2: 選択時、以下のaria2Hostとaria2Secretの入力が必要 60 | 61 | biu.download.aria2Host: "" 62 | # aria2 RPCのアドレス、例: localhost:6800 63 | # ダウンロード時 64 | # "dir, out, all-proxy, max-tries, check-certificate" の設定項目はプログラムが管理 65 | # その他はデフォルトまたはカスタム設定 66 | 67 | biu.download.aria2Secret: "" 68 | # aria2 RPCのシークレット 69 | 70 | biu.download.deterPaths: true 71 | # 動的ファイル名 72 | # 有効時、画像はランダム名で保存後、下記の{saveFileName}に変更 73 | # この過程で 74 | # ダウンロードフォルダ内に同じ最終名の画像が複数ある場合 75 | # プログラムは自動的に同一画像かを判定 76 | # 同一の場合、1つのみ保持 77 | # 異なる場合、新規画像は{saveFileName}_{hash}、{saveFileName}_{hash}_{time}などに変更 78 | # 無効時、画像は直接{saveFileName}として保存 79 | # {saveFileName}形式が一意、またはaria2の重複ファイル名戦略を使用したい場合、パフォーマンス向上のため無効化可能 80 | # aria2アドレスが"localhost"または"127.0.0.1"以外の場合、常に無効 81 | # オプション: true, false 82 | 83 | biu.download.maxDownloading: 8 84 | # 同時ダウンロードタスクの最大数 85 | 86 | biu.download.saveURI: "{ROOTPATH}/downloads/{date_today}/" 87 | # ダウンロードディレクトリ、フォルダ、"/"で終わる 88 | # "\"記号は使用せず、"/"または"\\"に置き換え 89 | # 変数: 90 | # - {ROOTPATH}: プログラムディレクトリ 91 | # - {HOMEPATH}: ユーザーホームディレクトリ、"/Users/user/"、"C:/Users/user/"、"/home/user/"など 92 | # - {KT}: 検索キーワード 93 | # - {title}: 作品タイトル 94 | # - {work_id}: 作品ID 95 | # - {user_name}: 作者名 96 | # - {user_id}: 作者ID 97 | # - {type}: 作品タイプ 98 | # - {date_image}: 作品の日付 99 | # - {date_today}: 今日の日付 100 | 101 | biu.download.saveFileName: "{title}" 102 | # ダウンロードファイル名 103 | # "\"記号は使用せず、"/"または"\\"に置き換え 104 | # 変数: 105 | # - {title}: 作品タイトル 106 | # - {work_id}: 作品ID 107 | # - {user_name}: 作者名 108 | # - {user_id}: 作者ID 109 | # - {type}: 作品タイプ 110 | # - {date_image}: 作品の日付 111 | # - {date_today}: 今日の日付 112 | 113 | biu.download.autoArchive: true 114 | # 複数画像を自動的にアーカイブ(フォルダに)するかどうか 115 | # オプション: true, false 116 | 117 | biu.download.whatsUgoira: "webp" 118 | # うごイラを指定フォーマットに変換 119 | # オプション: webp, gif 120 | 121 | biu.download.imageHost: "" 122 | # バックエンドプログラムが使用するPixiv画像アドレス 123 | # 空白の場合、自動検出します 124 | # 例: 125 | # - https://i.pximg.net (公式画像アドレス) 126 | # - https://i.pixiv.cat 127 | # - https://i.pixiv.re 128 | 129 | 130 | ## シークレット ## 131 | secret.key.apiSauceNAO: "" 132 | # SauceNAOのAPIキー 133 | # 入力すると画像検索機能が利用可能に 134 | # https://saucenao.com/user.php?page=search-api 135 | # 上記URLにログイン後、"api key"が直接確認可能 136 | 137 | # This file is translated by Claude 138 | -------------------------------------------------------------------------------- /app/config/language/en.yml: -------------------------------------------------------------------------------- 1 | app.core.biu: 2 | common: 3 | press_to_exit: "Press any key to exit..." 4 | success_to_login: "Login successful" 5 | config: 6 | hint_port_is_in_use: "Current port is in use, please modify the 'sys.host' item in config.yml" 7 | hint_proxy_in_use: "Proxy %s enabled" 8 | fail_to_load_config: "Failed to load configuration file, program cannot run normally" 9 | outdated: 10 | hint_latest: "Latest" 11 | hint_exist_new: "New version available" 12 | fail_to_check: "Failed to check for updates" 13 | fail_to_check_duo_to_network: "Failed to check for updates, target server may be unresponsive for a long time" 14 | tell_to_download: "Visit https://github.com/txperl/PixivBiu/releases to download" 15 | press_to_use_old: "Press any key to continue using old version..." 16 | network: 17 | hint_in_check: "Checking network status..." 18 | fail_pixiv_and_use_bypass: "Cannot access Pixiv, enabling bypassSNI route (may not work properly)" 19 | login: 20 | hint_token_only: "Due to Pixiv blocking username/password login, only token login is available now" 21 | hint_before_start: "Network detection will begin shortly, this process can reduce the probability of being unable to use PixivBiu normally due to network issues" 22 | fail_to_get_token_due_to_network: "Your network cannot properly perform Pixiv token login, please adjust and try again" 23 | fail_to_get_token_anyway: "Error occurred while getting token, please try again" 24 | fail_by_cloudflare_captcha: "Encountered Cloudflare protection during login, may be due to network environment issues. You can try to get a new token or wait a moment before logging in again" 25 | is_need_to_get_token: "Program will guide you to get the token. Continue? (y / n): " 26 | ready: 27 | hint_run: "Running:" 28 | hint_how_to_use: "Open the address in a modern browser" 29 | hint_version: "Version:" 30 | hint_function_types: "API Types:" 31 | hint_image_service: "Image Service:" 32 | hint_download_path: "Download Path:" 33 | hint_program_path: "{APP_DIRECTORY}" 34 | done_init: "Started" 35 | others: 36 | hint_in_update_token: "Attempting token update" 37 | 38 | app.common.loginHelper: 39 | network: 40 | press_need_to_type_proxy: "Please enter proxy address (can be empty): " 41 | hint_detect_proxy: "Detected proxy address as %s, need to change? (y / n): " 42 | is_need_to_type_proxy: "No proxy address detected, need to set manually? (y / n): " 43 | login: 44 | hint_intro_step_head: "[Login] Please follow these steps:" 45 | hint_intro_step_1: "1. Visit: %s?%s" 46 | hint_intro_step_2: "2. Open browser's 'Dev Console', switch to 'Network' tab" 47 | hint_intro_step_3: "3. Enable 'Preserve log'" 48 | hint_intro_step_4: "4. Enter 'callback?' in the 'Filter' textbox" 49 | hint_intro_step_5: "5. Login to your Pixiv account" 50 | hint_intro_step_6: "6. After successful login, an item like 'https://app-api.pixiv.net/...&code=...' will appear in the list" 51 | hint_intro_step_7: "7. Enter the parameter after 'code' into this program" 52 | fail_code_918: "Code error. Note that the Code required each time the program starts is different, cannot reuse previously obtained Codes, and codes do not include quotation marks." 53 | fail_code_1508: "Code has expired. Please be quicker when obtaining the code." 54 | -------------------------------------------------------------------------------- /app/config/language/es.yml: -------------------------------------------------------------------------------- 1 | app.core.biu: 2 | common: 3 | press_to_exit: "Presiona cualquier tecla para salir..." 4 | success_to_login: "inicio de sesión exitoso 5 | config: 6 | hint_port_is_in_use: "El puerto esta ocupado, please change the sys-host setting item in config.yml" 7 | hint_proxy_in_use: "Proxy %s habilitado" 8 | fail_to_load_config: "Fallo para cargar el archivo de configuracion, PixivBiu no puede correr correctamente" 9 | outdated: 10 | hint_latest: "Reciente" 11 | hint_exist_new: "Una nueva versón esta disponible" 12 | fail_to_check: "Fallo al checar nuevas actualizaciones" 13 | fail_to_check_duo_to_network: "No se pudo comprobar si había actualizaciones, probablemente porque el servidor no respondió durante mucho tiempo" 14 | tell_to_download: "Ve a https://biu.tls.moe/ y descarga" 15 | press_to_use_old: "Pulse cualquier tecla para continuar con la versión anterior..." 16 | network: 17 | hint_in_check: "Comprobando estado de la red..." 18 | fail_pixiv_and_use_bypass: "No se puede conectar a Pixiv, habilitar la ruta bypassSNI (puede no estar disponible temporalmente)" 19 | login: 20 | hint_token_only: "Dado que Pixiv deshabilita el inicio de sesión con contraseña, por ahora solo puedes usar token para iniciar sesión" 21 | hint_before_start: "La detección de red está a punto de comenzar, este proceso puede reducir la probabilidad de no poder usar PixivBiu debido a problemas de red" 22 | fail_to_get_token_due_to_network: "Su red no está funcionando correctamente para iniciar sesión con el token Pixiv; ajústela y vuelva a intentarlo." 23 | fail_to_get_token_anyway: "Hubo un error al obtener el token, por favor intente nuevamente." 24 | fail_by_cloudflare_captcha: "Se encontró con la protección de Cloudflare al iniciar sesión. Es posible que se deba a problemas con el entorno de red. Puede intentar obtener el token nuevamente o esperar un momento e iniciar sesión nuevamente." 25 | is_need_to_get_token: "El programa te guiará para obtener el token. Continuar? (y / n): " 26 | ready: 27 | hint_run: "Compilar:" 28 | hint_how_to_use: "Visita la dirección en un navegador moderno" 29 | hint_version: "Versión:" 30 | hint_function_types: "Tipos de funciones:" 31 | hint_image_service: "Servicio de imagen:" 32 | hint_download_path: "Descargar carpeta:" 33 | hint_program_path: "Carpeta PixivBiu" 34 | done_init: "Inicialización realizada" 35 | others: 36 | hint_in_update_token: "Intenta actualizar el token" 37 | 38 | app.common.loginHelper: 39 | network: 40 | press_need_to_type_proxy: "Por favor, introduzca la dirección del proxy (puede estar en blanco): " 41 | hint_detect_proxy: "Dirección de proxy detectada como %s, ¿es necesario cambiarla? (y / n): " 42 | is_need_to_type_proxy: "No se detectó la dirección proxy del sistema, ¿necesita configurarla manualmente? (y / n): " 43 | login: 44 | hint_intro_step_head: "[Iniciar sesión] Siga los pasos a continuación:" 45 | hint_intro_step_1: "1. Visita: %s?%s" 46 | hint_intro_step_2: "2. Abra [Herramientas para desarrolladores / F12] del navegador y cambie a la pestaña [Red]" 47 | hint_intro_step_3: "3. Activar [Conservar registro]" 48 | hint_intro_step_4: "4. Escriba [callback?] en el cuadro de entrada [Filtro]" 49 | hint_intro_step_5: "5. Inicie sesión en su cuenta de Pixiv" 50 | hint_intro_step_6: "6. Después de iniciar sesión correctamente, aparecerá un mensaje como [https://app-api.pixiv.net/...&code=...]" 51 | hint_intro_step_7: "7. Introduzca el parámetro después de [código] en este programa" 52 | fail_code_918: "Error de código. Tenga en cuenta que el programa requiere un código diferente cada vez que se inicia y que el código no contiene comillas." 53 | fail_code_1508: "Código expirado. Por favor, intenta ser un poco más rápido al realizar la operación de obtención del código." -------------------------------------------------------------------------------- /app/config/language/zh.yml: -------------------------------------------------------------------------------- 1 | app.core.biu: 2 | common: 3 | press_to_exit: "按任意键退出..." 4 | success_to_login: "登录成功" 5 | config: 6 | hint_port_is_in_use: "现端口已被占用,请修改 config.yml 中的 sys.host 配置项" 7 | hint_proxy_in_use: "已启用代理 %s" 8 | fail_to_load_config: "读取配置文件失败,程序无法正常运行" 9 | outdated: 10 | hint_latest: "最新" 11 | hint_exist_new: "有新版本可用" 12 | fail_to_check: "检测更新失败" 13 | fail_to_check_duo_to_network: "检测更新失败,可能是目标服务器过长时间未响应" 14 | tell_to_download: "访问 https://github.com/txperl/PixivBiu/releases 即可下载" 15 | press_to_use_old: "按任意键以继续使用旧版本..." 16 | network: 17 | hint_in_check: "检测网络状态..." 18 | fail_pixiv_and_use_bypass: "无法访问 Pixiv,启用 bypassSNI 线路 (可能无法正常使用)" 19 | login: 20 | hint_token_only: "由于 Pixiv 禁止了账号密码登陆方式,目前只能使用 Token 进行登录" 21 | hint_before_start: "即将开始进行网络检测,此过程可以减少因网络问题导致的无法正常使用 PixivBiu 的概率" 22 | fail_to_get_token_due_to_network: "您的网络无法正常进行 Pixiv Token 登陆,请调整后重试" 23 | fail_to_get_token_anyway: "获取 Token 时出错,请重试" 24 | fail_by_cloudflare_captcha: "登录时遇到 Cloudflare 保护,可能是网络环境问题导致。您可以尝试重新获取 Token,或稍等片刻后再次登录" 25 | is_need_to_get_token: "程序将引导你获取 Token,是否继续? (y / n): " 26 | ready: 27 | hint_run: "运行:" 28 | hint_how_to_use: "将地址输入现代浏览器即可使用" 29 | hint_version: "版本:" 30 | hint_function_types: "功能类型:" 31 | hint_image_service: "图片服务:" 32 | hint_download_path: "下载保存路径:" 33 | hint_program_path: "程序目录" 34 | done_init: "初始化完成" 35 | others: 36 | hint_in_update_token: "尝试 Token 更新" 37 | 38 | app.common.loginHelper: 39 | network: 40 | press_need_to_type_proxy: "请输入代理地址(可留空): " 41 | hint_detect_proxy: "检测到为 %s 的代理地址,是否需要更改? (y / n): " 42 | is_need_to_type_proxy: "未能检测到代理地址,是否需要手动设置? (y / n): " 43 | login: 44 | hint_intro_step_head: "[Login] 请按以下步骤进行操作:" 45 | hint_intro_step_1: "1. 访问: %s?%s" 46 | hint_intro_step_2: "2. 打开浏览器的「Dev Console / 开发者工具」,切换至「Network / 网络」标签页" 47 | hint_intro_step_3: "3. 开启「Preserve log / 持续记录」" 48 | hint_intro_step_4: "4. 在「Filter / 筛选」文本框中输入「callback?」" 49 | hint_intro_step_5: "5. 登录您的 Pixiv 账号" 50 | hint_intro_step_6: "6. 成功登录后,会出现一个类似「https://app-api.pixiv.net/...&code=...」的字段" 51 | hint_intro_step_7: "7. 将「code」后面的参数输入本程序" 52 | fail_code_918: "Code 错误。请注意程序每次启动时要求获取的 Code 都不同,不可复用之前获取到的,且 Code 不带有引号。" 53 | fail_code_1508: "Code 已过期。请在进行 Code 获取操作时快一些。" 54 | -------------------------------------------------------------------------------- /app/lib/common/login_helper/main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from altfe.interface.root import interRoot 4 | from app.lib.common.login_helper.token import TokenGetter 5 | from app.v2.utils.sprint import SPrint 6 | 7 | _ln = lambda val, header="Login Helper": print(f"[{header}] " + val if header else val) 8 | 9 | 10 | @interRoot.bind("loginHelper", "LIB_COMMON") 11 | class CommonLoginHelper(interRoot): 12 | """ 13 | Pixiv 登陆助手。 14 | 可以优先进行网络检测以及筛选,以提高在网络不佳情况下的 Token 获取概率。 15 | """ 16 | 17 | SOME_URLS = ( 18 | "https://public-api.secure.pixiv.net", 19 | "https://1.0.0.1/dns-query", 20 | # "https://1.1.1.1/dns-query", 21 | # "https://doh.dns.sb/dns-query", 22 | # "https://cloudflare-dns.com/dns-query", 23 | ) 24 | 25 | def __init__(self): 26 | self.lang = self.INS.i18n.get_bundle("app.common.loginHelper", func=True) 27 | self.requests = requests.Session() 28 | # self.requests.mount("https://", CustomAdapter()) 29 | self.token_getter = TokenGetter(lang=self.lang, requests=self.requests) 30 | self.proxy = "" 31 | self.auth_token_url = "" 32 | 33 | def check_network(self, is_no_proxy: bool = False, is_silent: bool = False): 34 | """ 35 | 网络检测,并筛选出本机可通的 Pixiv API 服务器。 36 | :param URLS: 全部 URL 37 | :param silent: 是否静默进行 38 | :param proxy_: 代理设置,auto 为程序自动判断 39 | :return: bool 40 | """ 41 | if is_no_proxy: 42 | return self._test_pixiv_connection(proxy="") 43 | 44 | is_pixiv_accessible = True 45 | 46 | # Determine the proxy address, or empty 47 | proxy = self.STATIC.util.get_system_proxy() 48 | if proxy != "" and self._test_pixiv_connection(proxy=proxy): 49 | pass 50 | elif self._test_pixiv_connection(proxy=""): 51 | proxy = "" 52 | elif is_silent is False: 53 | # Require to set proxy manually 54 | if input( 55 | self.lang("network.hint_detect_proxy") % proxy 56 | if proxy 57 | else self.lang("network.is_need_to_type_proxy") 58 | ) in ["y", ""]: 59 | proxy = input(self.lang("network.press_need_to_type_proxy")) 60 | is_pixiv_accessible = self._test_pixiv_connection(proxy=proxy) 61 | 62 | # For now, the bypass mode is disabled 63 | self.proxy = proxy 64 | self.auth_token_url = self.SOME_URLS[0] 65 | 66 | if not is_silent: 67 | _ln( 68 | ( 69 | SPrint.green("- Pixiv.net, yep!") 70 | if is_pixiv_accessible 71 | else SPrint.red("- Pixiv.net, ops...") 72 | ), 73 | header=None, 74 | ) 75 | 76 | return is_pixiv_accessible 77 | 78 | # Determine the final auth host 79 | # Check if the hosts are accessible 80 | # is_conn_array = [ 81 | # self._test_access(url=url, proxy=self.proxy, is_silent=False) 82 | # for url in self.SOME_URLS 83 | # ] 84 | 85 | # If Pixiv is accessible, just use the official 86 | # if is_conn_array[0]: 87 | # self.auth_token_url = self.SOME_URLS[0] 88 | # return True 89 | 90 | # Or, get the real ip of auth service from DoH service to bypass SNI 91 | # for i in range(len(self.SOME_URLS)): 92 | # if is_conn_array[i] is False: 93 | # continue 94 | # final_ip = self._get_host_ip( 95 | # hostname=self.SOME_URLS[0], url=self.SOME_URLS[i] 96 | # ) 97 | # if final_ip is not False: 98 | # self.auth_token_url = final_ip 99 | # return True 100 | 101 | # return False 102 | 103 | def login(self): 104 | """ 105 | 登陆操作。 106 | :return: tuple(access token, refresh token, user id) || tuple(false, false, false) 107 | """ 108 | kw = ( 109 | {"proxies": {"http": self.proxy, "https": self.proxy}} 110 | if self.proxy != "" 111 | else {} 112 | ) 113 | try: 114 | return self.token_getter.login( 115 | host=self.auth_token_url, newCode=True, kw=kw 116 | ) 117 | except Exception as e: 118 | err = str(e) 119 | if "'code': 918" in err: 120 | self.STATIC.localMsger.red(self.lang("login.fail_code_918")) 121 | elif "'code': 1508" in err: 122 | self.STATIC.localMsger.red(self.lang("login.fail_code_1508")) 123 | else: 124 | self.STATIC.localMsger.error(e, header=False) 125 | return False, False, False 126 | 127 | def refresh(self, refresh_token): 128 | """ 129 | Token 刷新操作。 130 | :param refresh_token: 目前已有的 refresh token 131 | :return: tuple(access token, refresh token, user id) || tuple(false, false, false) 132 | """ 133 | kw = ( 134 | {"proxies": {"http": self.proxy, "https": self.proxy}} 135 | if self.proxy != "" 136 | else {} 137 | ) 138 | try: 139 | return self.token_getter.refresh( 140 | refresh_token=refresh_token, host=self.auth_token_url, kw=kw 141 | ) 142 | except Exception as e: 143 | if "Invalid refresh token" in str(e): 144 | self.STATIC.localMsger.red( 145 | "Common.LoginHelper.refresh: invalid refresh token" 146 | ) 147 | else: 148 | self.STATIC.localMsger.error(e, header=False) 149 | return False, False, False 150 | 151 | def _get_host_ip(self, hostname, timeout=3, url="https://1.0.0.1/dns-query"): 152 | """ 153 | 通过 DNS over HTTPS 服务获取主机的真实 IP 地址。 154 | :param hostname: 主机名 155 | :param timeout: 超时时间 156 | :return: str:{host ip} | False 157 | """ 158 | hostname = hostname.replace("https://", "").replace("http://", "") 159 | headers = {"Accept": "application/dns-json"} 160 | params = { 161 | "name": hostname, 162 | "type": "A", 163 | "do": "false", 164 | "cd": "false", 165 | } 166 | try: 167 | response = self.requests.get( 168 | url, headers=headers, params=params, timeout=timeout 169 | ) 170 | r = "https://" + response.json()["Answer"][0]["data"] 171 | except: 172 | return False 173 | return r 174 | 175 | def get_proxy(self) -> str: 176 | return self.proxy 177 | 178 | def is_bypass(self) -> bool: 179 | return self.auth_token_url != self.SOME_URLS[0] 180 | 181 | @classmethod 182 | def _test_access(cls, url: str, proxy: str = "", is_silent: bool = False): 183 | """ 184 | request get 请求。 185 | :param url: URL 186 | :param proxy: 代理,留空则不使用 187 | :param silent: 是否静默运行 188 | :return: bool 189 | """ 190 | is_ok = False 191 | try: 192 | if proxy != "": 193 | requests.head(url, proxies={"http": proxy, "https": proxy}, timeout=3) 194 | is_ok = True 195 | else: 196 | requests.head(url, timeout=3) 197 | except: 198 | pass 199 | if not is_silent: 200 | if is_ok: 201 | _ln(SPrint.green(f"- {url} [yep]"), header=None) 202 | else: 203 | _ln(SPrint.red(f"- {url} [ops]"), header=None) 204 | return is_ok 205 | 206 | @classmethod 207 | def _test_pixiv_connection(cls, proxy: str = "") -> bool: 208 | proxies = {"https": proxy, "http": proxy} if proxy != "" else None 209 | try: 210 | requests.head("https://pixiv.net/", proxies=proxies, timeout=2) 211 | except: 212 | return False 213 | return True 214 | 215 | 216 | # class CustomAdapter(requests.adapters.HTTPAdapter): 217 | # """ 218 | # 防止在请求 Cloudflare 时可能的 SSL 相关错误。 219 | # Thanks to @github/grawity. 220 | # """ 221 | 222 | # def init_poolmanager(self, *args, **kwargs): 223 | # # When urllib3 hand-rolls a SSLContext, it sets 'options |= OP_NO_TICKET' 224 | # # and CloudFlare really does not like this. We cannot control this behavior 225 | # # in urllib3, but we can just pass our own standard context instead. 226 | # import ssl 227 | 228 | # ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 229 | # ctx.load_default_certs() 230 | # ctx.set_alpn_protocols(["http/1.1"]) 231 | # return super().init_poolmanager(*args, **kwargs, ssl_context=ctx) 232 | -------------------------------------------------------------------------------- /app/lib/common/login_helper/token.py: -------------------------------------------------------------------------------- 1 | # thanks to @github/ZipFile, https://gist.github.com/ZipFile/c9ebedb224406f4f11845ab700124362 2 | import datetime 3 | import hashlib 4 | from base64 import urlsafe_b64encode 5 | from hashlib import sha256 6 | from secrets import token_urlsafe 7 | from urllib.parse import urlencode 8 | 9 | import requests 10 | 11 | REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback" 12 | LOGIN_URL = "https://app-api.pixiv.net/web/v1/login" 13 | AUTH_TOKEN_URL_HOST = "https://oauth.secure.pixiv.net" 14 | CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT" 15 | CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj" 16 | HASH_SECRET = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c" 17 | 18 | 19 | class TokenGetter(object): 20 | def __init__(self, lang, requests=requests): 21 | self.lang = lang 22 | self.code = "" 23 | self.requests = requests 24 | self.code_verifier, self.code_challenge = self.oauth_pkce(self.s256) 25 | self.login_params = { 26 | "code_challenge": self.code_challenge, 27 | "code_challenge_method": "S256", 28 | "client": "pixiv-android", 29 | } 30 | 31 | def s256(self, data): 32 | """S256 transformation method.""" 33 | 34 | return urlsafe_b64encode(sha256(data).digest()).rstrip(b"=").decode("ascii") 35 | 36 | def oauth_pkce(self, transform): 37 | """Proof Key for Code Exchange by OAuth Public Clients (RFC7636).""" 38 | 39 | code_verifier = token_urlsafe(32) 40 | code_challenge = transform(code_verifier.encode("ascii")) 41 | 42 | return code_verifier, code_challenge 43 | 44 | def login(self, host=AUTH_TOKEN_URL_HOST, kw={}, newCode=False): 45 | """ 46 | 尝试通过 Code 获取 Refresh Token。 47 | :param host: token api 的主机域 48 | :param kw: requests 请求的额外参数 49 | :param newCode: 是否继承使用 code 50 | :return: tuple(access token, refresh token, user id) || except: raise error 51 | """ 52 | if newCode is False and self.code != "": 53 | code = self.code 54 | else: 55 | print("---") 56 | print(self.lang("login.hint_intro_step_head")) 57 | print( 58 | self.lang("login.hint_intro_step_1") 59 | % (LOGIN_URL, urlencode(self.login_params)) 60 | ) 61 | print(self.lang("login.hint_intro_step_2")) 62 | print(self.lang("login.hint_intro_step_3")) 63 | print(self.lang("login.hint_intro_step_4")) 64 | print(self.lang("login.hint_intro_step_5")) 65 | print(self.lang("login.hint_intro_step_6")) 66 | print(self.lang("login.hint_intro_step_7")) 67 | code = input("Code: ").strip() 68 | self.code = code 69 | 70 | response = self.requests.post( 71 | "%s/auth/token" % host, 72 | data={ 73 | "client_id": CLIENT_ID, 74 | "client_secret": CLIENT_SECRET, 75 | "code": code, 76 | "code_verifier": self.code_verifier, 77 | "grant_type": "authorization_code", 78 | "include_policy": "true", 79 | "redirect_uri": REDIRECT_URI, 80 | }, 81 | timeout=10, 82 | headers=self.get_header({"host": "oauth.secure.pixiv.net"}), 83 | **kw, 84 | ) 85 | rst = response.json() 86 | if "access_token" in rst and "refresh_token" in rst: 87 | return rst["access_token"], rst["refresh_token"], rst["user"]["id"] 88 | raise Exception("Request Error.\nResponse: " + str(rst)) 89 | 90 | def refresh(self, refresh_token, host=AUTH_TOKEN_URL_HOST, kw={}): 91 | """ 92 | 刷新 refresh token。 93 | :param refresh_token: 目前可用的 refresh token 94 | :param host: token api 的主机域 95 | :param kw: requests 请求的额外参数 96 | :return: tuple(access token, refresh token, user id) || except: raise error 97 | """ 98 | response = self.requests.post( 99 | "%s/auth/token" % host, 100 | data={ 101 | "client_id": CLIENT_ID, 102 | "client_secret": CLIENT_SECRET, 103 | "grant_type": "refresh_token", 104 | "include_policy": "true", 105 | "refresh_token": refresh_token, 106 | }, 107 | timeout=10, 108 | headers=self.get_header({"host": "oauth.secure.pixiv.net"}), 109 | **kw, 110 | ) 111 | 112 | rst = response.json() 113 | if "access_token" in rst and "refresh_token" in rst: 114 | return rst["access_token"], rst["refresh_token"], rst["user"]["id"] 115 | raise Exception("Request Error.\nResponse: " + str(rst)) 116 | 117 | @staticmethod 118 | def get_header(headers={}): 119 | local_time = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00") 120 | headers["x-client-time"] = local_time 121 | headers["x-client-hash"] = hashlib.md5( 122 | (local_time + HASH_SECRET).encode("utf-8") 123 | ).hexdigest() 124 | if ( 125 | headers.get("User-Agent", None) is None 126 | and headers.get("user-agent", None) is None 127 | ): 128 | headers["app-os"] = "ios" 129 | headers["app-os-version"] = "14.6" 130 | headers["user-agent"] = "PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)" 131 | return headers 132 | -------------------------------------------------------------------------------- /app/lib/core/dl/main.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from concurrent.futures.thread import ThreadPoolExecutor 3 | 4 | from altfe.interface.root import interRoot 5 | from app.lib.core.dl.model.dler_aria2 import Aria2Dler 6 | from app.lib.core.dl.model.dler_dl import DlDler 7 | from app.lib.core.dl.model.dler_dl_single import DlSingleDler 8 | 9 | 10 | @interRoot.bind("dl", "LIB_CORE") 11 | class core_module_dl(interRoot): 12 | def __init__(self): 13 | self.WAYS = {"aria2": Aria2Dler, "dl": DlDler, "dl-single": DlSingleDler} 14 | self.modName = None 15 | self.mod = None 16 | self.sets = self.INS.conf.dict("biu_default") 17 | self.tasks = {} 18 | self._lock = threading.Lock() 19 | self._pool = ThreadPoolExecutor(max_workers=self.sets["biu"]["download"]["maxDownloading"]) 20 | self.auto() 21 | 22 | def __del__(self): 23 | for key in self.tasks: 24 | self.cancel(key) 25 | self._pool.shutdown(False) 26 | 27 | def auto(self): 28 | mode = self.sets["biu"]["download"]["mode"] \ 29 | if self.sets["biu"]["download"]["mode"] in self.WAYS \ 30 | else "dl-single" 31 | if mode == "aria2": 32 | a2 = (self.sets["biu"]["download"]["aria2Host"].split(":"), self.sets["biu"]["download"]["aria2Secret"]) 33 | self.WAYS[mode].HOST = a2[0][0] 34 | self.WAYS[mode].PORT = a2[0][1] 35 | self.WAYS[mode].SECRET = a2[1] 36 | self.mod = self.WAYS[mode] 37 | self.modName = mode 38 | return self 39 | 40 | def add(self, key, args): 41 | group = [self.mod(**kw) for kw in args] 42 | self._lock.acquire() 43 | self.tasks[key] = group 44 | self._lock.release() 45 | for obj in group: 46 | self._pool.submit(obj.run) 47 | return True 48 | 49 | def cancel(self, key): 50 | r = [] 51 | if key in self.tasks: 52 | for x in self.tasks[key]: 53 | r.append(x.cancel()) 54 | return r 55 | 56 | def status(self, key="__all__"): 57 | r = {} 58 | if key in self.tasks: 59 | return self._status(key) 60 | if key == "__all__": 61 | for x in self.tasks.copy(): 62 | r[x] = self._status(x) 63 | return r 64 | 65 | def _status(self, key): 66 | if key not in self.tasks: 67 | return [] 68 | r = [] 69 | group = self.tasks[key] 70 | for obj in group: 71 | tmp = "unknown" 72 | if obj.status(DlDler.CODE_GOOD_SUCCESS): 73 | tmp = "done" 74 | elif obj.status(DlDler.CODE_GOOD): 75 | tmp = "running" 76 | elif obj.status(DlDler.CODE_WAIT): 77 | tmp = "waiting" 78 | elif obj.status(DlDler.CODE_BAD): 79 | tmp = "failed" 80 | r.append(tmp) 81 | return r 82 | 83 | def info(self, key="__all__"): 84 | r = {} 85 | if key == "__all__": 86 | for x in self.tasks: 87 | r[x] = (self._info(x)) 88 | else: 89 | if key in self.tasks: 90 | return self._info(key) 91 | return r 92 | 93 | def _info(self, key): 94 | if key not in self.tasks: 95 | return {} 96 | 97 | totalSize = 0 98 | totalIngSize = 0 99 | totalIngSpeed = 0 100 | 101 | group = self.tasks[key] 102 | tmp = [obj.info() for obj in group] 103 | 104 | for x in tmp: 105 | totalSize += x["size"] 106 | totalIngSize += x["ingSize"] 107 | totalIngSpeed += x["ingSpeed"] 108 | 109 | return { 110 | "totalSize": totalSize, 111 | "totalIngSize": totalIngSize, 112 | "totalIngSpeed": totalIngSpeed, 113 | "tasks": tmp 114 | } 115 | -------------------------------------------------------------------------------- /app/lib/core/dl/model/dler.py: -------------------------------------------------------------------------------- 1 | import re 2 | import traceback 3 | import uuid 4 | 5 | 6 | class Dler(object): 7 | """ 8 | biu-dl 下载模块接口类 9 | """ 10 | 11 | CODE_BAD = 0 12 | CODE_BAD_FAILED = (0, 0) 13 | CODE_BAD_CANCELLED = (0, 1) 14 | CODE_GOOD = 1 15 | CODE_GOOD_RUNNING = (1, 0) 16 | CODE_GOOD_SUCCESS = (1, 1) 17 | CODE_WAIT = 2 18 | CODE_WAIT_PAUSE = (2, 0) 19 | 20 | TEMP_dlArgs = {"_headers": {}, "@requests": {}, "@aria2": {}} 21 | 22 | def __init__(self, url, folder, name, dlArgs, dlRetryMax, callback): 23 | self._id = str(uuid.uuid1()) 24 | self._dlUrl = url 25 | self._dlArgs = dlArgs 26 | self._dlFileSize = -1 27 | self._dlSaveUri = None 28 | self._dlSaveDir = folder 29 | self._dlSaveName = name 30 | self._dlRetryMax = dlRetryMax 31 | self._dlRetryNum = 0 32 | self._funCallback = callback 33 | self._stuING = 2 34 | self._stuExtra = -1 35 | self._stuIngFileSize = 0 36 | self._stuIngSpeed = 0 37 | self._errMsg = (0, "None") 38 | 39 | # 线程启动函数 40 | def run(self): 41 | return True 42 | 43 | def pause(self): 44 | if self.status(Dler.CODE_GOOD_RUNNING): 45 | return self.status(Dler.CODE_WAIT_PAUSE, True) 46 | return False 47 | 48 | def unpause(self): 49 | if self.status(Dler.CODE_WAIT): 50 | return self.status(Dler.CODE_GOOD_RUNNING, True) 51 | return False 52 | 53 | def cancel(self): 54 | if self.status(Dler.CODE_GOOD_RUNNING): 55 | return self.status(Dler.CODE_BAD_CANCELLED, True) 56 | return False 57 | 58 | # 下载任务信息 59 | def info(self): 60 | r = { 61 | "url": self._dlUrl, 62 | "size": self._dlFileSize, 63 | "saveDir": self._dlSaveDir, 64 | "saveName": self._dlSaveName, 65 | "retryNum": self._dlRetryNum, 66 | "ingSize": self._stuIngFileSize, 67 | "ingSpeed": self._stuIngSpeed 68 | } 69 | return r 70 | 71 | # 下载任务状态值 72 | def status(self, code, isBool=None): 73 | if isBool is True: 74 | if type(code) == tuple: 75 | self._stuING, self._stuExtra = code 76 | return True 77 | if type(code) != tuple and self._stuING == code: 78 | return True 79 | if type(code) == tuple and (self._stuING, self._stuExtra) == code: 80 | return True 81 | return False 82 | 83 | # 下载回调 84 | def callback(self): 85 | if self._funCallback is None: 86 | return 87 | r = [self._funCallback] if type(self._funCallback) is not list else self._funCallback 88 | for fun in r: 89 | if not hasattr(fun, "__call__"): 90 | return 91 | try: 92 | fun(self) 93 | except Exception: 94 | print(traceback.format_exc()) 95 | 96 | @staticmethod 97 | def pure_size(size, dig=2, space=1): 98 | """ 99 | 格式化文件 size。 100 | :param size: int: 文件大小 101 | :param dig: int: 保留小数位数 102 | :param space: int: 大小与单位之间的空格数量 103 | :return: 104 | str: 格式化的 size,如 "1.23 MB" 105 | """ 106 | units = ["B", "KB", "MB", "GB", "TB", "PB"] 107 | unit_index = 0 108 | K = 1024.0 109 | while size >= K: 110 | size = size / K 111 | unit_index += 1 112 | return ("%." + str(dig) + "f" + " " * space + "%s") % (size, units[unit_index]) 113 | 114 | @staticmethod 115 | def get_dl_filename(url, headers): 116 | """ 117 | 获取预下载文件的名称,判断过程如下: 118 | 1. 以 "/" 分割,若最后一项包含 ".",则返回该项 119 | 2. 请求目标 url header,若 content-disposition 中存在 filename 项,则返回该项 120 | 3. 若 1、2 皆未成功获取,则直接返回以 "/" 分割的最后一项 121 | :param url: str: 目标 URL 122 | :param headers: str: 请求头 123 | :return: 124 | str: 名称 125 | """ 126 | urlLastPart = url.split("/")[-1] 127 | if "." in urlLastPart: 128 | return urlLastPart 129 | if "content-disposition" in headers: 130 | name = re.findall("filename=(.+)", headers["content-disposition"])[0] 131 | return re.sub(r"[\/\\\:\*\?\"\<\>\|]", "", name) 132 | return urlLastPart 133 | -------------------------------------------------------------------------------- /app/lib/core/dl/model/dler_aria2.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | import requests 5 | 6 | from app.lib.core.dl.model.dler import Dler 7 | 8 | 9 | class Aria2Dler(Dler): 10 | HOST = "localhost" 11 | PORT = 6800 12 | SECRET = "" 13 | 14 | def __init__(self, url, folder="./downloads/", name=None, dlArgs=Dler.TEMP_dlArgs, dlRetryMax=2, callback=None): 15 | super(Aria2Dler, self).__init__(url, folder, name, dlArgs, dlRetryMax, callback) 16 | self._a2TaskID = -1 17 | self.args = {"dir": self._dlSaveDir, "max-tries": self._dlRetryMax, "check-certificate": False} 18 | if self._dlSaveName is not None: 19 | self.args["out"] = self._dlSaveName 20 | 21 | def run(self): 22 | if self.status(Dler.CODE_WAIT): 23 | args = self.args.copy() 24 | args.update(self._dlArgs["@aria2"]) 25 | params = [ 26 | self._dlUrl if type(self._dlUrl) == list else [self._dlUrl], 27 | dict(args), 28 | ] 29 | rep = self.call(params, "aria2.addUri") 30 | if "error" in rep: 31 | self.status(Dler.CODE_BAD_FAILED, True) 32 | else: 33 | self._a2TaskID = rep["result"] 34 | self.__monitor_schedule() 35 | self.callback() 36 | 37 | def __monitor_schedule(self): 38 | while self.status(Dler.CODE_GOOD_RUNNING) or self.status(Dler.CODE_WAIT): 39 | msg = self.tell_status() 40 | if not self.is_success(msg): 41 | self.status(Dler.CODE_BAD_FAILED, True) 42 | break 43 | tmp = msg["result"] 44 | if self._dlSaveUri is None and int(tmp["totalLength"]) > 0: 45 | self._dlSaveUri = tmp["files"][0]["path"] 46 | self._dlSaveName = tmp["files"][0]["path"].split("/")[-1] 47 | self._dlFileSize = int(tmp["totalLength"]) 48 | self.status(Dler.CODE_GOOD_RUNNING, True) 49 | self._stuIngFileSize = int(tmp["completedLength"]) 50 | self._stuIngSpeed = int(tmp["downloadSpeed"]) 51 | if self._stuIngFileSize == self._dlFileSize: 52 | self.status(Dler.CODE_GOOD_SUCCESS, True) 53 | break 54 | else: 55 | time.sleep(0.5) 56 | self._stuIngSpeed = 0 57 | 58 | def tell_status(self): 59 | return self.call([self._a2TaskID], "aria2.tellStatus") 60 | 61 | def pause(self): 62 | if self.is_success(self.call([self._a2TaskID], "aria2.pause")): 63 | self.status(Dler.CODE_WAIT_PAUSE, True) 64 | return True 65 | return False 66 | 67 | def unpause(self): 68 | if self.is_success(self.call([self._a2TaskID], "aria2.unpause")): 69 | self.status(Dler.CODE_GOOD_RUNNING, True) 70 | return True 71 | return False 72 | 73 | def cancel(self): 74 | if self.is_success(self.call([self._a2TaskID], "aria2.remove")): 75 | self.status(Dler.CODE_BAD_CANCELLED, True) 76 | return True 77 | return False 78 | 79 | def call(self, params, method): 80 | msg = {"error": "404 not found"} 81 | try: 82 | params.insert(0, "token:%s" % Aria2Dler.SECRET) 83 | json_req = { 84 | "jsonrpc": "2.0", 85 | "id": self._id, 86 | "method": method, 87 | "params": params, 88 | } 89 | rep = requests.post("http://%s:%s/jsonrpc" % (Aria2Dler.HOST, Aria2Dler.PORT), data=json.dumps(json_req)) 90 | msg = json.loads(rep.text) 91 | finally: 92 | return msg 93 | 94 | def is_success(self, rep): 95 | if "error" in rep: 96 | return False 97 | elif "result" in rep and (type(rep["result"]) != dict or rep["result"].get("errorCode") in (None, "0", 0)): 98 | return True 99 | else: 100 | return False 101 | -------------------------------------------------------------------------------- /app/lib/core/dl/model/dler_dl.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | import time 4 | 5 | import requests 6 | 7 | from altfe.interface.root import interRoot 8 | from app.lib.core.dl.model.dler import Dler 9 | 10 | requests.packages.urllib3.disable_warnings() 11 | 12 | 13 | class DlDler(Dler): 14 | def __init__(self, url, folder="./downloads/", name=None, dlArgs=Dler.TEMP_dlArgs, dlCacheDir=None, dlRetryMax=2, 15 | callback=None, size=None, dlCacheBlockNum=6): 16 | super(DlDler, self).__init__(url, folder, name, dlArgs, dlRetryMax, callback) 17 | 18 | if dlCacheDir is None: 19 | self._dlCacheDir = os.path.join(self._dlSaveDir) 20 | else: 21 | self._dlCacheDir = dlCacheDir 22 | self._dlSaveName, self._dlFileSize = self.__get_dl_name_size(name, size) 23 | self._dlSaveUri = os.path.join(self._dlSaveDir, self._dlSaveName) 24 | self._dlCacheBlockNum = dlCacheBlockNum 25 | self._dlCacheBlockArr = self.__get_cache_blocks() 26 | 27 | interRoot.STATIC.file.mkdir(self._dlCacheDir) 28 | interRoot.STATIC.file.mkdir(self._dlSaveDir) 29 | 30 | def run(self): 31 | """ 32 | 单下载任务的启动函数。 33 | :return: none 34 | """ 35 | # 判断是否超过最大尝试次数 36 | if self._dlFileSize == -1 or self._dlRetryNum > self._dlRetryMax: 37 | self.status(Dler.CODE_BAD_FAILED, True) 38 | return False 39 | 40 | # 开始下载 41 | threads = [] 42 | threadIndex = 0 43 | # 以分块为单位启动线程 44 | for cacheUri, begin, end in self._dlCacheBlockArr: 45 | thread = threading.Thread(target=self.__thread_download, args=(cacheUri, begin, end, threadIndex)) 46 | threads.append(thread) 47 | thread.setDaemon(True) 48 | thread.start() 49 | threadIndex += 1 50 | # 启动进度监控线程 51 | monitor = threading.Thread(target=self.__thread_monitor_schedule, args=()) 52 | monitor.setDaemon(True) 53 | monitor.start() 54 | 55 | self.status(Dler.CODE_GOOD_RUNNING, True) 56 | # 阻塞 57 | for t in threads: 58 | t.join() 59 | 60 | # 合并 61 | if self.__merge(): 62 | # 下载成功 63 | self.status(Dler.CODE_GOOD_SUCCESS, True) 64 | self.callback() 65 | self.clear_cache() 66 | else: 67 | # 数据合并失败,开始重试 68 | if self.status(Dler.CODE_BAD_CANCELLED): 69 | self._dlRetryNum += self._dlRetryMax 70 | else: 71 | self._dlRetryNum += 1 72 | self.callback() 73 | self.run() 74 | 75 | def clear_cache(self, isAllCache=False): 76 | if isAllCache: 77 | interRoot.STATIC.file.clearDIR(self._dlCacheDir) 78 | else: 79 | interRoot.STATIC.file.rm([x[0] for x in self._dlCacheBlockArr]) 80 | 81 | def __merge(self): 82 | """ 83 | 合并分块文件。 84 | :return: 85 | none 86 | """ 87 | if self.status(Dler.CODE_BAD): 88 | return False 89 | 90 | isDone = True 91 | 92 | # 若存在同名目标文件,则删除 93 | interRoot.STATIC.file.rm(self._dlSaveUri) 94 | 95 | # 合并 96 | with open(self._dlSaveUri, "ab") as rst: 97 | for i in range(self._dlCacheBlockNum): 98 | cacheUri, begin, end = self._dlCacheBlockArr[i] 99 | # 判断分块文件大小是否正确 100 | if os.path.getsize(cacheUri) != (end - begin + 1): 101 | isDone = False 102 | break 103 | # 写入数据 104 | with open(cacheUri, "rb") as f: 105 | rst.write(f.read()) 106 | return isDone 107 | 108 | def __thread_download(self, cacheUri, begin, end, index): 109 | """ 110 | 分块下载线程函数 111 | :param begin: int: 分块文件序号头 112 | :param end: int: 分块文件序号尾 113 | :param index: int: 分块序号 114 | :return: 115 | none 116 | """ 117 | hopeFileSize = end - begin + 1 # 本次任务分块总大小 118 | 119 | try: 120 | # 判断已下载分块大小 121 | if os.path.exists(cacheUri): 122 | existFileSize = os.path.getsize(cacheUri) 123 | else: 124 | existFileSize = 0 125 | 126 | # 判断已下载文件大小是否与期望大小相同 127 | if hopeFileSize - existFileSize > 0: 128 | headers = { 129 | "Range": "Bytes=%d-%s" % (existFileSize + int(begin), end), 130 | "Accept-Encoding": "*", 131 | } 132 | headers.update(self._dlArgs["_headers"]) 133 | 134 | rep = requests.get(self._dlUrl, headers=headers, **self._dlArgs["@requests"], stream=True) 135 | with open(cacheUri, "ab", buffering=1024) as f: 136 | for chunk in rep.iter_content(chunk_size=2048): 137 | # 若 CODE_BAD,则退出 138 | if self.status(Dler.CODE_BAD): 139 | break 140 | # 流写入 141 | if chunk: 142 | existFileSize += len(chunk) 143 | f.write(chunk) 144 | # 若 CODE_WAIT,则等待 145 | while self.status(Dler.CODE_WAIT): 146 | time.sleep(1) 147 | return True 148 | except: 149 | return False 150 | 151 | def __thread_monitor_schedule(self): 152 | """ 153 | 下载进度、速度监控线程。通过获取目标文件大小来判断进度、速度。 154 | :return: 155 | none 156 | """ 157 | prevFileSize = 0 158 | while self.status(Dler.CODE_GOOD_RUNNING) or self.status(Dler.CODE_WAIT): 159 | sumSize = 0 160 | try: 161 | for cacheUri, begin, end in self._dlCacheBlockArr: 162 | sumSize += os.path.getsize(cacheUri) 163 | except: 164 | continue 165 | self._stuIngFileSize = sumSize 166 | self._stuIngSpeed = sumSize - prevFileSize 167 | prevFileSize = sumSize 168 | time.sleep(1) 169 | if self.status(Dler.CODE_GOOD_SUCCESS): 170 | self._stuIngFileSize = self._dlFileSize 171 | self._stuIngSpeed = 0 172 | 173 | def __get_dl_name_size(self, name, size): 174 | """ 175 | 自动判断预下载文件的名称与大小。若目标服务器不返回相关大小,则启用单线程下载。 176 | :return: 177 | tuple: (str: name, int: length) 178 | """ 179 | if name is not None and size is not None: 180 | return name, size 181 | contentName = name 182 | contentLength = -1 183 | try: 184 | with requests.head(self._dlUrl, headers=self._dlArgs["_headers"], **self._dlArgs["@requests"], 185 | allow_redirects=True) as rep: 186 | # 获取文件名称 187 | if name is None: 188 | contentName = Dler.get_dl_filename(self._dlUrl, rep.headers) 189 | # 获取文件大小 190 | if size is None and "Content-Length" in rep.headers: 191 | contentLength = int(rep.headers["Content-Length"]) 192 | if rep.url != self._dlUrl: 193 | self._dlUrl = rep.url 194 | finally: 195 | return contentName, contentLength 196 | 197 | def __get_cache_blocks(self): 198 | """ 199 | 根据 预下载文件大小 与 设置的分块数 得到各分块大小。 200 | :return: 201 | list[(begin, end), (), ...]: 分块序列列表 202 | """ 203 | if self._dlFileSize == -1: 204 | return [(self._dlSaveUri, -1, -1)] 205 | 206 | r = [] 207 | offset = int(self._dlFileSize / self._dlCacheBlockNum) 208 | for i in range(self._dlCacheBlockNum): 209 | # 分块缓存名称、路径 210 | tmp = [os.path.join(self._dlCacheDir, ("%s.p%s" % (self._dlSaveName, str(i))))] 211 | if i == self._dlCacheBlockNum - 1: 212 | r.append(tuple(tmp + [i * offset, self._dlFileSize - 1])) 213 | else: 214 | r.append(tuple(tmp + [i * offset, (i + 1) * offset - 1])) 215 | return r 216 | -------------------------------------------------------------------------------- /app/lib/core/dl/model/dler_dl_single.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import requests 5 | 6 | from altfe.interface.root import interRoot 7 | from app.lib.core.dl.model.dler import Dler 8 | 9 | 10 | class DlSingleDler(Dler): 11 | def __init__(self, url, folder="./downloads/", name=None, dlArgs=Dler.TEMP_dlArgs, dlRetryMax=2, callback=None): 12 | super(DlSingleDler, self).__init__(url, folder, name, dlArgs, dlRetryMax, callback) 13 | if self._dlSaveName is None: 14 | try: 15 | headers = {} 16 | with requests.head(self._dlUrl, headers=self._dlArgs["_headers"], **self._dlArgs["@requests"], 17 | allow_redirects=True) as rep: 18 | headers = rep.headers 19 | finally: 20 | self._dlSaveName = Dler.get_dl_filename(self._dlUrl, headers) 21 | self._dlSaveUri = os.path.join(self._dlSaveDir, self._dlSaveName) 22 | self._dlFileSize = -1 23 | interRoot.STATIC.file.mkdir(self._dlSaveDir) 24 | 25 | def run(self): 26 | if self.status(Dler.CODE_WAIT): 27 | self.status(Dler.CODE_GOOD_RUNNING, True) 28 | if self.__download_single(): 29 | if self._dlFileSize != -1 and self._dlFileSize != os.path.getsize(self._dlSaveUri): 30 | self.status(Dler.CODE_BAD_FAILED, True) 31 | else: 32 | self.status(Dler.CODE_GOOD_SUCCESS, True) 33 | else: 34 | self.status(Dler.CODE_BAD_FAILED, True) 35 | self.callback() 36 | 37 | def __download_single(self): 38 | """ 39 | 单线程下载。 40 | :return: bool 41 | """ 42 | try: 43 | with requests.get(self._dlUrl, headers=self._dlArgs["_headers"], stream=True, 44 | **self._dlArgs["@requests"]) as rep: 45 | self._dlFileSize = int(rep.headers.get("Content-Length", -1)) 46 | with open(self._dlSaveUri, "wb", buffering=1024) as f: 47 | for chunk in rep.iter_content(chunk_size=2048): 48 | # 若 CODE_BAD,则退出 49 | if self.status(Dler.CODE_BAD): 50 | return False 51 | # 流写入 52 | if chunk: 53 | f.write(chunk) 54 | # 若 CODE_WAIT,则等待 55 | while self.status(Dler.CODE_WAIT): 56 | time.sleep(1) 57 | return True 58 | except: 59 | return False 60 | finally: 61 | self._stuIngFileSize = -1 62 | -------------------------------------------------------------------------------- /app/lib/ins/conf/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from altfe.interface.root import interRoot 4 | from app.lib.ins.conf.wrapper import ConfigWrapper 5 | 6 | 7 | @interRoot.bind("conf", "LIB_INS") 8 | class InsConf(interRoot): 9 | def __init__(self): 10 | self.rootConfigFolderPath = self.getENV("rootPathFrozen") + "app/config/" 11 | self.customConfigPath = self.getENV("rootPath") + "config.yml" 12 | self._configs = {} 13 | self._dictWrapper = {} 14 | self.load_config() 15 | 16 | def load_config(self): 17 | """ 18 | Loads all files(.yml, .yaml) in './app/config/' folder. 19 | Then, load or create the user's customized config file(./config.yml). 20 | :return: none 21 | """ 22 | self._configs.update({"config": ConfigWrapper(path=self.customConfigPath, error=False)}) 23 | for fileName in os.listdir(self.rootConfigFolderPath): 24 | if ".yml" in fileName or ".yaml" in fileName: 25 | fileName_ = ".".join(fileName.split(".")[:-1]) 26 | self._configs.update({fileName_: ConfigWrapper(self.rootConfigFolderPath + fileName)}) 27 | self.dict(fileName_, wrapper=True, reload=True) 28 | 29 | def dict(self, configName: str, flat=False, wrapper=False, reload=False): 30 | """ 31 | Export the final dict data that includes customized and default configs. 32 | Of course, if the customized setting item conflicts with the default, only the customized one is retained. 33 | Environment > Custom > Default. 34 | :param configName: name of config file(the same with "./app/config/xxx.yml"), customized one is called "config" 35 | :param flat: weather to return a flat-like dict 36 | :param wrapper: weather to return a wrapper object 37 | :param reload: weather to reload 38 | :return: final config dict 39 | """ 40 | if self._dictWrapper.get(configName) is None or reload is True: 41 | # load default config 42 | defaultDic = self.get_wrapper(configName) 43 | if defaultDic is None: 44 | return None 45 | # generate final config wrapper 46 | finalDic = ConfigWrapper(config=defaultDic.dict(), error=False) 47 | for key in defaultDic.format2flat(): 48 | maybe = ConfigWrapper.SIGN_EMPTY 49 | # load environment variable config 50 | envVar = os.environ.get(key, ConfigWrapper.SIGN_EMPTY) 51 | if envVar != ConfigWrapper.SIGN_EMPTY: 52 | maybe = ConfigWrapper.literal_eval(envVar) 53 | else: 54 | # load customized config 55 | customVar = self.get_wrapper("config").get(key, default=ConfigWrapper.SIGN_EMPTY) 56 | if customVar != ConfigWrapper.SIGN_EMPTY: 57 | maybe = customVar 58 | if maybe != ConfigWrapper.SIGN_EMPTY: 59 | finalDic.set(key, maybe) 60 | self._dictWrapper[configName] = finalDic 61 | else: 62 | # load the cache 63 | finalDic = self._dictWrapper[configName] 64 | if flat is True: 65 | return finalDic.format2flat() 66 | if wrapper is True: 67 | return finalDic 68 | return finalDic.dict() 69 | 70 | def get_wrapper(self, configName: str, default=None): 71 | """ 72 | Get the config's ConfigWrapper object(original type). 73 | :param configName: name of config file 74 | :param default: the default value returned if the key doesn't exist 75 | :return: ConfigWrapper object or $default$ 76 | """ 77 | if configName in self._configs: 78 | return self._configs[configName] 79 | return default 80 | 81 | def get_bundle(self, configName: str, beforeKey: str, default=None, func=True): 82 | """ 83 | Get the bundle of config. 84 | :param configName: name of config file 85 | :param beforeKey: header 86 | :param default: default value 87 | :param func: weather to return a lambda function 88 | :return: dict or lambda function 89 | """ 90 | if func: 91 | return lambda key: self.get(configName, f"{beforeKey}.{key}", default=default) 92 | else: 93 | return self.get(configName, beforeKey, default=default) 94 | 95 | def get(self, configName: str, key: str, default=None): 96 | """ 97 | Get the setting item via $subKey$ in the config called $key$. 98 | If there is a hierarchical relationship in the subKey, use dot instead. E.g, ["a"]["b"] -> "a.b". 99 | If the setting item exists in customized config, system will return it instead of the one in default config. 100 | Environment > Custom > Default. 101 | :param configName: name of config file 102 | :param key: key of the setting item in the dict 103 | :param default: the default value returned if the key doesn't exist 104 | :return: the value of setting item 105 | """ 106 | dic = self.dict(configName, wrapper=True) 107 | if dic is None: 108 | return default 109 | return dic.get(key, default=default) 110 | 111 | def set(self, key: str, value: any): 112 | """ 113 | Set the value of setting item. 114 | :param key: key of the setting item in the dict 115 | :param value: new value 116 | :return: true or false 117 | """ 118 | return self.get_wrapper("config").set(key, value) 119 | 120 | def remove(self, key: str): 121 | """ 122 | Remove/Delete the setting item. 123 | :param key: key of the setting item in the dict 124 | :return: true or false 125 | """ 126 | return self.get_wrapper("config").remove(key) 127 | -------------------------------------------------------------------------------- /app/lib/ins/conf/wrapper.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import errno 3 | 4 | from altfe.interface.root import interRoot 5 | 6 | 7 | class ConfigWrapper(object): 8 | """ 9 | 配置、语言文件的通用包装器,用于基础的格式化、获取、修改、转换等操作。 10 | 可格式化「平坦、平坦-字典、字典」三种格式的文本内容,但最终将全部转换为字典数据结构进行存储。 11 | """ 12 | SIGN_EMPTY = "__rm__" 13 | 14 | def __init__(self, path=None, config=None, error=True): 15 | if config is None: 16 | config = interRoot.loadConfig(path) 17 | if config is False or config is None: 18 | if error: 19 | raise FileNotFoundError(errno.ENOENT, "Cannot load the language file", path) 20 | else: 21 | config = {} 22 | self._configDict = ConfigWrapper.format2dict(config) 23 | 24 | def format2flat(self, dic=None, finalKeys=None): 25 | """ 26 | 将 dic 的字典数据结构平坦化,如 dic["a"]["b"] -> dic["a.b"]。 27 | :param dic: 字典数据 28 | :param finalKeys: 递归使用,记录当前层级 key 29 | :return: 平坦化的字典 30 | """ 31 | if dic is None: 32 | dic = self._configDict 33 | if finalKeys is None: 34 | finalKeys = [] 35 | r = {} 36 | for key in dic: 37 | item = dic[key] 38 | if type(item) != dict: 39 | r.update({".".join(finalKeys + [str(key)]): item}) 40 | else: 41 | r.update(self.format2flat(item, finalKeys + [str(key)])) 42 | return r 43 | 44 | def dict(self): 45 | """ 46 | 对原 config 拷贝后返回。 47 | :return: config.copy() 48 | """ 49 | return self._configDict.copy() 50 | 51 | def get(self, key: str, default=None): 52 | """ 53 | 获取 config 中的值。 54 | :param key: 以 "." 连接的多层级键,如 "a.b" 代表 dict["a"]["b"] 55 | :param default: 若值不存在,则默认返回此字段 56 | :return: value or default 57 | """ 58 | now = self._configDict 59 | for subKey in key.split("."): 60 | if subKey not in now: 61 | return default 62 | now = now[subKey] 63 | return now if now != ConfigWrapper.SIGN_EMPTY else default 64 | 65 | def update_dict(self, dic: dict): 66 | """ 67 | 更新 config 字典,与 python dict update 功能相同。 68 | :param dic: 需要更新的信息 69 | :return: none 70 | """ 71 | self._configDict.update(dic) 72 | 73 | def set(self, key: str, value: any, strict=False): 74 | """ 75 | 设置 config 中的值。 76 | :param key: 以 "." 连接的多层级键,如 "a.b" 代表 dict["a"]["b"] 77 | :param value: 值 78 | :param strict: 是否以严格模式进行。若是,则若键不存在会返回 false;若否,则若字段不存在会创建新键 79 | :return: true or false 80 | """ 81 | now = self._configDict 82 | keys = key.split(".") 83 | for i in range(len(keys) - 1): 84 | nowKey = keys[i] 85 | if nowKey not in now: 86 | if strict: 87 | return False 88 | else: 89 | now.update({nowKey: {}}) 90 | now = now[nowKey] 91 | now.update({keys[-1]: value}) 92 | return True 93 | 94 | def remove(self, key: str): 95 | """ 96 | 移除 config 中的值。 97 | :param key: 以 "." 连接的多层级键,如 "a.b" 代表 dict["a"]["b"] 98 | :return: true or false 99 | """ 100 | return self.set(key, value=ConfigWrapper.SIGN_EMPTY, strict=True) 101 | 102 | @staticmethod 103 | def format2dict(configFlat): 104 | """ 105 | 将三种类型的初始化字典转化为最终字典。 106 | :param configFlat: 待转换的字典 107 | :return: 最终字典 108 | """ 109 | r = {} 110 | for key in configFlat: 111 | keys = key.split(".") 112 | now = r 113 | for i in range(len(keys)): 114 | subKey = keys[i] 115 | if i == len(keys) - 1: 116 | now.update({subKey: configFlat[key]}) 117 | break 118 | if subKey not in now: 119 | now.update({subKey: {}}) 120 | now = now[subKey] 121 | return r 122 | 123 | @staticmethod 124 | def literal_eval(_: str): 125 | if _ == "false": 126 | return False 127 | if _ == "true": 128 | return True 129 | try: 130 | return ast.literal_eval(_) 131 | except: 132 | return _ 133 | -------------------------------------------------------------------------------- /app/lib/ins/i18n.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import os 3 | 4 | from altfe.interface.root import classRoot 5 | from app.lib.ins.conf.wrapper import ConfigWrapper 6 | 7 | 8 | @classRoot.bind("i18n", "LIB_INS") 9 | class InsI18n(classRoot): 10 | def __init__(self): 11 | self.rootLangFolderPath = self.getENV("rootPathFrozen") + "app/config/language/" 12 | self.langCode = self.__deter_lang_code() 13 | self._lang = ConfigWrapper(self.rootLangFolderPath + self.langCode + ".yml", error=False) 14 | 15 | def __deter_lang_code(self, code_=None): 16 | """ 17 | Determine the language code({ISO 639-1}-{ISO 3166-1}). 18 | If code_ is None, it will recognize the local language of the system automatically. Default is 'en'. 19 | :param: code_: language code 20 | :return: final code 21 | """ 22 | if code_ is not None: 23 | code = code_ 24 | else: 25 | code = classRoot.osGet("LIB_INS", "conf").get("biu_default", "sys.language", "") 26 | if code == "": 27 | code, _ = locale.getdefaultlocale() 28 | code = "en" if code is None else code.replace("_", "-") 29 | if not os.path.exists(self.rootLangFolderPath + code + ".yml"): 30 | code = code.split("-")[0] 31 | if not os.path.exists(self.rootLangFolderPath + code + ".yml"): 32 | code = "en" 33 | return code 34 | 35 | def get_wrapper(self, code_=None): 36 | """ 37 | Get the language's ConfigWrapper object(original type). 38 | :param code_: language code 39 | :return: ConfigWrapper object 40 | """ 41 | if code_ is None: 42 | return self._lang 43 | code = self.__deter_lang_code(code_) 44 | return ConfigWrapper(self.rootLangFolderPath + code + ".yml") 45 | 46 | def get_bundle(self, beforeKey, default=None, func=True): 47 | """ 48 | Get the bundle of one language. 49 | :param beforeKey: father key 50 | :param default: default value 51 | :param func: weather to return a lambda function 52 | :return: dict or lambda function 53 | """ 54 | if func: 55 | return lambda key: self.get(f"{beforeKey}.{key}", default=default) 56 | else: 57 | return self.get(beforeKey, default=default) 58 | 59 | def get(self, key, default=None): 60 | """ 61 | Get the language text. 62 | :param key: key of the text in the language file 63 | :param default: default value 64 | :return: any 65 | """ 66 | if default is None: 67 | default = "{" + key + "}" 68 | return self._lang.get(key, default=default) 69 | -------------------------------------------------------------------------------- /app/lib/static/arg.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from altfe.interface.root import interRoot 4 | 5 | 6 | @interRoot.bind("arg", "LIB_STATIC") 7 | class static_arg(object): 8 | @staticmethod 9 | def getArgs(method, li, way="GET"): 10 | rst = {"ops": {"method": method}, "fun": {}} 11 | data = request.args if way == "GET" else request.form 12 | for x in li: 13 | c = x.split("=") 14 | group = "fun" 15 | if c[0][:1] == "&": 16 | group = "ops" 17 | c[0] = c[0][1:] 18 | if not data.get(c[0]): 19 | if len(c) != 2: 20 | raise AttributeError("missing parameters: %s" % c[0]) 21 | rst[group][c[0]] = c[1] 22 | else: 23 | rst[group][c[0]] = data.get(c[0]) 24 | return rst 25 | 26 | @staticmethod 27 | def argsPurer(fun, li): 28 | for x in li: 29 | fun[li[x]] = fun[x] 30 | del fun[x] 31 | -------------------------------------------------------------------------------- /app/lib/static/file.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import os 4 | import shutil 5 | import time 6 | import zipfile 7 | 8 | import yaml 9 | from PIL import Image 10 | 11 | from altfe.interface.root import interRoot 12 | 13 | 14 | @interRoot.bind("file", "LIB_STATIC") 15 | class static_file(object): 16 | @staticmethod 17 | def ain(uri, mode="r"): 18 | if not os.path.exists(uri): 19 | return False 20 | try: 21 | with open(uri, mode) as f: 22 | fileType = uri.split(".")[-1] 23 | if fileType == "json": 24 | r = json.load(f) 25 | elif fileType == "yml" or fileType == "yaml": 26 | r = yaml.safe_load(f) 27 | else: 28 | r = f.read() 29 | except: 30 | print("\033[31m[load@failed] %s\033[0m" % (uri)) 31 | return False 32 | return r 33 | 34 | @staticmethod 35 | def aout(uri, data, mode="w", dRename=True, msg=False): 36 | if not uri: 37 | return False 38 | 39 | uri = uri.replace("\\\\", "/").replace("\\", "/").replace("//", "/") 40 | 41 | uriDir = "" 42 | fileName = "" 43 | # 获取文件路径 44 | for x in uri.split("/")[:-1]: 45 | uriDir = uriDir + x + "/" 46 | # 获取文件名 47 | for x in uri.split("/")[-1].split(".")[:-1]: 48 | fileName = fileName + x + "." 49 | fileName = fileName[:-1] 50 | # 获取文件类型 51 | fileType = uri.split("/")[-1].split(".")[-1] 52 | # 检测路径中文件夹是否存在,无则创建 53 | if uriDir != "" and not os.path.exists(uriDir): 54 | os.makedirs(uriDir) 55 | # 检测是否有重名文件,有则将文件名改为 x_time 56 | if dRename and os.path.exists(uri): 57 | uri = uriDir + fileName + "_" + str(int(time.time())) + "." + fileType 58 | 59 | try: 60 | with open(uri, mode) as f: 61 | if fileType == "json": 62 | data = json.dumps(data) 63 | elif fileType == "yml" or fileType == "yaml": 64 | data = yaml.dump(data) 65 | f.write(data) 66 | except: 67 | print("\033[31m[save@failed] %s -> %s\033[0m" % (fileName, uri)) 68 | return False 69 | if msg: 70 | print( 71 | "\033[32m[save]\033[0m \033[36m%s\033[0m -> \033[36m%s\033[0m" 72 | % (fileName, uri) 73 | ) 74 | return True 75 | 76 | @staticmethod 77 | def get_dir_size_mib(path): 78 | try: 79 | if os.path.exists(path): 80 | return sum(d.stat().st_size for d in os.scandir(path) if d.is_file()) / (1024.0 * 1024.0) 81 | except Exception as e: 82 | print("\033[31m%s\033[0m" % e) 83 | return -1 84 | 85 | @staticmethod 86 | def mkdir(path): 87 | try: 88 | if path != "" and not os.path.exists(path): 89 | os.makedirs(path) 90 | except Exception as e: 91 | print("\033[31m%s\033[0m" % e) 92 | return False 93 | return True 94 | 95 | @staticmethod 96 | def clearDIR(folder, nameList=[], nothing=False): 97 | if not folder or not os.path.exists(folder): 98 | return False 99 | for filename in os.listdir(folder): 100 | if len(nameList) > 0 and filename not in nameList: 101 | continue 102 | file_path = os.path.join(folder, filename) 103 | try: 104 | if os.path.isfile(file_path) or os.path.islink(file_path): 105 | os.unlink(file_path) 106 | elif os.path.isdir(file_path): 107 | shutil.rmtree(file_path) 108 | except Exception as e: 109 | print("Failed to delete %s. Reason: %s" % (file_path, e)) 110 | return False 111 | if nothing: 112 | os.rmdir(folder) 113 | return True 114 | 115 | @staticmethod 116 | def rm(uri, msg=False): 117 | uris = uri if type(uri) == list else [uri] 118 | r = [] 119 | for x in uris: 120 | if not x or not os.path.exists(x): 121 | r.append(False) 122 | continue 123 | try: 124 | os.remove(x) 125 | except: 126 | print("\033[31m[remove@failed] %s\033[0m" % (x)) 127 | r.append(False) 128 | if msg: 129 | print("\033[32m[remove]\033[0m \033[36m%s\033[0m" % (x)) 130 | r.append(True) 131 | return r if len(r) > 1 else r[0] 132 | 133 | @staticmethod 134 | def rename(oriPath, dstPath): 135 | if not os.path.exists(oriPath) or os.path.exists(dstPath): 136 | return False 137 | os.rename(oriPath, dstPath) 138 | return True 139 | 140 | @staticmethod 141 | def unzip(ruri, furi, msg=False): 142 | try: 143 | f = zipfile.ZipFile(furi, "r") 144 | for name in f.namelist(): 145 | f.extract(name, ruri) 146 | f.close() 147 | except: 148 | print("\033[31m[unzip@failed] %s\033[0m" % (furi)) 149 | return False 150 | if msg: 151 | print("\033[32m[unzip]\033[0m \033[36m%s\033[0m" % (furi)) 152 | return True 153 | 154 | @staticmethod 155 | def cov2webp(uri, plist, dlist, quality=100): 156 | imgs = [] 157 | try: 158 | for x in plist: 159 | imgs.append(Image.open(x)) 160 | imgs[0].save( 161 | uri, 162 | "webp", 163 | quality=quality, 164 | save_all=True, 165 | append_images=imgs[1:], 166 | duration=dlist, 167 | ) 168 | except: 169 | return False 170 | return True 171 | 172 | @staticmethod 173 | def cov2gif(uri, plist, dlist): 174 | imgs = [] 175 | try: 176 | for x in plist: 177 | imgs.append(Image.open(x)) 178 | imgs[0].save( 179 | uri, 180 | "gif", 181 | save_all=True, 182 | append_images=imgs[1:], 183 | duration=dlist, 184 | loop=0, 185 | ) 186 | except: 187 | return False 188 | return True 189 | 190 | @staticmethod 191 | def md5(filePath=None, StringList=None): 192 | hash_md5 = hashlib.md5() 193 | if filePath is not None and os.path.exists(filePath): 194 | with open(filePath, "rb") as f: 195 | for chunk in iter(lambda: f.read(4096), b""): 196 | hash_md5.update(chunk) 197 | elif StringList is not None: 198 | for x in StringList: 199 | hash_md5.update(str(x).encode("utf-8")) 200 | else: 201 | return None 202 | return hash_md5.hexdigest() 203 | 204 | @classmethod 205 | def folderMD5(cls, folderPath): 206 | if not os.path.exists(folderPath): 207 | return None 208 | r = [] 209 | for file in os.listdir(folderPath): 210 | nowPath = os.path.join(folderPath, file) 211 | if os.path.isdir(nowPath): 212 | r.append(cls.folderMD5(nowPath)) 213 | else: 214 | r.append(cls.md5(filePath=nowPath)) 215 | return cls.md5(StringList=r) 216 | -------------------------------------------------------------------------------- /app/lib/static/msg.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import platform 4 | import time 5 | import traceback 6 | 7 | BIUMSG_METHODS = { 8 | "default": 0, "highlight": 1, "underline": 4, "flash": 5, "anti": 7, "disable": 8 9 | } 10 | 11 | BIUMSG_COLORS = { 12 | "black": (30, 40), 13 | "red": (31, 41), 14 | "green": (32, 42), 15 | "yellow": (33, 43), 16 | "blue": (34, 44), 17 | "purple-red": (35, 45), 18 | "cyan-blue": (36, 46), 19 | "white": (37, 47), 20 | "default": (38, 38) 21 | } 22 | 23 | BIUMSG_isColor = True 24 | if os.name == "nt": 25 | os.system("color") 26 | if "10" not in platform.platform(): 27 | BIUMSG_isColor = False 28 | 29 | from altfe.interface.root import interRoot 30 | 31 | 32 | @interRoot.bind("localMsger", "LIB_STATIC") 33 | class static_local_msger(object): 34 | @classmethod 35 | def msg(cls, text, header="PixivBiu", out=True): 36 | r = cls.mformat(text, "default", header=header) 37 | if not out: 38 | return r 39 | print(r) 40 | 41 | @classmethod 42 | def sign(cls, text, header=None, out=True): 43 | r = cls.mformat(text, "white", "black", "highlight", header=header) 44 | if not out: 45 | return r 46 | print(r) 47 | 48 | @classmethod 49 | def error(cls, text, header=None, out=True): 50 | r = cls.mformat(text, "red", header=header) 51 | if not out: 52 | return r 53 | print("!Error at %s:" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) 54 | print(r) 55 | 56 | @classmethod 57 | def green(cls, text, header="PixivBiu", out=True): 58 | r = cls.mformat(text, "green", header=header) 59 | if not out: 60 | return r 61 | print(r) 62 | 63 | @classmethod 64 | def red(cls, text, header="PixivBiu", out=True): 65 | r = cls.mformat(text, "red", header=header) 66 | if not out: 67 | return r 68 | print(r) 69 | 70 | @classmethod 71 | def arr(cls, *text): 72 | for x in text: 73 | if type(x) == str: 74 | print(cls.mformat(x, "default")) 75 | elif type(x) == tuple: 76 | print(cls.mformat("%s: %s" % x, "default")) 77 | 78 | @classmethod 79 | def mformat(cls, text, front, back=None, method="default", header=None): 80 | if isinstance(text, Exception): 81 | text = traceback.format_exc() 82 | else: 83 | text = str(text) 84 | if header in (None, False): 85 | finalText = text 86 | else: 87 | finalText = "[%s] %s" % (header, text) 88 | if BIUMSG_isColor is False: 89 | return finalText 90 | if back is None: 91 | r = f"\033[{BIUMSG_COLORS[front][0]}m{finalText}\033[0m" 92 | else: 93 | r = f"\033[{BIUMSG_METHODS[method]};{BIUMSG_COLORS[front][0]};{BIUMSG_COLORS[back][1]}m{finalText}\033[0m" 94 | return r 95 | -------------------------------------------------------------------------------- /app/lib/static/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import re 4 | import socket 5 | import time 6 | 7 | from altfe.interface.root import interRoot 8 | 9 | 10 | @interRoot.bind("util", "LIB_STATIC") 11 | class StaticUtil(object): 12 | @staticmethod 13 | def get_system_proxy(sys_plc=None): 14 | """ 15 | 检测系统本地设置中的代理地址。 16 | @Windows: 通过注册表项获取 17 | @macOS: 通过 scutil 获取 18 | @Linux: 暂时未实现 19 | """ 20 | if sys_plc is None: 21 | sys_plc = platform.system() 22 | # 命令选择 23 | if sys_plc == "Windows": 24 | cmd = r'reg query "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings" | ' \ 25 | r'findstr "ProxyServer AutoConfigURL" ' 26 | elif sys_plc == "Darwin": 27 | cmd = "scutil --proxy" 28 | else: 29 | return "" 30 | 31 | # 获取系统终端执行结果 32 | cmd_rst_obj = os.popen(cmd) 33 | cmd_rst = cmd_rst_obj.read() 34 | cmd_rst_obj.close() 35 | 36 | # 获取代理地址 37 | dic = {} 38 | if sys_plc == "Windows": 39 | # Windows 40 | maybe = ["AutoConfigURL", "ProxyServer"] 41 | for x in [re.split(r"\s+", x)[1:] for x in cmd_rst.split("\n")]: 42 | if len(x) != 3: 43 | continue 44 | dic[x[0]] = x[2] 45 | for key in maybe: 46 | if key in dic: 47 | return dic[key] 48 | elif sys_plc == "Darwin": 49 | # macOS 50 | maybe = ["HTTP", "HTTPS", "SOCKS", "ProxyAutoConfig"] 51 | for x in cmd_rst.replace(" ", "").split("\n"): 52 | if ":" not in x: 53 | continue 54 | tmp = x.split(":") 55 | dic[tmp[0]] = ":".join(tmp[1:]) 56 | for ptl in maybe: 57 | subkey = f"{ptl}Enable" 58 | if subkey in dic and dic[subkey] == "1": 59 | if ptl == "ProxyAutoConfig": 60 | proxy = dic["ProxyAutoConfigURLString"] 61 | else: 62 | proxy = "%s://%s:%s/" % ( 63 | ptl.lower() if ptl != "SOCKS" else "socks5", dic[ptl + "Proxy"], dic[ptl + "Port"] 64 | ) 65 | return proxy 66 | return "" 67 | 68 | @staticmethod 69 | def is_local_connect(add, prt): 70 | # 检测本地是否可通 71 | try: 72 | port = int(port) 73 | if port >= 0 and port <= 65535: 74 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 75 | s.settimeout(1) 76 | return s.connect_ex((add, port)) == 0 77 | except: 78 | return False 79 | 80 | @staticmethod 81 | def is_prot_in_use(port): 82 | try: 83 | port = int(port) 84 | if port >= 0 and port <= 65535: 85 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 86 | s.settimeout(1) 87 | return s.connect_ex(("localhost", port)) == 0 88 | except: 89 | return False 90 | 91 | @staticmethod 92 | def format_time(date_string, style, to="%Y-%m-%d %H:%M:%S"): 93 | try: 94 | return time.strftime(to, time.strptime(str(date_string), style)) 95 | except: 96 | return "Unknown" 97 | -------------------------------------------------------------------------------- /app/plugin/do/dl_stop.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/do/dl_stop/", "PLUGIN") 5 | class doDlStop(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs("dl_stop", ["key"]) 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | 12 | key = str(args["fun"]["key"]) 13 | 14 | if key not in self.CORE.dl.tasks: 15 | return {"code": 0, "msg": "unknown parameters"} 16 | 17 | rep = self.CORE.dl.cancel(key) 18 | 19 | return { 20 | "code": 1, 21 | "msg": {"way": "do", "args": args, "rst": rep}, 22 | } 23 | -------------------------------------------------------------------------------- /app/plugin/do/follow.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/do/follow/", "PLUGIN") 5 | class doFollow(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs("follow", ["userID", "publicity=public"]) 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | 12 | return { 13 | "code": 1, 14 | "msg": { 15 | "way": "do", 16 | "args": args, 17 | "rst": self.follow(args["ops"].copy(), args["fun"].copy()), 18 | }, 19 | } 20 | 21 | def follow(self, opsArg, funArg): 22 | self.STATIC.arg.argsPurer( 23 | funArg, {"userID": "user_id", "publicity": "restrict"} 24 | ) 25 | r = self.CORE.biu.api.user_follow_add(**funArg) 26 | return {"api": "app", "data": r} 27 | -------------------------------------------------------------------------------- /app/plugin/do/mark.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/do/mark/", "PLUGIN") 5 | class getRank(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs("mark", ["workID", "publicity=public"]) 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | 12 | return { 13 | "code": 1, 14 | "msg": { 15 | "way": "do", 16 | "args": args, 17 | "rst": self.mark(args["ops"].copy(), args["fun"].copy()), 18 | }, 19 | } 20 | 21 | def mark(self, opsArg, funArg): 22 | self.STATIC.arg.argsPurer( 23 | funArg, {"workID": "illust_id", "publicity": "restrict"} 24 | ) 25 | r = self.CORE.biu.api.illust_bookmark_add(**funArg) 26 | return {"api": "app", "data": r} 27 | -------------------------------------------------------------------------------- /app/plugin/do/unfollow.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/do/unfollow/", "PLUGIN") 5 | class doUnFollow(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "unfollow", 10 | ["userID"], 11 | ) 12 | except: 13 | return {"code": 0, "msg": "missing parameters"} 14 | 15 | return { 16 | "code": 1, 17 | "msg": { 18 | "way": "do", 19 | "args": args, 20 | "rst": self.unFollow(args["ops"].copy(), args["fun"].copy()), 21 | }, 22 | } 23 | 24 | def unFollow(self, opsArg, funArg): 25 | self.STATIC.arg.argsPurer(funArg, {"userID": "user_id"}) 26 | r = self.CORE.biu.api.user_follow_delete(**funArg) 27 | return {"api": "public", "data": r} 28 | -------------------------------------------------------------------------------- /app/plugin/do/unmark.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/do/unmark/", "PLUGIN") 5 | class getRank(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs("mark", ["workID"]) 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | 12 | return { 13 | "code": 1, 14 | "msg": { 15 | "way": "do", 16 | "args": args, 17 | "rst": self.unmark(args["ops"].copy(), args["fun"].copy()), 18 | }, 19 | } 20 | 21 | def unmark(self, opsArg, funArg): 22 | self.STATIC.arg.argsPurer(funArg, {"workID": "illust_id"}) 23 | r = self.CORE.biu.api.illust_bookmark_delete(**funArg) 24 | return {"api": "app", "data": r} 25 | -------------------------------------------------------------------------------- /app/plugin/do/update_token.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/do/update_token/", "PLUGIN") 5 | class doUpdateToken(interRoot): 6 | def run(self, cmd): 7 | try: 8 | self.STATIC.arg.getArgs("update_token", li=["pass"], way="POST") 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | return { 12 | "code": 1, 13 | "msg": self.CORE.biu.update_token(), 14 | } 15 | -------------------------------------------------------------------------------- /app/plugin/get/idfollowing.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/idfollowing/", "PLUGIN") 5 | class getIDFollowing(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "userFollowing", 10 | [ 11 | "userID=%s" % self.CORE.biu.api.user_id, 12 | "restrict=public", 13 | "&totalPage=5", 14 | "&groupIndex=0", 15 | ], 16 | ) 17 | except: 18 | return {"code": 0, "msg": "missing parameters"} 19 | 20 | return { 21 | "code": 1, 22 | "msg": { 23 | "way": "get", 24 | "args": args, 25 | "rst": self.gank(args["ops"].copy(), args["fun"].copy()), 26 | }, 27 | } 28 | 29 | def gank(self, opsArg, funArg): 30 | self.STATIC.arg.argsPurer(funArg, {"userID": "user_id"}) 31 | status_arg = [] 32 | r = [] 33 | 34 | grpIdx = int(opsArg["groupIndex"]) # 组序号 35 | ttlPage = int(opsArg["totalPage"]) # 每组页数 36 | 37 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 38 | argg = funArg.copy() 39 | argg["offset"] = p * 30 40 | status_arg.append(argg) 41 | 42 | for x in self.CORE.biu.pool_srh.map(self.__thread_gank, status_arg): 43 | r += x 44 | 45 | return {"api": "app", "data": r} 46 | 47 | def __thread_gank(self, kw): 48 | try: 49 | data = self.CORE.biu.api.user_following(**kw) 50 | except: 51 | return [] 52 | if "user_previews" in data and len(data["user_previews"]) != 0: 53 | return [x["user"] for x in data["user_previews"]] 54 | return [] 55 | -------------------------------------------------------------------------------- /app/plugin/get/idmarks.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/idmarks/", "PLUGIN") 5 | class getIDWorks(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "userMarks", 10 | [ 11 | "userID=%s" % self.CORE.biu.api.user_id, 12 | "restrict=public", 13 | "&sortMode=0", 14 | "&isSort=0", 15 | "&totalPage=5", 16 | "&groupIndex=0", 17 | "&markNex=0", 18 | "&tmp=0@0" 19 | ], 20 | ) 21 | except: 22 | return {"code": 0, "msg": "missing parameters"} 23 | 24 | return { 25 | "code": 1, 26 | "msg": { 27 | "way": "get", 28 | "args": args, 29 | "rst": self.gank(args["ops"], args["fun"].copy()), 30 | }, 31 | } 32 | 33 | def gank(self, opsArg, funArg): 34 | self.STATIC.arg.argsPurer(funArg, {"userID": "user_id"}) 35 | r = [] 36 | 37 | ttlPage = int(opsArg["totalPage"]) # 页数 38 | 39 | if str(opsArg["groupIndex"]) != "0": 40 | mstart = str(opsArg["groupIndex"]) 41 | else: 42 | mstart = None 43 | opsArg["markNex"] = "None" 44 | 45 | argg = funArg.copy() 46 | argg["max_bookmark_id"] = mstart 47 | for p in range(ttlPage): 48 | t = self.CORE.biu.api.user_bookmarks_illust(**argg) 49 | if "illusts" in t and len(t["illusts"]) != 0: 50 | r = r + t["illusts"] 51 | if not t["next_url"]: 52 | opsArg["markNex"] = "None" 53 | break 54 | argg = self.CORE.biu.api.parse_qs(t["next_url"]) 55 | opsArg["markNex"] = argg["max_bookmark_id"] 56 | else: 57 | opsArg["markNex"] = "None" 58 | break 59 | 60 | if int(opsArg["isSort"]) == 1: 61 | if str(opsArg["sortMode"]) == "1": 62 | r = sorted(r, key=lambda kv: kv["total_view"], reverse=True) 63 | else: 64 | r = sorted(r, key=lambda kv: kv["total_bookmarks"], reverse=True) 65 | self.CORE.biu.app_works_purer(r) 66 | 67 | return {"api": "app", "data": r} 68 | -------------------------------------------------------------------------------- /app/plugin/get/idworks.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/idworks/", "PLUGIN") 5 | class getIDWorks(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "userWorks", 10 | [ 11 | "userID=%s" % self.CORE.biu.api.user_id, 12 | "type", 13 | "&sortMode=0", 14 | "&isSort=0", 15 | "&totalPage=5", 16 | "&groupIndex=0", 17 | ], 18 | ) 19 | except: 20 | return {"code": 0, "msg": "missing parameters"} 21 | return { 22 | "code": 1, 23 | "msg": { 24 | "way": "get", 25 | "args": args, 26 | "rst": self.gank(args["ops"].copy(), args["fun"].copy()), 27 | }, 28 | } 29 | 30 | def gank(self, opsArg, funArg): 31 | self.STATIC.arg.argsPurer(funArg, {"userID": "user_id"}) 32 | status_arg = [] 33 | r = [] 34 | 35 | grpIdx = int(opsArg["groupIndex"]) # 组序号 36 | ttlPage = int(opsArg["totalPage"]) # 每组页数 37 | 38 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 39 | argg = funArg.copy() 40 | argg["offset"] = p * 30 41 | status_arg.append(argg) 42 | 43 | for x in self.CORE.biu.pool_srh.map(self.__thread_gank, status_arg): 44 | r += x 45 | if int(opsArg["isSort"]) == 1: 46 | if str(opsArg["sortMode"]) == "1": 47 | r = sorted(r, key=lambda kv: kv["total_view"], reverse=True) 48 | else: 49 | r = sorted(r, key=lambda kv: kv["total_bookmarks"], reverse=True) 50 | self.CORE.biu.app_works_purer(r) 51 | 52 | return {"api": "app", "data": r} 53 | 54 | def __thread_gank(self, kw): 55 | try: 56 | data = self.CORE.biu.api.user_illusts(**kw) 57 | except: 58 | return [] 59 | if "illusts" in data and len(data["illusts"]) != 0: 60 | return data["illusts"] 61 | return [] 62 | -------------------------------------------------------------------------------- /app/plugin/get/newtome.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/newtome/", "PLUGIN") 5 | class getNewWorksFromFollowings(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "newToMe", 10 | [ 11 | "restrict=public", 12 | "&sortMode=0", 13 | "&isSort=0", 14 | "&totalPage=5", 15 | "&groupIndex=0", 16 | ], 17 | ) 18 | except: 19 | return {"code": 0, "msg": "missing parameters"} 20 | 21 | return { 22 | "code": 1, 23 | "msg": { 24 | "way": "get", 25 | "args": args, 26 | "rst": self.gank(args["ops"].copy(), args["fun"].copy()), 27 | }, 28 | } 29 | 30 | def gank(self, opsArg, funArg): 31 | status_arg = [] 32 | r = [] 33 | 34 | grpIdx = int(opsArg["groupIndex"]) # 组序号 35 | ttlPage = int(opsArg["totalPage"]) # 每组页数 36 | 37 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 38 | argg = funArg.copy() 39 | argg["offset"] = p * 30 40 | status_arg.append(argg) 41 | 42 | for x in self.CORE.biu.pool_srh.map(self.__thread_gank, status_arg): 43 | r += x 44 | 45 | if int(opsArg["isSort"]) == 1: 46 | if str(opsArg["sortMode"]) == "1": 47 | r = sorted(r, key=lambda kv: kv["total_view"], reverse=True) 48 | else: 49 | r = sorted(r, key=lambda kv: kv["total_bookmarks"], reverse=True) 50 | self.CORE.biu.app_works_purer(r) 51 | 52 | return {"api": "app", "data": r} 53 | 54 | def __thread_gank(self, kw): 55 | try: 56 | data = self.CORE.biu.api.illust_follow(**kw) 57 | except: 58 | return [] 59 | if "illusts" in data and len(data["illusts"]) != 0: 60 | return data["illusts"] 61 | return [] 62 | -------------------------------------------------------------------------------- /app/plugin/get/onework.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/onework/", "PLUGIN") 5 | class getRank(interRoot): 6 | def __init__(self): 7 | self.code = 1 8 | 9 | def run(self, cmd): 10 | try: 11 | args = self.STATIC.arg.getArgs( 12 | "oneWork", ["workID", "&totalPage=all", "&groupIndex=all"]) 13 | except: 14 | return {"code": 0, "msg": "missing parameters"} 15 | 16 | return { 17 | "code": self.code, 18 | "msg": { 19 | "way": "get", 20 | "args": args, 21 | "rst": self.one(args["ops"].copy(), args["fun"].copy()), 22 | }, 23 | } 24 | 25 | def one(self, opsArg, funArg): 26 | r = self.CORE.biu.api.illust_detail(funArg["workID"]) 27 | 28 | if "illust" not in r: 29 | self.code = 0 30 | return "error" 31 | r = [r["illust"]] 32 | 33 | self.CORE.biu.app_works_purer(r) 34 | 35 | if len(r[0]["all"]["meta_pages"]) > 0: 36 | num = len(r[0]["all"]["meta_pages"]) 37 | for i in range(1, num): 38 | r += [r[0].copy()] 39 | del r[i]['image_urls'] 40 | r[i]['image_urls'] = {} 41 | r[i]["image_urls"]["small"] = r[i]["all"]["meta_pages"][i]["image_urls"][ 42 | "square_medium" 43 | ] 44 | r[i]["image_urls"]["medium"] = r[i]["all"]["meta_pages"][i]["image_urls"][ 45 | "medium" 46 | ] 47 | r[i]["image_urls"]["large"] = r[i]["all"]["meta_pages"][i]["image_urls"][ 48 | "large" 49 | ] 50 | r[i]['title'] = '~' + str(i) 51 | 52 | return {"api": "app", "data": r} 53 | -------------------------------------------------------------------------------- /app/plugin/get/rank.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/rank/", "PLUGIN") 5 | class getRank(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "rank", ["mode=day", "date=0", "&totalPage=5", "&groupIndex=0"] 10 | ) 11 | except: 12 | return {"code": 0, "msg": "missing parameters"} 13 | 14 | if len(str(args["fun"]["date"]).split("-")) != 3: 15 | args["fun"]["date"] = None 16 | 17 | return { 18 | "code": 1, 19 | "msg": { 20 | "way": "get", 21 | "args": args, 22 | "rst": self.gank(args["ops"].copy(), args["fun"].copy()), 23 | }, 24 | } 25 | 26 | def gank(self, opsArg, funArg): 27 | status_arg = [] 28 | r = [] 29 | 30 | grpIdx = int(opsArg["groupIndex"]) # 组序号 31 | ttlPage = int(opsArg["totalPage"]) # 每组页数 32 | 33 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 34 | argg = funArg.copy() 35 | argg["offset"] = p * 30 36 | status_arg.append(argg) 37 | 38 | for x in self.CORE.biu.pool_srh.map(self.__thread_rank, status_arg): 39 | r += x 40 | self.CORE.biu.app_works_purer(r) 41 | 42 | return {"api": "app", "data": r} 43 | 44 | def __thread_rank(self, kw): 45 | try: 46 | data = self.CORE.biu.api.illust_ranking(**kw) 47 | return data["illusts"] 48 | except Exception as e: 49 | self.STATIC.localMsger.error(e) 50 | return [] 51 | -------------------------------------------------------------------------------- /app/plugin/get/recommend.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/recommend/", "PLUGIN") 5 | class getRmd(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "recommend", 10 | [ 11 | "type=illust", 12 | "&sortMode=0", 13 | "&isSort=0", 14 | "&totalPage=5", 15 | "&groupIndex=0", 16 | ], 17 | ) 18 | except: 19 | return {"code": 0, "msg": "missing parameters"} 20 | 21 | return { 22 | "code": 1, 23 | "msg": { 24 | "way": "get", 25 | "args": args, 26 | "rst": self.gank(args["ops"].copy(), args["fun"].copy()), 27 | }, 28 | } 29 | 30 | def gank(self, opsArg, funArg): 31 | self.STATIC.arg.argsPurer(funArg, {"type": "content_type"}) 32 | status_arg = [] 33 | r = [] 34 | 35 | grpIdx = int(opsArg["groupIndex"]) # 组序号 36 | ttlPage = int(opsArg["totalPage"]) # 每组页数 37 | 38 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 39 | argg = funArg.copy() 40 | argg["offset"] = p * 30 41 | status_arg.append(argg) 42 | 43 | for x in self.CORE.biu.pool_srh.map(self.__thread_gank, status_arg): 44 | r += x 45 | 46 | if int(opsArg["isSort"]) == 1: 47 | if str(opsArg["sortMode"]) == "1": 48 | r = sorted(r, key=lambda kv: kv["total_view"], reverse=True) 49 | else: 50 | r = sorted(r, key=lambda kv: kv["total_bookmarks"], reverse=True) 51 | self.CORE.biu.app_works_purer(r) 52 | 53 | return {"api": "app", "data": r} 54 | 55 | def __thread_gank(self, kw): 56 | try: 57 | data = self.CORE.biu.api.illust_recommended(**kw) 58 | except: 59 | return [] 60 | if "illusts" in data and len(data["illusts"]) != 0: 61 | return data["illusts"] 62 | return [] 63 | -------------------------------------------------------------------------------- /app/plugin/search/images.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from flask import request 3 | 4 | from altfe.interface.root import interRoot 5 | 6 | 7 | @interRoot.bind("api/biu/search/images/", "PLUGIN") 8 | class searchImages(interRoot): 9 | def run(self, cmd): 10 | try: 11 | args = self.STATIC.arg.getArgs("searchImages", ["url=no"]) 12 | except: 13 | return {"code": 0, "msg": "missing parameters"} 14 | 15 | image_url = str(args["fun"]["url"]) 16 | image_file = request.files.get("image") 17 | if image_url == "no" and image_file is None: 18 | return {"code": 0, "msg": "need url or image file"} 19 | 20 | api_key = self.INS.conf.get("biu_default", "secret.key.apiSauceNAO") 21 | if api_key is None or api_key == "": 22 | return {"code": 0, "msg": "function offline"} 23 | 24 | params = { 25 | "output_type": 2, 26 | "dbmask": 96, 27 | "api_key": api_key 28 | } 29 | others = {} 30 | 31 | if image_url != "no": 32 | params.update({"url": image_url}) 33 | else: 34 | others.update({"files": {"file": image_file}}) 35 | if self.CORE.biu.proxy != "": 36 | others.update({"proxies": {"https": self.CORE.biu.proxy}}) 37 | 38 | rep = requests.post("https://saucenao.com/search.php", timeout=10, params=params, **others).json() 39 | 40 | if rep["header"].get("status") != 0: 41 | if "anonymous" in rep["header"].get("message"): 42 | return {"code": 0, "msg": "wrong key"} 43 | return {"code": 0, "msg": rep["header"].get("message")} 44 | 45 | return { 46 | "code": 1, 47 | "msg": { 48 | "way": "searchImages", 49 | "args": args, 50 | "rst": rep, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /app/plugin/search/users.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/search/users/", "PLUGIN") 5 | class searchUsers(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs( 9 | "searchUsers", 10 | [ 11 | "kt", 12 | "&totalPage=5", 13 | "&groupIndex=0", 14 | ], 15 | ) 16 | except: 17 | return {"code": 0, "msg": "missing parameters"} 18 | 19 | return { 20 | "code": 1, 21 | "msg": { 22 | "way": "search", 23 | "args": args, 24 | "rst": self.gank(args["ops"].copy(), args["fun"].copy()), 25 | }, 26 | } 27 | 28 | def gank(self, opsArg, funArg): 29 | self.STATIC.arg.argsPurer(funArg, {"kt": "word"}) 30 | status_arg = [] 31 | r = [] 32 | 33 | grpIdx = int(opsArg["groupIndex"]) # 组序号 34 | ttlPage = int(opsArg["totalPage"]) # 每组页数 35 | 36 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 37 | argg = funArg.copy() 38 | argg["offset"] = p * 30 39 | status_arg.append(argg) 40 | 41 | for x in self.CORE.biu.pool_srh.map(self.__thread_gank, status_arg): 42 | r += x 43 | 44 | return {"api": "app", "data": r} 45 | 46 | def __thread_gank(self, kw): 47 | try: 48 | data = self.CORE.biu.api.search_user(**kw) 49 | except: 50 | return [] 51 | if "user_previews" in data and len(data["user_previews"]) != 0: 52 | return [x["user"] for x in data["user_previews"]] 53 | return [] 54 | -------------------------------------------------------------------------------- /app/plugin/search/works.py: -------------------------------------------------------------------------------- 1 | import re 2 | from concurrent.futures import as_completed 3 | 4 | from altfe.interface.root import interRoot 5 | 6 | 7 | @interRoot.bind("api/biu/search/works/", "PLUGIN") 8 | class searchWorks(interRoot): 9 | def run(self, cmd): 10 | try: 11 | args = self.STATIC.arg.getArgs( 12 | "works", 13 | [ 14 | "kt", 15 | "mode=tag", 16 | "&totalPage=5", 17 | "&groupIndex=0", 18 | "&sortMode=0", 19 | "&isSort=0", 20 | "&isCache=1", 21 | ], 22 | ) 23 | except: 24 | return {"code": 0, "msg": "missing parameters"} 25 | 26 | code = 1 27 | isCache = int(args["ops"]["isCache"]) and self.CORE.biu.sets["biu"]["search"]["loadCacheFirst"] 28 | cachePath = self.getENV("rootPath") + "usr/cache/search/" 29 | fileName = "%s@%s_%sx%s_%s%s.json" % ( 30 | args["fun"]["kt"], 31 | args["fun"]["mode"], 32 | args["ops"]["totalPage"], 33 | args["ops"]["groupIndex"], 34 | args["ops"]["sortMode"], 35 | args["ops"]["isSort"], 36 | ) 37 | fileName = re.sub(r'[/\\:*?"<>|]', "_", fileName) 38 | 39 | if isCache: 40 | isCacheFile = self.STATIC.file.ain(cachePath + fileName) 41 | if isCache and isCacheFile: 42 | rst = isCacheFile 43 | code = 2 44 | else: 45 | rst = self.appWorks(args["ops"].copy(), args["fun"].copy()) 46 | self.STATIC.file.aout(cachePath + fileName, rst, "w", False) 47 | 48 | return { 49 | "code": code, 50 | "msg": {"way": "search", "args": args, "rst": rst}, 51 | } 52 | 53 | # app api 搜索 54 | def appWorks(self, opsArg, funArg): 55 | modes = { 56 | "tag": "partial_match_for_tags", 57 | "otag": "exact_match_for_tags", 58 | "des": "title_and_caption", 59 | } 60 | 61 | r = {"api": "app", "total": 0, "data": []} 62 | # search_target 63 | self.STATIC.arg.argsPurer(funArg, {"kt": "word", "mode": "search_target"}) 64 | funArg["search_target"] = modes[funArg["search_target"]] 65 | 66 | status = [] 67 | 68 | grpIdx = int(opsArg["groupIndex"]) # 组序号 69 | ttlPage = int(opsArg["totalPage"]) # 每组页数 70 | 71 | for p in range(grpIdx * ttlPage, (grpIdx + 1) * ttlPage): 72 | argg = funArg.copy() 73 | argg["offset"] = p * 30 74 | status.append(self.CORE.biu.pool_srh.submit(self.__thread_appWorks, **argg)) 75 | 76 | self.CORE.biu.update_status( 77 | "search", (funArg["word"] + "_" + str(ttlPage) + "+" + str(grpIdx)), status, 78 | ) 79 | 80 | for x in as_completed(status): 81 | r["data"] += x.result() 82 | 83 | r["total"] = len(r["data"]) 84 | 85 | if int(opsArg["isSort"]) == 1: 86 | if str(opsArg["sortMode"]) == "1": 87 | r["data"] = sorted( 88 | r["data"], key=lambda kv: kv["total_view"], reverse=True 89 | ) 90 | else: 91 | r["data"] = sorted( 92 | r["data"], key=lambda kv: kv["total_bookmarks"], reverse=True 93 | ) 94 | self.CORE.biu.app_works_purer(r["data"]) 95 | 96 | return r 97 | 98 | def __thread_appWorks(self, **kw): 99 | try: 100 | data = self.CORE.biu.api.search_illust(**kw) 101 | return data["illusts"] 102 | except Exception as e: 103 | self.STATIC.localMsger.error(e) 104 | return [] 105 | -------------------------------------------------------------------------------- /app/plugin/sys/language.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/language/", "PLUGIN") 5 | class PluginBiuLanguage(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs("biu_language", ["theme", "code=_"]) 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | 12 | theme = str(args["fun"]["theme"]) 13 | code = str(args["fun"]["code"]) 14 | 15 | tmp = self.INS.i18n.get_wrapper(None if code == "_" else code) 16 | 17 | return {"code": 1, "msg": tmp.get(f"theme.{theme}", default=None)} 18 | -------------------------------------------------------------------------------- /app/plugin/sys/outdated.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from altfe.interface.root import interRoot 6 | 7 | 8 | @interRoot.bind("api/biu/get/outdated/", "PLUGIN") 9 | class outdated(interRoot): 10 | def run(self, cmd): 11 | try: 12 | r = json.loads(requests.get("https://biu.tls.moe/d/biuinfo.json", timeout=6).text) 13 | except: 14 | r = self.CORE.biu.biuInfo 15 | return { 16 | "code": 1, 17 | "msg": { 18 | "latest": self.CORE.biu.ver >= r["version"], 19 | "current": self.CORE.biu.format_version(), 20 | "online": r, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/plugin/sys/status.py: -------------------------------------------------------------------------------- 1 | from altfe.interface.root import interRoot 2 | 3 | 4 | @interRoot.bind("api/biu/get/status/", "PLUGIN") 5 | class getStatus(interRoot): 6 | def run(self, cmd): 7 | try: 8 | args = self.STATIC.arg.getArgs("biu_status", ["type", "key"]) 9 | except: 10 | return {"code": 0, "msg": "missing parameters"} 11 | 12 | idx = "rate_" + args["fun"]["type"] 13 | key = str(args["fun"]["key"]) 14 | 15 | if args["fun"]["type"] == "search" and key not in self.CORE.biu.STATUS[idx]: 16 | return {"code": 0, "msg": "unknown parameters"} 17 | elif args["fun"]["type"] == "download" and key not in self.CORE.dl.tasks and key != "__all__": 18 | return {"code": 0, "msg": "unknown parameters"} 19 | 20 | rep = [] 21 | 22 | if idx == "rate_download": 23 | if key == "__all__": 24 | rep = self.CORE.dl.status() 25 | else: 26 | for x in self.CORE.dl.status(key): 27 | if x == "done": 28 | rep.append("done") 29 | elif x is None or x == "failed": 30 | rep.append("failed") 31 | else: 32 | rep.append("running") 33 | elif idx == "rate_search": 34 | for x in self.CORE.biu.STATUS[idx][key]: 35 | if x.done(): 36 | rep.append("done") 37 | else: 38 | rep.append("running") 39 | 40 | return { 41 | "code": 1, 42 | "msg": {"way": "get", "args": args, "rst": rep, }, 43 | } 44 | -------------------------------------------------------------------------------- /app/v2/utils/sprint.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import platform 4 | import time 5 | import traceback 6 | 7 | BIUMSG_METHODS = { 8 | "default": 0, 9 | "highlight": 1, 10 | "underline": 4, 11 | "flash": 5, 12 | "anti": 7, 13 | "disable": 8, 14 | } 15 | 16 | BIUMSG_COLORS = { 17 | "black": (30, 40), 18 | "red": (31, 41), 19 | "green": (32, 42), 20 | "yellow": (33, 43), 21 | "blue": (34, 44), 22 | "purple-red": (35, 45), 23 | "cyan-blue": (36, 46), 24 | "white": (37, 47), 25 | "default": (38, 38), 26 | } 27 | 28 | BIUMSG_isColor = True 29 | if os.name == "nt": 30 | os.system("color") 31 | if "10" not in platform.platform(): 32 | BIUMSG_isColor = False 33 | 34 | 35 | class SPrint(object): 36 | @classmethod 37 | def sign(cls, text: str): 38 | return cls.mformat(text, "white", "black", "highlight") 39 | 40 | @classmethod 41 | def error(cls, text: str): 42 | return "Error at %s\n" % time.strftime( 43 | "%Y-%m-%d %H:%M:%S", time.localtime() 44 | ) + cls.mformat(text, "red") 45 | 46 | @classmethod 47 | def green(cls, text: str): 48 | return cls.mformat(text, "green") 49 | 50 | @classmethod 51 | def red(cls, text: str): 52 | return cls.mformat(text, "red") 53 | 54 | @classmethod 55 | def mformat(cls, text, front, back=None, method="default", header=None): 56 | if isinstance(text, Exception): 57 | text = traceback.format_exc() 58 | else: 59 | text = str(text) 60 | if header in (None, False): 61 | finalText = text 62 | else: 63 | finalText = "[%s] %s" % (header, text) 64 | if BIUMSG_isColor is False: 65 | return finalText 66 | if back is None: 67 | r = f"\033[{BIUMSG_COLORS[front][0]}m{finalText}\033[0m" 68 | else: 69 | r = f"\033[{BIUMSG_METHODS[method]};{BIUMSG_COLORS[front][0]};{BIUMSG_COLORS[back][1]}m{finalText}\033[0m" 70 | return r 71 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # 自定义配置 2 | # 请参考默认配置文件 (./app/config/biu_default.yml) 3 | # 将需要修改的配置项添加到末尾即可 4 | # - 5 | # Custom Config 6 | # Please refer to the default config file (./app/config/biu_default.yml) 7 | # Add the config items that need to be modified to the end 8 | 9 | sys.host: "127.0.0.1:4001" 10 | sys.proxy: "" 11 | sys.language: "" 12 | -------------------------------------------------------------------------------- /docs/README_EN.md: -------------------------------------------------------------------------------- 1 | # PixivBiu 2 | 3 | PixivBiu, a nice Pixiv **assistant** tool. 4 | 5 | - [中文](/README.md) 6 | - [English](/docs/README_EN.md) 7 | - [日本語](/docs/README_JA.md) 8 | - [Español](/docs/README_ES.md) 9 | 10 | ## Features 11 | 12 | * Pixiv search, allows sorting by number of favorites, popularity, and date without membership 13 | * Download original images, including illustrations, comics, and animated GIFs 14 | * Multiple download modes, including single-threaded, multi-threaded, and support for aria2 15 | * Access user's works, collections, following list, recommendations, etc 16 | * Filter images by width, height, type, tags, etc 17 | 18 | ## Usage 19 | 20 | ### Source Code 21 | 22 | * Install dependencies by executing `pip install -r requirements.txt` 23 | + [Flask](https://github.com/pallets/flask), [requests](https://github.com/psf/requests), [PyYAML](https://github.com/yaml/pyyaml), [Pillow](https://github.com/python-pillow/Pillow), [PixivPy](https://github.com/upbit/pixivpy), [PySocks](https://github.com/Anorov/PySocks) 24 | * Modify the config items in `./config.yml`, and you can refer to the [default config file](/app/config/biu_en.yml) for details 25 | * Execute `python main.py` 26 | * Access the running address, which is by default `http://127.0.0.1:4001/`. 27 | 28 | ### Executable Binary File 29 | 30 | This project is written in `Python@3.10(+)` and is compiled using `PyInstaller`. 31 | 32 | Compiled versions are provided for Windows, macOS, and Ubuntu. If you have other requirements, please compile it yourself. 33 | 34 | You can download the specific versions from [GitHub Releases](https://github.com/txperl/PixivBiu/releases) or [here](https://biu.tls.moe/#/lib/dl). 35 | 36 | ### Docker 37 | 38 | - [Docker_Buildx_PixivBiu](https://github.com/zzcabc/Docker_Buildx_PixivBiu) by [zzcabc](https://github.com/zzcabc) 39 | 40 | ## Contribution 41 | 42 | If you want to participate in the development of this project, you are welcome to check [development document](https://biu.tls.moe/#/develop/quickin). 43 | 44 | ## Other 45 | 46 | ### Thanks to 47 | 48 | * [pixivpy](https://github.com/upbit/pixivpy) API support 49 | * [pixiv.cat](https://pixiv.cat/) Reverse proxy server support 50 | * [HTML5 UP](https://html5up.net/) Front-end code support 51 | 52 | ### Terms 53 | 54 | * This program (PixivBiu) is for learning and exchange purposes only. Please delete it after achieving its initial goal 55 | * The original author is not responsible for any unforeseen events that may occur after use, and does not assume any liability 56 | * [MIT License](https://choosealicense.com/licenses/mit/) 57 | -------------------------------------------------------------------------------- /docs/README_ES.md: -------------------------------------------------------------------------------- 1 | # PixivBiu 2 | 3 | PixivBiu, Una bonita herramienta **asistente** de Pixiv. 4 | 5 | - [中文](/README.md) 6 | - [English](/docs/README_EN.md) 7 | - [日本語](/docs/README_JA.md) 8 | - [Español](/docs/README_ES.md) 9 | 10 | ## Características 11 | 12 | * Búsqueda de Pixiv, permite ordenar por número de favoritos, popularidad y fecha sin membresía. 13 | * Descargue imágenes originales, incluidas ilustraciones, cómics y GIF animados. 14 | * Múltiples modos de descarga, incluidos un solo subproceso, subproceso múltiple y compatibilidad con aria2. 15 | * Acceda a las obras del usuario, colecciones, lista de seguidores, recomendaciones, etc. 16 | * Filtrar imágenes por ancho, alto, tipo, etiquetas, etc. 17 | 18 | ## Uso 19 | 20 | ### Código fuente 21 | 22 | * Instalar dependencias ejecutando `pip install -r requirements.txt` 23 | + [Flask](https://github.com/pallets/flask), [requests](https://github.com/psf/requests), [PyYAML](https://github.com/yaml/pyyaml), [Pillow](https://github.com/python-pillow/Pillow), [PixivPy](https://github.com/upbit/pixivpy), [PySocks](https://github.com/Anorov/PySocks) 24 | * Modificar los elementos de configuración relevantes en `./config.yml`, y puedes consultar el [archivo de configuración predeterminado](/app/config/biu_es.yml) para detalles 25 | * Ejecuta `python main.py` 26 | * Acceda a la dirección de ejecución, que es la predeterminada `http://127.0.0.1:4001/`. 27 | 28 | ### Archivo binario ejecutable 29 | 30 | Este proyecto esta escrito en `python@3.10(+)` y esta compilado usando `PyInstaller`. 31 | 32 | Se proporcionan versiones compiladas para Windows, macOS y Ubuntu. Si tienes otros requisitos, compílalos tú mismo. 33 | 34 | Puedes descargar la versión especifica desde [Github Releases](https://github.com/txperl/PixivBiu/releases) o [Aqui](https://biu.tls.moe/#/lib/dl). 35 | 36 | ### Docker 37 | 38 | - [Docker_Buildx_PixivBiu](https://github.com/zzcabc/Docker_Buildx_PixivBiu) por [zzcabc](https://github.com/zzcabc) 39 | 40 | ## Contribución 41 | 42 | Si deseas participar en el desarrollo de este proyecto, te invitamos a consultar [documento de desarrollo](https://biu.tls.moe/#/develop/quickin). 43 | 44 | ## Otros 45 | 46 | ### Gracias para 47 | 48 | * [pixivpy](https://github.com/upbit/pixivpy) Soporte de API 49 | * [pixiv.cat](https://pixiv.cat/) Compatibilidad con servidores proxy inversos 50 | * [HTML5 UP](https://html5up.net/) Soporte de código front-end 51 | 52 | ### Terminos 53 | 54 | * Este programa (PixivBiu) es solo para fines de aprendizaje e intercambio. Elimínelo después de lograr su objetivo inicial. 55 | * El autor original no es responsable de ningún evento imprevisto que pueda ocurrir después del uso y no asume ninguna responsabilidad. 56 | * [MIT License](https://choosealicense.com/licenses/mit/) -------------------------------------------------------------------------------- /docs/README_JA.md: -------------------------------------------------------------------------------- 1 | # PixivBiu 2 | 3 | PixivBiuはPixivのための**補助的な**ツールです。 4 | 5 | - [中文](/README.md) 6 | - [English](/docs/README_EN.md) 7 | - [日本語](/docs/README_JA.md) 8 | - [Español](/docs/README_ES.md) 9 | 10 | ## 機能 11 | 12 | * お気に入り数(会員の除外可)順や人気順でのPixiv検索 13 | * イラスト/漫画/うごイラを含む画像のオリジナル画質でのダウンロード 14 | * シングル/マルチスレッドや [aria2](https://github.com/aria2/aria2) などでのダウンロード 15 | * 指定したユーザの投稿作品/ブックマーク/フォロワー/関連するおすすめなどの取得 16 | * 画像の幅、高さ、タイプ、ラベルなどをフィルタリングします 17 | 18 | ## 使い方 19 | 20 | ### ソースコードから 21 | 22 | * 依存ライブラリのインストール: `pip install -r requirements.txt` 23 | * [Flask](https://github.com/pallets/flask), [requests](https://github.com/psf/requests), [PyYAML](https://github.com/yaml/pyyaml), [Pillow](https://github.com/python-pillow/Pillow), [PixivPy](https://github.com/upbit/pixivpy), [PySocks](https://github.com/Anorov/PySocks) 24 | * `./config.yml` の設定(例:[デフォルトの設定ファイル](/app/config/biu_ja.yml)) 25 | * 実行: `python main.py` 26 | * 実行中のページを開く(デフォルトのURL: `http://127.0.0.1:4001/`) 27 | 28 | ### 実行バイナリから 29 | 30 | このプロジェクトは Python 3.10 以上で開発されており、実行バイナリのビルドには `PyInstaller` を使用しています。 31 | 32 | Windows 版、macOS 版と Ubuntu 版が利用可能ですが、もし必要であれば自分でビルドを試してください。 33 | 34 | ビルド済バイナリは [GitHub Releases](https://github.com/txperl/PixivBiu/releases) もしくは[こちら](https://biu.tls.moe/#/lib/dl)からダウンロードできます。 35 | 36 | ### Dockerから 37 | 38 | - [Docker_Buildx_PixivBiu](https://github.com/zzcabc/Docker_Buildx_PixivBiu) by [zzcabc](https://github.com/zzcabc) 39 | 40 | ## 貢献 41 | 42 | もしこのプロジェクトの開発に参加したいのであれば、お気軽に[開発ドキュメント(中文)](https://biu.tls.moe/#/develop/quickin)をご参照ください。 43 | 44 | ## その他 45 | 46 | ### 感謝 47 | 48 | * [pixivpy](https://github.com/upbit/pixivpy) APIの提供 49 | * [pixiv.cat](https://pixiv.cat/) Anti-generationのためのサーバの提供 50 | * [HTML5 UP](https://html5up.net/) フロントエンドのコード提供 51 | 52 | ### 告知事項 53 | 54 | * 本プログラム(PixivBiu)は学習と交流のみを目的としておりますので、当初の目的を達成した後は自分で削除してください 55 | * 使用後の如何なる問題も作者には一切関係なく、また作者は一切の責任を負いません 56 | * [MITライセンス](https://choosealicense.com/licenses/mit/)です 57 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import traceback 5 | import webbrowser 6 | 7 | from flask import Flask, jsonify, render_template 8 | 9 | from altfe import bridge, handle 10 | from altfe.interface.root import classRoot 11 | 12 | rootPath = os.path.split(os.path.realpath(sys.argv[0]))[0] + "/" 13 | rootPathFrozen = sys._MEIPASS + "/" if getattr(sys, "frozen", False) else rootPath 14 | 15 | app = Flask( 16 | __name__, 17 | template_folder=rootPath + "usr/templates", 18 | static_folder=rootPath + "usr/static", 19 | ) 20 | 21 | 22 | # 路由 23 | @app.route("/") 24 | def home(): 25 | return render_template("%s/index.html" % (SETS["sys"]["theme"])) 26 | 27 | 28 | @app.route("/", methods=["GET", "POST"]) 29 | def api(path): 30 | return jsonify(handle.handleRoute.do(path)) 31 | 32 | 33 | if __name__ == "__main__": 34 | # Altfe 框架初始化 35 | classRoot.setENV("rootPath", rootPath) 36 | classRoot.setENV("rootPathFrozen", rootPathFrozen) 37 | bridge.bridgeInit().run(hint=True) 38 | 39 | # 加载配置项 40 | SETS = classRoot.osGet("LIB_INS", "conf").dict("biu_default") 41 | 42 | # 调整日志等级 43 | if not SETS["sys"]["debug"]: 44 | cli = sys.modules["flask.cli"] 45 | cli.show_server_banner = lambda *x: None 46 | logging.getLogger("werkzeug").setLevel(logging.ERROR) 47 | 48 | # 启动 49 | try: 50 | if SETS["sys"]["autoOpen"]: 51 | webbrowser.open("http://" + SETS["sys"]["host"]) 52 | app.run( 53 | host=SETS["sys"]["host"].split(":")[0], 54 | port=SETS["sys"]["host"].split(":")[1], 55 | debug=SETS["sys"]["debug"], 56 | threaded=True, 57 | use_reloader=False, 58 | ) 59 | except UnicodeDecodeError: 60 | print("您的计算机名可能存在特殊字符,程序无法正常运行。") 61 | print( 62 | "若是 Windows 系统,可以尝试进入「计算机-属性-高级系统设置-计算机名-更改」,修改计算机名,只可含有 ASCII 码支持的字符。" 63 | ) 64 | input("按任意键退出...") 65 | except Exception: 66 | print(traceback.format_exc()) 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.8.30 2 | charset-normalizer==3.3.2 3 | click==8.1.7 4 | cloudscraper==1.2.71 5 | Flask==2.2.5 6 | idna==3.8 7 | importlib-metadata==6.7.0 8 | itsdangerous==2.1.2 9 | Jinja2==3.1.4 10 | MarkupSafe==2.1.5 11 | Pillow==10.4.0 12 | pixivpy3==3.7.5 13 | pyparsing==3.1.4 14 | PySocks==1.7.1 15 | PyYAML==6.0.1 16 | requests==2.31.0 17 | requests-toolbelt==1.0.0 18 | typing_extensions==4.7.1 19 | urllib3==2.0.7 20 | Werkzeug==2.2.3 21 | zipp==3.15.0 22 | -------------------------------------------------------------------------------- /usr/static/multiverse/README.txt: -------------------------------------------------------------------------------- 1 | Multiverse by HTML5 UP 2 | html5up.net | @ajlkn 3 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 4 | 5 | 6 | Say hello to Multiverse, a slick, one-page gallery design with a fully functional lightbox 7 | (courtesy of my Poptrox plugin for jQuery) and a custom, reusable "panel" system (click the 8 | "About" button in the lower right to see what I mean). Had a ton of fun putting this one 9 | together, and I hope you have as much fun working with it :) 10 | 11 | Demo images* courtesy of Unsplash, a radtastic collection of CC0 (public domain) images 12 | you can use for pretty much whatever. 13 | 14 | (* = not included) 15 | 16 | AJ 17 | aj@lkn.io | @ajlkn 18 | 19 | 20 | Credits: 21 | 22 | Demo Images: 23 | Unsplash (unsplash.com) 24 | 25 | Icons: 26 | Font Awesome (fontawesome.io) 27 | 28 | Other: 29 | jQuery (jquery.com) 30 | Poptrox (github.com/ajlkn/jquery.poptrox) 31 | Responsive Tools (github.com/ajlkn/responsive-tools) -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/images/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/images/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/n.css: -------------------------------------------------------------------------------- 1 | .weight-6 { 2 | font-weight: 600 !important; 3 | } 4 | 5 | #main { 6 | transition-property: margin-top; 7 | transition-duration: 1s; 8 | transition-timing-function: ease; 9 | margin-bottom: 100px; 10 | } 11 | 12 | .aniMoveDown { 13 | margin-top: 100% !important; 14 | } 15 | 16 | .search-area { 17 | transition-property: padding; 18 | transition-duration: 1s; 19 | transition-timing-function: ease; 20 | width: 100%; 21 | padding: 100px 2.5em 3em 2.5em; 22 | } 23 | 24 | .search-area-by { 25 | padding: 100px 0 3em 0 !important; 26 | } 27 | 28 | .search-area .bar input { 29 | cursor: text; 30 | outline: 0; 31 | width: 100%; 32 | margin: 0; 33 | padding: 0; 34 | height: 1.5em; 35 | text-indent: .2em; 36 | border: 0; 37 | background: #242629; 38 | border-radius: 4px; 39 | font: inherit; 40 | font-size: 5em; 41 | } 42 | 43 | #main .thumb>a { 44 | transition-property: border; 45 | transition-duration: .5s; 46 | transition-timing-function: ease; 47 | } 48 | 49 | #main .thumb>h2 { 50 | bottom: 1.475em; 51 | font-size: 1.2em; 52 | left: 0; 53 | padding: 0 1.6875em; 54 | } 55 | 56 | #main .thumb .thumbAction a>b, 57 | #main .thumb .thumbAction a>d, 58 | #main .thumb .thumbAction a>op, 59 | #main .thumb .thumbAction a>sf { 60 | position: absolute; 61 | top: .4em; 62 | background: rgba(0, 0, 0, .2); 63 | backdrop-filter: blur(3px); 64 | color: #FFF; 65 | text-shadow: 0 1px 0 #000; 66 | font-size: 1.15em; 67 | font-weight: 700; 68 | line-height: 2em; 69 | padding: 0 .4em; 70 | border-radius: .2em; 71 | } 72 | 73 | #main .thumb .thumbAction a>b { 74 | left: .4em; 75 | } 76 | 77 | #main .thumb .thumbAction a>d { 78 | right: .4em; 79 | } 80 | 81 | #main .thumb .thumbAction a>op { 82 | right: 3.8em; 83 | } 84 | 85 | #main .thumb .thumbAction a>sf { 86 | top: 2.8em; 87 | right: .4em; 88 | } 89 | 90 | #main .thumb>.imageBtn { 91 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 92 | background-position: center; 93 | background-repeat: no-repeat; 94 | background-size: cover; 95 | border: 0; 96 | height: 100%; 97 | left: 0; 98 | position: absolute; 99 | top: 0; 100 | width: 100%; 101 | } 102 | 103 | .proer-dling { 104 | border: 25px solid #34a58e !important; 105 | } 106 | 107 | .proer-done { 108 | border: 50px solid #263238 !important; 109 | } 110 | 111 | .proer-error { 112 | border: 25px solid #880E4F !important; 113 | } 114 | 115 | .mask-r18 { 116 | /* opacity: 0.05 !important; */ 117 | filter: blur(15px) !important; 118 | } 119 | 120 | #setting { 121 | padding: 2em 4em 1em 4em; 122 | } 123 | 124 | .settings section { 125 | margin: 0 0 1.2em 0; 126 | } 127 | 128 | #filter { 129 | padding: 2em 4em 1em 4em; 130 | } 131 | 132 | .filters h2 { 133 | margin-bottom: 0.3em; 134 | } 135 | 136 | .filters p { 137 | margin: 0 0 1.5em 0; 138 | } 139 | 140 | .filters section { 141 | margin: 0 0 0.6em 0; 142 | } 143 | 144 | .filters button { 145 | height: 2.7em; 146 | line-height: 2.7em; 147 | padding: 0 1.5em; 148 | } 149 | 150 | .filters input { 151 | -moz-appearance: none; 152 | -webkit-appearance: none; 153 | -ms-appearance: none; 154 | appearance: none; 155 | color: #fff; 156 | background: #34363b; 157 | border: 0; 158 | border-radius: 0; 159 | outline: 0; 160 | text-decoration: none; 161 | padding: .35em .6em; 162 | margin: 5px 0; 163 | } 164 | 165 | .filters section div { 166 | display: grid; 167 | grid-template-columns: repeat(2, minmax(0, 1fr)); 168 | grid-gap: 0 3px; 169 | } 170 | 171 | .filters section .grid-3 { 172 | grid-template-columns: repeat(3, minmax(0, 1fr)); 173 | } 174 | 175 | #headerGrpIdxBox { 176 | cursor: text; 177 | outline: 0; 178 | border: 0; 179 | color: #fff; 180 | background-color: #1f2224; 181 | width: 3em; 182 | text-align: center; 183 | } 184 | 185 | #header nav>ul>li i.icon:before { 186 | color: #505051; 187 | } 188 | 189 | .thumb h2 a { 190 | border-bottom: none !important; 191 | } 192 | 193 | .poptrox-popup a { 194 | color: #fff; 195 | } 196 | 197 | .poptrox-popup .one-line { 198 | overflow: hidden; 199 | text-overflow: ellipsis; 200 | white-space: nowrap; 201 | } 202 | 203 | .poptrox-popup .no-des a { 204 | border-bottom: none; 205 | } 206 | 207 | .poptrox-popup .caption { 208 | position: fixed; 209 | background-image: none; 210 | text-shadow: 0 0 15px #000; 211 | } 212 | 213 | .poptrox-popup:before { 214 | display: none !important; 215 | } 216 | 217 | .poptrox-popup .nav-previous, 218 | .poptrox-popup .nav-next { 219 | display: block !important; 220 | filter: drop-shadow(0 0 3px rgb(0, 0, 0)); 221 | } 222 | 223 | @media screen and (max-width: 1680px) { 224 | #main .thumb { 225 | width: 25% !important; 226 | } 227 | } 228 | 229 | @media screen and (min-width: 1680px) { 230 | 231 | body, 232 | input, 233 | select, 234 | textarea { 235 | font-size: 12pt; 236 | } 237 | } 238 | 239 | @media screen and (max-width: 1280px) { 240 | #main .thumb { 241 | width: 33.333333% !important; 242 | } 243 | } 244 | 245 | @media screen and (max-width: 980px) { 246 | #main .thumb { 247 | width: 50% !important; 248 | } 249 | } 250 | 251 | @media screen and (max-width: 800px) { 252 | body { 253 | padding: 5px !important; 254 | } 255 | 256 | .search-area { 257 | padding: 100px 1em 3em 1em; 258 | } 259 | 260 | .poptrox-popup .caption { 261 | display: block !important; 262 | font-size: .9em; 263 | background-color: rgba(0, 0, 0, .5); 264 | } 265 | 266 | .poptrox-popup .caption p:first-child { 267 | max-width: 100% !important; 268 | } 269 | 270 | .poptrox-popup .nav-previous, 271 | .poptrox-popup .nav-next { 272 | position: absolute; 273 | opacity: 1; 274 | } 275 | } 276 | 277 | @media screen and (min-width: 800px) { 278 | .poptrox-popup .nav-previous { 279 | transform: scale(-1.5); 280 | } 281 | 282 | .poptrox-popup .nav-next { 283 | transform: scale(1.5); 284 | } 285 | } 286 | 287 | html::-webkit-scrollbar { 288 | width: 8px; 289 | height: 8px; 290 | } 291 | 292 | html::-webkit-scrollbar-thumb { 293 | height: 40px; 294 | background-color: #eee; 295 | border-radius: 16px; 296 | } 297 | 298 | html::-webkit-scrollbar-thumb:hover { 299 | background-color: #ddd; 300 | } 301 | 302 | ::selection { 303 | background: rgba(0, 149, 255, 0.1); 304 | } -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/noscript.css: -------------------------------------------------------------------------------- 1 | /* 2 | Multiverse by HTML5 UP 3 | html5up.net | @ajlkn 4 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | */ 6 | 7 | /* Wrapper */ 8 | 9 | body.is-preload #wrapper:before { 10 | display: none; 11 | } 12 | 13 | /* Main */ 14 | 15 | body.is-preload #main .thumb { 16 | pointer-events: auto; 17 | opacity: 1; 18 | } 19 | 20 | /* Header */ 21 | 22 | body.is-preload #header { 23 | -moz-transform: none; 24 | -webkit-transform: none; 25 | -ms-transform: none; 26 | transform: none; 27 | } -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #29d; 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 26 | opacity: 1.0; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #29d; 49 | border-left-color: #29d; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { -webkit-transform: rotate(0deg); } 68 | 100% { -webkit-transform: rotate(360deg); } 69 | } 70 | @keyframes nprogress-spinner { 71 | 0% { transform: rotate(0deg); } 72 | 100% { transform: rotate(360deg); } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/tooltipster.bundle.min.css: -------------------------------------------------------------------------------- 1 | .tooltipster-fall,.tooltipster-grow.tooltipster-show{-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1);-moz-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-ms-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-o-transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-base{display:flex;pointer-events:none;position:absolute}.tooltipster-box{flex:1 1 auto}.tooltipster-content{box-sizing:border-box;max-height:100%;max-width:100%;overflow:auto}.tooltipster-ruler{bottom:0;left:0;overflow:hidden;position:fixed;right:0;top:0;visibility:hidden}.tooltipster-fade{opacity:0;-webkit-transition-property:opacity;-moz-transition-property:opacity;-o-transition-property:opacity;-ms-transition-property:opacity;transition-property:opacity}.tooltipster-fade.tooltipster-show{opacity:1}.tooltipster-grow{-webkit-transform:scale(0,0);-moz-transform:scale(0,0);-o-transform:scale(0,0);-ms-transform:scale(0,0);transform:scale(0,0);-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;-ms-transition-property:-ms-transform;transition-property:transform;-webkit-backface-visibility:hidden}.tooltipster-grow.tooltipster-show{-webkit-transform:scale(1,1);-moz-transform:scale(1,1);-o-transform:scale(1,1);-ms-transform:scale(1,1);transform:scale(1,1);-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-swing{opacity:0;-webkit-transform:rotateZ(4deg);-moz-transform:rotateZ(4deg);-o-transform:rotateZ(4deg);-ms-transform:rotateZ(4deg);transform:rotateZ(4deg);-webkit-transition-property:-webkit-transform,opacity;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;-ms-transition-property:-ms-transform;transition-property:transform}.tooltipster-swing.tooltipster-show{opacity:1;-webkit-transform:rotateZ(0);-moz-transform:rotateZ(0);-o-transform:rotateZ(0);-ms-transform:rotateZ(0);transform:rotateZ(0);-webkit-transition-timing-function:cubic-bezier(.23,.635,.495,1);-webkit-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-moz-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-ms-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-o-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);transition-timing-function:cubic-bezier(.23,.635,.495,2.4)}.tooltipster-fall{-webkit-transition-property:top;-moz-transition-property:top;-o-transition-property:top;-ms-transition-property:top;transition-property:top;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-fall.tooltipster-initial{top:0!important}.tooltipster-fall.tooltipster-dying{-webkit-transition-property:all;-moz-transition-property:all;-o-transition-property:all;-ms-transition-property:all;transition-property:all;top:0!important;opacity:0}.tooltipster-slide{-webkit-transition-property:left;-moz-transition-property:left;-o-transition-property:left;-ms-transition-property:left;transition-property:left;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1);-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-moz-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-ms-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-o-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-slide.tooltipster-initial{left:-40px!important}.tooltipster-slide.tooltipster-dying{-webkit-transition-property:all;-moz-transition-property:all;-o-transition-property:all;-ms-transition-property:all;transition-property:all;left:0!important;opacity:0}@keyframes tooltipster-fading{0%{opacity:0}100%{opacity:1}}.tooltipster-update-fade{animation:tooltipster-fading .4s}@keyframes tooltipster-rotating{25%{transform:rotate(-2deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0)}}.tooltipster-update-rotate{animation:tooltipster-rotating .6s}@keyframes tooltipster-scaling{50%{transform:scale(1.1)}100%{transform:scale(1)}}.tooltipster-update-scale{animation:tooltipster-scaling .6s}.tooltipster-sidetip .tooltipster-box{background:#565656;border:2px solid #000;border-radius:4px}.tooltipster-sidetip.tooltipster-bottom .tooltipster-box{margin-top:8px}.tooltipster-sidetip.tooltipster-left .tooltipster-box{margin-right:8px}.tooltipster-sidetip.tooltipster-right .tooltipster-box{margin-left:8px}.tooltipster-sidetip.tooltipster-top .tooltipster-box{margin-bottom:8px}.tooltipster-sidetip .tooltipster-content{color:#fff;line-height:18px;padding:6px 14px}.tooltipster-sidetip .tooltipster-arrow{overflow:hidden;position:absolute}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow{height:10px;margin-left:-10px;top:0;width:20px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow{height:20px;margin-top:-10px;right:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow{height:20px;margin-top:-10px;left:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow{bottom:0;height:10px;margin-left:-10px;width:20px}.tooltipster-sidetip .tooltipster-arrow-background,.tooltipster-sidetip .tooltipster-arrow-border{height:0;position:absolute;width:0}.tooltipster-sidetip .tooltipster-arrow-background{border:10px solid transparent}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#565656;left:0;top:3px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background{border-left-color:#565656;left:-3px;top:0}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background{border-right-color:#565656;left:3px;top:0}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background{border-top-color:#565656;left:0;top:-3px}.tooltipster-sidetip .tooltipster-arrow-border{border:10px solid transparent;left:0;top:0}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#000}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border{border-left-color:#000}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border{border-right-color:#000}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border{border-top-color:#000}.tooltipster-sidetip .tooltipster-arrow-uncropped{position:relative}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped{top:-10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped{left:-10px} -------------------------------------------------------------------------------- /usr/static/multiverse/assets/css/tooltipster.theme.min.css: -------------------------------------------------------------------------------- 1 | .tooltipster-sidetip.tooltipster-noir .tooltipster-box{border-radius:0;border:3px solid #000;background:#fff}.tooltipster-sidetip.tooltipster-noir .tooltipster-content{color:#000}.tooltipster-sidetip.tooltipster-noir .tooltipster-arrow{height:11px;margin-left:-11px;width:22px}.tooltipster-sidetip.tooltipster-noir.tooltipster-left .tooltipster-arrow,.tooltipster-sidetip.tooltipster-noir.tooltipster-right .tooltipster-arrow{height:22px;margin-left:0;margin-top:-11px;width:11px}.tooltipster-sidetip.tooltipster-noir .tooltipster-arrow-background{border:11px solid transparent}.tooltipster-sidetip.tooltipster-noir.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#fff;top:4px}.tooltipster-sidetip.tooltipster-noir.tooltipster-left .tooltipster-arrow-background{border-left-color:#fff;left:-4px}.tooltipster-sidetip.tooltipster-noir.tooltipster-right .tooltipster-arrow-background{border-right-color:#fff;left:4px}.tooltipster-sidetip.tooltipster-noir.tooltipster-top .tooltipster-arrow-background{border-top-color:#fff;top:-4px}.tooltipster-sidetip.tooltipster-noir .tooltipster-arrow-border{border-width:11px}.tooltipster-sidetip.tooltipster-noir.tooltipster-bottom .tooltipster-arrow-uncropped{top:-11px}.tooltipster-sidetip.tooltipster-noir.tooltipster-right .tooltipster-arrow-uncropped{left:-11px} -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/biu/blocks/blockMain.js: -------------------------------------------------------------------------------- 1 | const blockMain = new Vue({ 2 | el: "#main", 3 | data: { 4 | advice_block_list: [] 5 | }, 6 | created: function () { }, 7 | mounted: function () { }, 8 | delimiters: ["[[", "]]"], 9 | watch: {}, 10 | methods: { 11 | searchImage(event) { 12 | const image = event.target.files[0]; 13 | const reader = new FileReader(); 14 | $("#title-block-searchImage").text("🔍 图片搜索中..."); 15 | reader.readAsDataURL(image); 16 | reader.onload = (e) => { 17 | $("#preview-searchImage").css("background-image", `url(${e.target.result})`); 18 | }; 19 | let data = new FormData(); 20 | data.append("image", image); 21 | const vm = this; 22 | axios.post("api/biu/search/images/", data).then(rep => { 23 | const content = rep.data.code === 1 ? rep.data.msg.rst : {}; 24 | if (Object.keys(content).includes("results") && content.results.length > 0) { 25 | $("#srhBox").val(`@w=${content.results[0].data.pixiv_id}`); 26 | srhBoxDo(); 27 | } else { 28 | if (rep.data.msg.includes("offline")) 29 | vm.advice_block_list = ["若要使用图片搜索功能,必须设置", "secret.key.apiSauceNAO", "你可以在配置文件的末尾找到它"]; 30 | else if (rep.data.msg.includes("wrong")) 31 | vm.advice_block_list = ["设置的 SauceNAO API Key 错误"]; 32 | else if (rep.data.msg.includes("plugin")) 33 | vm.advice_block_list = ["程序错误,可能是由于网络问题所致", "请重新尝试"]; 34 | else 35 | vm.advice_block_list = [rep.data.msg]; 36 | $("#title-block-searchImage").text("搜索失败,可以查看左侧建议中的可能原因"); 37 | $("#codeBox .pop_ctrl").click() 38 | } 39 | }, err => { 40 | $("#title-block-searchImage").text("搜索失败,原因未知"); 41 | }); 42 | } 43 | } 44 | }); -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/biu/functions.js: -------------------------------------------------------------------------------- 1 | // 获取版本号 2 | function getVersion() { 3 | $.ajax({ 4 | type: "GET", 5 | url: 'api/biu/get/outdated/', 6 | success: function (rep) { 7 | rep = jQuery.parseJSON(JSON.stringify(rep)); 8 | if (rep.code) { 9 | biuInfo.pPximgRProxyURL = rep.msg.online.pPximgRProxyURL; 10 | let currentVer = rep.msg.current; 11 | if (!rep.msg.latest) 12 | currentVer += " (有新版本啦)"; 13 | $('#hint-current-verson').html(currentVer); 14 | } 15 | }, 16 | error: function (e) { 17 | console.log(e); 18 | } 19 | }); 20 | } 21 | 22 | // 检测更新 23 | function checkOutdated() { 24 | $('#btnCheckUP').tooltipster('content', '检测中...'); 25 | $.ajax({ 26 | type: "GET", 27 | url: 'api/biu/get/outdated/', 28 | success: function (rep) { 29 | rep = jQuery.parseJSON(JSON.stringify(rep)); 30 | if (rep.code) { 31 | if (rep.msg.latest) { 32 | $('#btnCheckUP').tooltipster('content', '哇哦,这就是最新版本哦'); 33 | } else { 34 | $('#btnCheckUP').attr('onclick', 'javascript: window.open("https://biu.tls.moe/", "_blank")'); 35 | $('#btnCheckUP').html("可以更新啦"); 36 | $('#btnCheckUP').tooltipster('content', '有新版本了,点击进入官网下载'); 37 | } 38 | } 39 | }, 40 | error: function (e) { 41 | console.log(e); 42 | } 43 | }); 44 | } 45 | 46 | // 结果 HTML 内容加载 47 | function btnGetHTML(type) { 48 | if (type === 'none') { 49 | return '
无

什么都没有找到...

这里什么都没有哦~

'; 50 | } 51 | } 52 | 53 | // 获取 get 内容 54 | function getGetArg(variable) { 55 | var query = window.location.search.substring(1); 56 | var vars = query.split("&"); 57 | for (var i = 0; i < vars.length; i++) { 58 | var pair = vars[i].split("="); 59 | if (pair[0] == variable) { return pair[1]; } 60 | } 61 | return false; 62 | } 63 | 64 | // 修改搜索框内容 65 | function changeSrhBox(c, is = 1) { 66 | if (c !== '') { 67 | $('#srhBox').val(c); 68 | srhBoxStu(); 69 | } 70 | if (is > 0) { 71 | $('.poptrox-popup').trigger('poptrox_close'); 72 | $(window).scrollTop(0); 73 | $('#srhBox').focus(); 74 | if (is > 1 && $('#srhBox').val() !== '') { 75 | srhBoxDo(); 76 | } 77 | } 78 | } 79 | 80 | // tooltip 初始化 81 | function loadTooltip(c = '.tooltip') { 82 | $(c).tooltipster({ 83 | debug: false, 84 | trigger: 'custom', 85 | triggerOpen: { 86 | mouseenter: true, 87 | click: true, 88 | touchstart: true, 89 | tap: true 90 | }, 91 | triggerClose: { 92 | mouseleave: true, 93 | scroll: true, 94 | touchleave: true, 95 | tap: true 96 | }, 97 | theme: 'tooltipster-noir', 98 | animation: 'fade', 99 | animationDuration: 250, 100 | arrow: false, 101 | delay: 200, 102 | }); 103 | } 104 | 105 | // 搜索设置显示动画 106 | // function isShowSettings() { 107 | // if ($('#settings').css('display') === 'none') { 108 | // $('#main').addClass('aniMoveDown'); 109 | // $('#settings').css('z-index', '999'); 110 | // $('#settings').delay(300).fadeIn(); 111 | // } else { 112 | // $('#settings').fadeOut(300); 113 | // $('#settings').css('z-index', '-999'); 114 | // $('#main').removeClass('aniMoveDown'); 115 | // } 116 | // } 117 | 118 | // 搜索动画 119 | function cssShowLoading() { 120 | $('#settings').fadeOut(200); 121 | $('#settings').css('z-index', '-999'); 122 | $('#main').removeClass('aniMoveDown'); 123 | $('body').addClass('is-preload'); 124 | } 125 | 126 | // 正则匹配 127 | function regMatch(regex, str) { 128 | r = [] 129 | while ((m = regex.exec(str)) !== null) { 130 | if (m.index === regex.lastIndex) { 131 | regex.lastIndex++; 132 | } 133 | m.forEach((match, groupIndex) => { 134 | r[groupIndex] = match; 135 | }); 136 | } 137 | return r; 138 | } 139 | 140 | // 下载全部图片 141 | function dlPageAll() { 142 | if (confirm("你确定要一下子下载全部的图片嘛?!还是原图哎...\n其实点击图片右上角也可以下载。")) 143 | $('.thumbAction').each(function () { 144 | if ($(this).children('a:last')[0]['target'] !== '_blank') { 145 | $(this).children('a:last')[0].click(); 146 | } 147 | }); 148 | } 149 | 150 | // 切换面板 151 | function togglePanel(id) { 152 | function toggleClassActive(_, className = "active") { 153 | if (_) { 154 | if (_.attr("class") && _.attr("class").includes(className)) 155 | _.removeClass(className); 156 | else 157 | _.addClass(className); 158 | } 159 | } 160 | toggleClassActive($(id)); 161 | // toggleClassActive($("body"), "content-active"); 162 | } 163 | 164 | // 分割并排除空白项 165 | function splitNoEmpty(c, symbol) { 166 | let r = []; 167 | const li = c.replace(" ", "").split(symbol); 168 | for (let i = 0; i < li.length; i++) { 169 | if (li[i]) 170 | r.push(li[i]) 171 | } 172 | return r; 173 | } 174 | 175 | // encodeURIComponent with [",'] 176 | function maybeEncode(c) { 177 | return encodeURIComponent(c).replaceAll("\"", "%22").replaceAll("'", "%27"); 178 | } 179 | 180 | // XSS Prevention 181 | function maybeXSS(c) { 182 | CHARS = { "<": "<", "\"": """, "'": "&_quot;", "&": "&" }; 183 | const KEYS = Object.keys(CHARS); 184 | for (let i = 0; i < KEYS.length; i++) { 185 | const key = KEYS[i]; 186 | c = c.replaceAll(key, CHARS[key]); 187 | } 188 | return c; 189 | } 190 | 191 | // Title Name 192 | function changeTitleName(name, from = null) { 193 | if (from === null) from = "PixivBiu"; 194 | name = name.replace(/[a-z]/, fl => fl.toUpperCase()); 195 | const final = from ? `${name} - ${from}` : name; 196 | const _title = $("title"); 197 | if (_title) _title.html(final); else return false; 198 | return true; 199 | } 200 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/biu/settings.js: -------------------------------------------------------------------------------- 1 | function loadSearchSettings(mods = settingsMods) { 2 | const _cookie = Cookies.get(); 3 | for (var id in mods) { 4 | if (!mods.hasOwnProperty(id)) continue; 5 | const name = mods[id][0] 6 | const arg = mods[id][1] 7 | const des = mods[id][2] 8 | tmpSearchSettings[name] = _cookie[name] ? _cookie[name] : arg; 9 | $(id).attr('placeholder', des + ': ' + tmpSearchSettings[name]); 10 | } 11 | } 12 | 13 | function saveSettingsCookie(reset = false, mods = settingsMods, only = null) { 14 | for (const id in mods) { 15 | if (!mods.hasOwnProperty(id)) continue; 16 | if (only !== null && !only.includes(id)) continue; 17 | const name = mods[id][0]; 18 | if (reset) { 19 | Cookies.remove(name); 20 | } else if ($(id).val() != "") { 21 | Cookies.remove(name); 22 | Cookies.set(name, $(id).val(), { expires: 30, sameSite: "strict" }); 23 | } 24 | $(id).val(""); 25 | } 26 | loadSearchSettings(mods); 27 | } 28 | 29 | function loadFilters(mods = filtersMods) { 30 | const keys = Object.keys(mods); 31 | for (let i = 0; i < keys.length; i++) { 32 | const cookieID = mods[keys[i]][0]; 33 | const cookie = Cookies.get(); 34 | if (cookie[cookieID]) 35 | tmpFilters[cookieID] = cookie[cookieID]; 36 | else 37 | tmpFilters[cookieID] = ""; 38 | if ($(keys[i])) 39 | $(keys[i]).val(tmpFilters[cookieID]); 40 | } 41 | if ($(".label-btn-filter")) { 42 | if (checkCookies(Object.keys(filtersMods), filtersMods)) 43 | $(".label-btn-filter").addClass("weight-6"); 44 | else 45 | $(".label-btn-filter").removeClass("weight-6"); 46 | } 47 | } 48 | 49 | function saveFiltersCookie(reset = false, mods = filtersMods) { 50 | const keys = Object.keys(mods); 51 | for (let i = 0; i < keys.length; i++) { 52 | const cookieID = mods[keys[i]][0]; 53 | const obj = $(keys[i]); 54 | Cookies.remove(cookieID); 55 | if (!reset && obj && obj.val()) 56 | Cookies.set(cookieID, obj.val(), { expires: 7, sameSite: "strict" }); 57 | } 58 | loadFilters(mods); 59 | } 60 | 61 | function checkCookies(li, mods = null) { 62 | const cookies = Cookies.get(); 63 | for (let i = 0; i < li.length; i++) { 64 | const cookieID = !mods ? li[i] : mods[li[i]][0]; 65 | if (cookies[cookieID]) 66 | return true; 67 | } 68 | return false; 69 | } -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/biu/statusMsg.js: -------------------------------------------------------------------------------- 1 | NProgress.configure({ parent: 'html' }); 2 | 3 | function progresserSearching(key, errors = 0) { 4 | $.ajax({ 5 | type: "GET", 6 | async: true, 7 | url: "api/biu/get/status/", 8 | data: { 9 | 'type': 'search', 10 | 'key': key 11 | }, 12 | success: function (rep) { 13 | rep = jQuery.parseJSON(JSON.stringify(rep)); 14 | if (rep['code']) { 15 | now = 0; 16 | num = rep['msg']['rst'].length; 17 | for (let i = 0; i < num; i++) { 18 | if (rep['msg']['rst'][i] === "done") { 19 | now++; 20 | } 21 | } 22 | srher = now / num; 23 | } else { 24 | NProgress.done(); 25 | return; 26 | } 27 | if (srher !== 1) { 28 | NProgress.set(Number(srher)); 29 | setTimeout((c = key) => progresserSearching(c), 500); 30 | } else { 31 | NProgress.done(); 32 | return; 33 | } 34 | }, 35 | error: function (e) { 36 | if (errors > 5) { 37 | console.log(e); 38 | return; 39 | } 40 | NProgress.done(); 41 | setTimeout((c = key, err = errors) => progresserSearching(c, err + 1), 500); 42 | } 43 | }); 44 | } 45 | 46 | function progresserDownloading_auto() { 47 | $.ajax({ 48 | type: "GET", 49 | async: true, 50 | url: "api/biu/get/status/", 51 | data: { 52 | 'type': 'download', 53 | 'key': "__all__" 54 | }, 55 | success: function (rep) { 56 | rep = jQuery.parseJSON(JSON.stringify(rep)); 57 | let data = rep["msg"]["rst"]; 58 | for (const key in downloadList) { 59 | let tmp = downloadList[key]; 60 | let hrefBak = tmp[0], errors = tmp[1]; 61 | if (data.hasOwnProperty(key)) { 62 | let id = '#dl_' + key + ' d'; 63 | if ($(id).length <= 0) 64 | continue 65 | let tips = $('#dl_' + key + ' d'); 66 | let thu = '#art_' + key + " a:first"; 67 | let num = 1, fin = 0, err = 0; 68 | 69 | num = data[key].length; 70 | for (let i = 0; i < num; i++) { 71 | if (data[key][i] === "done") 72 | fin++; 73 | else if (data[key][i] === "failed") 74 | err++; 75 | } 76 | srher = (fin + err) / num; 77 | 78 | if (err > 0) { 79 | $(thu).attr('class', 'image proer-error'); 80 | $(id).html('失败, 点击重试'); 81 | restoreBlockDownloadHref(key, hrefBak); 82 | continue; 83 | } 84 | if (srher === 1) { 85 | $(thu).attr('class', 'image proer-done'); 86 | $(id).html('完成'); 87 | restoreBlockDownloadHref(key, hrefBak); 88 | } else { 89 | $(thu).attr('class', 'image proer-dling'); 90 | if (tips.tooltipster('content') !== '取消下载') { 91 | $('#dl_' + key).attr('href', `javascript: doDownloadStopPic('${key}');`); 92 | tips.tooltipster('content', '取消下载'); 93 | } 94 | if (num > 1) 95 | $(id).html('下载中 ' + fin + '/' + num); 96 | else 97 | $(id).html('下载中'); 98 | } 99 | } 100 | } 101 | setTimeout(progresserDownloading_auto, 1000); 102 | }, 103 | error: function () { 104 | setTimeout(progresserDownloading_auto, 3000); 105 | } 106 | }); 107 | } 108 | 109 | function restoreBlockDownloadHref(key, href) { 110 | if (!Object.keys(downloadList).includes(key)) 111 | return false; 112 | delete downloadList[key]; 113 | $(`#dl_${key}`).attr('href', decodeURIComponent(href).replaceAll("%sq%", "'").replaceAll("%dq%", "\"")); 114 | $(`#dl_${key} d`).tooltipster('content', "下载"); 115 | return true; 116 | } 117 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/breakpoints.min.js: -------------------------------------------------------------------------------- 1 | /* breakpoints.js v1.0 | @ajlkn | MIT licensed */ 2 | var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser}); 3 | -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/jquery.popmenu.min.js: -------------------------------------------------------------------------------- 1 | (function (b) { 2 | b.fn.popmenu = function (d) { 3 | var a = b.extend({ controller: !0 }, d), g = !0 === a.controller ? "none" : "block"; d = b(this); var c = d.children("ul"), f = c.children("li"), h = f.children("a"), e = d.children(".pop_ctrl"); return function () { 4 | c.css({ display: g, position: "absolute", width: "100%", padding: "0", "z-index": "999"}); 5 | f.css({ display: "block", color: "#fff", width: "100%", "text-align": "right", padding: "0 10px", "font-size": "1.1em", "font-weight": "400"}); h.css({ "text-decoration": "none", color: "#fff" }); e.hover(function () { e.css("cursor", "pointer") }, function () { e.css("cursor", "default") }); e.click(function (a) { a.preventDefault(); c.fadeIn(300); b(document).mouseup(function (a) { c.is(a.target) || 0 !== c.has(a.target).length || c.fadeOut(300) }) }); f.hover(function () { b(this).css({ background: a.focusColor, cursor: "pointer" }) }, function () { 6 | b(this).css({ 7 | background: a.background, 8 | cursor: "default" 9 | }) 10 | }) 11 | }() 12 | } 13 | })(jQuery); -------------------------------------------------------------------------------- /usr/static/multiverse/assets/js/js.cookie.min.js: -------------------------------------------------------------------------------- 1 | /*! js-cookie v3.0.1 | MIT */ 2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t= 0) { 22 | // Hack: Enable IE workarounds. 23 | if (browser.name == 'ie') 24 | $body.addClass('ie'); 25 | 26 | // Touch? 27 | if (browser.mobile) 28 | $body.addClass('touch'); 29 | 30 | // Transitions supported? 31 | if (browser.canUse('transition')) { 32 | 33 | // Play initial animations on page load. 34 | $window.on('load', function () { 35 | window.setTimeout(function () { 36 | $body.removeClass('is-preload'); 37 | }, 100); 38 | }); 39 | 40 | // Prevent transitions/animations on resize. 41 | var resizeTimeout; 42 | 43 | $window.on('resize', function () { 44 | 45 | window.clearTimeout(resizeTimeout); 46 | 47 | $body.addClass('is-resizing'); 48 | 49 | resizeTimeout = window.setTimeout(function () { 50 | $body.removeClass('is-resizing'); 51 | }, 100); 52 | 53 | }); 54 | 55 | } 56 | } 57 | 58 | // Scroll back to top. 59 | $window.scrollTop(0); 60 | 61 | // part@panels 62 | if ($.inArray('panels', parts) >= 0) { 63 | // Panels. 64 | var $panels = $('.panel'); 65 | 66 | $panels.each(function () { 67 | 68 | var $this = $(this), 69 | $toggles = $('[href="#' + $this.attr('id') + '"]'), 70 | $closer = $('
').appendTo($this); 71 | 72 | // Closer. 73 | $closer 74 | .on('click', function (event) { 75 | $this.trigger('---hide'); 76 | }); 77 | 78 | // Events. 79 | $this 80 | .on('click', function (event) { 81 | event.stopPropagation(); 82 | }) 83 | .on('---toggle', function () { 84 | 85 | if ($this.hasClass('active')) 86 | $this.triggerHandler('---hide'); 87 | else 88 | $this.triggerHandler('---show'); 89 | 90 | }) 91 | .on('---show', function () { 92 | 93 | // Hide other content. 94 | if ($body.hasClass('content-active')) 95 | $panels.trigger('---hide'); 96 | 97 | // Activate content, toggles. 98 | $this.addClass('active'); 99 | $toggles.addClass('active'); 100 | 101 | // Activate body. 102 | $body.addClass('content-active'); 103 | 104 | }) 105 | .on('---hide', function () { 106 | 107 | // Deactivate content, toggles. 108 | $this.removeClass('active'); 109 | $toggles.removeClass('active'); 110 | 111 | // Deactivate body. 112 | $body.removeClass('content-active'); 113 | 114 | }); 115 | 116 | // Toggles. 117 | $toggles 118 | .removeAttr('href') 119 | .css('cursor', 'pointer') 120 | .on('click', function (event) { 121 | 122 | event.preventDefault(); 123 | event.stopPropagation(); 124 | 125 | $this.trigger('---toggle'); 126 | 127 | }); 128 | 129 | }); 130 | } 131 | 132 | // part@events 133 | if ($.inArray('events', parts) >= 0) { 134 | // Global events. 135 | $body 136 | .on('click', function (event) { 137 | 138 | if ($body.hasClass('content-active')) { 139 | 140 | event.preventDefault(); 141 | event.stopPropagation(); 142 | 143 | $panels.trigger('---hide'); 144 | 145 | } 146 | 147 | }); 148 | 149 | $window 150 | .on('keyup', function (event) { 151 | 152 | if (event.keyCode == 27 153 | && $body.hasClass('content-active')) { 154 | 155 | event.preventDefault(); 156 | event.stopPropagation(); 157 | 158 | $panels.trigger('---hide'); 159 | 160 | } 161 | 162 | }); 163 | } 164 | 165 | // part@header 166 | if ($.inArray('header', parts) >= 0) { 167 | // Header. 168 | var $header = $('#header'); 169 | 170 | // Links. 171 | // $header.find('a').each(function () { 172 | 173 | // var $this = $(this), 174 | // href = $this.attr('href'); 175 | 176 | // // Internal link? Skip. 177 | // if (!href 178 | // || href.charAt(0) == '#') 179 | // return; 180 | 181 | // // Redirect on click. 182 | // $this 183 | // .removeAttr('href') 184 | // .css('cursor', 'pointer') 185 | // .on('click', function (event) { 186 | 187 | // event.preventDefault(); 188 | // event.stopPropagation(); 189 | 190 | // window.location.href = href; 191 | 192 | // }); 193 | 194 | // }); 195 | } 196 | 197 | // part@footer 198 | if ($.inArray('footer', parts) >= 0) { 199 | // Footer. 200 | var $footer = $('#footer'); 201 | 202 | // Copyright. 203 | // This basically just moves the copyright line to the end of the *last* sibling of its current parent 204 | // when the "medium" breakpoint activates, and moves it back when it deactivates. 205 | $footer.find('.copyright').each(function () { 206 | 207 | var $this = $(this), 208 | $parent = $this.parent(), 209 | $lastParent = $parent.parent().children().last(); 210 | 211 | breakpoints.on('<=medium', function () { 212 | $this.appendTo($lastParent); 213 | }); 214 | 215 | breakpoints.on('>medium', function () { 216 | $this.appendTo($parent); 217 | }); 218 | 219 | }); 220 | } 221 | 222 | // part@main 223 | if ($.inArray('main', parts) >= 0) { 224 | // Main. 225 | var $main = $('#main'); 226 | 227 | // Thumbs. 228 | $main.children('.thumb').each(function () { 229 | 230 | var $this = $(this), 231 | $image = $this.find('.image'), $image_img = $image.children('img'), 232 | x; 233 | 234 | var $btn = $this.find('.imageBtn'); 235 | var $btn_img = $btn.children('img'); 236 | 237 | // No image? Bail. 238 | if ($image.length == 0 && $btn.length == 0) 239 | return; 240 | 241 | // Image. 242 | // This sets the background of the "image" to the image pointed to by its child 243 | // (which is then hidden). Gives us way more flexibility. 244 | 245 | // Set background. 246 | $image.css('background-image', 'url(' + $image_img.attr('src') + ')'); 247 | $btn.css('background-image', 'url(' + $btn_img.attr('src') + ')'); 248 | 249 | // Set background position. 250 | if (x = $image_img.data('position')) 251 | $image.css('background-position', x); 252 | 253 | if (x = $btn_img.data('position')) 254 | $btn.css('background-position', x); 255 | 256 | // Hide original img. 257 | $image_img.hide(); 258 | $btn_img.hide(); 259 | }); 260 | 261 | // Poptrox. 262 | $main.poptrox({ 263 | baseZIndex: 20000, 264 | caption: function ($a) { 265 | 266 | var s = ''; 267 | 268 | $a.nextAll().each(function () { 269 | if (this.outerHTML.indexOf('
') == -1) 270 | s += this.outerHTML; 271 | }); 272 | 273 | return s; 274 | 275 | }, 276 | fadeSpeed: 300, 277 | onPopupClose: function () { $body.removeClass('modal-active'); }, 278 | onPopupOpen: function () { $body.addClass('modal-active'); }, 279 | overlayOpacity: 0, 280 | popupCloserText: '', 281 | popupHeight: 150, 282 | popupLoaderText: '', 283 | popupSpeed: 250, 284 | popupWidth: 150, 285 | selector: '.thumb > a.image', 286 | usePopupCaption: true, 287 | usePopupCloser: true, 288 | usePopupDefaultStyling: false, 289 | usePopupForceClose: true, 290 | usePopupLoader: true, 291 | usePopupNav: true, 292 | usePopupEasyClose: false, 293 | windowMargin: 50, 294 | }); 295 | 296 | // Hack: Set margins to 0 when 'xsmall' activates. 297 | breakpoints.on('<=xsmall', function () { 298 | $main[0]._poptrox.windowMargin = 0; 299 | }); 300 | 301 | breakpoints.on('>xsmall', function () { 302 | $main[0]._poptrox.windowMargin = 50; 303 | }); 304 | } 305 | } 306 | 307 | (function ($) { 308 | reMainJs($); 309 | })(jQuery); -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /usr/static/multiverse/assets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/assets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /usr/static/multiverse/images/tea.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/tea.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/01.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/02.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/03.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/04.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/05.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/06.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/07.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/08.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/08.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/09.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/09.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/10.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/11.jpg -------------------------------------------------------------------------------- /usr/static/multiverse/images/thumbs/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/multiverse/images/thumbs/12.jpg -------------------------------------------------------------------------------- /usr/static/pixiv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txperl/PixivBiu/1aef6f29f9cad940b869e226f4a7c6b5b4c334e3/usr/static/pixiv.png --------------------------------------------------------------------------------