新增监听剪贴板下载作品功能

This commit is contained in:
yongquan 2024-01-27 16:29:52 +08:00
parent f6c232d14c
commit 2c8efd0c27
20 changed files with 169 additions and 37 deletions

View File

@ -13,7 +13,7 @@
</div>
<br>
<p>🔥 <b>小红书链接提取/作品采集工具</b>:提取账号发布、收藏、点赞作品链接;提取搜索结果作品、用户链接;采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
<p>❤️ 作者仅在 GitHub 发布 XHS-Downloader没有任何收费计划谨防上当受骗</p>
<p>❤️ 作者仅在 GitHub 发布 XHS-Downloader未与任何个人或网站合作发布,项目没有任何收费计划,谨防上当受骗!</p>
<h1>📑 项目功能</h1>
<ul><b>程序功能</b>
<li>✅ 采集小红书作品信息</li>
@ -24,7 +24,7 @@
<li>✅ 自定义图文作品文件下载格式</li>
<li>✅ 持久化储存作品信息至文件</li>
<li>✅ 作品文件储存至单独文件夹</li>
<li>☑️ 后台监听剪贴板下载作品</li>
<li> 后台监听剪贴板下载作品</li>
<li>☑️ 支持 API 调用功能</li>
</ul>
<ul><b>脚本功能</b>
@ -41,6 +41,8 @@
<a href="https://www.bilibili.com/video/BV1nQ4y137it/"><img src="static/screenshot/程序运行截图CN1.png" alt=""></a>
<hr>
<a href="https://www.bilibili.com/video/BV1nQ4y137it/"><img src="static/screenshot/程序运行截图CN2.png" alt=""></a>
<hr>
<a href="https://www.bilibili.com/video/BV1nQ4y137it/"><img src="static/screenshot/程序运行截图CN3.png" alt=""></a>
<h1>🔗 支持链接</h1>
<ul>
<li><code>https://www.xiaohongshu.com/explore/作品ID</code></li>
@ -110,6 +112,7 @@ async with XHS(work_path=work_path,
</pre>
<h1>⚙️ 配置文件</h1>
<p>项目根目录下的 <code>settings.json</code> 文件,首次运行自动生成,可以自定义部分运行参数。</p>
<p>建议自行设置 <code>cookie</code> 参数,若不设置该参数,程序功能可能无法正常使用!</p>
<table>
<thead>
<tr>
@ -141,8 +144,8 @@ async with XHS(work_path=work_path,
<tr>
<td align="center">cookie</td>
<td align="center">str</td>
<td align="center">小红书网页版 Cookie<b>无需登录,建议修改</b></td>
<td align="center">默认 Cookie</td>
<td align="center">小红书网页版 Cookie<b>无需登录</b></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">proxy</td>

View File

@ -15,6 +15,7 @@ from source.translator import (
)
from .index import Index
from .loading import Loading
from .monitor import Monitor
from .setting import Setting
from .update import Update
@ -84,3 +85,6 @@ class XHSDownloader(App):
async def action_check_update(self):
await self.push_screen(Update(self.APP, self.prompt), callback=self.update_result)
async def action_clipboard(self):
await self.push_screen(Monitor(self.APP, self.prompt))

View File

@ -18,9 +18,7 @@ from textual.widgets import RichLog
from source.application import XHS
from source.module import (
VERSION_MAJOR,
VERSION_MINOR,
VERSION_BETA,
PROJECT,
PROMPT,
MASTER,
ERROR,
@ -44,6 +42,7 @@ class Index(Screen):
Binding(key="u", action="check_update", description="检查更新/Update"),
Binding(key="m", action="user_script", description="获取脚本/Script"),
Binding(key="s", action="settings", description="程序设置/Settings"),
Binding(key="c", action="clipboard", description="监听链接/ClipBoard"),
]
def __init__(self, app: XHS, language: Chinese | English):
@ -82,8 +81,7 @@ class Index(Screen):
yield Footer()
def on_mount(self) -> None:
self.title = f"XHS-Downloader V{VERSION_MAJOR}.{
VERSION_MINOR}{" Beta" if VERSION_BETA else ""}"
self.title = PROJECT
self.url = self.query_one(Input)
self.tip = self.query_one(RichLog)
self.tip.write(Text("\n".join(self.prompt.disclaimer), style=MASTER))

62
source/TUI/monitor.py Normal file
View File

@ -0,0 +1,62 @@
from rich.text import Text
from textual import on
from textual import work
from textual.app import ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Button
from textual.widgets import Footer
from textual.widgets import Header
from textual.widgets import Label
from textual.widgets import RichLog
from source.application import XHS
from source.module import (
PROJECT,
MASTER,
INFO,
)
from source.translator import (
English,
Chinese,
)
__all__ = ["Monitor"]
class Monitor(Screen):
BINDINGS = [
Binding(key="q", action="quit", description="退出程序/Quit"),
Binding(key="c", action="close", description="关闭监听/Close"),
]
def __init__(self, app: XHS, language: Chinese | English):
super().__init__()
self.xhs = app
self.prompt = language
def compose(self) -> ComposeResult:
yield Header()
yield Label(Text(self.prompt.monitor_mode, style=INFO), id="monitor")
yield RichLog(markup=True, wrap=True)
yield Button(self.prompt.close_monitor, id="close")
yield Footer()
@on(Button.Pressed, "#close")
def close_button(self):
self.action_close()
@work()
async def run_monitor(self):
await self.xhs.monitor(download=True, log=self.query_one(RichLog))
self.action_close()
def on_mount(self) -> None:
self.title = PROJECT
self.query_one(RichLog).write(
Text(self.prompt.monitor_text, style=MASTER))
self.run_monitor()
def action_close(self):
self.xhs.stop_monitor()
self.app.pop_screen()

View File

@ -44,7 +44,7 @@ class Setting(Screen):
Input(self.data["user_agent"], placeholder=self.prompt.user_agent_placeholder, valid_empty=True,
id="user_agent", ),
Label(self.prompt.cookie, classes="params", ),
Input(self.data["cookie"], placeholder=self.prompt.cookie_placeholder, valid_empty=True, id="cookie", ),
Input(placeholder=self.__check_cookie(), valid_empty=True, id="cookie", ),
Label(self.prompt.proxy, classes="params", ),
Input(self.data["proxy"], placeholder=self.prompt.proxy_placeholder, valid_empty=True, id="proxy", ),
Label(self.prompt.timeout, classes="params", ),
@ -80,6 +80,11 @@ class Setting(Screen):
)
yield Footer()
def __check_cookie(self) -> str:
if self.data["cookie"]:
return self.prompt.cookie_placeholder_true
return self.prompt.cookie_placeholder_false
def on_mount(self) -> None:
self.title = self.prompt.settings_title
@ -89,7 +94,7 @@ class Setting(Screen):
"work_path": self.query_one("#work_path").value,
"folder_name": self.query_one("#folder_name").value,
"user_agent": self.query_one("#user_agent").value,
"cookie": self.query_one("#cookie").value,
"cookie": self.query_one("#cookie").value or self.data["cookie"],
"proxy": self.query_one("#proxy").value or None,
"timeout": int(self.query_one("#timeout").value),
"chunk": int(self.query_one("#chunk").value),

