发布 1.8 版本
99
README.md
@ -25,7 +25,9 @@
|
||||
<li>✅ 持久化储存作品信息至文件</li>
|
||||
<li>✅ 作品文件储存至单独文件夹</li>
|
||||
<li>✅ 后台监听剪贴板下载作品</li>
|
||||
<li>✅ 记录已下载作品 ID</li>
|
||||
<li>☑️ 支持 API 调用功能</li>
|
||||
<li>☑️ 支持命令行参数下载作品文件</li>
|
||||
</ul>
|
||||
<ul><b>脚本功能</b>
|
||||
<li>✅ 下载小红书无水印作品文件</li>
|
||||
@ -38,11 +40,11 @@
|
||||
</ul>
|
||||
<h1>📸 程序截图</h1>
|
||||
<p><b>🎥 点击图片观看演示视频</b></p>
|
||||
<a href="https://www.bilibili.com/video/BV1nQ4y137it/"><img src="static/screenshot/程序运行截图CN1.png" alt=""></a>
|
||||
<a href="https://www.bilibili.com/video/BV1PJ4m1Y7Jt/"><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>
|
||||
<a href="https://www.bilibili.com/video/BV1PJ4m1Y7Jt/"><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>
|
||||
<a href="https://www.bilibili.com/video/BV1PJ4m1Y7Jt/"><img src="static/screenshot/程序运行截图CN3.png" alt=""></a>
|
||||
<h1>🔗 支持链接</h1>
|
||||
<ul>
|
||||
<li><code>https://www.xiaohongshu.com/explore/作品ID</code></li>
|
||||
@ -74,41 +76,44 @@
|
||||
<h1>💻 二次开发</h1>
|
||||
<p>如果有其他需求,可以根据 <code>main.py</code> 的注释提示进行代码调用或修改!</p>
|
||||
<pre>
|
||||
# 示例链接
|
||||
error_link = "https://github.com/JoeanAmier/XHS_Downloader"
|
||||
demo_link = "https://www.xiaohongshu.com/explore/xxxxxxxxxx"
|
||||
multiple_links = f"{demo_link} {demo_link} {demo_link}"
|
||||
# 实例对象
|
||||
work_path = "D:\\" # 作品数据/文件保存根路径,默认值:项目根路径
|
||||
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
|
||||
user_agent = "" # 请求头 User-Agent
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录
|
||||
proxy = None # 网络代理
|
||||
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
|
||||
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
|
||||
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
|
||||
record_data = False # 是否记录作品数据至文件
|
||||
image_format = "WEBP" # 图文作品文件下载格式,支持:PNG、WEBP
|
||||
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
|
||||
async with XHS() as xhs:
|
||||
pass # 使用默认参数
|
||||
async with XHS(work_path=work_path,
|
||||
folder_name=folder_name,
|
||||
user_agent=user_agent,
|
||||
cookie=cookie,
|
||||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
chunk=chunk,
|
||||
max_retry=max_retry,
|
||||
record_data=record_data,
|
||||
image_format=image_format,
|
||||
folder_mode=folder_mode,
|
||||
) as xhs: # 使用自定义参数
|
||||
download = True # 是否下载作品文件,默认值:False
|
||||
# 返回作品详细信息,包括下载地址
|
||||
print(await xhs.extract(error_link, download)) # 获取数据失败时返回空字典
|
||||
print(await xhs.extract(demo_link, download))
|
||||
print(await xhs.extract(multiple_links, download)) # 支持传入多个作品链接
|
||||
async def example():
|
||||
"""通过代码设置参数,适合二次开发"""
|
||||
# 示例链接
|
||||
error_link = "https://github.com/JoeanAmier/XHS_Downloader"
|
||||
demo_link = "https://www.xiaohongshu.com/explore/xxxxxxxxxx"
|
||||
multiple_links = f"{demo_link} {demo_link} {demo_link}"
|
||||
# 实例对象
|
||||
work_path = "D:\\" # 作品数据/文件保存根路径,默认值:项目根路径
|
||||
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
|
||||
user_agent = "" # 请求头 User-Agent,可选参数
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录,必需参数
|
||||
proxy = None # 网络代理
|
||||
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
|
||||
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
|
||||
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
|
||||
record_data = False # 是否记录作品数据至文件
|
||||
image_format = "WEBP" # 图文作品文件下载格式,支持:PNG、WEBP
|
||||
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
|
||||
async with XHS() as xhs:
|
||||
pass # 使用默认参数
|
||||
async with XHS(work_path=work_path,
|
||||
folder_name=folder_name,
|
||||
user_agent=user_agent,
|
||||
cookie=cookie,
|
||||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
chunk=chunk,
|
||||
max_retry=max_retry,
|
||||
record_data=record_data,
|
||||
image_format=image_format,
|
||||
folder_mode=folder_mode,
|
||||
) as xhs: # 使用自定义参数
|
||||
download = True # 是否下载作品文件,默认值:False
|
||||
efficient = True # 高效模式,禁用请求延时
|
||||
# 返回作品详细信息,包括下载地址
|
||||
print(await xhs.extract(error_link, download, efficient)) # 获取数据失败时返回空字典
|
||||
print(await xhs.extract(demo_link, download, efficient))
|
||||
print(await xhs.extract(multiple_links, download, efficient)) # 支持传入多个作品链接
|
||||
</pre>
|
||||
<h1>⚙️ 配置文件</h1>
|
||||
<p>项目根目录下的 <code>settings.json</code> 文件,首次运行自动生成,可以自定义部分运行参数。</p>
|
||||
@ -199,14 +204,19 @@ async with XHS(work_path=work_path,
|
||||
</table>
|
||||
<h1>🌐 Cookie</h1>
|
||||
<ol>
|
||||
<li>打开浏览器(可选无痕模式启动),访问小红书任意网页</li>
|
||||
<li>按 <code>F12</code> 打开开发人员工具</li>
|
||||
<li>选择 <code>控制台</code> 选项卡</li>
|
||||
<li>输入 <code>document.cookie</code> 后回车确认</li>
|
||||
<li>输出内容即为所需 Cookie</li>
|
||||
<li>打开浏览器(可选无痕模式启动),访问 <code>https://www.xiaohongshu.com/explore</code></li>
|
||||
<li>按下 <code>F12</code> 打开开发人员工具</li>
|
||||
<li>选择 <code>网络</code> 选项卡</li>
|
||||
<li>选择 <code>Fetch/XHR</code> 筛选器</li>
|
||||
<li>点击小红书页面任意作品</li>
|
||||
<li>在 <code>网络</code> 选项卡挑选包含 Cookie 的数据包</li>
|
||||
<li>检查 Cookie 是否包含 <code>web_session</code> 字段</li>
|
||||
<li>全选复制包含 <code>web_session</code> 字段的 Cookie</li>
|
||||
</ol>
|
||||
<br>
|
||||
<img src="static/screenshot/获取Cookie示意图.png" alt="">
|
||||
<h1>🗳 下载记录</h1>
|
||||
<p>XHS-Downloader 会将下载过的作品 ID 储存至数据库,当重复下载相同的作品时,XHS-Downloader 会自动跳过该作品的文件下载(即使作品文件不存在),如果想要重新下载作品文件,请先删除数据库中对应的作品 ID,再使用 XHS-Downloader 下载作品文件!</p>
|
||||
<h1>♥️ 支持项目</h1>
|
||||
<p>如果 <b>XHS-Downloader</b> 对您有帮助,请考虑为它点个 <b>Star</b> ⭐,感谢您的支持!</p>
|
||||
<table>
|
||||
@ -230,6 +240,9 @@ async with XHS(work_path=work_path,
|
||||
<li>Email: yonglelolu@gmail.com</li>
|
||||
</ul>
|
||||
<p>
|
||||
<b>如果您在使用 XHS-Downloader 的时候遇到问题,请先阅读<a href="https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md">《提问的智慧》</a>,然后加入 QQ 群聊寻求帮助!</b>
|
||||
</p>
|
||||
<p>
|
||||
<b>如果您通过 Email 联系我,我可能无法及时查看并回复信息,我会尽力在七天内回复您的邮件;如果有紧急事项或需要更快的回复,请通过其他方式与我联系,谢谢理解!</b>
|
||||
</p>
|
||||
<p><b>如果您对抖音 / TikTok 感兴趣,可以了解一下我的另一个开源项目 <a href="https://github.com/JoeanAmier/TikTokDownloader">TikTokDownloader</a></b></p>
|
||||
|
||||
11
main.py
@ -13,8 +13,8 @@ async def example():
|
||||
# 实例对象
|
||||
work_path = "D:\\" # 作品数据/文件保存根路径,默认值:项目根路径
|
||||
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
|
||||
user_agent = "" # 请求头 User-Agent
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录
|
||||
user_agent = "" # 请求头 User-Agent,可选参数
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录,必需参数
|
||||
proxy = None # 网络代理
|
||||
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
|
||||
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
|
||||
@ -37,10 +37,11 @@ async def example():
|
||||
folder_mode=folder_mode,
|
||||
) as xhs: # 使用自定义参数
|
||||
download = True # 是否下载作品文件,默认值:False
|
||||
efficient = True # 高效模式,禁用请求延时
|
||||
# 返回作品详细信息,包括下载地址
|
||||
print(await xhs.extract(error_link, download)) # 获取数据失败时返回空字典
|
||||
print(await xhs.extract(demo_link, download))
|
||||
print(await xhs.extract(multiple_links, download)) # 支持传入多个作品链接
|
||||
print(await xhs.extract(error_link, download, efficient)) # 获取数据失败时返回空字典
|
||||
print(await xhs.extract(demo_link, download, efficient))
|
||||
print(await xhs.extract(multiple_links, download, efficient)) # 支持传入多个作品链接
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
@ -3,4 +3,4 @@ textual>=0.47.1
|
||||
pyperclip>=1.8.2
|
||||
lxml>=5.1.0
|
||||
PyYAML>=6.0.1
|
||||
aiosqlite>=0.19.0
|
||||
aiosqlite>=0.20.0
|
||||
|
||||
45
source/TUI/about.py
Normal file
@ -0,0 +1,45 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Footer
|
||||
from textual.widgets import Header
|
||||
from textual.widgets import Label
|
||||
|
||||
from source.module import (
|
||||
PROJECT,
|
||||
)
|
||||
from source.translator import (
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
|
||||
__all__ = ["About"]
|
||||
|
||||
|
||||
class About(Screen):
|
||||
BINDINGS = [
|
||||
Binding(
|
||||
key="q",
|
||||
action="quit",
|
||||
description="退出程序/Quit"),
|
||||
Binding(
|
||||
key="u",
|
||||
action="check_update_about",
|
||||
description="检查更新/Update"),
|
||||
Binding(
|
||||
key="b",
|
||||
action="index",
|
||||
description="返回首页/Back"),
|
||||
]
|
||||
|
||||
def __init__(self, language: Chinese | English):
|
||||
super().__init__()
|
||||
self.prompt = language
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label()
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = PROJECT
|
||||
@ -13,9 +13,11 @@ from source.translator import (
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
# from .about import About
|
||||
from .index import Index
|
||||
from .loading import Loading
|
||||
from .monitor import Monitor
|
||||
from .record import Record
|
||||
from .setting import Setting
|
||||
from .update import Update
|
||||
|
||||
@ -53,6 +55,8 @@ class XHSDownloader(App):
|
||||
name="setting")
|
||||
self.install_screen(Index(self.APP, self.prompt), name="index")
|
||||
self.install_screen(Loading(self.prompt), name="loading")
|
||||
# self.install_screen(About(self.prompt), name="about")
|
||||
self.install_screen(Record(self.APP, self.prompt), name="record")
|
||||
await self.push_screen("index")
|
||||
|
||||
async def action_settings(self):
|
||||
@ -62,15 +66,26 @@ class XHSDownloader(App):
|
||||
|
||||
await self.push_screen("setting", save_settings)
|
||||
|
||||
async def action_about(self):
|
||||
await self.push_screen("about")
|
||||
|
||||
async def action_index(self):
|
||||
await self.push_screen("index")
|
||||
|
||||
async def action_record(self):
|
||||
await self.push_screen("record")
|
||||
|
||||
async def refresh_screen(self):
|
||||
self.pop_screen()
|
||||
await self.APP.recorder.database.close()
|
||||
await self.APP.close()
|
||||
self.__initialization()
|
||||
await self.__aenter__()
|
||||
self.uninstall_screen("index")
|
||||
self.uninstall_screen("setting")
|
||||
self.uninstall_screen("loading")
|
||||
# self.uninstall_screen("about")
|
||||
self.uninstall_screen("record")
|
||||
self.install_screen(Index(self.APP, self.prompt), name="index")
|
||||
self.install_screen(
|
||||
Setting(
|
||||
@ -78,13 +93,21 @@ class XHSDownloader(App):
|
||||
self.prompt),
|
||||
name="setting")
|
||||
self.install_screen(Loading(self.prompt), name="loading")
|
||||
# self.install_screen(About(self.prompt), name="about")
|
||||
self.install_screen(Record(self.APP, self.prompt), name="record")
|
||||
await self.push_screen("index")
|
||||
|
||||
def update_result(self, tip: str) -> None:
|
||||
self.query_one(RichLog).write(tip)
|
||||
log = self.query_one(RichLog)
|
||||
log.write(tip)
|
||||
log.write(">" * 50)
|
||||
|
||||
async def action_check_update(self):
|
||||
await self.push_screen(Update(self.APP, self.prompt), callback=self.update_result)
|
||||
|
||||
async def action_clipboard(self):
|
||||
async def action_check_update_about(self):
|
||||
await self.push_screen("index")
|
||||
await self.action_check_update()
|
||||
|
||||
async def action_monitor(self):
|
||||
await self.push_screen(Monitor(self.APP, self.prompt))
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
from asyncio import create_task
|
||||
from webbrowser import open
|
||||
|
||||
from pyperclip import paste
|
||||
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.containers import HorizontalScroll
|
||||
@ -26,7 +24,6 @@ from source.module import (
|
||||
LICENCE,
|
||||
REPOSITORY,
|
||||
GENERAL,
|
||||
USERSCRIPT,
|
||||
)
|
||||
from source.translator import (
|
||||
English,
|
||||
@ -40,9 +37,10 @@ class Index(Screen):
|
||||
BINDINGS = [
|
||||
Binding(key="q", action="quit", description="退出程序/Quit"),
|
||||
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"),
|
||||
Binding(key="r", action="record", description="下载记录/Record"),
|
||||
Binding(key="m", action="monitor", description="开启监听/Monitor"),
|
||||
# Binding(key="a", action="about", description="关于项目/About"),
|
||||
]
|
||||
|
||||
def __init__(self, app: XHS, language: Chinese | English):
|
||||
@ -68,7 +66,7 @@ class Index(Screen):
|
||||
Label(
|
||||
Text(
|
||||
self.prompt.input_box_title,
|
||||
style=PROMPT), id="prompt",
|
||||
style=PROMPT), classes="prompt",
|
||||
),
|
||||
Input(placeholder=self.prompt.input_prompt),
|
||||
HorizontalScroll(
|
||||
@ -89,7 +87,7 @@ class Index(Screen):
|
||||
@on(Button.Pressed, "#deal")
|
||||
async def deal_button(self):
|
||||
if self.url.value:
|
||||
await create_task(self.deal())
|
||||
self.deal()
|
||||
else:
|
||||
self.tip.write(Text(self.prompt.invalid_link, style=WARNING))
|
||||
self.tip.write(Text(">" * 50, style=GENERAL))
|
||||
@ -102,6 +100,7 @@ class Index(Screen):
|
||||
def paste_button(self):
|
||||
self.query_one(Input).value = paste()
|
||||
|
||||
@work()
|
||||
async def deal(self):
|
||||
await self.app.push_screen("loading")
|
||||
if any(await self.xhs.extract(self.url.value, True, log=self.tip)):
|
||||
@ -109,7 +108,3 @@ class Index(Screen):
|
||||
else:
|
||||
self.tip.write(Text(self.prompt.download_failure, style=ERROR))
|
||||
self.app.pop_screen()
|
||||
|
||||
@staticmethod
|
||||
def action_user_script():
|
||||
open(USERSCRIPT)
|
||||
|
||||
@ -37,7 +37,7 @@ class Monitor(Screen):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label(Text(self.prompt.monitor_mode, style=INFO), id="monitor")
|
||||
yield Label(Text(self.prompt.monitor_mode, style=INFO), classes="prompt")
|
||||
yield RichLog(markup=True, wrap=True)
|
||||
yield Button(self.prompt.close_monitor, id="close")
|
||||
yield Footer()
|
||||
|
||||
46
source/TUI/record.py
Normal file
@ -0,0 +1,46 @@
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid
|
||||
from textual.containers import HorizontalScroll
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button
|
||||
from textual.widgets import Input
|
||||
from textual.widgets import Label
|
||||
|
||||
from source.application import XHS
|
||||
from source.translator import (
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
|
||||
__all__ = ["Record"]
|
||||
|
||||
|
||||
class Record(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.record_title, classes="prompt"),
|
||||
Input(placeholder=self.prompt.record_placeholder, id="id", ),
|
||||
HorizontalScroll(
|
||||
Button(self.prompt.record_enter_button, id="enter", ),
|
||||
Button(self.prompt.record_close_button, id="close"), ),
|
||||
id="record",
|
||||
)
|
||||
|
||||
async def delete(self, text: str):
|
||||
await self.xhs.recorder.delete_many(text.split())
|
||||
|
||||
@on(Button.Pressed, "#enter")
|
||||
async def save_settings(self):
|
||||
text = self.query_one(Input)
|
||||
await self.delete(text.value)
|
||||
text.value = ""
|
||||
|
||||
@on(Button.Pressed, "#close")
|
||||
def reset(self):
|
||||
self.dismiss()
|
||||
@ -10,6 +10,7 @@ from pyperclip import paste
|
||||
|
||||
from source.expansion import Converter
|
||||
from source.expansion import Namespace
|
||||
from source.module import IDRecorder
|
||||
from source.module import Manager
|
||||
from source.module import (
|
||||
ROOT,
|
||||
@ -81,6 +82,7 @@ class XHS:
|
||||
self.explore = Explore()
|
||||
self.convert = Converter()
|
||||
self.download = Download(self.manager)
|
||||
self.recorder = IDRecorder(self.manager)
|
||||
self.clipboard_cache: str = ""
|
||||
self.queue = Queue()
|
||||
self.event = Event()
|
||||
@ -96,11 +98,19 @@ class XHS:
|
||||
name = self.__naming_rules(container)
|
||||
path = self.manager.folder
|
||||
if (u := container["下载地址"]) and download:
|
||||
path = await self.download.run(u, name, container["作品类型"], log, bar)
|
||||
if await self.skip_download(i := container["作品ID"]):
|
||||
logging(log, self.prompt.exist_record(i))
|
||||
else:
|
||||
path, result = await self.download.run(u, name, container["作品类型"], log, bar)
|
||||
await self.__add_record(i, result)
|
||||
elif not u:
|
||||
logging(log, self.prompt.download_link_error, ERROR)
|
||||
self.manager.save_data(path, name, container)
|
||||
|
||||
async def __add_record(self, id_: str, result: tuple) -> None:
|
||||
if all(result):
|
||||
await self.recorder.add(id_)
|
||||
|
||||
async def extract(self, url: str, download=False, efficient=False, log=None, bar=None) -> list[dict]:
|
||||
# return # 调试代码
|
||||
urls = await self.__extract_links(url, log)
|
||||
@ -179,6 +189,9 @@ class XHS:
|
||||
def stop_monitor(self):
|
||||
self.event.set()
|
||||
|
||||
async def skip_download(self, id_: str) -> bool:
|
||||
return bool(await self.recorder.select(id_))
|
||||
|
||||
@staticmethod
|
||||
async def __suspend(efficient: bool) -> None:
|
||||
if efficient:
|
||||
@ -186,9 +199,11 @@ class XHS:
|
||||
await wait()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.recorder.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.recorder.__aexit__(exc_type, exc_value, traceback)
|
||||
await self.close()
|
||||
|
||||
async def close(self):
|
||||
|
||||
@ -33,7 +33,7 @@ class Download:
|
||||
self.video_format = "mp4"
|
||||
self.image_format = manager.image_format
|
||||
|
||||
async def run(self, urls: list, name: str, type_: str, log, bar) -> Path:
|
||||
async def run(self, urls: list, name: str, type_: str, log, bar) -> tuple[Path, tuple]:
|
||||
path = self.__generate_path(name)
|
||||
match type_:
|
||||
case "视频":
|
||||
@ -52,8 +52,8 @@ class Download:
|
||||
bar) for url,
|
||||
name,
|
||||
format_ in tasks]
|
||||
await gather(*tasks)
|
||||
return path
|
||||
result = await gather(*tasks)
|
||||
return path, result
|
||||
|
||||
def __generate_path(self, name: str):
|
||||
path = self.manager.archive(self.folder, name, self.folder_mode)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from .extend import Account
|
||||
from .manager import Manager
|
||||
from .recorder import Recorder
|
||||
from .recorder import IDRecorder
|
||||
from .settings import Settings
|
||||
from .static import (
|
||||
VERSION_MAJOR,
|
||||
@ -31,7 +31,7 @@ from .tools import (
|
||||
__all__ = [
|
||||
"Account",
|
||||
"Settings",
|
||||
"Recorder",
|
||||
"IDRecorder",
|
||||
"Manager",
|
||||
"VERSION_MAJOR",
|
||||
"VERSION_MINOR",
|
||||
|
||||
@ -1,5 +1,46 @@
|
||||
__all__ = ["Recorder"]
|
||||
from aiosqlite import connect
|
||||
|
||||
from source.module import Manager
|
||||
|
||||
__all__ = ["IDRecorder"]
|
||||
|
||||
|
||||
class Recorder:
|
||||
pass
|
||||
class IDRecorder:
|
||||
def __init__(self, manager: Manager):
|
||||
self.file = manager.root.joinpath("XHS-Downloader.db")
|
||||
self.database = None
|
||||
self.cursor = None
|
||||
|
||||
async def __connect_database(self):
|
||||
self.database = await connect(self.file)
|
||||
self.cursor = await self.database.cursor()
|
||||
await self.cursor.execute("CREATE TABLE IF NOT EXISTS explore_ids (ID TEXT PRIMARY KEY);")
|
||||
await self.database.commit()
|
||||
|
||||
async def select(self, id_: str):
|
||||
await self.cursor.execute("SELECT ID FROM explore_ids WHERE ID=?", (id_,))
|
||||
return await self.cursor.fetchone()
|
||||
|
||||
async def add(self, id_: str) -> None:
|
||||
await self.cursor.execute("REPLACE INTO explore_ids VALUES (?);", (id_,))
|
||||
await self.database.commit()
|
||||
|
||||
async def delete(self, id_: str) -> None:
|
||||
if id_:
|
||||
await self.cursor.execute("DELETE FROM explore_ids WHERE ID=?", (id_,))
|
||||
await self.database.commit()
|
||||
|
||||
async def delete_many(self, ids: list | tuple):
|
||||
[await self.delete(i) for i in ids]
|
||||
|
||||
async def all(self):
|
||||
await self.cursor.execute("SELECT ID FROM explore_ids")
|
||||
return [i[0] for i in await self.cursor.fetchmany()]
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.__connect_database()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.cursor.close()
|
||||
await self.database.close()
|
||||
|
||||
@ -23,7 +23,7 @@ __all__ = [
|
||||
|
||||
VERSION_MAJOR = 1
|
||||
VERSION_MINOR = 8
|
||||
VERSION_BETA = True
|
||||
VERSION_BETA = False
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
PROJECT = f"XHS-Downloader V{VERSION_MAJOR}.{
|
||||
VERSION_MINOR}{" Beta" if VERSION_BETA else ""}"
|
||||
|
||||
@ -74,13 +74,18 @@ class Chinese:
|
||||
monitor_text: str = "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
|
||||
close_monitor: str = "退出监听剪贴板模式"
|
||||
|
||||
record_title: str = "请输入待删除的小红书作品链接或作品 ID:"
|
||||
record_placeholder: str = "支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"
|
||||
record_enter_button: str = "删除指定作品 ID"
|
||||
record_close_button: str = "返回"
|
||||
|
||||
@staticmethod
|
||||
def request_error(url: str) -> str:
|
||||
return f"网络异常,请求 {url} 失败!"
|
||||
|
||||
@staticmethod
|
||||
def skip_download(name: str) -> str:
|
||||
return f"{name} 已存在,跳过下载!"
|
||||
return f"{name} 文件已存在,跳过下载!"
|
||||
|
||||
@staticmethod
|
||||
def download_success(name: str) -> str:
|
||||
@ -113,3 +118,7 @@ class Chinese:
|
||||
@staticmethod
|
||||
def official_version_update(major: int, minor: int) -> str:
|
||||
return f"检测到新版本:{major}.{minor}"
|
||||
|
||||
@staticmethod
|
||||
def exist_record(id_: str) -> str:
|
||||
return f"作品 {id_} 存在下载记录,跳过下载!"
|
||||
|
||||
@ -95,6 +95,13 @@ class English(Chinese):
|
||||
"please click the close button or write the \"close\" text to the clipboard!")
|
||||
close_monitor: str = "Exit monitoring clipboard mode"
|
||||
|
||||
record_title: str = "Please enter the link or ID of the Xiaohongshu work to be deleted:"
|
||||
record_placeholder: str = (
|
||||
"Support input of works ID or links containing work ID, with multiple links or IDs "
|
||||
"separated by spaces")
|
||||
record_enter_button: str = "Delete specified works ID"
|
||||
record_close_button: str = "return"
|
||||
|
||||
@staticmethod
|
||||
def request_error(url: str) -> str:
|
||||
return f"Network error, failed to access {url}!"
|
||||
@ -134,3 +141,7 @@ class English(Chinese):
|
||||
@staticmethod
|
||||
def official_version_update(major: int, minor: int) -> str:
|
||||
return f"New version detected: {major}.{minor}"
|
||||
|
||||
@staticmethod
|
||||
def exist_record(id_: str) -> str:
|
||||
return f"works {id_} has a download record, skipping download!"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
ScrollableContainer, RichLog, Monitor {
|
||||
ScrollableContainer, RichLog, Monitor, About {
|
||||
background: #2f3542;
|
||||
}
|
||||
Button {
|
||||
@ -17,7 +17,7 @@ Button {
|
||||
.horizontal-layout > * {
|
||||
width: 25vw;
|
||||
}
|
||||
Button#deal, Button#paste, Button#save {
|
||||
Button#deal, Button#paste, Button#save, Button#enter {
|
||||
tint: #27ae60 60%;
|
||||
}
|
||||
Button#reset, Button#abandon, Button#close {
|
||||
@ -32,7 +32,7 @@ Label {
|
||||
Label.params {
|
||||
margin: 1 0 0 0;
|
||||
}
|
||||
Label#prompt, Label#monitor {
|
||||
Label.prompt {
|
||||
padding: 1;
|
||||
}
|
||||
Bar {
|
||||
@ -52,6 +52,13 @@ Bar > .bar--complete {
|
||||
background: #353b48;
|
||||
border: double #747d8c;
|
||||
}
|
||||
#record {
|
||||
grid-size: 1 3;
|
||||
width: 80vw;
|
||||
height: 12;
|
||||
background: #353b48;
|
||||
border: double #747d8c;
|
||||
}
|
||||
ModalScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 75 KiB |