mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2025-12-26 04:48:05 +08:00
feat: 新增文件断点续传功能
1. 新增自动删除空文件夹功能 2. 引入 aiofiles 库 3. 修正 Actions 错误 Closes #142 Closes #143
This commit is contained in:
parent
24dc8a1a53
commit
7cedd8d4b8
6
.github/workflows/executable_build.yml
vendored
6
.github/workflows/executable_build.yml
vendored
@ -29,7 +29,11 @@ jobs:
|
||||
|
||||
- name: 构建可执行文件
|
||||
run: |
|
||||
pyinstaller --icon=/static/images/TikTokDownloader.ico --add-data "static;static" --add-data "templates;templates" main.py
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
pyinstaller --icon=./static/XHS-Downloader.ico --add-data "static;static" --add-data "locale;locale" .\main.py
|
||||
else
|
||||
pyinstaller --icon=./static/XHS-Downloader.ico --add-data "static:static" --add-data "locale:locale" main.py
|
||||
fi
|
||||
|
||||
- name: 上传文件
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@ -419,6 +419,7 @@ async def example():
|
||||
* https://github.com/tiangolo/fastapi
|
||||
* https://github.com/textualize/textual/
|
||||
* https://textual.textualize.io/
|
||||
* https://github.com/omnilib/aiosqlite
|
||||
* https://aiosqlite.omnilib.dev/en/stable/
|
||||
* https://click.palletsprojects.com/en/8.1.x/
|
||||
* https://github.com/thewh1teagle/rookie
|
||||
|
||||
@ -420,6 +420,7 @@ async def example():
|
||||
* https://github.com/tiangolo/fastapi
|
||||
* https://github.com/textualize/textual/
|
||||
* https://textual.textualize.io/
|
||||
* https://github.com/omnilib/aiosqlite
|
||||
* https://aiosqlite.omnilib.dev/en/stable/
|
||||
* https://click.palletsprojects.com/en/8.1.x/
|
||||
* https://github.com/thewh1teagle/rookie
|
||||
|
||||
@ -8,3 +8,4 @@ rookiepy>=0.5.2
|
||||
httpx>=0.27.0
|
||||
fastapi>=0.111.0
|
||||
uvicorn>=0.30.1
|
||||
aiofiles>=24.1.0
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
from asyncio import gather
|
||||
from pathlib import Path
|
||||
|
||||
from aiofiles import open
|
||||
from httpx import HTTPError
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from source.module import ERROR
|
||||
from source.module import Manager
|
||||
from source.module import logging
|
||||
from source.module import retry as re_download
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from httpx import AsyncClient
|
||||
|
||||
__all__ = ['Download']
|
||||
|
||||
|
||||
@ -26,7 +29,8 @@ class Download:
|
||||
self.folder = manager.folder
|
||||
self.temp = manager.temp
|
||||
self.chunk = manager.chunk
|
||||
self.client = manager.download_client
|
||||
self.client: "AsyncClient" = manager.download_client
|
||||
self.headers = manager.blank_headers
|
||||
self.retry = manager.retry
|
||||
self.message = manager.message
|
||||
self.folder_mode = manager.folder_mode
|
||||
@ -122,20 +126,32 @@ class Download:
|
||||
|
||||
@re_download
|
||||
async def __download(self, url: str, path: Path, name: str, format_: str, log, bar):
|
||||
temp = self.temp.joinpath(f"{name}.{format_}")
|
||||
try:
|
||||
async with self.client.stream("GET", url, ) as response:
|
||||
length, suffix = await self.__hand_file(url, format_, )
|
||||
except HTTPError as error:
|
||||
logging(log, str(error), ERROR)
|
||||
logging(
|
||||
log,
|
||||
self.message(
|
||||
"网络异常,{0} 请求失败").format(name),
|
||||
ERROR,
|
||||
)
|
||||
return False
|
||||
temp = self.temp.joinpath(f"{name}.{suffix}")
|
||||
real = path.joinpath(f"{name}.{suffix}")
|
||||
self.__update_headers_range(temp, )
|
||||
try:
|
||||
async with self.client.stream("GET", url, headers=self.headers) as response:
|
||||
response.raise_for_status()
|
||||
suffix = self.__extract_type(
|
||||
response.headers.get("Content-Type")) or format_
|
||||
real = path.joinpath(f"{name}.{suffix}")
|
||||
# self.__create_progress(
|
||||
# bar, int(
|
||||
# bar,
|
||||
# int(
|
||||
# response.headers.get(
|
||||
# 'content-length', 0)) or None)
|
||||
with temp.open("wb") as f:
|
||||
# 'content-length', 0)) or None,
|
||||
# )
|
||||
async with open(temp, "ab") as f:
|
||||
async for chunk in response.aiter_bytes(self.chunk):
|
||||
f.write(chunk)
|
||||
await f.write(chunk)
|
||||
# self.__update_progress(bar, len(chunk))
|
||||
self.manager.move(temp, real)
|
||||
# self.__create_progress(bar, None)
|
||||
@ -146,14 +162,17 @@ class Download:
|
||||
# self.__create_progress(bar, None)
|
||||
logging(log, str(error), ERROR)
|
||||
logging(
|
||||
log, self.message(
|
||||
"网络异常,{0} 下载失败").format(name), ERROR)
|
||||
log,
|
||||
self.message(
|
||||
"网络异常,{0} 下载失败").format(name),
|
||||
ERROR,
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __create_progress(bar, total: int | None):
|
||||
def __create_progress(bar, total: int | None, completed=0, ):
|
||||
if bar:
|
||||
bar.update(total=total)
|
||||
bar.update(total=total, completed=completed)
|
||||
|
||||
@staticmethod
|
||||
def __update_progress(bar, advance: int):
|
||||
@ -163,3 +182,26 @@ class Download:
|
||||
@classmethod
|
||||
def __extract_type(cls, content: str) -> str:
|
||||
return cls.CONTENT_TYPE_MAP.get(content, "")
|
||||
|
||||
async def __hand_file(self,
|
||||
url: str,
|
||||
suffix: str,
|
||||
) -> [int, str]:
|
||||
response = await self.client.head(url,
|
||||
headers=self.headers | {
|
||||
"Range": "bytes=0-",
|
||||
}, )
|
||||
response.raise_for_status()
|
||||
suffix = self.__extract_type(
|
||||
response.headers.get("Content-Type")) or suffix
|
||||
length = response.headers.get(
|
||||
"Content-Length", 0)
|
||||
return int(length), suffix
|
||||
|
||||
@staticmethod
|
||||
def __get_resume_byte_position(file: Path) -> int:
|
||||
return file.stat().st_size if file.is_file() else 0
|
||||
|
||||
def __update_headers_range(self, file: Path) -> int:
|
||||
self.headers["Range"] = f"bytes={(p := self.__get_resume_byte_position(file))}-"
|
||||
return p
|
||||
|
||||
@ -4,3 +4,5 @@ from .namespace import Namespace
|
||||
from .truncate import beautify_string
|
||||
from .truncate import trim_string
|
||||
from .truncate import truncate_string
|
||||
from .file_folder import file_switch
|
||||
from .file_folder import remove_empty_directories
|
||||
|
||||
21
source/expansion/file_folder.py
Normal file
21
source/expansion/file_folder.py
Normal file
@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def file_switch(path: Path) -> None:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
else:
|
||||
path.touch()
|
||||
|
||||
|
||||
def remove_empty_directories(path: Path) -> None:
|
||||
exclude = {
|
||||
"\\.",
|
||||
"\\_",
|
||||
"\\__",
|
||||
}
|
||||
for dir_path, dir_names, file_names in path.walk(top_down=False, ):
|
||||
if any(i in str(dir_path) for i in exclude):
|
||||
continue
|
||||
if not dir_names and not file_names:
|
||||
dir_path.rmdir()
|
||||
@ -17,6 +17,7 @@ from .static import SEC_CH_UA_PLATFORM
|
||||
from .static import USERAGENT
|
||||
from .static import WARNING
|
||||
from .tools import logging
|
||||
from source.expansion import remove_empty_directories
|
||||
|
||||
__all__ = ["Manager"]
|
||||
|
||||
@ -171,7 +172,9 @@ class Manager:
|
||||
async def close(self):
|
||||
await self.request_client.aclose()
|
||||
await self.download_client.aclose()
|
||||
self.__clean()
|
||||
# self.__clean()
|
||||
remove_empty_directories(self.root)
|
||||
remove_empty_directories(self.folder)
|
||||
|
||||
def __check_name_format(self, format_: str) -> str:
|
||||
keys = format_.split()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user