diff --git a/README.md b/README.md
index 3496cd1..eb788c8 100644
--- a/README.md
+++ b/README.md
@@ -199,6 +199,18 @@ async def example():
PNG |
+| image_download |
+bool |
+图文作品文件下载开关 |
+true |
+
+
+| video_download |
+bool |
+视频作品文件下载开关 |
+true |
+
+
| folder_mode |
bool |
是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致 |
diff --git a/locale/en_GB/LC_MESSAGES/xhs.mo b/locale/en_GB/LC_MESSAGES/xhs.mo
index a2ba0e8..42653b7 100644
Binary files a/locale/en_GB/LC_MESSAGES/xhs.mo and b/locale/en_GB/LC_MESSAGES/xhs.mo differ
diff --git a/locale/en_GB/LC_MESSAGES/xhs.po b/locale/en_GB/LC_MESSAGES/xhs.po
index dbc5285..de12c37 100644
--- a/locale/en_GB/LC_MESSAGES/xhs.po
+++ b/locale/en_GB/LC_MESSAGES/xhs.po
@@ -215,7 +215,7 @@ msgid "检测到新版本:{0}.{1}"
msgstr "New version detected: {0}.{1}"
msgid "作品 {0} 存在下载记录,跳过下载"
-msgstr "works {0} has a download record, skipping download"
+msgstr "works {0} has a download record, skip download"
msgid "从指定的浏览器读取小红书网页版 Cookie,需要关闭对应的浏览器,支持:1 Chrome, 2 Chromium, 3 Opera, 4 Opera GX, 5 Brave, 6 Edge, 7 Vivaldi, 8 Firefox, 9 LibreWolf, 10 Safari,输入浏览器类型或序号"
msgstr "To read the Xiaohongshu web version cookie from the specified browser, the corresponding browser needs to be closed. Supports: 1 Chrome, 2 Chromium, 3 Opera, 4 Opera GX, 5 Brave, 6 Edge, 7 Vivaldi, 8 Firefox, 9 LibreWolf, 10 Safari, enter the browser type or serial number"
@@ -231,3 +231,21 @@ msgstr "Other open-source projects of the author"
msgid "文件 {0} 请求失败,响应码 {1}"
msgstr "File {0} request failed with response code {1}"
+
+msgid "视频作品下载功能已关闭,跳过下载"
+msgstr "The video download function has been turned off, skip download"
+
+msgid "图集作品下载功能已关闭,跳过下载"
+msgstr "The image download function has been turned off, skip download"
+
+msgid "作品 {0} 存在下载记录,跳过处理"
+msgstr "Works {0} has a download record, skip processing"
+
+msgid "视频作品下载开关"
+msgstr "Video works download switch"
+
+msgid "图文作品下载开关"
+msgstr "Image works download switch"
+
+msgid "配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!"
+msgstr "The configuration file settings.json is missing necessary parameters. Please delete the file and run the program again to automatically generate the default configuration file!"
diff --git a/locale/zh_CN/LC_MESSAGES/xhs.po b/locale/zh_CN/LC_MESSAGES/xhs.po
index 9110cb1..8ae7acd 100644
--- a/locale/zh_CN/LC_MESSAGES/xhs.po
+++ b/locale/zh_CN/LC_MESSAGES/xhs.po
@@ -231,3 +231,21 @@ msgstr ""
msgid "文件 {0} 请求失败,响应码 {1}"
msgstr ""
+
+msgid "视频作品下载功能已关闭,跳过下载"
+msgstr ""
+
+msgid "图集作品下载功能已关闭,跳过下载"
+msgstr ""
+
+msgid "作品 {0} 存在下载记录,跳过处理"
+msgstr ""
+
+msgid "视频作品下载开关"
+msgstr ""
+
+msgid "图文作品下载开关"
+msgstr ""
+
+msgid "配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!"
+msgstr ""
diff --git a/source/TUI/app.py b/source/TUI/app.py
index ffffa9b..2cf7844 100644
--- a/source/TUI/app.py
+++ b/source/TUI/app.py
@@ -6,9 +6,11 @@ from textual.widgets import RichLog
from source.application import XHS
from source.module import (
ROOT,
+ ERROR,
)
from source.module import Settings
from source.module import Translate
+from source.module import logging
from .about import About
from .index import Index
from .loading import Loading
@@ -54,6 +56,16 @@ class XHSDownloader(App):
self.install_screen(About(self.message), name="about")
self.install_screen(Record(self.APP, self.message), name="record")
await self.push_screen("index")
+ self.SETTINGS.check_keys(
+ self.parameter,
+ logging,
+ self.query_one(RichLog),
+ self.message("配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!") +
+ f"\n{
+ ">" *
+ 50}",
+ ERROR,
+ )
async def action_settings(self):
async def save_settings(data: dict) -> None:
diff --git a/source/TUI/index.py b/source/TUI/index.py
index 197505e..944e977 100644
--- a/source/TUI/index.py
+++ b/source/TUI/index.py
@@ -107,7 +107,7 @@ class Index(Screen):
@work()
async def deal(self):
await self.app.push_screen("loading")
- if any(await self.xhs.extract(self.url.value, True, log=self.tip)):
+ if any(await self.xhs.extract(self.url.value, True, log=self.tip, data=False, )):
self.url.value = ""
else:
self.tip.write(Text(self.message("下载小红书作品文件失败"), style=ERROR))
diff --git a/source/TUI/monitor.py b/source/TUI/monitor.py
index 9c31d8c..14bd698 100644
--- a/source/TUI/monitor.py
+++ b/source/TUI/monitor.py
@@ -46,7 +46,7 @@ class Monitor(Screen):
@work()
async def run_monitor(self):
- await self.xhs.monitor(download=True, log=self.query_one(RichLog))
+ await self.xhs.monitor(download=True, log=self.query_one(RichLog), data=False, )
self.action_close()
def on_mount(self) -> None:
diff --git a/source/TUI/setting.py b/source/TUI/setting.py
index b693204..ff1dd41 100644
--- a/source/TUI/setting.py
+++ b/source/TUI/setting.py
@@ -27,6 +27,7 @@ class Setting(Screen):
super().__init__()
self.data = data
self.message = message
+
def compose(self) -> ComposeResult:
yield Header()
yield ScrollableContainer(
@@ -49,15 +50,17 @@ class Setting(Screen):
Label(self.message("请求数据失败时,重试的最大次数"), classes="params", ),
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ),
Container(
- Label("", classes="params", ),
- Label("", classes="params", ),
+ Checkbox(self.message("记录作品数据"), id="record_data", value=self.data["record_data"], ),
+ Checkbox(self.message("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ),
+ Checkbox(self.message("视频作品下载开关"), id="video_download", value=self.data["video_download"], ),
+ Checkbox(self.message("图文作品下载开关"), id="image_download", value=self.data["image_download"], ),
+ classes="horizontal-layout"),
+ Container(
Label(self.message("图片下载格式"), classes="params", ),
Label(self.message("程序语言"), classes="params", ),
classes="horizontal-layout",
),
Container(
- Checkbox(self.message("记录作品数据"), id="record_data", value=self.data["record_data"], ),
- Checkbox(self.message("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ),
Select.from_values(
("PNG", "WEBP"),
value=self.data["image_format"],
@@ -98,6 +101,8 @@ class Setting(Screen):
"image_format": self.query_one("#image_format").value,
"folder_mode": self.query_one("#folder_mode").value,
"language": self.query_one("#language").value,
+ "image_download": self.query_one("#image_download").value,
+ "video_download": self.query_one("#video_download").value,
})
@on(Button.Pressed, "#abandon")
diff --git a/source/application/app.py b/source/application/app.py
index 3a55975..482df55 100644
--- a/source/application/app.py
+++ b/source/application/app.py
@@ -7,6 +7,7 @@ from contextlib import suppress
from datetime import datetime
from re import compile
from typing import Callable
+from urllib.parse import urlparse
from pyperclip import paste
@@ -77,6 +78,8 @@ class XHS:
max_retry,
record_data,
image_format,
+ image_download,
+ video_download,
folder_mode,
self.message,
)
@@ -91,6 +94,7 @@ class XHS:
self.clipboard_cache: str = ""
self.queue = Queue()
self.event = Event()
+ self.server = server
def __extract_image(self, container: dict, data: Namespace):
container["下载地址"] = self.image.get_image_link(
@@ -126,7 +130,8 @@ class XHS:
download=False,
index: list | tuple = None,
log=None,
- bar=None) -> list[dict]:
+ bar=None,
+ data=True, ) -> list[dict]:
# return # 调试代码
urls = await self.__extract_links(url, log)
if not urls:
@@ -135,19 +140,20 @@ class XHS:
logging(
log, self.message("共 {0} 个小红书作品待处理...").format(len(urls)))
# return urls # 调试代码
- return [await self.__deal_extract(i, download, index, log, bar, ) for i in urls]
+ return [await self.__deal_extract(i, download, index, log, bar, data, ) for i in urls]
async def extract_cli(self,
url: str,
download=True,
index: list | tuple = None,
log=None,
- bar=None) -> None:
+ bar=None,
+ data=False, ) -> None:
url = await self.__extract_links(url, log)
if not url:
logging(log, self.message("提取小红书作品链接失败"), WARNING)
else:
- await self.__deal_extract(url[0], download, index, log, bar)
+ await self.__deal_extract(url[0], download, index, log, bar, data, )
async def __extract_links(self, url: str, log) -> list:
urls = []
@@ -161,7 +167,11 @@ class XHS:
urls.append(u.group())
return urls
- async def __deal_extract(self, url: str, download: bool, index: list | tuple | None, log, bar):
+ async def __deal_extract(self, url: str, download: bool, index: list | tuple | None, log, bar, data: bool, ):
+ if not data and await self.skip_download(i := self.__extract_link_id(url)):
+ msg = self.message("作品 {0} 存在下载记录,跳过处理").format(i)
+ logging(log, msg)
+ return {"message": msg}
logging(log, self.message("开始处理作品:{0}").format(url))
html = await self.html.request_url(url, log=log)
namespace = self.__generate_data_object(html)
@@ -184,6 +194,11 @@ class XHS:
logging(log, self.message("作品处理完成:{0}").format(url))
return data
+ @staticmethod
+ def __extract_link_id(url: str) -> str:
+ link = urlparse(url)
+ return link.path.split("/")[-1]
+
def __generate_data_object(self, html: str) -> Namespace:
data = self.convert.run(html)
return Namespace(data)
@@ -194,7 +209,7 @@ class XHS:
title = self.manager.filter_name(data["作品标题"]) or data["作品ID"]
return f"{time_}_{author}_{title[:64]}"
- async def monitor(self, delay=1, download=False, log=None, bar=None) -> None:
+ async def monitor(self, delay=1, download=False, log=None, bar=None, data=True, ) -> None:
logging(
None,
self.message(
@@ -202,7 +217,7 @@ class XHS:
style=MASTER,
)
self.event.clear()
- await gather(self.__push_link(delay), self.__receive_link(delay, download, None, log, bar))
+ await gather(self.__push_link(delay), self.__receive_link(delay, download, None, log, bar, data))
async def __push_link(self, delay: int):
while not self.event.is_set():
diff --git a/source/application/download.py b/source/application/download.py
index 63f40ce..9d485da 100644
--- a/source/application/download.py
+++ b/source/application/download.py
@@ -17,6 +17,7 @@ class Download:
"image/jpeg": "jpg",
"image/webp": "webp",
"application/octet-stream": "",
+ "video/mp4": "mp4",
"video/quicktime": "mov",
}
@@ -32,6 +33,8 @@ class Download:
self.folder_mode = manager.folder_mode
self.video_format = "mp4"
self.image_format = manager.image_format
+ self.image_download = manager.image_download
+ self.video_download = manager.video_download
async def run(self, urls: list, index: list | tuple | None, name: str, type_: str, log, bar) -> tuple[Path, tuple]:
path = self.__generate_path(name)
@@ -67,6 +70,9 @@ class Download:
path: Path,
name: str,
log) -> list:
+ if not self.video_download:
+ logging(log, self.message("视频作品下载功能已关闭,跳过下载"))
+ return []
if any(path.glob(f"{name}.*")):
logging(log, self.message("{0} 文件已存在,跳过下载").format(name))
return []
@@ -80,6 +86,9 @@ class Download:
name: str,
log) -> list:
tasks = []
+ if not self.image_download:
+ logging(log, self.message("图文作品下载功能已关闭,跳过下载"))
+ return tasks
for i, j in enumerate(urls, start=1):
if index and i not in index:
continue
@@ -94,6 +103,7 @@ class Download:
@re_download
async def __download(self, url: str, path: Path, name: str, format_: str, log, bar):
+ temp = self.temp.joinpath(name)
try:
async with self.session.get(url, proxy=self.proxy) as response:
if response.status != 200:
@@ -103,7 +113,6 @@ class Download:
return False
suffix = self.__extract_type(
response.headers.get("Content-Type")) or format_
- temp = self.temp.joinpath(name)
real = path.joinpath(f"{name}.{suffix}")
# self.__create_progress(
# bar, int(
diff --git a/source/module/manager.py b/source/module/manager.py
index 4263992..2cdd395 100644
--- a/source/module/manager.py
+++ b/source/module/manager.py
@@ -30,6 +30,8 @@ class Manager:
retry: int,
record_data: bool,
image_format: str,
+ image_download: bool,
+ video_download: bool,
folder_mode: bool,
transition: Callable[[str], str],
):
@@ -42,9 +44,9 @@ class Manager:
self.headers = self.blank_headers | {"Cookie": cookie}
self.retry = retry
self.chunk = chunk
- self.record_data = record_data
+ self.record_data = self.check_bool(record_data, False)
self.image_format = self.__check_image_format(image_format)
- self.folder_mode = folder_mode
+ self.folder_mode = self.check_bool(folder_mode, False)
self.proxy = proxy
self.request_session = ClientSession(
headers=self.headers | {
@@ -55,6 +57,8 @@ class Manager:
headers=self.blank_headers,
timeout=ClientTimeout(connect=timeout))
self.message = transition
+ self.image_download = self.check_bool(image_download, True)
+ self.video_download = self.check_bool(video_download, True)
def __check_path(self, path: str) -> Path:
if not path:
@@ -88,7 +92,8 @@ class Manager:
@staticmethod
def delete(path: Path):
- path.unlink()
+ if path.exists():
+ path.unlink()
@staticmethod
def archive(root: Path, name: str, folder_mode: bool) -> Path:
@@ -105,6 +110,10 @@ class Manager:
name = self.NAME.sub("_", name)
return sub(r"_+", "_", name).strip("_")
+ @staticmethod
+ def check_bool(value: bool, default: bool) -> bool:
+ return value if isinstance(value, bool) else default
+
async def close(self):
await self.request_session.close()
await self.download_session.close()
diff --git a/source/module/settings.py b/source/module/settings.py
index f386207..0a4ecca 100644
--- a/source/module/settings.py
+++ b/source/module/settings.py
@@ -44,3 +44,18 @@ class Settings:
def update(self, data: dict):
with self.file.open("w", encoding=self.encode) as f:
dump(data, f, indent=4, ensure_ascii=False)
+
+ @classmethod
+ def check_keys(
+ cls,
+ data: dict,
+ callback: callable,
+ *args,
+ **kwargs,
+ ) -> dict:
+ needful_keys = set(cls.default.keys())
+ given_keys = set(data.keys())
+ if not needful_keys.issubset(given_keys):
+ callback(*args, **kwargs)
+ return cls.default
+ return data
diff --git a/source/module/static.py b/source/module/static.py
index fa304a9..7488835 100644
--- a/source/module/static.py
+++ b/source/module/static.py
@@ -21,9 +21,9 @@ __all__ = [
"PROJECT",
]
-VERSION_MAJOR = 1
-VERSION_MINOR = 9
-VERSION_BETA = False
+VERSION_MAJOR = 2
+VERSION_MINOR = 0
+VERSION_BETA = True
ROOT = Path(__file__).resolve().parent.parent.parent
PROJECT = f"XHS-Downloader V{VERSION_MAJOR}.{
VERSION_MINOR}{" Beta" if VERSION_BETA else ""}"