优化项目代码

This commit is contained in:
yongquan
2024-01-20 16:48:22 +08:00
parent c4a9307f24
commit f6c232d14c
15 changed files with 201 additions and 116 deletions

View File

@@ -245,3 +245,8 @@ async with XHS(work_path=work_path,
<li>基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。</li>
</ul>
<b>在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。</b>
# 💡 代码参考
* https://textual.textualize.io/
* https://docs.aiohttp.org/en/stable/

View File

@@ -1,5 +1,6 @@
aiohttp>=3.9.0
textual>=0.40.0
aiohttp>=3.9.1
textual>=0.47.1
pyperclip>=1.8.2
lxml>=4.9.3
lxml>=5.1.0
PyYAML>=6.0.1
aiosqlite>=0.19.0

View File

@@ -1,6 +1,7 @@
from typing import Type
from textual.app import App
from textual.widgets import RichLog
from source.application import XHS
from source.module import (
@@ -13,13 +14,16 @@ from source.translator import (
English,
)
from .index import Index
from .loading import Loading
from .setting import Setting
from .update import Update
__all__ = ["XHSDownloader"]
class XHSDownloader(App):
settings = Settings(ROOT)
CSS_PATH = ROOT.joinpath("static/XHS-Downloader.tcss")
SETTINGS = Settings(ROOT)
def __init__(self):
super().__init__()
@@ -36,7 +40,7 @@ class XHSDownloader(App):
await self.APP.__aexit__(exc_type, exc_value, traceback)
def __initialization(self) -> None:
self.parameter = self.settings.run()
self.parameter = self.SETTINGS.run()
self.prompt = LANGUAGE.get(self.parameter["language"], Chinese)
self.APP = XHS(**self.parameter, language_object=self.prompt)
@@ -47,11 +51,12 @@ class XHSDownloader(App):
self.prompt),
name="setting")
self.install_screen(Index(self.APP, self.prompt), name="index")
self.install_screen(Loading(self.prompt), name="loading")
await self.push_screen("index")
async def action_settings(self):
async def save_settings(data: dict) -> None:
self.settings.update(data)
self.SETTINGS.update(data)
await self.refresh_screen()
await self.push_screen("setting", save_settings)
@@ -62,12 +67,20 @@ class XHSDownloader(App):
async def refresh_screen(self):
self.pop_screen()
self.__initialization()
self.uninstall_screen("index")
self.uninstall_screen("setting")
self.uninstall_screen("loading")
self.install_screen(Index(self.APP, self.prompt), name="index")
self.install_screen(
Setting(
self.parameter,
self.prompt),
name="setting")
self.uninstall_screen("index")
self.install_screen(Index(self.APP, self.prompt), name="index")
self.install_screen(Loading(self.prompt), name="loading")
await self.push_screen("index")
def update_result(self, tip: str) -> None:
self.query_one(RichLog).write(tip)
async def action_check_update(self):
await self.push_screen(Update(self.APP, self.prompt), callback=self.update_result)

View File

