Full Code of findix/ArtStationDownloader for AI

master 6c659c5b7231 cached
16 files
28.4 KB
7.1k tokens
43 symbols
1 requests
Download .txt
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)
Download .txt
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
Download .txt
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.

Copied to clipboard!