View File

@ -1,5 +1,13 @@
from asyncio import Event
from asyncio import Queue
from asyncio import QueueEmpty
from asyncio import gather
from asyncio import sleep
from contextlib import suppress
from re import compile
from pyperclip import paste
from source.expansion import Converter
from source.expansion import Namespace
from source.module import Manager
@ -73,6 +81,9 @@ class XHS:
self.explore = Explore()
self.convert = Converter()
self.download = Download(self.manager)
self.clipboard_cache: str = ""
self.queue = Queue()
self.event = Event()
def __extract_image(self, container: dict, data: Namespace):
container["下载地址"] = self.image.get_image_link(
@ -141,10 +152,32 @@ class XHS:
return Namespace(data)
def __naming_rules(self, data: dict) -> str:
"""下载文件默认使用 作品标题 或 作品 ID 作为文件名称,可修改此方法自定义文件名称格式"""
time_ = data["发布时间"].replace(":", ".")
author = self.manager.filter_name(data["作者昵称"]) or data["作者ID"]
title = self.manager.filter_name(data["作品标题"]) or data["作品ID"]
return f"{author}-{title}"
return f"{time_}_{author}_{title[:64]}"
async def monitor(self, delay=1, download=False, efficient=False, log=None, bar=None) -> None:
self.event.clear()
await gather(self.__push_link(delay), self.__receive_link(delay, download, efficient, log, bar))
async def __push_link(self, delay: int):
while not self.event.is_set():
if (t := paste()).lower() == "close":
self.stop_monitor()
elif t != self.clipboard_cache:
self.clipboard_cache = t
[await self.queue.put(i) for i in await self.__extract_links(t, None)]
await sleep(delay)
async def __receive_link(self, delay: int, *args, **kwargs):
while not self.event.is_set() or self.queue.qsize() > 0:
with suppress(QueueEmpty):
await self.__deal_extract(self.queue.get_nowait(), *args, **kwargs)
await sleep(delay)
def stop_monitor(self):
self.event.set()
@staticmethod
async def __suspend(efficient: bool) -> None:

View File

@ -42,7 +42,16 @@ class Download:
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]
tasks = [
self.__download(
url,
path,
name,
format_,
log,
bar) for url,
name,
format_ in tasks]
await gather(*tasks)
return path
@ -51,13 +60,23 @@ class Download:
path.mkdir(exist_ok=True)
return path
def __ready_download_video(self, urls: list[str], path: Path, name: str, log) -> list:
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:
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}"
@ -76,7 +95,7 @@ class Download:
suffix = self.__extract_type(
response.headers.get("Content-Type")) or format_
temp = self.temp.joinpath(name)
real = path.joinpath(name).with_suffix(f".{suffix}")
real = path.joinpath(f"{name}.{suffix}")
# self.__create_progress(
# bar, int(
# response.headers.get(

