Skip to content

Commit 2eba5a6

Browse files
authored
fix: finish chunks (#7)
2 parents 1b231e5 + f476610 commit 2eba5a6

File tree

5 files changed

+56
-27
lines changed

5 files changed

+56
-27
lines changed

bunkrr_uploader/api/api.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,28 @@
1919

2020
logger = logging.getLogger("bunkr-uploader")
2121

22+
DEFAULT_HEADERS = {
23+
"Accept": "application/json, text/plain, */*",
24+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
25+
"Referer": "https://dash.bunkr.cr/",
26+
"striptags": "null",
27+
"Origin": "https://dash.bunkr.cr",
28+
"Sec-Fetch-Dest": "empty",
29+
"Sec-Fetch-Mode": "cors",
30+
"Sec-Fetch-Site": "cross-site",
31+
"Pragma": "no-cache",
32+
}
33+
2234

2335
class BunkrrAPI:
2436
RATE_LIMIT = 50
2537

2638
def __init__(self, token: str, chunk_size: int | None = None):
2739
self._token = token
2840
self._api_entrypoint = URL("https://dash.bunkr.cr/api/")
29-
self._session_headers = {
30-
"Accept": "application/json",
31-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0",
32-
"token": self._token,
33-
}
41+
self._session_headers = DEFAULT_HEADERS | {"token": self._token}
3442
self._session = ClientSession(self._api_entrypoint, headers=self._session_headers)
35-
self._chunk_size: int = chunk_size # type: ignore
43+
self._chunk_size: int = chunk_size or 0
3644
self._info = None
3745
self._semaphore = asyncio.Semaphore(self.RATE_LIMIT)
3846
self._server_sessions: dict[URL, ClientSession] = {}
@@ -50,15 +58,22 @@ async def _get_json(self, path: str) -> dict:
5058
resp.raise_for_status()
5159
response: dict = await resp.json()
5260
record = {"url": str(resp.url), "headers": dict(resp.headers), "response": response}
53-
logger.debug(f"response: \n {json.dumps(record, indent=4)}")
61+
logger.debug(f"response: \n {json.dumps(record, indent=4, ensure_ascii=False)}")
5462
return response
5563

5664
async def _post(self, path: str, *, data: FormData | dict | None = None, server: URL | None = None) -> dict:
5765
data = data or {}
5866
if isinstance(data, dict) and "finishchunks" not in path:
5967
data["token"] = data.get("token") or self._token
60-
session = self.server_sessions.get(server) or self._session # type: ignore
61-
async with self._semaphore, session.post(path, data=data) as resp:
68+
69+
session = self.server_sessions.get(server) if server else None
70+
session = session or self._session
71+
headers = session.headers
72+
if "finishchunks" in path:
73+
headers = dict(session.headers) | {"Content-Type": "application/json;charset=utf-8"}
74+
data = json.dumps(data) # type: ignore
75+
76+
async with self._semaphore, session.post(path, data=data, headers=headers) as resp:
6277
resp.raise_for_status()
6378
response = await resp.json()
6479
record = {"url": str(resp.url), "headers": dict(resp.headers), "response": response}
@@ -116,10 +131,12 @@ async def create_album(
116131
async def upload(self, file: FileInfo | Path, server: URL, album_id: str | None = None) -> UploadResponse:
117132
if isinstance(file, Path):
118133
file = FileInfo(file, album_id=album_id)
134+
119135
file_info = file
120136
assert file_info.size <= self.info.maxSize
121137
async with aiofiles.open(file_info.path, "rb") as file_data:
122138
chunk_data = await file_data.read(self._chunk_size)
139+
123140
data = FormData()
124141
data.add_field("files[]", chunk_data, filename=file_info.path.name, content_type=file_info.mimetype)
125142
if album_id:
@@ -130,5 +147,6 @@ async def upload(self, file: FileInfo | Path, server: URL, album_id: str | None
130147

131148
async def finish_chunks(self, file_info: FileInfo, server: URL):
132149
data = {"files": [file_info.dump_json()]}
150+
logger.info(data)
133151
response = await self._post("upload/finishchunks", data=data, server=server)
134152
return UploadResponse(**response)

bunkrr_uploader/api/files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def dump_json(self) -> dict:
5151
return {
5252
"uuid": self.uuid,
5353
"original": self.original_name,
54-
"type": "",
54+
"type": self.mimetype,
5555
"albumid": self.album_id or None,
5656
"filelength": None,
5757
"age": None,

bunkrr_uploader/api/responses.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# ruff: noqa: N815
22
from datetime import datetime, timedelta
3-
from typing import Annotated
3+
from typing import Annotated, TypedDict
44

5-
from pydantic import AfterValidator, BaseModel, ByteSize, HttpUrl
5+
from pydantic import AfterValidator, BaseModel, ByteSize, ConfigDict, HttpUrl
66
from yarl import URL
77

88
HttpURL = Annotated[HttpUrl, AfterValidator(lambda x: URL(str(x)))]
@@ -14,21 +14,21 @@ class ChunkSize(BaseModel):
1414
timeout: timedelta
1515

1616

17-
class FileIdentifierLength(BaseModel):
17+
class FileIdentifierLength(TypedDict):
1818
min: int
1919
max: int
2020
default: int
2121
force: bool
2222

2323

24-
class StripTags(BaseModel):
24+
class StripTags(TypedDict):
2525
blacklistExtensions: set[str]
2626
default: bool
2727
force: bool
2828
video: bool
2929

3030

31-
class Permissions(BaseModel):
31+
class Permissions(TypedDict):
3232
admin: bool
3333
moderator: bool
3434
superadmin: bool
@@ -38,14 +38,19 @@ class Permissions(BaseModel):
3838

3939

4040
class BunkrrResponse(BaseModel):
41+
model_config = ConfigDict(populate_by_name=True)
4142
description: str = ""
42-
success: bool = False
43+
success: bool = True
4344

4445

4546
class UploadItemResponse(BunkrrResponse):
4647
name: str
4748
url: HttpURL | None
4849

50+
def model_post_init(self, *_):
51+
if self.url is None:
52+
self.success = False
53+
4954

5055
class UploadResponse(BunkrrResponse):
5156
files: list[UploadItemResponse] = []

bunkrr_uploader/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ async def async_main() -> None:
1414
logger.debug(f"Using params: \n {args.model_dump_json(indent=4)}")
1515
bunkrr_client = BunkrrUploader(**args.model_dump())
1616
try:
17-
await bunkrr_client.upload(args.path, album_name=args.album_name)
17+
responses = await bunkrr_client.upload(args.path, album_name=args.album_name)
18+
for r in responses:
19+
logger.info(r.model_dump_json(indent=4))
20+
1821
finally:
1922
await bunkrr_client.close()
2023

bunkrr_uploader/uploader.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import asyncio
44
import logging
5-
from dataclasses import asdict, dataclass, fields
5+
from dataclasses import dataclass, fields
66
from typing import TYPE_CHECKING
77

88
import aiofiles
@@ -26,26 +26,28 @@
2626

2727
@dataclass(frozen=True, slots=True)
2828
class BunkrUploaderSettings:
29+
path: Path
2930
concurrent_uploads: int = 1
3031
chunk_size: ByteSize | None = None
3132
upload_retries: int = 1
3233
use_max_chunk_size: bool = False
3334
chunk_retries: int = 2
3435
upload_delay: float = 0.5
3536

36-
def update(self, **kwargs) -> BunkrUploaderSettings:
37-
cls_fields = fields(self)
37+
@classmethod
38+
def update(cls, **kwargs) -> BunkrUploaderSettings:
39+
cls_fields = fields(cls)
3840
cls_fields_names = [f.name for f in cls_fields]
3941
valid_kwargs = {k: v for k, v in kwargs.items() if k in cls_fields_names}
4042
if not valid_kwargs:
4143
msg = "None of the provided attribute is in the class"
4244
raise ValueError(msg)
43-
values = asdict(self) | valid_kwargs
44-
return BunkrUploaderSettings(**values)
45+
return BunkrUploaderSettings(**valid_kwargs)
4546

4647

4748
class BunkrrUploader:
48-
def __init__(self, token: str, settings: BunkrUploaderSettings):
49+
def __init__(self, token: str, **kwargs):
50+
settings = BunkrUploaderSettings.update(**kwargs)
4951
self._api = BunkrrAPI(token, settings.chunk_size)
5052
self.settings = settings
5153
assert self.settings.concurrent_uploads <= self._api.RATE_LIMIT
@@ -68,7 +70,7 @@ def _prepare_files(self, files: list[Path]) -> list[FileInfo]:
6870
files_to_upload = []
6971
for file in files:
7072
file_info = FileInfo(file)
71-
if file.suffix.casefold() in self._api.info.stripTags.blacklistExtensions:
73+
if file.suffix.casefold() in self._api.info.stripTags["blacklistExtensions"]:
7274
logger.error(f"File {file} has blacklisted extension {file.suffix}")
7375

7476
elif file_info.size > self._api.info.maxSize:
@@ -198,6 +200,7 @@ async def upload(self, path: Path, recurse: bool = False, album_name: str | None
198200

199201
if not self._ready:
200202
await self.startup()
203+
201204
files_to_upload = self._prepare_files(files_to_upload)
202205
if not files_to_upload:
203206
logger.error("No files left to upload")
@@ -217,8 +220,8 @@ async def worker(file_info: FileInfo, server: URL) -> UploadResponse:
217220
logger.error(str(e), exc_info=True)
218221
return UploadResponse(**default_response)
219222

220-
responses = []
221-
tasks = []
223+
responses: list[UploadResponse] = []
224+
tasks: list = []
222225
for file_info in files_to_upload:
223226
default_response = {"success": False, "files": [file_info.as_item]}
224227
server = await self._get_server(album_id)
@@ -228,7 +231,7 @@ async def worker(file_info: FileInfo, server: URL) -> UploadResponse:
228231
tasks.append(asyncio.create_task(worker(file_info, server)))
229232

230233
uploads = await asyncio.gather(*tasks)
231-
responses.append(uploads)
234+
responses.extend(uploads)
232235
return responses
233236

234237
async def close(self) -> None:

0 commit comments

Comments
 (0)