@@ -6,7 +6,6 @@ from rich.text import Text
from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Center
from textual.containers import HorizontalScroll
from textual.containers import ScrollableContainer
from textual.screen import Screen
@@ -15,11 +14,9 @@ from textual.widgets import Footer
from textual.widgets import Header
from textual.widgets import Input
from textual.widgets import Label
from textual.widgets import ProgressBar
from textual.widgets import RichLog
from source.application import XHS
from source.module import ROOT
from source.module import (
VERSION_MAJOR,
VERSION_MINOR,
@@ -28,32 +25,20 @@ from source.module import (
MASTER,
ERROR,
WARNING,
INFO,
LICENCE,
REPOSITORY,
RELEASES,
GENERAL,
USERSCRIPT,
)
from source.translator import (English, Chinese)
from source.translator import (
English,
Chinese,
)
__all__ = ["Index"]
def show_state(function):
async def inner(self, *args, **kwargs):
self.close_disclaimer()
self.bar.update(total=100, progress=100)
result = await function(self, *args, **kwargs)
self.bar.update(total=None)
self.tip.write(Text(">" * 50, style=GENERAL))
return result
return inner
class Index(Screen):
CSS_PATH = ROOT.joinpath("static/XHS-Downloader.tcss")
BINDINGS = [
Binding(key="q", action="quit", description="退出程序/Quit"),
Binding(key="u", action="check_update", description="检查更新/Update"),
@@ -63,12 +48,10 @@ class Index(Screen):
def __init__(self, app: XHS, language: Chinese | English):
super().__init__()
self.app_ = app
self.xhs = app
self.prompt = language
self.url = None
self.tip = None
self.bar = None
self.disclaimer = True
def compose(self) -> ComposeResult:
yield Header()
@@ -95,9 +78,7 @@ class Index(Screen):
Button(self.prompt.reset_button, id="reset"),
),
)
with Center():
yield ProgressBar(total=None, show_percentage=False, show_eta=False)
yield RichLog(markup=True)
yield RichLog(markup=True, wrap=True)
yield Footer()
def on_mount(self) -> None:
@@ -105,17 +86,15 @@ class Index(Screen):
VERSION_MINOR}{" Beta" if VERSION_BETA else ""}"
self.url = self.query_one(Input)
self.tip = self.query_one(RichLog)
self.bar = self.query_one(ProgressBar)
self.tip.write(Text("\n".join(self.prompt.disclaimer), style=MASTER))
def close_disclaimer(self):
if self.disclaimer:
self.tip.clear()
self.disclaimer = False
@on(Button.Pressed, "#deal")
def deal_button(self):
create_task(self.deal())
async def deal_button(self):
if self.url.value:
await create_task(self.deal())
else:
self.tip.write(Text(self.prompt.invalid_link, style=WARNING))
self.tip.write(Text(">" * 50, style=GENERAL))
@on(Button.Pressed, "#reset")
def reset_button(self):
@@ -125,52 +104,13 @@ class Index(Screen):
def paste_button(self):
self.query_one(Input).value = paste()
@show_state
async def deal(self):
if not self.url.value:
self.tip.write(Text(self.prompt.invalid_link, style=WARNING))
return
if any(await self.app_.extract(self.url.value, True, log=self.tip)):
await self.app.push_screen("loading")
if any(await self.xhs.extract(self.url.value, True, log=self.tip)):
self.url.value = ""
else:
self.tip.write(Text(self.prompt.download_failure, style=ERROR))
@show_state
async def action_check_update(self):
self.tip.write(
Text(
self.prompt.check_update_notification,
style=WARNING))
try:
url = await self.app_.html.request_url(RELEASES, False, self.tip)
latest_major, latest_minor = map(
int, url.split("/")[-1].split(".", 1))
if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR:
self.tip.write(
Text(
self.prompt.official_version_update(
latest_major,
latest_minor),
style=WARNING))
self.tip.write(RELEASES)
elif latest_minor == VERSION_MINOR and VERSION_BETA:
self.tip.write(
Text(
self.prompt.development_version_update,
style=WARNING))
self.tip.write(RELEASES)
elif VERSION_BETA:
self.tip.write(
Text(
self.prompt.latest_development_version,
style=WARNING))
else:
self.tip.write(
Text(
self.prompt.latest_official_version,
style=INFO))
except ValueError:
self.tip.write(Text(self.prompt.check_update_failure, style=ERROR))
self.app.pop_screen()
@staticmethod
def action_user_script():

View File

@@ -1,10 +1,25 @@
from textual.app import ComposeResult
from textual.screen import Screen
from textual.containers import Grid
from textual.screen import ModalScreen
from textual.widgets import Label
from textual.widgets import LoadingIndicator
from source.translator import (
English,
Chinese,
)
__all__ = ["Loading"]
class Loading(Screen):
class Loading(ModalScreen):
def __init__(self, language: Chinese | English):
super().__init__()
self.prompt = language
def compose(self) -> ComposeResult:
yield LoadingIndicator()
yield Grid(
Label(self.prompt.processing),
LoadingIndicator(),
classes="loading",
)

9
source/TUI/progress.py Normal file
View File

@@ -0,0 +1,9 @@
from textual.app import ComposeResult
from textual.screen import Screen
__all__ = ["Progress"]
class Progress(Screen):
def compose(self) -> ComposeResult:
pass

View File

