understanding commited on
Commit
212e9ab
·
verified ·
1 Parent(s): b6c28c9

Create youtube/resume.py

Browse files
Files changed (1) hide show
  1. bot/youtube/resume.py +79 -0
bot/youtube/resume.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ async def start_resumable_session(access_token: str, metadata: dict) -> str:
12
+ """
13
+ Returns upload_url (Location header)
14
+ """
15
+ headers = {
16
+ "Authorization": f"Bearer {access_token}",
17
+ "Content-Type": "application/json; charset=UTF-8",
18
+ "X-Upload-Content-Type": "video/*",
19
+ }
20
+ params = {"uploadType": "resumable", "part": "snippet,status"}
21
+ async with httpx.AsyncClient(timeout=60, follow_redirects=True) as c:
22
+ r = await c.post(YOUTUBE_RESUMABLE_INIT, params=params, headers=headers, json=metadata)
23
+ if r.status_code >= 400:
24
+ raise RuntimeError(f"resumable_init_failed:{r.status_code}:{r.text[:200]}")
25
+ upload_url = r.headers.get("Location") or r.headers.get("location")
26
+ if not upload_url:
27
+ raise RuntimeError("resumable_init_no_location")
28
+ return upload_url
29
+
30
+ async def upload_resumable(
31
+ upload_url: str,
32
+ file_path: str,
33
+ access_token: str,
34
+ progress_cb: ProgressCB | None = None,
35
+ ) -> dict:
36
+ """
37
+ Upload file in chunks to upload_url.
38
+ Returns YouTube API JSON (contains id).
39
+ """
40
+ total = os.path.getsize(file_path)
41
+ sent = 0
42
+
43
+ headers_base = {
44
+ "Authorization": f"Bearer {access_token}",
45
+ }
46
+
47
+ async with httpx.AsyncClient(timeout=120, follow_redirects=True) as c:
48
+ with open(file_path, "rb") as f:
49
+ while sent < total:
50
+ chunk = f.read(YOUTUBE_CHUNK_SIZE)
51
+ if not chunk:
52
+ break
53
+ start = sent
54
+ end = sent + len(chunk) - 1
55
+
56
+ headers = dict(headers_base)
57
+ headers["Content-Length"] = str(len(chunk))
58
+ headers["Content-Type"] = "video/*"
59
+ headers["Content-Range"] = f"bytes {start}-{end}/{total}"
60
+
61
+ r = await c.put(upload_url, headers=headers, content=chunk)
62
+
63
+ # 308 Resume Incomplete is normal for chunked upload
64
+ if r.status_code == 308:
65
+ sent = end + 1
66
+ if progress_cb:
67
+ await progress_cb(sent, total)
68
+ continue
69
+
70
+ if r.status_code >= 400:
71
+ raise RuntimeError(f"resumable_put_failed:{r.status_code}:{r.text[:200]}")
72
+
73
+ # success: final response JSON
74
+ j = r.json()
75
+ if progress_cb:
76
+ await progress_cb(total, total)
77
+ return j
78
+
79
+ raise RuntimeError("resumable_upload_incomplete")