新增视频/图文作品文件下载开关

This commit is contained in:
JoeanAmier 2024-04-13 13:25:31 +08:00
parent 3b2f1bda9e
commit ea349048c3
13 changed files with 134 additions and 21 deletions

View File

@ -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.

View File

@ -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!"

View 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 ""

View File

@ -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:

View File

@ -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))

View File

@ -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:

View File

@ -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")

View File

@ -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():

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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 ""}"