understanding commited on
Commit
f27ed93
·
verified ·
1 Parent(s): 341445e

Update bot/youtube/resume.py

Browse files
Files changed (1) hide show
  1. bot/youtube/resume.py +52 -70
bot/youtube/resume.py CHANGED
@@ -1,101 +1,83 @@
1
  # PATH: bot/youtube/resume.py
2
- import asyncio
3
  import os
4
- from typing import Callable, Awaitable, Optional
5
  import httpx
6
-
7
- from bot.core.settings import YOUTUBE_CHUNK_SIZE, HTTP_TIMEOUT_SEC
8
 
9
  ProgressCB = Callable[[int, int], Awaitable[None]]
10
 
11
- YOUTUBE_RESUMABLE_ENDPOINT = (
12
- "https://www.googleapis.com/upload/youtube/v3/videos"
13
- "?uploadType=resumable&part=snippet,status"
14
- )
15
 
16
- async def start_resumable_session(access_token: str, metadata: dict, mime_type: str = "video/mp4") -> str:
 
 
 
17
  headers = {
18
  "Authorization": f"Bearer {access_token}",
19
  "Content-Type": "application/json; charset=UTF-8",
20
- "X-Upload-Content-Type": mime_type,
21
  }
 
22
 
23
- async with httpx.AsyncClient(timeout=HTTP_TIMEOUT_SEC, follow_redirects=True) as client:
24
- r = await client.post(YOUTUBE_RESUMABLE_ENDPOINT, headers=headers, json=metadata)
25
- if r.status_code not in (200, 201):
26
- raise RuntimeError(f"initiate_failed status={r.status_code} body={r.text[:300]}")
27
- upload_url = r.headers.get("Location")
28
  if not upload_url:
29
- raise RuntimeError("initiate_failed: missing Location header")
30
  return upload_url
31
 
32
- async def _query_offset(client: httpx.AsyncClient, upload_url: str, total: int) -> int:
33
- r = await client.put(
34
- upload_url,
35
- headers={"Content-Range": f"bytes */{total}", "Content-Length": "0"},
36
- content=b"",
37
- )
38
- if r.status_code == 308:
39
- rng = r.headers.get("Range")
40
- if rng and "-" in rng:
41
- return int(rng.split("-")[1]) + 1
42
- return 0
43
- if r.status_code in (200, 201):
44
- return total
45
- return 0
46
-
47
- async def upload_resumable(upload_url: str, file_path: str, progress_cb: Optional[ProgressCB] = None) -> dict:
48
  total = os.path.getsize(file_path)
49
- timeout = httpx.Timeout(HTTP_TIMEOUT_SEC, connect=HTTP_TIMEOUT_SEC)
50
 
51
- async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
52
- offset = 0
53
- retries = 0
54
 
 
55
  with open(file_path, "rb") as f:
56
- while offset < total:
57
- f.seek(offset)
58
  chunk = f.read(YOUTUBE_CHUNK_SIZE)
59
  if not chunk:
60
  break
61
- end = offset + len(chunk) - 1
62
-
63
- try:
64
- r = await client.put(
65
- upload_url,
66
- headers={
67
- "Content-Length": str(len(chunk)),
68
- "Content-Range": f"bytes {offset}-{end}/{total}",
69
- },
70
- content=chunk,
71
- )
72
- except Exception:
73
- retries += 1
74
- if retries > 5:
75
- raise
76
- await asyncio.sleep(min(5 * retries, 20))
77
- offset = await _query_offset(client, upload_url, total)
78
- continue
79
 
80
- if r.status_code in (200, 201):
81
- if progress_cb:
82
- await progress_cb(total, total)
83
- return r.json()
 
 
 
84
 
 
 
 
85
  if r.status_code == 308:
86
- retries = 0
87
- rng = r.headers.get("Range")
88
- offset = (int(rng.split("-")[1]) + 1) if (rng and "-" in rng) else (end + 1)
89
  if progress_cb:
