mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2025-12-26 04:48:05 +08:00
新增视频/图文作品文件下载开关
This commit is contained in:
parent
3b2f1bda9e
commit
ea349048c3
12
README.md
12
README.md
@ -199,6 +199,18 @@ async def example():
|
||||
<td align="center">PNG</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">image_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">图文作品文件下载开关</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">video_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">视频作品文件下载开关</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">folder_mode</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
|
||||
|
||||
Binary file not shown.
@ -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!"
|
||||
|
||||
@ -231,3 +231,21 @@ msgstr ""
|
||||
|
||||
msgid "文件 {0} 请求失败,响应码 {1}"
|
||||
msgstr ""
|
||||
|
||||
msgid "视频作品下载功能已关闭,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
msgid "图集作品下载功能已关闭,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
msgid "作品 {0} 存在下载记录,跳过处理"
|
||||
msgstr ""
|
||||
|
||||
msgid "视频作品下载开关"
|
||||
msgstr ""
|
||||
|
||||
msgid "图文作品下载开关"
|
||||
msgstr ""
|
||||
|
||||
msgid "配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!"
|
||||
msgstr ""
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ""}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user