@@ -12,7 +12,6 @@ from textual.widgets import Input
from textual.widgets import Label
from textual.widgets import Select
from source.module import ROOT
from source.translator import (
LANGUAGE,
Chinese,
@@ -23,7 +22,6 @@ __all__ = ["Setting"]
class Setting(Screen):
CSS_PATH = ROOT.joinpath("static/XHS-Downloader.tcss")
BINDINGS = [
Binding(key="q", action="quit", description="退出程序/Quit"),
Binding(key="b", action="index", description="返回首页/Back"),

70
source/TUI/update.py Normal file
View File

@@ -0,0 +1,70 @@
from aiohttp import ClientTimeout
from rich.text import Text
from textual import work
from textual.app import ComposeResult
from textual.containers import Grid
from textual.screen import ModalScreen
from textual.widgets import Label
from textual.widgets import LoadingIndicator
from source.application import XHS
from source.module import (
VERSION_MAJOR,
VERSION_MINOR,
VERSION_BETA,
ERROR,
WARNING,
INFO,
RELEASES,
)
from source.translator import (
English,
Chinese,
)
__all__ = ["Update"]
class Update(ModalScreen):
def __init__(self, app: XHS, language: Chinese | English):
super().__init__()
self.xhs = app
self.prompt = language
def compose(self) -> ComposeResult:
yield Grid(
Label(self.prompt.check_update_notification),
LoadingIndicator(),
classes="loading",
)
@work()
async def check_update(self) -> None:
try:
url = await self.xhs.html.request_url(RELEASES, False, None, timeout=ClientTimeout(connect=5))
latest_major, latest_minor = map(
int, url.split("/")[-1].split(".", 1))
if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR:
tip = Text(
f"{self.prompt.official_version_update(
latest_major,
latest_minor)}\n{RELEASES}",
style=WARNING)
elif latest_minor == VERSION_MINOR and VERSION_BETA:
tip = Text(
f"{self.prompt.development_version_update}\n{RELEASES}",
style=WARNING)
elif VERSION_BETA:
tip = Text(
self.prompt.latest_development_version,
style=WARNING)
else:
tip = Text(
self.prompt.latest_official_version,
style=INFO)
except ValueError:
tip = Text(self.prompt.check_update_failure, style=ERROR)
self.dismiss(tip)
def on_mount(self) -> None:
self.check_update()

View File

@@ -1,3 +1,4 @@
from asyncio import gather
from pathlib import Path
from aiohttp import ClientError
@@ -36,12 +37,13 @@ class Download:
path = self.__generate_path(name)
match type_:
case "视频":
await self.__download(urls[0], path, f"{name}", self.video_format, log, bar)
tasks = self.__ready_download_video(urls, path, name, log)
case "图文":
for index, url in enumerate(urls, start=1):
await self.__download(url, path, f"{name}_{index}", self.image_format, log, bar)
tasks = self.__ready_download_image(urls, path, name, log)
case _:
raise ValueError
tasks = [self.__download(url, path, name, format_, log, bar) for url, name, format_ in tasks]
await gather(*tasks)
return path
def __generate_path(self, name: str):
@@ -49,6 +51,22 @@ class Download:
path.mkdir(exist_ok=True)
return path
def __ready_download_video(self, urls: list[str], path: Path, name: str, log) -> list:
if any(path.glob(f"{name}.*")):
logging(log, self.prompt.skip_download(name))
return []
return [(urls[0], name, self.video_format)]
def __ready_download_image(self, urls: list[str], path: Path, name: str, log) -> list:
tasks = []
for i, j in enumerate(urls, start=1):
file = f"{name}_{i}"
if any(path.glob(f"{file}.*")):
logging(log, self.prompt.skip_download(file))
continue
tasks.append([j, file, self.image_format])
return tasks
@re_download
async def __download(self, url: str, path: Path, name: str, format_: str, log, bar):
try:
@@ -58,10 +76,7 @@ class Download:
suffix = self.__extract_type(
response.headers.get("Content-Type")) or format_
temp = self.temp.joinpath(name)
file = path.joinpath(name).with_suffix(f".{suffix}")
if self.manager.is_exists(file):
logging(log, self.prompt.skip_download(name))
return True
real = path.joinpath(name).with_suffix(f".{suffix}")
# self.__create_progress(
# bar, int(
# response.headers.get(
@@ -70,7 +85,7 @@ class Download:
async for chunk in response.content.iter_chunked(self.chunk):
f.write(chunk)
# self.__update_progress(bar, len(chunk))
self.manager.move(temp, file)
self.manager.move(temp, real)
# self.__create_progress(bar, None)
logging(log, self.prompt.download_success(name))
return True