90
- await progress_cb(offset, total)
91
  continue
92
 
93
- if r.status_code in (500, 502, 503, 504):
94
- retries += 1
95
- await asyncio.sleep(min(5 * retries, 20))
96
- offset = await _query_offset(client, upload_url, total)
97
- continue
98
 
99
- raise RuntimeError(f"upload_failed status={r.status_code} body={r.text[:300]}")
 
 
 
100
 
101
  raise RuntimeError("resumable_upload_incomplete")
 
1
  # PATH: bot/youtube/resume.py
 
2
  import os
3
+ from typing import Callable, Awaitable
4
  import httpx
5
+ from bot.core.settings import YOUTUBE_CHUNK_SIZE
 
6
 
7
  ProgressCB = Callable[[int, int], Awaitable[None]]
8
 
9
+ YOUTUBE_RESUMABLE_INIT = "https://www.googleapis.com/upload/youtube/v3/videos"
10
+
 
 
11
 
12
+ async def start_resumable_session(access_token: str, metadata: dict) -> str:
13
+ """
14
+ Returns upload_url (Location header)
15
+ """
16
  headers = {
17
  "Authorization": f"Bearer {access_token}",
18
  "Content-Type": "application/json; charset=UTF-8",
19
+ "X-Upload-Content-Type": "video/*",
20
  }
21
+ params = {"uploadType": "resumable", "part": "snippet,status"}
22
 
23
+ async with httpx.AsyncClient(timeout=60, follow_redirects=True) as c:
24
+ r = await c.post(YOUTUBE_RESUMABLE_INIT, params=params, headers=headers, json=metadata)
25
+ if r.status_code >= 400:
26
+ raise RuntimeError(f"resumable_init_failed:{r.status_code}:{r.text[:200]}")
27
+ upload_url = r.headers.get("Location") or r.headers.get("location")
28
  if not upload_url:
29
+ raise RuntimeError("resumable_init_no_location")
30
  return upload_url
31
 
32
+
33
+ async def upload_resumable(
34
+ upload_url: str,
35
+ file_path: str,
36
+ access_token: str,
37
+ *,
38
+ progress_cb: ProgressCB | None = None, # ✅ keyword-only (prevents “multiple values”)
39
+ ) -> dict:
40
+ """
41
+ Upload file in chunks to upload_url.
42
+ Returns YouTube API JSON (contains id).
43
+ """
 
 
 
 
44
  total = os.path.getsize(file_path)
45
+ sent = 0
46
 
47
+ headers_base = {
48
+ "Authorization": f"Bearer {access_token}",
49
+ }
50
 
51
+ async with httpx.AsyncClient(timeout=120, follow_redirects=True) as c:
52
  with open(file_path, "rb") as f:
53
+ while sent < total:
 
54
  chunk = f.read(YOUTUBE_CHUNK_SIZE)
55
  if not chunk:
56
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ start = sent
59
+ end = sent + len(chunk) - 1
60
+
61
+ headers = dict(headers_base)
62
+ headers["Content-Length"] = str(len(chunk))
63
+ headers["Content-Type"] = "video/*"
64
+ headers["Content-Range"] = f"bytes {start}-{end}/{total}"
65
 
66
+ r = await c.put(upload_url, headers=headers, content=chunk)
67
+
68
+ # 308 Resume Incomplete is normal for chunked upload
69
  if r.status_code == 308:
70
+ sent = end + 1
 
 
71
  if progress_cb:
72
+ await progress_cb(sent, total)
73
  continue
74
 
75
+ if r.status_code >= 400:
76
+ raise RuntimeError(f"resumable_put_failed:{r.status_code}:{r.text[:200]}")
 
 
 
77
 
78
+ j = r.json()
79
+ if progress_cb:
80
+ await progress_cb(total, total)
81
+ return j
82
 
83
  raise RuntimeError("resumable_upload_incomplete")