View File

@ -6,7 +6,7 @@ __all__ = ['Explore']
class Explore:
time_format = "%Y-%m-%d %H:%M:%S"
time_format = "%Y-%m-%d_%H:%M:%S"
explore_type = {"video": "视频", "normal": "图文"}
def run(self, data: Namespace) -> dict:

View File

@ -19,8 +19,8 @@ from .static import (
INFO,
USERSCRIPT,
USERAGENT,
COOKIE,
HEADERS,
PROJECT,
)
from .tools import (
retry,
@ -49,9 +49,9 @@ __all__ = [
"INFO",
"USERSCRIPT",
"USERAGENT",
"COOKIE",
"HEADERS",
"retry",
"logging",
"wait",
"PROJECT",
]

View File

@ -11,7 +11,6 @@ from aiohttp import ClientTimeout
from source.translator import Chinese
from source.translator import English
from .static import COOKIE
from .static import HEADERS
from .static import USERAGENT
@ -19,7 +18,7 @@ __all__ = ["Manager"]
class Manager:
NAME = compile(r"[^\u4e00-\u9fa5a-zA-Z0-9]")
NAME = compile(r"[^\u4e00-\u9fffa-zA-Z0-9“”《》]")
def __init__(
self,
@ -43,7 +42,7 @@ class Manager:
self.folder = self.__check_folder(folder)
self.blank_headers = HEADERS | {
"User-Agent": user_agent or USERAGENT, }
self.headers = self.blank_headers | {"Cookie": cookie or COOKIE}
self.headers = self.blank_headers | {"Cookie": cookie}
self.retry = retry
self.chunk = chunk
self.record_data = record_data

View File

@ -17,14 +17,16 @@ __all__ = [
"INFO",
"USERSCRIPT",
"USERAGENT",
"COOKIE",
"HEADERS",
"PROJECT",
]
VERSION_MAJOR = 1
VERSION_MINOR = 8
VERSION_BETA = True
ROOT = Path(__file__).resolve().parent.parent.parent
PROJECT = f"XHS-Downloader V{VERSION_MAJOR}.{
VERSION_MINOR}{" Beta" if VERSION_BETA else ""}"
REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader"
LICENCE = "GNU General Public License v3.0"
@ -49,14 +51,8 @@ HEADERS = {
"Upgrade-Insecure-Requests": "1",
}
USERAGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 "
"Safari/537.36 Edg/120.0.0.0")
COOKIE = (
"abRequestId=a1c55c3d-edcd-5753-938b-15d22a78cb8a; webBuild=3.23.2; "
"a1=18ceecc41c5d2gkprctahn1jayh458m5eoos9grxb50000267832; webId=79879aaf1b46fa2120dfba20d6155928; "
"websectiga=3fff3a6f9f07284b62c0f2ebf91a3b10193175c06e4f71492b60e056edcdebb2; "
"sec_poison_id=52bff38d-96eb-40b6-a46b-5e7cc86014e4; web_session=030037a2ae3713ec49882425e5224a3cbb4eef; "
"gid=yYSddSS2DKdyyYSddSS4ylkFS2fJkTUFS90xlCDIyV0vxM2842Y62j888JKWYqJ8iDD4KY2d; xsecappid=xhs-pc-web")
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 "
"Safari/537.36 Edg/121.0.0.0")
MASTER = "b #fff200"
PROMPT = "b turquoise2"

