mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2026-03-22 06:57:16 +08:00
fix: 修复项目功能异常
1. 更新 Cookie 参数处理 2. 优化作品数据返回格式 3. 更新用户脚本链接提取功能 4. 优化项目运行信息提示 5. 修复 record_data 参数无效的问题 6. 新增作品下载记录开关 7. 适配小红书平台规则 8. 默认开启局域网访问 9. 格式化项目代码 10. 更新英语翻译 Closes #127 Closes #128 Closes #130 Closes #132
This commit is contained in:
Binary file not shown.
@@ -85,7 +85,7 @@ msgstr "Xiaohongshu Web Cookie"
|
|||||||
msgid "网络代理"
|
msgid "网络代理"
|
||||||
msgstr "Network proxy"
|
msgstr "Network proxy"
|
||||||
|
|
||||||
msgid "记录作品数据"
|
msgid "记录作品详细数据"
|
||||||
msgstr "Record works data"
|
msgstr "Record works data"
|
||||||
|
|
||||||
msgid "图片下载格式"
|
msgid "图片下载格式"
|
||||||
@@ -291,3 +291,6 @@ msgstr "Agent {0} test failed: {1}"
|
|||||||
|
|
||||||
msgid "浏览器名称或序号输入错误!"
|
msgid "浏览器名称或序号输入错误!"
|
||||||
msgstr "Browser name or serial number input error!"
|
msgstr "Browser name or serial number input error!"
|
||||||
|
|
||||||
|
msgid "作品下载记录开关"
|
||||||
|
msgstr "Works download record switch"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ msgstr ""
|
|||||||
msgid "网络代理"
|
msgid "网络代理"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "记录作品数据"
|
msgid "记录作品详细数据"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "图片下载格式"
|
msgid "图片下载格式"
|
||||||
@@ -291,3 +291,6 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "浏览器名称或序号输入错误!"
|
msgid "浏览器名称或序号输入错误!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "作品下载记录开关"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Setting(Screen):
|
|||||||
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ),
|
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ),
|
||||||
Label(),
|
Label(),
|
||||||
Container(
|
Container(
|
||||||
Checkbox(self.message("记录作品数据"), id="record_data", value=self.data["record_data"], ),
|
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="folder_mode", value=self.data["folder_mode"], ),
|
||||||
Checkbox(self.message("视频作品下载开关"), id="video_download", value=self.data["video_download"], ),
|
Checkbox(self.message("视频作品下载开关"), id="video_download", value=self.data["video_download"], ),
|
||||||
Checkbox(self.message("图文作品下载开关"), id="image_download", value=self.data["image_download"], ),
|
Checkbox(self.message("图文作品下载开关"), id="image_download", value=self.data["image_download"], ),
|
||||||
@@ -69,6 +69,7 @@ class Setting(Screen):
|
|||||||
Label(),
|
Label(),
|
||||||
Container(
|
Container(
|
||||||
Checkbox(self.message("动图文件下载开关"), id="live_download", value=self.data["live_download"], ),
|
Checkbox(self.message("动图文件下载开关"), id="live_download", value=self.data["live_download"], ),
|
||||||
|
Checkbox(self.message("作品下载记录开关"), id="download_record", value=self.data["download_record"], ),
|
||||||
classes="horizontal-layout"),
|
classes="horizontal-layout"),
|
||||||
Container(
|
Container(
|
||||||
Label(self.message("图片下载格式"), classes="params", ),
|
Label(self.message("图片下载格式"), classes="params", ),
|
||||||
@@ -123,6 +124,7 @@ class Setting(Screen):
|
|||||||
"image_download": self.query_one("#image_download").value,
|
"image_download": self.query_one("#image_download").value,
|
||||||
"video_download": self.query_one("#video_download").value,
|
"video_download": self.query_one("#video_download").value,
|
||||||
"live_download": self.query_one("#live_download").value,
|
"live_download": self.query_one("#live_download").value,
|
||||||
|
"download_record": self.query_one("#download_record").value,
|
||||||
# "server": False,
|
# "server": False,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ from textual.widgets import LoadingIndicator
|
|||||||
|
|
||||||
from source.application import XHS
|
from source.application import XHS
|
||||||
from source.module import (
|
from source.module import (
|
||||||
VERSION_MAJOR,
|
|
||||||
VERSION_MINOR,
|
|
||||||
VERSION_BETA,
|
|
||||||
ERROR,
|
ERROR,
|
||||||
WARNING,
|
WARNING,
|
||||||
INFO,
|
INFO,
|
||||||
@@ -40,10 +37,10 @@ class Update(ModalScreen):
|
|||||||
try:
|
try:
|
||||||
url = await self.xhs.html.request_url(RELEASES, False, None, timeout=5, )
|
url = await self.xhs.html.request_url(RELEASES, False, None, timeout=5, )
|
||||||
version = url.split("/")[-1]
|
version = url.split("/")[-1]
|
||||||
match self.compare_versions(f"{VERSION_MAJOR}.{VERSION_MINOR}", version, VERSION_BETA):
|
match self.compare_versions(f"{XHS.VERSION_MAJOR}.{XHS.VERSION_MINOR}", version, XHS.VERSION_BETA):
|
||||||
case 4:
|
case 4:
|
||||||
tip = Text(f"{self.message("检测到新版本:{0}.{1}").format(
|
tip = Text(f"{self.message("检测到新版本:{0}.{1}").format(
|
||||||
VERSION_MAJOR, VERSION_MINOR)}\n{RELEASES}", style=WARNING)
|
XHS.VERSION_MAJOR, XHS.VERSION_MINOR)}\n{RELEASES}", style=WARNING)
|
||||||
case 3:
|
case 3:
|
||||||
tip = Text(
|
tip = Text(
|
||||||
f"{self.message("当前版本为开发版, 可更新至正式版")}\n{RELEASES}",
|
f"{self.message("当前版本为开发版, 可更新至正式版")}\n{RELEASES}",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from source.module import (
|
|||||||
REPOSITORY,
|
REPOSITORY,
|
||||||
VERSION_MAJOR,
|
VERSION_MAJOR,
|
||||||
VERSION_MINOR,
|
VERSION_MINOR,
|
||||||
|
VERSION_BETA,
|
||||||
)
|
)
|
||||||
from source.module import Translate
|
from source.module import Translate
|
||||||
from source.module import logging
|
from source.module import logging
|
||||||
@@ -44,10 +45,25 @@ from .video import Video
|
|||||||
__all__ = ["XHS"]
|
__all__ = ["XHS"]
|
||||||
|
|
||||||
|
|
||||||
|
def _data_cache(function):
|
||||||
|
async def inner(self, data: dict, ):
|
||||||
|
if self.manager.record_data:
|
||||||
|
download = data["下载地址"]
|
||||||
|
lives = data["动图地址"]
|
||||||
|
await function(self, data, )
|
||||||
|
data["下载地址"] = download
|
||||||
|
data["动图地址"] = lives
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
class XHS:
|
class XHS:
|
||||||
LINK = compile(r"https?://www\.xiaohongshu\.com/explore/[a-z0-9]+")
|
VERSION_MAJOR = VERSION_MAJOR
|
||||||
SHARE = compile(r"https?://www\.xiaohongshu\.com/discovery/item/[a-z0-9]+")
|
VERSION_MINOR = VERSION_MINOR
|
||||||
SHORT = compile(r"https?://xhslink\.com/[A-Za-z0-9]+")
|
VERSION_BETA = VERSION_BETA
|
||||||
|
LINK = compile(r"https?://www\.xiaohongshu\.com/explore/\S+")
|
||||||
|
SHARE = compile(r"https?://www\.xiaohongshu\.com/discovery/item/\S+")
|
||||||
|
SHORT = compile(r"https?://xhslink\.com/\S+")
|
||||||
__INSTANCE = None
|
__INSTANCE = None
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
@@ -74,6 +90,7 @@ class XHS:
|
|||||||
video_download=True,
|
video_download=True,
|
||||||
live_download=False,
|
live_download=False,
|
||||||
folder_mode=False,
|
folder_mode=False,
|
||||||
|
download_record=True,
|
||||||
language="zh_CN",
|
language="zh_CN",
|
||||||
# server=False,
|
# server=False,
|
||||||
transition: Callable[[str], str] = None,
|
transition: Callable[[str], str] = None,
|
||||||
@@ -101,6 +118,7 @@ class XHS:
|
|||||||
image_download,
|
image_download,
|
||||||
video_download,
|
video_download,
|
||||||
live_download,
|
live_download,
|
||||||
|
download_record,
|
||||||
folder_mode,
|
folder_mode,
|
||||||
# server,
|
# server,
|
||||||
self.message,
|
self.message,
|
||||||
@@ -127,7 +145,7 @@ class XHS:
|
|||||||
|
|
||||||
def __extract_video(self, container: dict, data: Namespace):
|
def __extract_video(self, container: dict, data: Namespace):
|
||||||
container["下载地址"] = self.video.get_video_link(data)
|
container["下载地址"] = self.video.get_video_link(data)
|
||||||
container["动图地址"] = ""
|
container["动图地址"] = [None, ]
|
||||||
|
|
||||||
async def __download_files(self, container: dict, download: bool, index, log, bar):
|
async def __download_files(self, container: dict, download: bool, index, log, bar):
|
||||||
name = self.__naming_rules(container)
|
name = self.__naming_rules(container)
|
||||||
@@ -136,13 +154,21 @@ class XHS:
|
|||||||
logging(
|
logging(
|
||||||
log, self.message("作品 {0} 存在下载记录,跳过下载").format(i))
|
log, self.message("作品 {0} 存在下载记录,跳过下载").format(i))
|
||||||
else:
|
else:
|
||||||
path, result = await self.download.run(u, container["动图地址"], index, name, container["作品类型"],
|
path, result = await self.download.run(
|
||||||
log, bar)
|
u,
|
||||||
|
container["动图地址"],
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
container["作品类型"],
|
||||||
|
log,
|
||||||
|
bar,
|
||||||
|
)
|
||||||
await self.__add_record(i, result)
|
await self.__add_record(i, result)
|
||||||
elif not u:
|
elif not u:
|
||||||
logging(log, self.message("提取作品文件下载地址失败"), ERROR)
|
logging(log, self.message("提取作品文件下载地址失败"), ERROR)
|
||||||
await self.save_data(container)
|
await self.save_data(container)
|
||||||
|
|
||||||
|
@_data_cache
|
||||||
async def save_data(self, data: dict, ):
|
async def save_data(self, data: dict, ):
|
||||||
data["采集时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
data["采集时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
data["下载地址"] = " ".join(data["下载地址"])
|
data["下载地址"] = " ".join(data["下载地址"])
|
||||||
@@ -196,20 +222,20 @@ class XHS:
|
|||||||
return urls
|
return urls
|
||||||
|
|
||||||
async def __deal_extract(self, url: str, download: bool, index: list | tuple | None, log, bar, data: bool, ):
|
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)):
|
if await self.skip_download(i := self.__extract_link_id(url)) and not data:
|
||||||
msg = self.message("作品 {0} 存在下载记录,跳过处理").format(i)
|
msg = self.message("作品 {0} 存在下载记录,跳过处理").format(i)
|
||||||
logging(log, msg)
|
logging(log, msg)
|
||||||
return {"message": msg}
|
return {"message": msg}
|
||||||
logging(log, self.message("开始处理作品:{0}").format(url))
|
logging(log, self.message("开始处理作品:{0}").format(i))
|
||||||
html = await self.html.request_url(url, log=log)
|
html = await self.html.request_url(url, log=log)
|
||||||
namespace = self.__generate_data_object(html)
|
namespace = self.__generate_data_object(html)
|
||||||
if not namespace:
|
if not namespace:
|
||||||
logging(log, self.message("{0} 获取数据失败").format(url), ERROR)
|
logging(log, self.message("{0} 获取数据失败").format(i), ERROR)
|
||||||
return {}
|
return {}
|
||||||
data = self.explore.run(namespace)
|
data = self.explore.run(namespace)
|
||||||
# logging(log, data) # 调试代码
|
# logging(log, data) # 调试代码
|
||||||
if not data:
|
if not data:
|
||||||
logging(log, self.message("{0} 提取数据失败").format(url), ERROR)
|
logging(log, self.message("{0} 提取数据失败").format(i), ERROR)
|
||||||
return {}
|
return {}
|
||||||
match data["作品类型"]:
|
match data["作品类型"]:
|
||||||
case "视频":
|
case "视频":
|
||||||
@@ -219,7 +245,7 @@ class XHS:
|
|||||||
case _:
|
case _:
|
||||||
data["下载地址"] = []
|
data["下载地址"] = []
|
||||||
await self.__download_files(data, download, index, log, bar)
|
await self.__download_files(data, download, index, log, bar)
|
||||||
logging(log, self.message("作品处理完成:{0}").format(url))
|
logging(log, self.message("作品处理完成:{0}").format(i))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -352,7 +378,7 @@ class XHS:
|
|||||||
# await self.runner.cleanup()
|
# await self.runner.cleanup()
|
||||||
# logging(log, self.message("Web API 服务器已关闭!"))
|
# logging(log, self.message("Web API 服务器已关闭!"))
|
||||||
|
|
||||||
async def run_server(self, host="127.0.0.1", port=8000, log_level="info", ):
|
async def run_server(self, host="0.0.0.0", port=8000, log_level="info", ):
|
||||||
self.server = FastAPI(
|
self.server = FastAPI(
|
||||||
title="XHS-Downloader",
|
title="XHS-Downloader",
|
||||||
version=f"{VERSION_MAJOR}.{VERSION_MINOR}")
|
version=f"{VERSION_MAJOR}.{VERSION_MINOR}")
|
||||||
|
|||||||
@@ -37,8 +37,16 @@ class Download:
|
|||||||
self.video_download = manager.video_download
|
self.video_download = manager.video_download
|
||||||
self.live_download = manager.live_download
|
self.live_download = manager.live_download
|
||||||
|
|
||||||
async def run(self, urls: list, lives: list, index: list | tuple | None, name: str, type_: str, log, bar) -> tuple[
|
async def run(
|
||||||
Path, tuple]:
|
self,
|
||||||
|
urls: list,
|
||||||
|
lives: list,
|
||||||
|
index: list | tuple | None,
|
||||||
|
name: str,
|
||||||
|
type_: str,
|
||||||
|
log,
|
||||||
|
bar,
|
||||||
|
) -> tuple[Path, tuple]:
|
||||||
path = self.__generate_path(name)
|
path = self.__generate_path(name)
|
||||||
match type_:
|
match type_:
|
||||||
case "视频":
|
case "视频":
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ class Explore:
|
|||||||
|
|
||||||
def __extract_info(self, container: dict, data: Namespace):
|
def __extract_info(self, container: dict, data: Namespace):
|
||||||
container["作品ID"] = data.safe_extract("noteId")
|
container["作品ID"] = data.safe_extract("noteId")
|
||||||
container["作品链接"] = f"https://www.xiaohongshu.com/explore/{
|
container["作品链接"] = f"https://www.xiaohongshu.com/explore/{container["作品ID"]}"
|
||||||
container["作品ID"]}"
|
|
||||||
container["作品标题"] = data.safe_extract("title")
|
container["作品标题"] = data.safe_extract("title")
|
||||||
container["作品描述"] = data.safe_extract("desc")
|
container["作品描述"] = data.safe_extract("desc")
|
||||||
container["作品类型"] = self.explore_type.get(
|
container["作品类型"] = self.explore_type.get(
|
||||||
|
|||||||
@@ -37,9 +37,12 @@ class Image:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_live_link(items: list) -> list:
|
def __get_live_link(items: list) -> list:
|
||||||
links = []
|
return [
|
||||||
for item in items:
|
(
|
||||||
links.append(
|
Html.format_url(
|
||||||
Html.format_url(Namespace.object_extract(
|
Namespace.object_extract(item, "stream.h264[0].masterUrl")
|
||||||
item, "stream.h264[0].masterUrl")))
|
)
|
||||||
return links
|
or None
|
||||||
|
)
|
||||||
|
for item in items
|
||||||
|
]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class Html:
|
|||||||
self.retry = manager.retry
|
self.retry = manager.retry
|
||||||
self.message = manager.message
|
self.message = manager.message
|
||||||
self.client = manager.request_client
|
self.client = manager.request_client
|
||||||
|
self.headers = manager.headers
|
||||||
|
self.blank_headers = manager.blank_headers
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
async def request_url(
|
async def request_url(
|
||||||
@@ -25,6 +27,7 @@ class Html:
|
|||||||
try:
|
try:
|
||||||
response = await self.client.get(
|
response = await self.client.get(
|
||||||
url,
|
url,
|
||||||
|
headers=self.select_headers(url, ),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -38,3 +41,6 @@ class Html:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def format_url(url: str) -> str:
|
def format_url(url: str) -> str:
|
||||||
return bytes(url, "utf-8").decode("unicode_escape")
|
return bytes(url, "utf-8").decode("unicode_escape")
|
||||||
|
|
||||||
|
def select_headers(self, url: str) -> dict:
|
||||||
|
return self.blank_headers if "discovery/item" in url else self.headers
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .browser import BrowserCookie
|
from .browser import BrowserCookie
|
||||||
from .converter import Converter
|
from .converter import Converter
|
||||||
from .namespace import Namespace
|
from .namespace import Namespace
|
||||||
|
from .truncate import beautify_string
|
||||||
__all__ = ["Converter", "Namespace", "BrowserCookie", ]
|
from .truncate import trim_string
|
||||||
|
from .truncate import truncate_string
|
||||||
|
|||||||
35
source/expansion/truncate.py
Normal file
35
source/expansion/truncate.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from unicodedata import name
|
||||||
|
|
||||||
|
|
||||||
|
def is_chinese_char(char: str) -> bool:
|
||||||
|
return 'CJK' in name(char, "")
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_string(s: str, length: int = 64) -> str:
|
||||||
|
count = 0
|
||||||
|
result = ""
|
||||||
|
for char in s:
|
||||||
|
count += 2 if is_chinese_char(char) else 1
|
||||||
|
if count > length:
|
||||||
|
break
|
||||||
|
result += char
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def trim_string(s: str, length: int = 64) -> str:
|
||||||
|
length = length // 2 - 2
|
||||||
|
return f"{s[:length]}...{s[-length:]}" if len(s) > length else s
|
||||||
|
|
||||||
|
|
||||||
|
def beautify_string(s: str, length: int = 64) -> str:
|
||||||
|
count = 0
|
||||||
|
for char in s:
|
||||||
|
count += 2 if is_chinese_char(char) else 1
|
||||||
|
if count > length:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
length //= 2
|
||||||
|
start = truncate_string(s, length)
|
||||||
|
end = truncate_string(s[::-1], length)[::-1]
|
||||||
|
return f"{start}...{end}"
|
||||||
@@ -6,6 +6,7 @@ from shutil import rmtree
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
from httpx import HTTPStatusError
|
||||||
from httpx import RequestError
|
from httpx import RequestError
|
||||||
from httpx import TimeoutException
|
from httpx import TimeoutException
|
||||||
from httpx import get
|
from httpx import get
|
||||||
@@ -64,6 +65,7 @@ class Manager:
|
|||||||
image_download: bool,
|
image_download: bool,
|
||||||
video_download: bool,
|
video_download: bool,
|
||||||
live_download: bool,
|
live_download: bool,
|
||||||
|
download_record: bool,
|
||||||
folder_mode: bool,
|
folder_mode: bool,
|
||||||
# server: bool,
|
# server: bool,
|
||||||
transition: Callable[[str], str],
|
transition: Callable[[str], str],
|
||||||
@@ -80,7 +82,7 @@ class Manager:
|
|||||||
"Sec-Ch-Ua-Platform": sec_ch_ua_platform or SEC_CH_UA_PLATFORM,
|
"Sec-Ch-Ua-Platform": sec_ch_ua_platform or SEC_CH_UA_PLATFORM,
|
||||||
}
|
}
|
||||||
self.headers = self.blank_headers | {
|
self.headers = self.blank_headers | {
|
||||||
"Cookie": self.clean_cookie(cookie),
|
"Cookie": cookie,
|
||||||
}
|
}
|
||||||
self.retry = retry
|
self.retry = retry
|
||||||
self.chunk = chunk
|
self.chunk = chunk
|
||||||
@@ -88,6 +90,7 @@ class Manager:
|
|||||||
self.record_data = self.check_bool(record_data, False)
|
self.record_data = self.check_bool(record_data, False)
|
||||||
self.image_format = self.__check_image_format(image_format)
|
self.image_format = self.__check_image_format(image_format)
|
||||||
self.folder_mode = self.check_bool(folder_mode, False)
|
self.folder_mode = self.check_bool(folder_mode, False)
|
||||||
|
self.download_record = self.check_bool(download_record, True)
|
||||||
self.proxy_tip = None
|
self.proxy_tip = None
|
||||||
self.proxy = self.__check_proxy(proxy)
|
self.proxy = self.__check_proxy(proxy)
|
||||||
self.print_proxy_tip(_print, )
|
self.print_proxy_tip(_print, )
|
||||||
@@ -200,13 +203,16 @@ class Manager:
|
|||||||
response = get(
|
response = get(
|
||||||
url,
|
url,
|
||||||
**kwarg, )
|
**kwarg, )
|
||||||
if response.status_code < 400:
|
response.raise_for_status()
|
||||||
self.proxy_tip = (self.message("代理 {0} 测试成功").format(proxy),)
|
self.proxy_tip = (self.message("代理 {0} 测试成功").format(proxy),)
|
||||||
return kwarg
|
return kwarg
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
self.proxy_tip = (
|
self.proxy_tip = (
|
||||||
self.message("代理 {0} 测试超时").format(proxy), WARNING,)
|
self.message("代理 {0} 测试超时").format(proxy), WARNING,)
|
||||||
except RequestError as e:
|
except (
|
||||||
|
RequestError,
|
||||||
|
HTTPStatusError,
|
||||||
|
) as e:
|
||||||
self.proxy_tip = (
|
self.proxy_tip = (
|
||||||
self.message("代理 {0} 测试失败:{1}").format(
|
self.message("代理 {0} 测试失败:{1}").format(
|
||||||
proxy, e), WARNING,)
|
proxy, e), WARNING,)
|
||||||
@@ -218,17 +224,19 @@ class Manager:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clean_cookie(cls, cookie_string: str) -> str:
|
def clean_cookie(cls, cookie_string: str) -> str:
|
||||||
for i in (
|
return cls.delete_cookie(
|
||||||
|
cookie_string,
|
||||||
|
(
|
||||||
cls.WEB_ID,
|
cls.WEB_ID,
|
||||||
cls.WEB_SESSION,
|
cls.WEB_SESSION,
|
||||||
):
|
),
|
||||||
cookie_string = cls.delete_cookie(cookie_string, i)
|
)
|
||||||
return cookie_string
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_cookie(cls, cookie_string: str, pattern) -> str:
|
def delete_cookie(cls, cookie_string: str, patterns: list | tuple) -> str:
|
||||||
# 使用空字符串替换匹配到的部分
|
for pattern in patterns:
|
||||||
cookie_string = sub(pattern, "", cookie_string)
|
# 使用空字符串替换匹配到的部分
|
||||||
|
cookie_string = sub(pattern, "", cookie_string)
|
||||||
# 去除多余的分号和空格
|
# 去除多余的分号和空格
|
||||||
cookie_string = sub(r';\s*$', "", cookie_string) # 删除末尾的分号和空格
|
cookie_string = sub(r';\s*$', "", cookie_string) # 删除末尾的分号和空格
|
||||||
cookie_string = sub(r';\s*;', ";", cookie_string) # 删除中间多余分号后的空格
|
cookie_string = sub(r';\s*;', ";", cookie_string) # 删除中间多余分号后的空格
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from asyncio import CancelledError
|
||||||
|
from contextlib import suppress
|
||||||
from re import compile
|
from re import compile
|
||||||
|
|
||||||
from aiosqlite import connect
|
from aiosqlite import connect
|
||||||
@@ -12,6 +14,7 @@ class IDRecorder:
|
|||||||
|
|
||||||
def __init__(self, manager: Manager):
|
def __init__(self, manager: Manager):
|
||||||
self.file = manager.root.joinpath("ExploreID.db")
|
self.file = manager.root.joinpath("ExploreID.db")
|
||||||
|
self.switch = manager.download_record
|
||||||
self.database = None
|
self.database = None
|
||||||
self.cursor = None
|
self.cursor = None
|
||||||
|
|
||||||
@@ -22,12 +25,14 @@ class IDRecorder:
|
|||||||
await self.database.commit()
|
await self.database.commit()
|
||||||
|
|
||||||
async def select(self, id_: str):
|
async def select(self, id_: str):
|
||||||
await self.cursor.execute("SELECT ID FROM explore_id WHERE ID=?", (id_,))
|
if self.switch:
|
||||||
return await self.cursor.fetchone()
|
await self.cursor.execute("SELECT ID FROM explore_id WHERE ID=?", (id_,))
|
||||||
|
return await self.cursor.fetchone()
|
||||||
|
|
||||||
async def add(self, id_: str) -> None:
|
async def add(self, id_: str) -> None:
|
||||||
await self.database.execute("REPLACE INTO explore_id VALUES (?);", (id_,))
|
if self.switch:
|
||||||
await self.database.commit()
|
await self.database.execute("REPLACE INTO explore_id VALUES (?);", (id_,))
|
||||||
|
await self.database.commit()
|
||||||
|
|
||||||
async def __delete(self, id_: str) -> None:
|
async def __delete(self, id_: str) -> None:
|
||||||
if id_:
|
if id_:
|
||||||
@@ -35,19 +40,22 @@ class IDRecorder:
|
|||||||
await self.database.commit()
|
await self.database.commit()
|
||||||
|
|
||||||
async def delete(self, ids: str):
|
async def delete(self, ids: str):
|
||||||
ids = [i.group(1) for i in self.URL.finditer(ids)]
|
if self.switch:
|
||||||
[await self.__delete(i) for i in ids]
|
ids = [i.group(1) for i in self.URL.finditer(ids)]
|
||||||
|
[await self.__delete(i) for i in ids]
|
||||||
|
|
||||||
async def all(self):
|
async def all(self):
|
||||||
await self.cursor.execute("SELECT ID FROM explore_id")
|
if self.switch:
|
||||||
return [i[0] for i in await self.cursor.fetchmany()]
|
await self.cursor.execute("SELECT ID FROM explore_id")
|
||||||
|
return [i[0] for i in await self.cursor.fetchmany()]
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
await self._connect_database()
|
await self._connect_database()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||||
await self.cursor.close()
|
with suppress(CancelledError):
|
||||||
|
await self.cursor.close()
|
||||||
await self.database.close()
|
await self.database.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +84,7 @@ class DataRecorder(IDRecorder):
|
|||||||
def __init__(self, manager: Manager):
|
def __init__(self, manager: Manager):
|
||||||
super().__init__(manager)
|
super().__init__(manager)
|
||||||
self.file = manager.folder.joinpath("ExploreData.db")
|
self.file = manager.folder.joinpath("ExploreData.db")
|
||||||
|
self.switch = manager.record_data
|
||||||
|
|
||||||
async def _connect_database(self):
|
async def _connect_database(self):
|
||||||
self.database = await connect(self.file)
|
self.database = await connect(self.file)
|
||||||
@@ -89,12 +98,13 @@ class DataRecorder(IDRecorder):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def add(self, **kwargs) -> None:
|
async def add(self, **kwargs) -> None:
|
||||||
await self.database.execute(f"""REPLACE INTO explore_data (
|
if self.switch:
|
||||||
|
await self.database.execute(f"""REPLACE INTO explore_data (
|
||||||
{", ".join(i[0] for i in self.DATA_TABLE)}
|
{", ".join(i[0] for i in self.DATA_TABLE)}
|
||||||
) VALUES (
|
) VALUES (
|
||||||
{", ".join("?" for _ in kwargs)}
|
{", ".join("?" for _ in kwargs)}
|
||||||
);""", self.__generate_values(kwargs))
|
);""", self.__generate_values(kwargs))
|
||||||
await self.database.commit()
|
await self.database.commit()
|
||||||
|
|
||||||
async def __delete(self, id_: str) -> None:
|
async def __delete(self, id_: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class Settings:
|
|||||||
"video_download": True,
|
"video_download": True,
|
||||||
"live_download": False,
|
"live_download": False,
|
||||||
"folder_mode": False,
|
"folder_mode": False,
|
||||||
|
"download_record": True,
|
||||||
"language": "zh_CN",
|
"language": "zh_CN",
|
||||||
# "server": False,
|
# "server": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name XHS-Downloader
|
// @name XHS-Downloader
|
||||||
// @namespace https://github.com/JoeanAmier/XHS-Downloader
|
// @namespace https://github.com/JoeanAmier/XHS-Downloader
|
||||||
// @version 1.5.2
|
// @version 1.6.0
|
||||||
// @description 提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件
|
// @description 提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件
|
||||||
// @author JoeanAmier
|
// @author JoeanAmier
|
||||||
// @match http*://xhslink.com/*
|
// @match http*://xhslink.com/*
|
||||||
@@ -304,43 +304,43 @@
|
|||||||
|
|
||||||
const extractNotesInfo = order => {
|
const extractNotesInfo = order => {
|
||||||
const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order];
|
const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order];
|
||||||
return new Set(notesRawValue.map(({id}) => id));
|
return notesRawValue.map(item => [item.id, item.xsecToken]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractFeedInfo = () => {
|
const extractFeedInfo = () => {
|
||||||
const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue;
|
const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue;
|
||||||
return new Set(notesRawValue.map(({id}) => id));
|
return notesRawValue.map(item => [item.id, item.xsecToken]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractSearchNotes = () => {
|
const extractSearchNotes = () => {
|
||||||
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue;
|
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue;
|
||||||
return new Set(notesRawValue.map(({id}) => id));
|
return notesRawValue.map(item => [item.id, item.xsecToken]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractSearchUsers = () => {
|
const extractSearchUsers = () => {
|
||||||
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.userLists._rawValue;
|
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.userLists._rawValue;
|
||||||
return new Set(notesRawValue.map(({id}) => id));
|
return notesRawValue.map(item => item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateNoteUrls = ids => [...ids].map(id => `https://www.xiaohongshu.com/explore/${id}`).join(" ");
|
const generateNoteUrls = data => data.map(([id, token]) => `https://www.xiaohongshu.com/explore/${id}?xsec_token=${token}&xsec_source=pc_feed`).join(" ");
|
||||||
|
|
||||||
const generateUserUrls = ids => [...ids].map(id => `https://www.xiaohongshu.com/user/profile/${id}`).join(" ");
|
const generateUserUrls = data => data.map(id => `https://www.xiaohongshu.com/user/profile/${id}`).join(" ");
|
||||||
|
|
||||||
const extractAllLinks = (callback, order) => {
|
const extractAllLinks = (callback, order) => {
|
||||||
scrollScreen(() => {
|
scrollScreen(() => {
|
||||||
let ids;
|
let data;
|
||||||
if (order >= 0 && order <= 2) {
|
if (order >= 0 && order <= 2) {
|
||||||
ids = extractNotesInfo(order);
|
data = extractNotesInfo(order);
|
||||||
} else if (order === 3) {
|
} else if (order === 3) {
|
||||||
ids = extractSearchNotes();
|
data = extractSearchNotes();
|
||||||
} else if (order === 4) {
|
} else if (order === 4) {
|
||||||
ids = extractSearchUsers();
|
data = extractSearchUsers();
|
||||||
} else if (order === -1) {
|
} else if (order === -1) {
|
||||||
ids = extractFeedInfo()
|
data = extractFeedInfo()
|
||||||
} else {
|
} else {
|
||||||
ids = [];
|
data = [];
|
||||||
}
|
}
|
||||||
let urlsString = order !== 4 ? generateNoteUrls(ids) : generateUserUrls(ids);
|
let urlsString = order !== 4 ? generateNoteUrls(data) : generateUserUrls(data);
|
||||||
callback(urlsString);
|
callback(urlsString);
|
||||||
}, order === -1, [3, 4].includes(order))
|
}, order === -1, [3, 4].includes(order))
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user