From 5f09dccf3ce7583ad54630cd129ec88dd769ea5c Mon Sep 17 00:00:00 2001 From: Sean Feng Date: Fri, 24 Mar 2023 22:06:55 +0800 Subject: [PATCH] =?UTF-8?q?[feat]=20=E6=A1=86=E6=9E=B6=E6=8D=A2=E7=94=A8?= =?UTF-8?q?=20PySimpleGUI=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ArtStationDownloader.py | 14 +- src/app.py | 268 ++++++++++++++++++------------------ src/console.py | 4 +- src/core.py | 58 +++++--- 4 files changed, 187 insertions(+), 157 deletions(-) diff --git a/src/ArtStationDownloader.py b/src/ArtStationDownloader.py index 6ad8e78..8afa880 100644 --- a/src/ArtStationDownloader.py +++ b/src/ArtStationDownloader.py @@ -6,12 +6,13 @@ Copyright 2018 Sean Feng(sean@fantablade.com) """ -__version__ = "0.2.2" +__version__ = "0.3.0" # $Source$ import argparse from app import App +from core import DownloadSorting from console import Console @@ -37,6 +38,13 @@ def main(): 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" ) @@ -45,12 +53,12 @@ def main(): if args.username: if args.directory: console = Console() - console.download_by_usernames(args.username, args.directory, args.type) + 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.mainloop() # 进入主循环,程序运行 + app.run() # 进入主循环,程序运行 if __name__ == "__main__": diff --git a/src/app.py b/src/app.py index 680a73c..bd70c50 100644 --- a/src/app.py +++ b/src/app.py @@ -1,65 +1,99 @@ # -*- coding: utf-8 -*- import os -from concurrent import futures - -from tkinter import ( - Tk, - Frame, - Label, - Button, - Scrollbar, - Text, - Entry, - messagebox, - filedialog, - StringVar, -) # 引入Tkinter工具包 -from tkinter import TOP, LEFT, BOTTOM, BOTH, X, Y, END -from tkinter import ttk +import PySimpleGUI as sg import config -from core import Core +from core import Core, DownloadSorting -class App(Frame): +class App: + def __init__(self, version): + self.core = Core(self.log) + self.user_settings = sg.UserSettings() + + # 兼容模式 + if not self.user_settings.exists("root_path"): + root_path = config.read_config("config.ini", "Paths", "root_path") + if root_path: + self.user_settings.set("root_path", root_path) + + 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, + "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.text.configure(state="normal") - self.text.insert(END, value + "\n") - self.text.see(END) - self.text.configure(state="disabled") + self.window.write_event_value("log", value) def _set_download_buttons(self, state): - if state: - self.btn_download.configure(state="normal") - self.btn_download_txt.configure(state="normal") - else: - self.btn_download.configure(state="disabled") - self.btn_download_txt.configure(state="disabled") + 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.entry_filename.get() + username_text = self.window["-USERNAME-"].get() if not username_text: - messagebox.showinfo(title="Warning", message="Please input usernames") + self.window.write_event_value( + "popup", ("Please input usernames", "Warning") + ) return - self._set_download_buttons(False) + self.window.write_event_value("set_download_buttons", False) usernames = username_text.split(",") - self.core.root_path = self.root_path.get() - self.core.download_by_usernames(usernames, self.combobox_type.current()) - self._set_download_buttons(True) - - def download_txt(self): - self.btn_download.configure(state="disabled") - self.btn_download_txt.configure(state="disabled") - filename = os.path.normpath( - filedialog.askopenfilename( - filetypes=(("text files", "*.txt"), ("all files", "*.*")) - ) + self.core.root_path = self.root_path + self.core.download_by_usernames( + usernames, self.window["-TYPE-"].get(), self.download_sorting ) - if filename != ".": + self.window.write_event_value("set_download_buttons", True) + + 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: @@ -70,98 +104,64 @@ def download_txt(self): if sharp_at != -1: username = username[:sharp_at] usernames.append(username.strip()) - self.core.root_path = self.root_path.get() - self.core.download_by_usernames(usernames, self.combobox_type.current()) - self.btn_download.configure(state="normal") - self.btn_download_txt.configure(state="normal") - - def load_root_path(self): - return config.read_config("config.ini", "Paths", "root_path") - - def save_root_path(self, root_path): - config.write_config("config.ini", "Paths", "root_path", root_path) + 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 = os.path.normpath(filedialog.askdirectory()) + root_path = sg.popup_get_folder("Select a folder") if root_path: - self.root_path.set(root_path) - self.save_root_path(root_path) - - def createWidgets(self): - frame_tool = Frame(self.window) - frame_path = Frame(self.window) - frame_log = Frame(self.window) - self.lbl_username = Label(frame_tool, text="Usernames(split by ','):") - self.entry_filename = Entry(frame_tool) - self.btn_download = Button( - frame_tool, - text="Download", - command=lambda: self.invoke(self.download), - ) - self.btn_download_txt = Button( - frame_tool, - text="Download txt", - command=lambda: self.invoke(self.download_txt), - ) - self.lbl_type = Label(frame_path, text="Type:") - self.combobox_type = ttk.Combobox(frame_path, state="readonly") - self.combobox_type["values"] = ("all", "image", "video") - self.combobox_type.current(0) - self.lbl_path = Label(frame_path, text="Path:") - self.entry_path = Entry(frame_path, textvariable=self.root_path) - self.btn_path_dialog = Button( - frame_path, text="Browse", command=self.browse_directory - ) - self.scrollbar = Scrollbar(frame_log) - self.text = Text(frame_log) - self.text.configure(state="disabled") - self.lbl_status = Label( - self.window, - text="Feel free to use! Support: Sean Feng(sean@fantablade.com)", - ) + self.root_path = root_path + self.window["-PATH-"].update(root_path) + self.user_settings.set("root_path", root_path) - frame_tool.pack(side=TOP, fill=X) - self.lbl_username.pack(side=LEFT) - self.entry_filename.pack(side=LEFT, fill=X, expand=True) - self.btn_download.pack(side=LEFT) - self.btn_download_txt.pack(side=LEFT) - frame_path.pack(side=TOP, fill=X) - self.lbl_type.pack(side=LEFT) - self.combobox_type.pack(side=LEFT) - self.lbl_path.pack(side=LEFT) - self.entry_path.pack(side=LEFT, fill=X, expand=True) - self.btn_path_dialog.pack(side=LEFT) - self.text.pack(side=LEFT, fill=BOTH, expand=True) - self.scrollbar.pack(side=LEFT, fill=Y) - frame_log.pack(side=TOP, fill=BOTH, expand=True) - self.scrollbar.config(command=self.text.yview) - self.text.config(yscrollcommand=self.scrollbar.set) - self.text.focus() - self.lbl_status.pack(side=LEFT, fill=X, expand=True) - - def invoke(self, func): - def done_callback(worker): - worker_exception = worker.exception() - if worker_exception: - self.log(str(worker_exception)) - self._set_download_buttons(True) - raise(worker_exception) - return self.executor_ui.submit(func).add_done_callback(done_callback) + def create_layout(self): + sg.theme("Dark Blue 3") + layout = [ + [sg.Text('Usernames (split by ","):'), sg.InputText(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), + sg.FolderBrowse("Browse", key="-BROWSE-"), + ], + [ + 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 __init__(self, version): - self.core = Core(self.log) - master = Tk() - Frame.__init__(self, master) - master.title("ArtStation Downloader " + version) # 定义窗体标题 - self.root_path = StringVar() - self.root_path.trace_add( - "write", lambda name, index, mode: self.save_root_path(self.root_path.get()) - ) - root_path_config = self.load_root_path() - self.root_path.set( - root_path_config or os.path.join(os.path.expanduser("~"), "ArtStation") - ) - self.executor_ui = futures.ThreadPoolExecutor(1) - self.window = master - self.pack() - self.createWidgets() + 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() diff --git a/src/console.py b/src/console.py index f7b9b2e..3071eeb 100644 --- a/src/console.py +++ b/src/console.py @@ -7,6 +7,6 @@ class Console: def __init__(self): self.core = Core() - def download_by_usernames(self, usernames, directory, type): + def download_by_usernames(self, usernames, directory, download_type, download_sorting): self.core.root_path = directory - self.core.download_by_usernames(usernames, type) + self.core.download_by_usernames(usernames, download_type, download_sorting) diff --git a/src/core.py b/src/core.py index 5d3ef94..e6a3e8f 100644 --- a/src/core.py +++ b/src/core.py @@ -4,8 +4,8 @@ Copyright 2018-2019 Sean Feng(sean@FantaBlade.com) """ +from enum import Enum import os -import re from concurrent import futures from multiprocessing import cpu_count from urllib.parse import urlparse @@ -17,6 +17,15 @@ from config import Config +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): print(message) @@ -30,13 +39,18 @@ def __init__(self, log_print=None): self.executor_video = futures.ThreadPoolExecutor(1) self.invoke = self._get_invoke() self.invoke_video = self._get_invoke("video") - self.root_path = None + self.root_path: str = None + self.download_sorting: DownloadSorting = None self.futures = [] self._session = requests.session() self.proxy_setup() def http_get(self, url): - r = self._session.get(url, timeout=10) + try: + r = self._session.get(url, timeout=10) + except requests.exceptions.InvalidURL: + print(f'"{url}" is not valid url') + return return r def proxy_setup(self): @@ -94,9 +108,15 @@ def download_project(self, hash_id): username = j["user"]["username"] for asset in assets: assert self.root_path - user_path = os.path.join(self.root_path, username) + 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) - file_path = os.path.join(user_path, title) + 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 = urlparse(url).path.split("/")[-1] @@ -107,16 +127,15 @@ def download_project(self, hash_id): except Exception as e: print(e) if not self.no_video and asset["has_embedded_player"]: # 包含视频 - player_embedded = asset["player_embedded"] - id = re.search( - r"(?<=https://www\.youtube\.com/embed/)[\w_]+", player_embedded - ).group() - try: - self.futures.append( - self.invoke_video(self.download_video, id, file_path) - ) - except Exception as e: - print(e) + player_embedded = BeautifulSoup(asset["player_embedded"], "html.parser") + src = player_embedded.find("iframe").get("src") + if "youtube" in src: + try: + self.futures.append( + self.invoke_video(self.download_video, src, file_path) + ) + except Exception as e: + print(e) def get_projects(self, username): data = [] @@ -156,10 +175,13 @@ def download_by_username(self, username): future_list.append(future) futures.wait(future_list) - def download_by_usernames(self, usernames, type): + def download_by_usernames( + self, usernames, download_type, download_sorting: DownloadSorting + ): self.proxy_setup() - self.no_image = type == "video" - self.no_video = type == "image" + self.no_image = download_type == "video" + self.no_video = download_type == "image" + self.download_sorting = download_sorting # 去重与处理网址 username_set = set() for username in usernames: