Repository: findix/ArtStationDownloader
Branch: master
Commit: 6c659c5b7231
Files: 16
Total size: 28.4 KB
Directory structure:
gitextract_ejxwnxrw/
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── CHANGELOG
├── LICENSE
├── README-zh.md
├── README.md
├── build.bat
├── requirements.txt
├── requirements_dev.txt
└── src/
├── ArtStationDownloader.py
├── app.py
├── config.py
├── console.py
├── core.py
└── http_client.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
temp/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Custom
config.ini
================================================
FILE: .vscode/launch.json
================================================
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: ArtStationDownloader",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/ArtStationDownloader.py",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Python: ArtStationDownload in Terminal",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/ArtStationDownloader.py",
"args": [
"-u",
"ase",
"ikaired",
"-d",
"${env:UserProfile}\\Desktop\\ArtStation",
"-t",
"all"
],
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Python: Current File (Integrated Terminal)",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Python: Current File (External Terminal)",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "externalTerminal",
"justMyCode": true
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"python.linting.pylintEnabled": true,
"python.formatting.provider": "black"
}
================================================
FILE: CHANGELOG
================================================
20180611 0.1.0-alpha1
允许在txt中使用Python风格的注释,即以#开头的内容会被忽略
对txt中的空白符进行处理
保存路径不再强制包含 ArtStation
默认保存路径现在为用户根路径
20190131 0.1.1-alpha
重构代码结构
加入命令行模式
GUI和命令行均加入允许选择下载文件类型(所有、图片、视频)功能
20190422 0.1.1-alpha1
对不同报错情况进行提示
增加独立的中文README
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Sean Feng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README-zh.md
================================================
# ArtStation Downloader
ArtStation Downloader 是一个帮助你批量从[ArtStation](https://www.artstation.com/)网站下载图片和视频的小工具
## 用法
### 从这里开始
[在此下载](https://github.com/findix/ArtStationDownloader/releases)
### 如何下载
输入你希望下载的作者的主页地址,或者其用户名
如
`https://www.artstation.com/xrnothing` 或者 `xrnothing`
然后点击 Download 按钮
你可以一次下载多位作者的作品
只需要在输入他们的 URL 或用户名时,在中间加入英文的","即可
或者,你也可以新建一个文本文件(.txt),每一行输入一位作者的信息。
(允许使用 Python 风格的注释,即 # 后的内容会被忽略,空白符与空行同样也会被忽略)
然后点击 Download txt 按钮选择文件即可。
Type 下拉选单可以设置下载资源的类型, 你可以选择“只下载图片”、“只下载视频”以及“全部下载”
Path 输入框内是下载文件夹位置,你可以按需求设置。
### Proxy
在 `config.ini` 配置文件中像这样设置代理信息(需要重启程序):
```ini
[Proxy]
http = http://127.0.0.1:7890
https = http://127.0.0.1:7890
```
## 缺陷与反馈
如果在使用过程中发现任何错误、问题或是希望讨论,请使用 [Github Issues](https://github.com/findix/ArtStationDownloader/issues).
非常欢迎提交 Pull requests!
## 打包
需要 Pyinstaller.
运行 `build.bat`
## For macOS/Linux/Shell
首先执行 `pip install -r requirements.txt` 安装依赖
在 shell 中执行 `python ./src/ArtStationDownloader.py` 开启图形界面
或者
直接运行类似下面的命令:
`python ./src/ArtStationDownloader.py -u username_of_artist other_username or_more -d where/you/what`
您可以尝试输入 `python ./src/ArtStationDownloader.py --help` 查看更多用法
## FAQ
> ~~**为什么我在点击下载后报错 `[Error] [403 Forbidden] You are blocked by artstation`?**~~
~~ArtStation 有一个 [验证码](https://zh.wikipedia.org/zh-hans/%E9%AA%8C%E8%AF%81%E7%A0%81) 系统 (由 Cloudflare 提供)。如果你无法证明你不是爬虫,就会被禁止访问,这个问题目前我还没有找到解决办法,如果您有方式解决,请告诉我。~~
使用 [samarthshrivas](https://github.com/findix/ArtStationDownloader/issues/24#issuecomment-1124734529) 推荐的方式,这个问题已经解决了。
## LICENSE
MIT License
Copyright (c) 2018 Sean Feng
================================================
FILE: README.md
================================================
# ArtStation Downloader
[中文说明](./README-zh.md)
ArtStation Downloader is a lightweight tool to help you download images and videos from the [ArtStation](https://www.artstation.com/)
## Usage
### Getting started
[Download here](https://github.com/findix/ArtStationDownloader/releases)
### Download from ArtStation
Input the URL of the artist or just the username
just like
`https://www.artstation.com/xrnothing` or `xrnothing`
and click the Download button.
You can download more then one artists' whole works at one time.
Just input all the URL or usernames split with ','.
or you can create a txt file with These, one artist one line.
(you can use python style comment, any text after # will be ignored, space charactor and empty line are also ignored)
and click the Download txt button to select file.
The combobox named Type means you can choose what resources are required. image only, video only or both
The Path means the path of the download folder,just set it.
### Proxy
Set proxy config in config.ini like this(need restart app):
```ini
[Proxy]
http = http://127.0.0.1:7890
https = http://127.0.0.1:7890
```
## Bugs and Feedback
For bugs, questions and discussions please use the [Github Issues](https://github.com/findix/ArtStationDownloader/issues).
Pull requests are all welcome!
## Package
Require Pyinstaller.
Just execute `build.bat`
## For macOS/Linux/Shell
install dependencies py run `pip install -r requirements.txt` first
just run `python ./src/ArtStationDownloader.py` in shell to run in GUI mode
or
run like this:
`python ./src/ArtStationDownloader.py -u username_of_artist other_username or_more -d where/you/what` in shell
try `python ./src/ArtStationDownloader.py --help` to get more usage
## FAQ
> ~~**Why I get a message says `[Error] [403 Forbidden] You are blocked by artstation`?**~~
~~The ArtStation has a [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA) system (by Cloudflare) which need you proof you are human otherwise forbid you. I haven't found a way solve this for now. If you have idea, please tell me.~~
It seems solved by inspire of [samarthshrivas](https://github.com/findix/ArtStationDownloader/issues/24#issuecomment-1124734529)
## LICENSE
MIT License
Copyright (c) 2018 Sean Feng
================================================
FILE: build.bat
================================================
pyinstaller -y --windowed .\src\ArtStationDownloader.py
del dist\ArtStationDownloader.zip
7z a -tzip dist\ArtStationDownloader.zip dist\ArtStationDownloader
pyinstaller -y --windowed -F .\src\ArtStationDownloader.py
@pause
================================================
FILE: requirements_dev.txt
================================================
black==23.7.0
pylint==2.17.5
================================================
FILE: src/ArtStationDownloader.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""批量下载ArtStation图片
Copyright 2018 Sean Feng(sean@fantablade.com)
"""
__version__ = "0.3.3"
# $Source$
import argparse
from app import App
from core import DownloadSorting
from console import Console
def main():
parser = argparse.ArgumentParser(
prog="ArtStationDownloader",
description="ArtStation Downloader is a lightweight tool to help you download images and videos from the ArtStation",
)
parser.add_argument(
"--version", action="version", version="%(prog)s " + __version__
)
parser.add_argument(
"-u",
"--username",
help="choose who's project you want to download, one or more",
nargs="*",
)
parser.add_argument("-d", "--directory", help="output directory")
parser.add_argument(
"-t",
"--type",
choices=["all", "image", "video"],
default="all",
help="what do you what to download, default is all",
)
parser.add_argument(
"-s",
"--sorting",
choices=[sorting.name for sorting in DownloadSorting],
default=DownloadSorting.TITLE_BASED.name,
help="download sorting",
)
parser.add_argument(
"-v", "--verbosity", action="count", help="increase output verbosity"
)
args = parser.parse_args()
if args.username:
if args.directory:
console = Console()
console.download_by_usernames(args.username, args.directory, args.type, DownloadSorting[args.sorting])
else:
print("no output directory, please use -d or --directory option to set")
else:
app = App(version=__version__)
app.run() # 进入主循环,程序运行
if __name__ == "__main__":
main()
================================================
FILE: src/app.py
================================================
# -*- coding: utf-8 -*-
import os
import PySimpleGUI as sg
import config
from core import Core, DownloadSorting
class App:
def __init__(self, version):
self.core = Core(self.log)
self.user_config = sg.UserSettings(
os.path.join(os.path.abspath("."), "config.ini"),
use_config_file=True,
convert_bools_and_none=True,
)
self.user_settings = self.user_config["Settings"]
# 兼容模式
root_path = self.user_config["Paths"]["root_path"]
if root_path:
self.user_settings["root_path"] = root_path
self.user_config["Paths"].delete_section()
self.root_path = self.user_settings.get(
"root_path", os.path.join(os.path.expanduser("~"), "ArtStation")
)
self.download_sorting: DownloadSorting = DownloadSorting[
self.user_settings.get("download_sorting", DownloadSorting.TITLE_BASED.name)
]
self.window = sg.Window(
"ArtStation Downloader " + version,
layout=self.create_layout(),
finalize=True,
)
self.window["-DOWNLOAD-SORTING-"].Update(self.download_sorting)
self.event_callbacks = {
"-DOWNLOAD-": lambda: self.window.perform_long_operation(self.download, ""),
"-DOWNLOAD_TXT-": self.get_download_txt_file,
"continue_download_txt": lambda args: self.window.perform_long_operation(
lambda: self.download_txt(args), ""
),
"-DOWNLOAD-SORTING-": self._set_download_sorting,
"-BROWSE-": self.browse_directory,
"log": self._log,
"popup": self._popup,
"set_download_buttons": self._set_download_buttons,
}
def _set_download_sorting(self, value: DownloadSorting):
self.download_sorting = value
self.user_settings.set("download_sorting", value.name)
def _log(self, value):
current_text = self.window["-LOG-"].get()
self.window["-LOG-"].update(f"{current_text}\n{value}\n")
self.window["-LOG-"].Widget.see("end")
def log(self, value):
self.window.write_event_value("log", value)
def _set_download_buttons(self, state):
self.window["-DOWNLOAD-"].update(disabled=not state)
self.window["-DOWNLOAD_TXT-"].update(disabled=not state)
def _popup(self, args):
message, title = args
sg.popup_ok(
message,
title=title,
modal=True,
)
def download(self):
username_text = self.window["-USERNAME-"].get()
if not username_text:
self.window.write_event_value(
"popup", ("Please input usernames", "Warning")
)
return
self.window.write_event_value("set_download_buttons", False)
usernames = username_text.split(",")
self.core.root_path = self.root_path
self.core.download_by_usernames(
usernames, self.window["-TYPE-"].get(), self.download_sorting
)
self.window.write_event_value("set_download_buttons", True)
self.user_settings.set("default_username", username_text)
def get_download_txt_file(self):
self.window.write_event_value("set_download_buttons", False)
filename = sg.popup_get_file(
"Select a file", file_types=(("Text Files", "*.txt"), ("All Files", "*.*"))
)
self.window.write_event_value("continue_download_txt", filename)
def download_txt(self, filename):
if filename and filename != ".":
with open(filename, "r", encoding="utf-8") as f:
usernames = []
for username in f.readlines():
username = username.strip()
if not username:
continue
sharp_at = username.find("#")
if sharp_at == 0:
continue
if sharp_at != -1:
username = username[:sharp_at]
usernames.append(username.strip())
self.core.root_path = self.root_path
self.core.download_by_usernames(
usernames, self.window["-TYPE-"].get(), self.download_sorting
)
self.window.write_event_value("set_download_buttons", True)
def browse_directory(self):
root_path = sg.popup_get_folder("Select a folder", default_path=self.root_path)
if root_path:
self.root_path = root_path
self.window["-PATH-"].update(root_path)
self.user_settings.set("root_path", root_path)
def create_layout(self):
sg.theme("Dark Blue 3")
layout = [
[
sg.Text('Usernames (split by ","):'),
sg.InputText(
self.user_settings.get("default_username", ""), key="-USERNAME-"
),
],
[
sg.Text("Type:"),
sg.Combo(
values=("all", "image", "video"),
key="-TYPE-",
default_value="all",
readonly=True,
enable_events=True,
),
sg.Text("File Download Sorting"),
sg.Combo(
tuple(DownloadSorting.__members__.values()),
key="-DOWNLOAD-SORTING-",
default_value=DownloadSorting.TITLE_BASED,
readonly=True,
enable_events=True,
),
],
[
sg.Text("Path:"),
sg.InputText(key="-PATH-", default_text=self.root_path, disabled=True),
sg.Button("Browse", key="-BROWSE-", bind_return_key=True),
],
[
sg.Button("Download", key="-DOWNLOAD-", bind_return_key=True),
sg.Button("Download txt", key="-DOWNLOAD_TXT-"),
],
[sg.Multiline(size=(80, 20), key="-LOG-", disabled=True)],
[sg.StatusBar("Feel free to use! Support: Sean Feng(sean@fantablade.com)")],
]
return layout
def run(self):
while True:
event, values = self.window.read()
if event == sg.WINDOW_CLOSED:
break
elif event in self.event_callbacks:
if event in values:
self.event_callbacks[event](values[event])
else:
self.event_callbacks[event]()
self.window.close()
================================================
FILE: src/config.py
================================================
#!/usr/bin/python
# -*- coding:utf-8 -*-
# author: lingyue.wkl
# desc: use to read ini
# ---------------------
# 2012-02-18 created
# 2012-09-02 changed for class support
# ---------------------
import sys
import configparser
class Config:
def __init__(self, path):
self.path = path
self.cf = configparser.ConfigParser()
self.cf.read(self.path)
def get(self, field, key):
result = ""
try:
result = self.cf.get(field, key)
except:
result = ""
return result
def set(self, filed, key, value):
try:
self.cf.set(field, key, value)
self.cf.write(open(self.path, "w", encoding="utf-8"))
except:
return False
return True
def read_config(config_file_path, field, key):
cf = configparser.ConfigParser()
try:
cf.read(config_file_path)
if field in cf:
result = cf[field][key]
else:
return ""
except configparser.Error as e:
print(e)
return ""
return result
def write_config(config_file_path, field, key, value):
cf = configparser.ConfigParser()
try:
cf.read(config_file_path)
if field not in cf:
cf.add_section(field)
cf[field][key] = value
cf.write(open(config_file_path, "w", encoding="utf-8"))
except configparser.Error as e:
print(e)
return False
return True
if __name__ == "__main__":
if len(sys.argv) < 4:
sys.exit(1)
config_file_path = sys.argv[1]
field = sys.argv[2]
key = sys.argv[3]
if len(sys.argv) == 4:
print(read_config(config_file_path, field, key))
else:
value = sys.argv[4]
write_config(config_file_path, field, key, value)
================================================
FILE: src/console.py
================================================
# -*- coding: utf-8 -*-
from core import Core
class Console:
def __init__(self):
self.core = Core()
def download_by_usernames(self, usernames, directory, download_type, download_sorting):
self.core.root_path = directory
self.core.download_by_usernames(usernames, download_type, download_sorting)
================================================
FILE: src/core.py
================================================
# -*- coding: utf-8 -*-
"""内核方法
Copyright 2018-2019 Sean Feng(sean@FantaBlade.com)
"""
import os
from concurrent import futures
from enum import Enum
from multiprocessing import cpu_count
from bs4 import BeautifulSoup, element
from pytube import YouTube
from http_client import HttpClient
class DownloadSorting(Enum):
TITLE_BASED = "Title-based"
USERNAME_BASED = "Username-based"
ALL_IN_ONE = "All-in-one"
def __str__(self) -> str:
return self.value
class Core:
def log(self, message):
if self._log_print:
self._log_print(message)
else:
print(message)
def __init__(self, log_print=None):
self._log_print = log_print
max_workers = cpu_count() * 4
self.executor = futures.ThreadPoolExecutor(max_workers)
self.executor_video = futures.ThreadPoolExecutor(1)
self.invoke = self._get_invoke()
self.invoke_video = self._get_invoke("video")
self.root_path: str = None
self.download_sorting: DownloadSorting = None
self.futures = []
self.http_client = HttpClient(log_print=log_print)
def download_file(self, url, file_path, file_name):
url = url.replace("/large/", "/4k/")
file_full_path = os.path.join(file_path, file_name)
if os.path.exists(file_full_path):
self.log("[Exist][image][{}]".format(file_full_path))
else:
resp = self.http_client.http_get(url)
os.makedirs(file_path, exist_ok=True)
with open(file_full_path, "wb") as code:
code.write(resp.content)
self.log("[Finish][image][{}]".format(file_full_path))
def download_video(self, youtube_id, file_path):
file_full_path = os.path.join(file_path, "{}.{}".format(youtube_id, "mp4"))
if os.path.exists(file_full_path):
self.log("[Exist][video][{}]".format(file_full_path))
else:
try:
yt = YouTube(f'https://www.youtube.com/watch?v={youtube_id}')
stream = yt.streams.filter(file_extension='mp4').first()
stream.download(output_path=file_path)
self.log("[Finish][video][{}]".format(file_full_path))
except Exception as e:
self.log("[Error][video][{}]".format(e))
def download_project(self, hash_id):
url = "https://www.artstation.com/projects/{}.json".format(hash_id)
resp = self.http_client.http_client_get_json(url)
j = resp
assets = j["assets"]
title = j["slug"].strip()
# self.log('=========={}=========='.format(title))
username = j["user"]["username"]
for asset in assets:
assert self.root_path
if self.download_sorting != DownloadSorting.ALL_IN_ONE:
user_path = os.path.join(self.root_path, username)
else:
user_path = self.root_path
os.makedirs(user_path, exist_ok=True)
if self.download_sorting == DownloadSorting.TITLE_BASED:
file_path = os.path.join(user_path, title)
else:
file_path = user_path
if not self.no_image and asset["has_image"]: # 包含图片
url = asset["image_url"]
file_name = HttpClient.urlparse(url).path.split("/")[-1]
try:
self.futures.append(
self.invoke(self.download_file, url, file_path, file_name)
)
except Exception as e:
self.log(e)
if not self.no_video and asset["has_embedded_player"]: # 包含视频
player_embedded = BeautifulSoup(asset["player_embedded"], "html.parser")
src = player_embedded.find("iframe").get("src")
if "youtube" in src:
youtube_id = self.http_client.urlparse(src).path[-11:]
try:
self.futures.append(
self.invoke_video(self.download_video, youtube_id, file_path)
)
except Exception as e:
self.log(e)
def get_projects(self, username) -> element.ResultSet[element.Tag]:
data = []
if username != "":
page = 0
while True:
page += 1
url = "https://{}.artstation.com/rss?page={}".format(username, page)
resp = self.http_client.http_client_get(url)
if resp.status != 200:
err = "[Error] [{} {}] ".format(resp.status, resp.reason)
if resp.status == 403:
self.log(err + "You are blocked by artstation")
elif resp.status == 404:
self.log(err + "Username not found")
else:
self.log(err + "Unknown error")
break
channel = BeautifulSoup(
resp.read().decode("utf-8"), "lxml-xml"
).rss.channel
links = channel.select("item > link")
if len(links) == 0:
break
if page == 1:
self.log("\n==========[{}] BEGIN==========".format(username))
data += links
self.log("\n==========Get page {}==========".format(page))
return data
def download_by_username(self, username):
data = self.get_projects(username)
if len(data) != 0:
future_list = []
for project in data:
if project.string.startswith("https://www.artstation.com/artwork/"):
future = self.invoke(
self.download_project, project.string.split("/")[-1]
)
future_list.append(future)
futures.wait(future_list)
def download_by_usernames(
self, usernames, download_type, download_sorting: DownloadSorting
):
self.http_client.proxy_setup()
self.no_image = download_type == "video"
self.no_video = download_type == "image"
self.download_sorting = download_sorting
# 去重与处理网址
username_set = set()
for username in usernames:
username = username.strip().split("/")[-1]
if username not in username_set:
username_set.add(username)
self.download_by_username(username)
futures.wait(self.futures)
self.log("\n========ALL DONE========")
def _get_invoke(self, executor=None):
def invoke(func, *args, **kwargs):
def done_callback(worker):
worker_exception = worker.exception()
if worker_exception:
self.log(str(worker_exception))
raise (worker_exception)
if executor == "video":
futurn = self.executor_video.submit(func, *args, **kwargs)
futurn.add_done_callback(done_callback)
else:
futurn = self.executor.submit(func, *args, **kwargs)
futurn.add_done_callback(done_callback)
return futurn
return invoke
================================================
FILE: src/http_client.py
================================================
import http.client as http_client
import ssl
import json
import os
import ssl
from urllib.parse import urlparse, ParseResult
import requests
from config import Config
class HttpClient:
def log(self, message):
if self._log_print:
self._log_print(message)
else:
print(message)
def __init__(self, log_print=None):
self._log_print = log_print
self._session = requests.session()
self.proxy_setup(self._session)
def http_client_get(self, url, ignoreCertificateError=None):
try:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
parsed_url = urlparse(url)
if ignoreCertificateError:
context = ssl._create_unverified_context()
else:
context = None
conn = http_client.HTTPSConnection(parsed_url.netloc, context=context)
conn.request(
"GET", parsed_url.path + "?" + parsed_url.query, headers=headers
)
resp = conn.getresponse()
except ssl.SSLCertVerificationError:
return self.http_client_get(url, ignoreCertificateError=True)
except Exception as e:
self.log(f"Connect error [{e}]")
return
return resp
def http_client_get_json(self, url):
resp = self.http_client_get(url)
try:
resp_str = resp.read().decode()
json_result = json.loads(resp_str)
except json.decoder.JSONDecodeError:
self.log(f"json decode error\nurl:{url}\n{resp_str}")
return
return json_result
def http_get(self, url):
try:
resp = self._session.get(url, timeout=10)
except requests.exceptions.InvalidURL:
self.log(f'"{url}" is not valid url')
return
return resp
def proxy_setup(self, session=None):
if not session:
session = self._session
# 设置 User Agent
session.headers.update(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"
}
)
# 设置代理
config = Config("config.ini")
http = config.get("Proxy", "http")
https = config.get("Proxy", "https")
if http or https:
proxys = {}
if http:
proxys["http"] = http
os.environ["HTTP_PROXY"] = http
if https:
proxys["https"] = https
os.environ["HTTPS_PROXY"] = https
session.proxies.update(proxys)
@staticmethod
def urlparse(url: str) -> ParseResult:
return urlparse(url)
gitextract_ejxwnxrw/
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── CHANGELOG
├── LICENSE
├── README-zh.md
├── README.md
├── build.bat
├── requirements.txt
├── requirements_dev.txt
└── src/
├── ArtStationDownloader.py
├── app.py
├── config.py
├── console.py
├── core.py
└── http_client.py
SYMBOL INDEX (43 symbols across 6 files)
FILE: src/ArtStationDownloader.py
function main (line 19) | def main():
FILE: src/app.py
class App (line 10) | class App:
method __init__ (line 11) | def __init__(self, version):
method _set_download_sorting (line 53) | def _set_download_sorting(self, value: DownloadSorting):
method _log (line 57) | def _log(self, value):
method log (line 62) | def log(self, value):
method _set_download_buttons (line 65) | def _set_download_buttons(self, state):
method _popup (line 69) | def _popup(self, args):
method download (line 77) | def download(self):
method get_download_txt_file (line 93) | def get_download_txt_file(self):
method download_txt (line 100) | def download_txt(self, filename):
method browse_directory (line 120) | def browse_directory(self):
method create_layout (line 127) | def create_layout(self):
method run (line 168) | def run(self):
FILE: src/config.py
class Config (line 15) | class Config:
method __init__ (line 16) | def __init__(self, path):
method get (line 21) | def get(self, field, key):
method set (line 29) | def set(self, filed, key, value):
function read_config (line 38) | def read_config(config_file_path, field, key):
function write_config (line 52) | def write_config(config_file_path, field, key, value):
FILE: src/console.py
class Console (line 6) | class Console:
method __init__ (line 7) | def __init__(self):
method download_by_usernames (line 10) | def download_by_usernames(self, usernames, directory, download_type, d...
FILE: src/core.py
class DownloadSorting (line 18) | class DownloadSorting(Enum):
method __str__ (line 23) | def __str__(self) -> str:
class Core (line 27) | class Core:
method log (line 28) | def log(self, message):
method __init__ (line 34) | def __init__(self, log_print=None):
method download_file (line 46) | def download_file(self, url, file_path, file_name):
method download_video (line 58) | def download_video(self, youtube_id, file_path):
method download_project (line 71) | def download_project(self, hash_id):
method get_projects (line 111) | def get_projects(self, username) -> element.ResultSet[element.Tag]:
method download_by_username (line 140) | def download_by_username(self, username):
method download_by_usernames (line 152) | def download_by_usernames(
method _get_invoke (line 169) | def _get_invoke(self, executor=None):
FILE: src/http_client.py
class HttpClient (line 13) | class HttpClient:
method log (line 14) | def log(self, message):
method __init__ (line 20) | def __init__(self, log_print=None):
method http_client_get (line 25) | def http_client_get(self, url, ignoreCertificateError=None):
method http_client_get_json (line 51) | def http_client_get_json(self, url):
method http_get (line 61) | def http_get(self, url):
method proxy_setup (line 69) | def proxy_setup(self, session=None):
method urlparse (line 93) | def urlparse(url: str) -> ParseResult:
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
{
"path": ".gitignore",
"chars": 1228,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\ntemp/\n# Distribution / pa"
},
{
"path": ".vscode/launch.json",
"chars": 1505,
"preview": "{\n // 使用 IntelliSense 了解相关属性。\n // 悬停以查看现有属性的描述。\n // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387"
},
{
"path": ".vscode/settings.json",
"chars": 87,
"preview": "{\n \"python.linting.pylintEnabled\": true,\n \"python.formatting.provider\": \"black\"\n}"
},
{
"path": "CHANGELOG",
"chars": 225,
"preview": "20180611 0.1.0-alpha1\n允许在txt中使用Python风格的注释,即以#开头的内容会被忽略\n对txt中的空白符进行处理\n保存路径不再强制包含 ArtStation\n默认保存路径现在为用户根路径\n\n20190131 0.1"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2018 Sean Feng\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README-zh.md",
"chars": 1591,
"preview": "# ArtStation Downloader\n\nArtStation Downloader 是一个帮助你批量从[ArtStation](https://www.artstation.com/)网站下载图片和视频的小工具\n\n## 用法\n\n#"
},
{
"path": "README.md",
"chars": 2262,
"preview": "# ArtStation Downloader\n\n[中文说明](./README-zh.md)\n\nArtStation Downloader is a lightweight tool to help you download images"
},
{
"path": "build.bat",
"chars": 222,
"preview": "pyinstaller -y --windowed .\\src\\ArtStationDownloader.py\ndel dist\\ArtStationDownloader.zip\n7z a -tzip dist\\ArtStationDown"
},
{
"path": "requirements_dev.txt",
"chars": 28,
"preview": "black==23.7.0\npylint==2.17.5"
},
{
"path": "src/ArtStationDownloader.py",
"chars": 1762,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n\"\"\"批量下载ArtStation图片\n\nCopyright 2018 Sean Feng(sean@fantablade.com)\n\"\"\"\n\n_"
},
{
"path": "src/app.py",
"chars": 6629,
"preview": "# -*- coding: utf-8 -*-\n\nimport os\nimport PySimpleGUI as sg\n\nimport config\nfrom core import Core, DownloadSorting\n\n\nclas"
},
{
"path": "src/config.py",
"chars": 1806,
"preview": "#!/usr/bin/python\n# -*- coding:utf-8 -*-\n# author: lingyue.wkl\n# desc: use to read ini\n# ---------------------\n# 2012-02"
},
{
"path": "src/console.py",
"chars": 332,
"preview": "# -*- coding: utf-8 -*-\n\nfrom core import Core\n\n\nclass Console:\n def __init__(self):\n self.core = Core()\n\n "
},
{
"path": "src/core.py",
"chars": 7287,
"preview": "# -*- coding: utf-8 -*-\n\n\"\"\"内核方法\nCopyright 2018-2019 Sean Feng(sean@FantaBlade.com)\n\"\"\"\n\nimport os\nfrom concurrent impor"
},
{
"path": "src/http_client.py",
"chars": 3011,
"preview": "import http.client as http_client\nimport ssl\nimport json\nimport os\nimport ssl\nfrom urllib.parse import urlparse, ParseRe"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the findix/ArtStationDownloader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (28.4 KB), approximately 7.1k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.