View File

@@ -21,11 +21,13 @@ class Html:
url: str,
content=True,
log=None,
**kwargs,
) -> str:
try:
async with self.session.get(
url,
proxy=self.proxy,
**kwargs,
) as response:
if response.status != 200:
return ""

View File

@@ -19,7 +19,7 @@ __all__ = ["Manager"]
class Manager:
NAME = compile(r"[^\u4e00-\u9fa5a-zA-Z0-9_]")
NAME = compile(r"[^\u4e00-\u9fa5a-zA-Z0-9]")
def __init__(
self,

View File

@@ -6,13 +6,13 @@ class Chinese:
disclaimer: tuple[str] = (
"关于 XHS-Downloader 的 免责声明:",
"",
"1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。",
"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。",
"3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。",
"4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。",
"5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。",
"6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。",
"7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因"
"1.使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。",
"2.本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。",
"3.使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。",
"4.使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。",
"5.使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。",
"6.本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。",
"7.基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因"
"二次开发可能带来的各种情况负全部责任。",
"",
"在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果"
@@ -67,6 +67,8 @@ class Chinese:
save_button: str = "保存配置"
abandon_button: str = "放弃更改"
processing: str = "程序处理中..."
@staticmethod
def request_error(url: str) -> str:
return f"网络异常,请求 {url} 失败!"

View File

@@ -85,6 +85,8 @@ class English(Chinese):
save_button: str = "Save configuration"
abandon_button: str = "Discard changes"
processing: str = "Processing..."
@staticmethod
def request_error(url: str) -> str:
return f"Network error, failed to access {url}!"

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @name XHS-Downloader
// @namespace https://github.com/JoeanAmier/XHS-Downloader
// @version 1.4
// @version 1.4.1
// @description 提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件
// @author JoeanAmier
// @match http*://www.xiaohongshu.com/explore*
@@ -385,16 +385,18 @@
const buttons = [createButton("Download", "下载无水印作品文件", extractDownloadLinks), createButton("Post", "提取发布作品链接", extractAllLinksEvent, 0), createButton("Collection", "提取收藏作品链接", extractAllLinksEvent, 1), createButton("Favorite", "提取点赞作品链接", extractAllLinksEvent, 2), createButton("Feed", "提取发现作品链接", extractAllLinksEvent, -1), createButton("Search", "提取搜索作品链接", extractAllLinksEvent, 3), createButton("User", "提取搜索用户链接", extractAllLinksEvent, 4), createButton("About", "关于 XHS-Downloader", about,)]
const run = url => {
if (url === "https://www.xiaohongshu.com/explore") {
updateContainer(buttons.slice(4, 5));
} else if (url.includes("https://www.xiaohongshu.com/explore/")) {
updateContainer(buttons.slice(0, 1));
} else if (url.includes("https://www.xiaohongshu.com/user/profile/")) {
updateContainer(buttons.slice(1, 4));
} else if (url.includes("https://www.xiaohongshu.com/search_result")) {
updateContainer(buttons.slice(5, 7));
}
};
setTimeout(function () {
if (url === "https://www.xiaohongshu.com/explore") {
updateContainer(buttons.slice(4, 5));
} else if (url.includes("https://www.xiaohongshu.com/explore/")) {
updateContainer(buttons.slice(0, 1));
} else if (url.includes("https://www.xiaohongshu.com/user/profile/")) {
updateContainer(buttons.slice(1, 4));
} else if (url.includes("https://www.xiaohongshu.com/search_result")) {
updateContainer(buttons.slice(5, 7));
}
}, 500)
}
let currentUrl = window.location.href;

View File

@@ -1,4 +1,4 @@
Screen, RichLog {
ScrollableContainer, RichLog {
background: #2f3542;
}
Button {
@@ -44,3 +44,14 @@ Bar > .bar--indeterminate {
Bar > .bar--complete {
color: #ff7f50;
}
.loading {
grid-size: 1 2;
grid-gutter: 1;
width: 40vw;
height: 5;
background: #353b48;
border: double #747d8c;
}
ModalScreen {
align: center middle;
}