mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2025-12-26 04:48:05 +08:00
更新项目代码
This commit is contained in:
parent
48cfa60862
commit
952cf3496a
@ -1,6 +1,7 @@
|
||||
<div align="center">
|
||||
<img src="static/XHS-Downloader.png" alt="" height="256" width="256"><br>
|
||||
<h1>XHS-Downloader</h1>
|
||||
<p>简体中文 | <a href="README_EN.md">English</a></p>
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/JoeanAmier/XHS-Downloader?style=for-the-badge&color=ff7a45">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/JoeanAmier/XHS-Downloader?style=for-the-badge&color=9254de">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=for-the-badge&color=ff7875">
|
||||
@ -9,7 +10,9 @@
|
||||
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JoeanAmier/XHS-Downloader?style=for-the-badge&color=73d13d">
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=for-the-badge&color=40a9ff">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=for-the-badge&color=f759ab">
|
||||
</div>
|
||||
<br>
|
||||
<div align="center">
|
||||
<p>🔥 <b>小红书作品采集工具</b>:采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
|
||||
<p>❤️ 作者仅在 GitHub 发布 XHS-Downloader,未与任何个人或网站合作,且没有任何收费计划!</p>
|
||||
</div>
|
||||
@ -172,6 +175,12 @@ async with XHS(work_path=work_path,
|
||||
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">language</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">设置程序语言,目前支持:<code>zh-CN</code></td>
|
||||
<td align="center">zh-CN</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1>🌐 Cookie</h1>
|
||||
|
||||
0
README_EN.md
Normal file
0
README_EN.md
Normal file
@ -1,70 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = [
|
||||
"VERSION_MAJOR",
|
||||
"VERSION_MINOR",
|
||||
"VERSION_BETA",
|
||||
"ROOT",
|
||||
"REPOSITORY",
|
||||
"LICENCE",
|
||||
"RELEASES",
|
||||
"MASTER",
|
||||
"PROMPT",
|
||||
"GENERAL",
|
||||
"PROGRESS",
|
||||
"ERROR",
|
||||
"WARNING",
|
||||
"INFO",
|
||||
"DISCLAIMER_TEXT",
|
||||
"USERSCRIPT",
|
||||
"USERAGENT",
|
||||
"COOKIE",
|
||||
]
|
||||
|
||||
VERSION_MAJOR = 1
|
||||
VERSION_MINOR = 8
|
||||
VERSION_BETA = True
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader"
|
||||
LICENCE = "GNU General Public License v3.0"
|
||||
RELEASES = "https://github.com/JoeanAmier/XHS-Downloader/releases/latest"
|
||||
DISCLAIMER_TEXT = (
|
||||
"关于 XHS-Downloader 的 免责声明:",
|
||||
"",
|
||||
"1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。",
|
||||
"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。",
|
||||
"3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。",
|
||||
"4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。",
|
||||
"5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。",
|
||||
"6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。",
|
||||
"7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因"
|
||||
"二次开发可能带来的各种情况负全部责任。",
|
||||
"",
|
||||
"在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果"
|
||||
"您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。",
|
||||
"",
|
||||
">" * 50,
|
||||
)
|
||||
|
||||
USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js"
|
||||
|
||||
USERAGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 "
|
||||
"Safari/537.36")
|
||||
COOKIE = (
|
||||
"abRequestId=54c534bb-a2c6-558f-8e03-5b4c5c45635c; xsecappid=xhs-pc-web; a1=18c286a400"
|
||||
"4jy56qvzejvp631col0hd3032h4zjez50000106381; webId=779c977da3a15b5623015be94bdcc9e9; g"
|
||||
"id=yYSJYK0qDW8KyYSJYK048quV84Vv2KAhudVhJduUKqySlx2818xfq4888y8KqYy8y2y2f8Jy; web_sess"
|
||||
"ion=030037a259ce5f15c8d560dc12224a9fdc2ed1; webBuild=3.19.4; websectiga=984412fef754c"
|
||||
"018e472127b8effd174be8a5d51061c991aadd200c69a2801d6; sec_poison_id=3dd48845-d604-4535"
|
||||
"-bcc2-a859e97518bf; unread={%22ub%22:%22655eb3d60000000032033955%22%2C%22ue%22:%22656"
|
||||
"e9ef2000000003801ff3d%22%2C%22uc%22:29}; cache_feeds=[]")
|
||||
|
||||
MASTER = "b #fff200"
|
||||
PROMPT = "b turquoise2"
|
||||
GENERAL = "b bright_white"
|
||||
PROGRESS = "b bright_magenta"
|
||||
ERROR = "b bright_red"
|
||||
WARNING = "b bright_yellow"
|
||||
INFO = "b bright_green"
|
||||
3
source/TUI/__init__.py
Normal file
3
source/TUI/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .index import XHSDownloader
|
||||
|
||||
__all__ = ['XHSDownloader']
|
||||
@ -16,9 +16,9 @@ from textual.widgets import Label
|
||||
from textual.widgets import ProgressBar
|
||||
from textual.widgets import RichLog
|
||||
|
||||
from .App import XHS
|
||||
from .Settings import Settings
|
||||
from .Static import (
|
||||
from source.application import XHS
|
||||
from source.module import Settings
|
||||
from source.module import (
|
||||
VERSION_MAJOR,
|
||||
VERSION_MINOR,
|
||||
VERSION_BETA,
|
||||
@ -32,9 +32,11 @@ from .Static import (
|
||||
REPOSITORY,
|
||||
RELEASES,
|
||||
GENERAL,
|
||||
DISCLAIMER_TEXT,
|
||||
USERSCRIPT,
|
||||
)
|
||||
from source.translator import Chinese
|
||||
from source.translator import LANGUAGE
|
||||
from .setting import Setting
|
||||
|
||||
__all__ = ["XHSDownloader"]
|
||||
|
||||
@ -53,20 +55,25 @@ def show_state(function):
|
||||
|
||||
class XHSDownloader(App):
|
||||
CSS_PATH = ROOT.joinpath(
|
||||
"static/XHS-Downloader.tcss")
|
||||
"static/css/index.tcss")
|
||||
BINDINGS = [
|
||||
Binding(key="q", action="quit", description="退出程序"),
|
||||
# ("d", "toggle_dark", "切换主题"),
|
||||
Binding(key="u", action="check_update", description="检查更新"),
|
||||
Binding(key="m", action="user_script", description="获取脚本"),
|
||||
# Binding(key="l", action="choose_language", description="切换语言"),
|
||||
# Binding(key="s", action="settings", description="程序设置"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.APP = XHS(**Settings(ROOT).run())
|
||||
settings = Settings(ROOT).run()
|
||||
self.prompt = LANGUAGE.get(settings["language"], Chinese)
|
||||
self.APP = XHS(**settings, language_object=self.prompt)
|
||||
self.url = None
|
||||
self.tip = None
|
||||
self.bar = None
|
||||
self.setting = None
|
||||
self.disclaimer = True
|
||||
|
||||
async def __aenter__(self):
|
||||
@ -78,18 +85,18 @@ class XHSDownloader(App):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ScrollableContainer(Label(Text(f"开源协议:{LICENCE}", style=MASTER)),
|
||||
yield ScrollableContainer(Label(Text(f"{self.prompt.open_source_protocol}{LICENCE}", style=MASTER)),
|
||||
Label(
|
||||
Text(
|
||||
f"项目地址:{REPOSITORY}",
|
||||
f"{self.prompt.project_address}{REPOSITORY}",
|
||||
style=MASTER)),
|
||||
Label(Text("请输入小红书图文/视频作品链接:",
|
||||
Label(Text(self.prompt.input_box_title,
|
||||
style=PROMPT), id="prompt"),
|
||||
Input(placeholder="多个链接之间使用空格分隔"),
|
||||
HorizontalScroll(Button("下载无水印图片/视频", id="deal"),
|
||||
Button("读取剪贴板", id="paste"),
|
||||
Button("清空输入框", id="reset"), ),
|
||||
# Label(Text("准备就绪", style=INFO), id="state"),
|
||||
Input(placeholder=self.prompt.input_prompt),
|
||||
HorizontalScroll(Button(self.prompt.download_button, id="deal"),
|
||||
Button(self.prompt.paste_button, id="paste"),
|
||||
Button(self.prompt.reset_button, id="reset"), ),
|
||||
id="index",
|
||||
)
|
||||
with Center():
|
||||
yield ProgressBar(total=None, show_percentage=False, show_eta=False)
|
||||
@ -104,7 +111,7 @@ class XHSDownloader(App):
|
||||
self.url = self.query_one(Input)
|
||||
self.tip = self.query_one(RichLog)
|
||||
self.bar = self.query_one(ProgressBar)
|
||||
self.tip.write(Text("\n".join(DISCLAIMER_TEXT), style=MASTER))
|
||||
self.tip.write(Text("\n".join(self.prompt.disclaimer), style=MASTER))
|
||||
|
||||
def close_disclaimer(self):
|
||||
if self.disclaimer:
|
||||
@ -122,16 +129,19 @@ class XHSDownloader(App):
|
||||
@show_state
|
||||
async def deal(self):
|
||||
if not self.url.value:
|
||||
self.tip.write(Text("未输入任何小红书作品链接!", style=WARNING))
|
||||
self.tip.write(Text(self.prompt.invalid_link, style=WARNING))
|
||||
return
|
||||
if any(await self.APP.extract(self.url.value, True, log=self.tip)):
|
||||
self.url.value = ""
|
||||
else:
|
||||
self.tip.write(Text("下载小红书作品文件失败!", style=ERROR))
|
||||
self.tip.write(Text(self.prompt.download_failure, style=ERROR))
|
||||
|
||||
@show_state
|
||||
async def action_check_update(self):
|
||||
self.tip.write(Text("正在检查新版本,请稍等...", style=WARNING))
|
||||
self.tip.write(
|
||||
Text(
|
||||
self.prompt.check_update_notification,
|
||||
style=WARNING))
|
||||
try:
|
||||
url = await self.APP.html.request_url(RELEASES, False, self.tip)
|
||||
latest_major, latest_minor = map(
|
||||
@ -139,20 +149,41 @@ class XHSDownloader(App):
|
||||
if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR:
|
||||
self.tip.write(
|
||||
Text(
|
||||
f"检测到新版本:{latest_major}.{latest_minor}",
|
||||
self.prompt.official_version_update(
|
||||
latest_major,
|
||||
latest_minor),
|
||||
style=WARNING))
|
||||
self.tip.write(RELEASES)
|
||||
elif latest_minor == VERSION_MINOR and VERSION_BETA:
|
||||
self.tip.write(
|
||||
Text("当前版本为开发版, 可更新至正式版!", style=WARNING))
|
||||
Text(
|
||||
self.prompt.development_version_update,
|
||||
style=WARNING))
|
||||
self.tip.write(RELEASES)
|
||||
elif VERSION_BETA:
|
||||
self.tip.write(Text("当前已是最新开发版!", style=WARNING))
|
||||
self.tip.write(
|
||||
Text(
|
||||
self.prompt.latest_development_version,
|
||||
style=WARNING))
|
||||
else:
|
||||
self.tip.write(Text("当前已是最新正式版!", style=INFO))
|
||||
self.tip.write(
|
||||
Text(
|
||||
self.prompt.latest_official_version,
|
||||
style=INFO))
|
||||
except ValueError:
|
||||
self.tip.write(Text("检测新版本失败!", style=ERROR))
|
||||
self.tip.write(Text(self.prompt.check_update_failure, style=ERROR))
|
||||
|
||||
@staticmethod
|
||||
def action_user_script():
|
||||
open(USERSCRIPT)
|
||||
|
||||
def action_choose_language(self):
|
||||
pass
|
||||
|
||||
def action_settings(self):
|
||||
if self.setting:
|
||||
self.setting.remove()
|
||||
self.setting = None
|
||||
else:
|
||||
self.setting = Setting()
|
||||
self.query_one("#index").mount(self.setting)
|
||||
16
source/TUI/setting.py
Normal file
16
source/TUI/setting.py
Normal file
@ -0,0 +1,16 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.widgets import Label
|
||||
from textual.widgets import Static
|
||||
|
||||
from source.module import ROOT
|
||||
|
||||
__all__ = ["Setting"]
|
||||
|
||||
|
||||
class Setting(Static):
|
||||
CSS_PATH = ROOT.joinpath(
|
||||
"static/css/setting.tcss")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets for the app."""
|
||||
yield Label("我是设置页")
|
||||
@ -1,4 +1,4 @@
|
||||
from .App import XHS
|
||||
from .TUI import XHSDownloader
|
||||
from .application import XHS
|
||||
|
||||
__all__ = ['XHS', 'XHSDownloader']
|
||||
|
||||
@ -2,10 +2,10 @@ from pathlib import Path
|
||||
|
||||
from aiohttp import ClientError
|
||||
|
||||
from .Manager import Manager
|
||||
from .Static import ERROR
|
||||
from .Tools import logging
|
||||
from .Tools import retry as re_download
|
||||
from source.module import ERROR
|
||||
from source.module import Manager
|
||||
from source.module import logging
|
||||
from source.module import retry as re_download
|
||||
|
||||
__all__ = ['Download']
|
||||
|
||||
@ -20,6 +20,7 @@ class Download:
|
||||
self.chunk = manager.chunk
|
||||
self.session = manager.download_session
|
||||
self.retry = manager.retry
|
||||
self.prompt = manager.prompt
|
||||
self.folder_mode = manager.folder_mode
|
||||
self.video_format = "mp4"
|
||||
self.image_format = manager.image_format
|
||||
@ -50,7 +51,7 @@ class Download:
|
||||
temp = self.temp.joinpath(name)
|
||||
file = path.joinpath(name).with_suffix(f".{suffix}")
|
||||
if self.manager.is_exists(file):
|
||||
logging(log, f"{name} 已存在,跳过下载!")
|
||||
logging(log, self.prompt.skip_download(name))
|
||||
return True
|
||||
# self.__create_progress(
|
||||
# bar, int(
|
||||
@ -62,13 +63,13 @@ class Download:
|
||||
# self.__update_progress(bar, len(chunk))
|
||||
self.manager.move(temp, file)
|
||||
# self.__create_progress(bar, None)
|
||||
logging(log, f"{name} 下载成功!")
|
||||
logging(log, self.prompt.download_success(name))
|
||||
return True
|
||||
except ClientError as error:
|
||||
self.manager.delete(temp)
|
||||
# self.__create_progress(bar, None)
|
||||
logging(log, error, ERROR)
|
||||
logging(log, f"网络异常,{name} 下载失败!", ERROR)
|
||||
logging(log, self.prompt.download_error(name), ERROR)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from .Converter import Namespace
|
||||
from source.expansion import Namespace
|
||||
|
||||
__all__ = ['Explore']
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from aiohttp import ClientError
|
||||
|
||||
from .Manager import Manager
|
||||
from .Static import ERROR
|
||||
from .Tools import logging
|
||||
from .Tools import retry
|
||||
from source.module import ERROR
|
||||
from source.module import Manager
|
||||
from source.module import logging
|
||||
from source.module import retry
|
||||
|
||||
__all__ = ["Html"]
|
||||
|
||||
@ -12,6 +12,7 @@ class Html:
|
||||
def __init__(self, manager: Manager, ):
|
||||
self.proxy = manager.proxy
|
||||
self.retry = manager.retry
|
||||
self.prompt = manager.prompt
|
||||
self.session = manager.request_session
|
||||
|
||||
@retry
|
||||
@ -29,7 +30,7 @@ class Html:
|
||||
return await response.text() if content else str(response.url)
|
||||
except ClientError as error:
|
||||
logging(log, error, ERROR)
|
||||
logging(log, f"网络异常,请求 {url} 失败!", ERROR)
|
||||
logging(log, self.prompt.request_error(url), ERROR)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@ -1,4 +1,4 @@
|
||||
from .Converter import Namespace
|
||||
from source.expansion import Namespace
|
||||
from .Html import Html
|
||||
|
||||
__all__ = ['Image']
|
||||
@ -1,4 +1,4 @@
|
||||
from .Converter import Namespace
|
||||
from source.expansion import Namespace
|
||||
from .Html import Html
|
||||
|
||||
__all__ = ['Video']
|
||||
3
source/application/__init__.py
Normal file
3
source/application/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .app import XHS
|
||||
|
||||
__all__ = ["XHS"]
|
||||
@ -1,18 +1,23 @@
|
||||
from re import compile
|
||||
|
||||
from .Converter import Converter
|
||||
from .Converter import Namespace
|
||||
from .Downloader import Download
|
||||
from .Explore import Explore
|
||||
from .Html import Html
|
||||
from .Image import Image
|
||||
from .Manager import Manager
|
||||
from .Static import (
|
||||
from source.expansion import Converter
|
||||
from source.expansion import Namespace
|
||||
from source.module import Manager
|
||||
from source.module import (
|
||||
ROOT,
|
||||
ERROR,
|
||||
WARNING,
|
||||
)
|
||||
from .Tools import logging
|
||||
from source.module import logging
|
||||
from source.translator import (
|
||||
LANGUAGE,
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
from .Downloader import Download
|
||||
from .Explore import Explore
|
||||
from .Html import Html
|
||||
from .Image import Image
|
||||
from .Video import Video
|
||||
|
||||
__all__ = ["XHS"]
|
||||
@ -42,7 +47,10 @@ class XHS:
|
||||
record_data=False,
|
||||
image_format="PNG",
|
||||
folder_mode=False,
|
||||
language="zh-CN",
|
||||
language_object: Chinese | English = None,
|
||||
):
|
||||
self.prompt = language_object or LANGUAGE.get(language, Chinese)
|
||||
self.manager = Manager(
|
||||
ROOT,
|
||||
work_path,
|
||||
@ -56,6 +64,7 @@ class XHS:
|
||||
record_data,
|
||||
image_format,
|
||||
folder_mode,
|
||||
self.prompt,
|
||||
)
|
||||
self.html = Html(self.manager)
|
||||
self.image = Image()
|
||||
@ -77,16 +86,16 @@ class XHS:
|
||||
if (u := container["下载地址"]) and download:
|
||||
path = await self.download.run(u, name, container["作品类型"], log, bar)
|
||||
elif not u:
|
||||
logging(log, "提取作品文件下载地址失败!", ERROR)
|
||||
logging(log, self.prompt.download_link_error, ERROR)
|
||||
self.manager.save_data(path, name, container)
|
||||
|
||||
async def extract(self, url: str, download=False, log=None, bar=None) -> list[dict]:
|
||||
# return # 调试代码
|
||||
urls = await self.__extract_links(url, log)
|
||||
if not urls:
|
||||
logging(log, "提取小红书作品链接失败!", WARNING)
|
||||
logging(log, self.prompt.extract_link_failure, WARNING)
|
||||
else:
|
||||
logging(log, f"共 {len(urls)} 个小红书作品待处理...")
|
||||
logging(log, self.prompt.pending_processing(len(urls)))
|
||||
# return urls # 调试代码
|
||||
return [await self.__deal_extract(i, download, log, bar) for i in urls]
|
||||
|
||||
@ -103,17 +112,17 @@ class XHS:
|
||||
return urls
|
||||
|
||||
async def __deal_extract(self, url: str, download: bool, log, bar):
|
||||
logging(log, f"开始处理作品:{url}")
|
||||
logging(log, self.prompt.start_processing(url))
|
||||
html = await self.html.request_url(url, log=log)
|
||||
# logging(log, html) # 调试代码
|
||||
if not html:
|
||||
logging(log, f"{url} 获取数据失败!", ERROR)
|
||||
logging(log, self.prompt.get_data_failure(url), ERROR)
|
||||
return {}
|
||||
namespace = self.__generate_data_object(html)
|
||||
data = self.explore.run(namespace)
|
||||
# logging(log, data) # 调试代码
|
||||
if not data:
|
||||
logging(log, f"{url} 提取数据失败!", ERROR)
|
||||
logging(log, self.prompt.extract_data_failure(url), ERROR)
|
||||
return {}
|
||||
match data["作品类型"]:
|
||||
case "视频":
|
||||
@ -123,7 +132,7 @@ class XHS:
|
||||
case _:
|
||||
data["下载地址"] = []
|
||||
await self.__download_files(data, download, log, bar)
|
||||
logging(log, f"作品处理完成:{url}")
|
||||
logging(log, self.prompt.processing_completed(url))
|
||||
return data
|
||||
|
||||
def __generate_data_object(self, html: str) -> Namespace:
|
||||
4
source/expansion/__init__.py
Normal file
4
source/expansion/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .converter import Converter
|
||||
from .namespace import Namespace
|
||||
|
||||
__all__ = ["Converter", "Namespace", ]
|
||||
51
source/expansion/converter.py
Normal file
51
source/expansion/converter.py
Normal file
@ -0,0 +1,51 @@
|
||||
from lxml.etree import HTML
|
||||
from yaml import safe_load
|
||||
|
||||
__all__ = ["Converter"]
|
||||
|
||||
|
||||
class Converter:
|
||||
INITIAL_STATE = "(//script)[last()]/text()"
|
||||
KEYS_LINK = (
|
||||
"note",
|
||||
"noteDetailMap",
|
||||
"[-1]",
|
||||
"note",
|
||||
)
|
||||
|
||||
def run(self, content: str) -> dict:
|
||||
return self.__filter_object(
|
||||
self.__convert_object(
|
||||
self.__extract_object(content)))
|
||||
|
||||
def __extract_object(self, html: str) -> str:
|
||||
html_tree = HTML(html)
|
||||
return d[0] if (d := html_tree.xpath(self.INITIAL_STATE)) else ""
|
||||
|
||||
@staticmethod
|
||||
def __convert_object(text: str) -> dict:
|
||||
return safe_load(text.lstrip("window.__INITIAL_STATE__="))
|
||||
|
||||
@classmethod
|
||||
def __filter_object(cls, data: dict) -> dict:
|
||||
return cls.deep_get(data, cls.KEYS_LINK) or {}
|
||||
|
||||
@classmethod
|
||||
def deep_get(cls, data: dict, keys: list | tuple, default=None):
|
||||
try:
|
||||
for key in keys:
|
||||
if key.startswith("[") and key.endswith("]"):
|
||||
data = cls.safe_get(data, int(key[1:-1]))
|
||||
else:
|
||||
data = data[key]
|
||||
return data
|
||||
except (KeyError, IndexError, ValueError):
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def safe_get(data: dict | list | tuple | set, index: int):
|
||||
if isinstance(data, dict):
|
||||
return list(data.values())[index]
|
||||
elif isinstance(data, list | tuple | set):
|
||||
return data[index]
|
||||
raise TypeError
|
||||
@ -1,57 +1,7 @@
|
||||
from copy import deepcopy
|
||||
from types import SimpleNamespace
|
||||
|
||||
from lxml.etree import HTML
|
||||
from yaml import safe_load
|
||||
|
||||
__all__ = ["Converter", "Namespace"]
|
||||
|
||||
|
||||
class Converter:
|
||||
INITIAL_STATE = "(//script)[last()]/text()"
|
||||
KEYS_LINK = (
|
||||
"note",
|
||||
"noteDetailMap",
|
||||
"[-1]",
|
||||
"note",
|
||||
)
|
||||
|
||||
def run(self, content: str) -> dict:
|
||||
return self.__filter_object(
|
||||
self.__convert_object(
|
||||
self.__extract_object(content)))
|
||||
|
||||
def __extract_object(self, html: str) -> str:
|
||||
html_tree = HTML(html)
|
||||
return d[0] if (d := html_tree.xpath(self.INITIAL_STATE)) else ""
|
||||
|
||||
@staticmethod
|
||||
def __convert_object(text: str) -> dict:
|
||||
return safe_load(text.lstrip("window.__INITIAL_STATE__="))
|
||||
|
||||
@classmethod
|
||||
def __filter_object(cls, data: dict) -> dict:
|
||||
return cls.deep_get(data, cls.KEYS_LINK) or {}
|
||||
|
||||
@classmethod
|
||||
def deep_get(cls, data: dict, keys: list | tuple, default=None):
|
||||
try:
|
||||
for key in keys:
|
||||
if key.startswith("[") and key.endswith("]"):
|
||||
data = cls.safe_get(data, int(key[1:-1]))
|
||||
else:
|
||||
data = data[key]
|
||||
return data
|
||||
except (KeyError, IndexError, ValueError):
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def safe_get(data: dict | list | tuple | set, index: int):
|
||||
if isinstance(data, dict):
|
||||
return list(data.values())[index]
|
||||
elif isinstance(data, list | tuple | set):
|
||||
return data[index]
|
||||
raise TypeError
|
||||
__all__ = ["Namespace"]
|
||||
|
||||
|
||||
class Namespace:
|
||||
53
source/module/__init__.py
Normal file
53
source/module/__init__.py
Normal file
@ -0,0 +1,53 @@
|
||||
from .extend import Account
|
||||
from .manager import Manager
|
||||
from .recorder import Recorder
|
||||
from .settings import Settings
|
||||
from .static import (
|
||||
VERSION_MAJOR,
|
||||
VERSION_MINOR,
|
||||
VERSION_BETA,
|
||||
ROOT,
|
||||
REPOSITORY,
|
||||
LICENCE,
|
||||
RELEASES,
|
||||
MASTER,
|
||||
PROMPT,
|
||||
GENERAL,
|
||||
PROGRESS,
|
||||
ERROR,
|
||||
WARNING,
|
||||
INFO,
|
||||
USERSCRIPT,
|
||||
USERAGENT,
|
||||
COOKIE,
|
||||
)
|
||||
from .tools import (
|
||||
retry,
|
||||
logging,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
"Settings",
|
||||
"Recorder",
|
||||
"Manager",
|
||||
"VERSION_MAJOR",
|
||||
"VERSION_MINOR",
|
||||
"VERSION_BETA",
|
||||
"ROOT",
|
||||
"REPOSITORY",
|
||||
"LICENCE",
|
||||
"RELEASES",
|
||||
"MASTER",
|
||||
"PROMPT",
|
||||
"GENERAL",
|
||||
"PROGRESS",
|
||||
"ERROR",
|
||||
"WARNING",
|
||||
"INFO",
|
||||
"USERSCRIPT",
|
||||
"USERAGENT",
|
||||
"COOKIE",
|
||||
"retry",
|
||||
"logging",
|
||||
]
|
||||
@ -9,8 +9,10 @@ from shutil import rmtree
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp import ClientTimeout
|
||||
|
||||
from .Static import COOKIE
|
||||
from .Static import USERAGENT
|
||||
from source.translator import Chinese
|
||||
from source.translator import English
|
||||
from .static import COOKIE
|
||||
from .static import USERAGENT
|
||||
|
||||
__all__ = ["Manager"]
|
||||
|
||||
@ -32,6 +34,7 @@ class Manager:
|
||||
record_data: bool,
|
||||
image_format: str,
|
||||
folder_mode: bool,
|
||||
language: Chinese | English,
|
||||
):
|
||||
self.root = root
|
||||
self.temp = root.joinpath("./temp")
|
||||
@ -54,6 +57,7 @@ class Manager:
|
||||
self.download_session = ClientSession(
|
||||
headers={"User-Agent": self.headers["User-Agent"]},
|
||||
timeout=ClientTimeout(connect=timeout))
|
||||
self.prompt = language
|
||||
|
||||
def __check_path(self, path: str) -> Path:
|
||||
if not path:
|
||||
@ -19,6 +19,8 @@ class Settings:
|
||||
"record_data": False,
|
||||
"image_format": "PNG",
|
||||
"folder_mode": False,
|
||||
"language": "zh-CN",
|
||||
# "server": False,
|
||||
}
|
||||
encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8"
|
||||
|
||||
52
source/module/static.py
Normal file
52
source/module/static.py
Normal file
@ -0,0 +1,52 @@
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = [
|
||||
"VERSION_MAJOR",
|
||||
"VERSION_MINOR",
|
||||
"VERSION_BETA",
|
||||
"ROOT",
|
||||
"REPOSITORY",
|
||||
"LICENCE",
|
||||
"RELEASES",
|
||||
"MASTER",
|
||||
"PROMPT",
|
||||
"GENERAL",
|
||||
"PROGRESS",
|
||||
"ERROR",
|
||||
"WARNING",
|
||||
"INFO",
|
||||
"USERSCRIPT",
|
||||
"USERAGENT",
|
||||
"COOKIE",
|
||||
]
|
||||
|
||||
VERSION_MAJOR = 1
|
||||
VERSION_MINOR = 8
|
||||
VERSION_BETA = True
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader"
|
||||
LICENCE = "GNU General Public License v3.0"
|
||||
RELEASES = "https://github.com/JoeanAmier/XHS-Downloader/releases/latest"
|
||||
|
||||
USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js"
|
||||
|
||||
USERAGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 "
|
||||
"Safari/537.36")
|
||||
COOKIE = (
|
||||
"abRequestId=54c534bb-a2c6-558f-8e03-5b4c5c45635c; xsecappid=xhs-pc-web; a1=18c286a400"
|
||||
"4jy56qvzejvp631col0hd3032h4zjez50000106381; webId=779c977da3a15b5623015be94bdcc9e9; g"
|
||||
"id=yYSJYK0qDW8KyYSJYK048quV84Vv2KAhudVhJduUKqySlx2818xfq4888y8KqYy8y2y2f8Jy; web_sess"
|
||||
"ion=030037a259ce5f15c8d560dc12224a9fdc2ed1; webBuild=3.19.4; websectiga=984412fef754c"
|
||||
"018e472127b8effd174be8a5d51061c991aadd200c69a2801d6; sec_poison_id=3dd48845-d604-4535"
|
||||
"-bcc2-a859e97518bf; unread={%22ub%22:%22655eb3d60000000032033955%22%2C%22ue%22:%22656"
|
||||
"e9ef2000000003801ff3d%22%2C%22uc%22:29}; cache_feeds=[]")
|
||||
|
||||
MASTER = "b #fff200"
|
||||
PROMPT = "b turquoise2"
|
||||
GENERAL = "b bright_white"
|
||||
PROGRESS = "b bright_magenta"
|
||||
ERROR = "b bright_red"
|
||||
WARNING = "b bright_yellow"
|
||||
INFO = "b bright_green"
|
||||
@ -1,6 +1,6 @@
|
||||
from rich.text import Text
|
||||
|
||||
from .Static import INFO
|
||||
from .static import INFO
|
||||
|
||||
__all__ = ["retry", "logging"]
|
||||
|
||||
13
source/translator/__init__.py
Normal file
13
source/translator/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
from .chinese import Chinese
|
||||
from .english import English
|
||||
|
||||
__all__ = [
|
||||
"LANGUAGE",
|
||||
"Chinese",
|
||||
"English",
|
||||
]
|
||||
|
||||
LANGUAGE = {
|
||||
Chinese.code: Chinese,
|
||||
English.code: English,
|
||||
}
|
||||
87
source/translator/chinese.py
Normal file
87
source/translator/chinese.py
Normal file
@ -0,0 +1,87 @@
|
||||
from .template import Language
|
||||
|
||||
__all__ = ["Chinese"]
|
||||
|
||||
|
||||
class Chinese(Language):
|
||||
code: str = "zh-CN"
|
||||
disclaimer: tuple[str] = (
|
||||
"关于 XHS-Downloader 的 免责声明:",
|
||||
"",
|
||||
"1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。",
|
||||
"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。",
|
||||
"3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。",
|
||||
"4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。",
|
||||
"5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。",
|
||||
"6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。",
|
||||
"7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因"
|
||||
"二次开发可能带来的各种情况负全部责任。",
|
||||
"",
|
||||
"在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果"
|
||||
"您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。",
|
||||
"",
|
||||
">" * 50,
|
||||
)
|
||||
|
||||
download_link_error: str = "提取作品文件下载地址失败!"
|
||||
extract_link_failure: str = "提取小红书作品链接失败!"
|
||||
invalid_link: str = "未输入任何小红书作品链接!"
|
||||
download_failure: str = "下载小红书作品文件失败!"
|
||||
check_update_notification: str = "正在检查新版本,请稍等..."
|
||||
development_version_update: str = "当前版本为开发版, 可更新至正式版!"
|
||||
latest_development_version: str = "当前已是最新开发版!"
|
||||
latest_official_version: str = "当前已是最新正式版!"
|
||||
check_update_failure: str = "检测新版本失败!"
|
||||
|
||||
open_source_protocol: str = "开源协议:"
|
||||
project_address: str = "项目地址:"
|
||||
input_box_title: str = "请输入小红书图文/视频作品链接:"
|
||||
input_prompt: str = "多个链接之间使用空格分隔"
|
||||
download_button: str = "下载无水印图片/视频"
|
||||
paste_button: str = "读取剪贴板"
|
||||
reset_button: str = "清空输入框"
|
||||
|
||||
exit_program: str = "退出程序"
|
||||
check_updates: str = "检查更新"
|
||||
get_script: str = "获取脚本"
|
||||
choose_language: str = "选择语言"
|
||||
|
||||
@staticmethod
|
||||
def request_error(url: str) -> str:
|
||||
return f"网络异常,请求 {url} 失败!"
|
||||
|
||||
@staticmethod
|
||||
def skip_download(name: str) -> str:
|
||||
return f"{name} 已存在,跳过下载!"
|
||||
|
||||
@staticmethod
|
||||
def download_success(name: str) -> str:
|
||||
return f"{name} 下载成功!"
|
||||
|
||||
@staticmethod
|
||||
def download_error(name: str) -> str:
|
||||
return f"网络异常,{name} 下载失败!"
|
||||
|
||||
@staticmethod
|
||||
def pending_processing(num: int) -> str:
|
||||
return f"共 {num} 个小红书作品待处理..."
|
||||
|
||||
@staticmethod
|
||||
def start_processing(url: str) -> str:
|
||||
return f"开始处理作品:{url}"
|
||||
|
||||
@staticmethod
|
||||
def get_data_failure(url: str) -> str:
|
||||
return f"{url} 获取数据失败!"
|
||||
|
||||
@staticmethod
|
||||
def extract_data_failure(url: str) -> str:
|
||||
return f"{url} 提取数据失败!"
|
||||
|
||||
@staticmethod
|
||||
def processing_completed(url: str) -> str:
|
||||
return f"作品处理完成:{url}"
|
||||
|
||||
@staticmethod
|
||||
def official_version_update(major: int, minor: int) -> str:
|
||||
return f"检测到新版本:{major}.{minor}"
|
||||
7
source/translator/english.py
Normal file
7
source/translator/english.py
Normal file
@ -0,0 +1,7 @@
|
||||
from .template import Language
|
||||
|
||||
__all__ = ["English"]
|
||||
|
||||
|
||||
class English(Language):
|
||||
pass
|
||||
69
source/translator/template.py
Normal file
69
source/translator/template.py
Normal file
@ -0,0 +1,69 @@
|
||||
__all__ = ["Language"]
|
||||
|
||||
|
||||
class Language:
|
||||
code: str = None
|
||||
disclaimer: tuple[str] = None
|
||||
|
||||
download_link_error: str = None
|
||||
extract_link_failure: str = None
|
||||
invalid_link: str = None
|
||||
download_failure: str = None
|
||||
check_update_notification: str = None
|
||||
development_version_update: str = None
|
||||
latest_development_version: str = None
|
||||
latest_official_version: str = None
|
||||
check_update_failure: str = None
|
||||
|
||||
open_source_protocol: str = None
|
||||
project_address: str = None
|
||||
input_box_title: str = None
|
||||
input_prompt: str = None
|
||||
download_button: str = None
|
||||
paste_button: str = None
|
||||
reset_button: str = None
|
||||
|
||||
exit_program: str = None
|
||||
check_updates: str = None
|
||||
get_script: str = None
|
||||
choose_language: str = None
|
||||
|
||||
@staticmethod
|
||||
def request_error(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def skip_download(name: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def download_success(name: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def download_error(name: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def pending_processing(num: int) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def start_processing(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_data_failure(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def extract_data_failure(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def processing_completed(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def official_version_update(major: int, minor: int) -> str:
|
||||
pass
|
||||
0
static/css/setting.tcss
Normal file
0
static/css/setting.tcss
Normal file
Loading…
x
Reference in New Issue
Block a user