feat: 新增文件断点续传功能

1. 新增自动删除空文件夹功能
2. 引入 aiofiles 库
3. 修正 Actions 错误

Closes #142
Closes #143
This commit is contained in:
JoeanAmier 2024-08-07 21:38:46 +08:00
parent 24dc8a1a53
commit 7cedd8d4b8
8 changed files with 93 additions and 18 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -8,3 +8,4 @@ rookiepy>=0.5.2
httpx>=0.27.0
fastapi>=0.111.0
uvicorn>=0.30.1
aiofiles>=24.1.0

View File

@ -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:
response.raise_for_status()
suffix = self.__extract_type(
response.headers.get("Content-Type")) or format_
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()
# 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

View File

@ -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

View 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()

View File

@ -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()