View File

@ -60,7 +60,8 @@ class Chinese:
work_path_placeholder: str = "程序根路径"
user_agent_placeholder: str = "默认 UA"
cookie_placeholder: str = "内置 Cookie建议自行设置"
cookie_placeholder_true: str = "小红书网页版 Cookie无需登录参数已设置"
cookie_placeholder_false: str = "小红书网页版 Cookie无需登录参数未设置"
proxy_placeholder: str = "无代理"
settings_title: str = "程序设置"
@ -69,6 +70,10 @@ class Chinese:
processing: str = "程序处理中..."
monitor_mode: str = "已启动监听剪贴板模式"
monitor_text: str = "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
close_monitor: str = "退出监听剪贴板模式"
@staticmethod
def request_error(url: str) -> str:
return f"网络异常,请求 {url} 失败!"

View File

@ -78,7 +78,8 @@ class English(Chinese):
work_path_placeholder: str = "Program root path"
user_agent_placeholder: str = "Default UA"
cookie_placeholder: str = "built-in cookie, it is recommended to set it manually"
cookie_placeholder_true: str = "Xiaohongshu web version cookie, no login required, parameters have been set"
cookie_placeholder_false: str = "Xiaohongshu web version cookie, no login required, parameters not set"
proxy_placeholder: str = "No proxy"
settings_title: str = "Settings"
@ -87,6 +88,13 @@ class English(Chinese):
processing: str = "Processing..."
monitor_mode: str = "Currently in monitoring clipboard mode"
monitor_text: str = (
"The program will automatically read and extract the link to Xiaohongshu's works from the "
"clipboard, and automatically download the corresponding work file. If you want to close it, "
"please click the close button or write the \"close\" text to the clipboard!")
close_monitor: str = "Exit monitoring clipboard mode"
@staticmethod
def request_error(url: str) -> str:
return f"Network error, failed to access {url}!"

View File

@ -1,4 +1,4 @@
ScrollableContainer, RichLog {
ScrollableContainer, RichLog, Monitor {
background: #2f3542;
}
Button {
@ -20,7 +20,7 @@ Button {
Button#deal, Button#paste, Button#save {
tint: #27ae60 60%;
}
Button#reset, Button#abandon {
Button#reset, Button#abandon, Button#close {
tint: #c0392b 60%;
}
Label {
@ -32,7 +32,7 @@ Label {
Label.params {
margin: 1 0 0 0;
}
Label#prompt {
Label#prompt, Label#monitor {
padding: 1;
}
Bar {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB