gemini-webapi 1.17.2__py3-none-any.whl → 1.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gemini_webapi/client.py +644 -307
- gemini_webapi/components/gem_mixin.py +35 -20
- gemini_webapi/constants.py +12 -8
- gemini_webapi/types/candidate.py +2 -0
- gemini_webapi/types/image.py +7 -6
- gemini_webapi/types/modeloutput.py +8 -0
- gemini_webapi/utils/__init__.py +1 -6
- gemini_webapi/utils/decorators.py +75 -30
- gemini_webapi/utils/get_access_token.py +52 -37
- gemini_webapi/utils/parsing.py +208 -37
- gemini_webapi/utils/rotate_1psidts.py +40 -21
- gemini_webapi/utils/upload_file.py +50 -17
- {gemini_webapi-1.17.2.dist-info → gemini_webapi-1.18.0.dist-info}/METADATA +32 -8
- gemini_webapi-1.18.0.dist-info/RECORD +25 -0
- {gemini_webapi-1.17.2.dist-info → gemini_webapi-1.18.0.dist-info}/WHEEL +1 -1
- gemini_webapi-1.17.2.dist-info/RECORD +0 -25
- {gemini_webapi-1.17.2.dist-info → gemini_webapi-1.18.0.dist-info}/licenses/LICENSE +0 -0
- {gemini_webapi-1.17.2.dist-info → gemini_webapi-1.18.0.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ import orjson as json
|
|
|
5
5
|
from ..constants import GRPC
|
|
6
6
|
from ..exceptions import APIError
|
|
7
7
|
from ..types import Gem, GemJar, RPCData
|
|
8
|
-
from ..utils import
|
|
8
|
+
from ..utils import extract_json_from_response, get_nested_value, logger
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class GemMixin:
|
|
@@ -41,7 +41,6 @@ class GemMixin:
|
|
|
41
41
|
|
|
42
42
|
return self._gems
|
|
43
43
|
|
|
44
|
-
@running(retry=2)
|
|
45
44
|
async def fetch_gems(self, include_hidden: bool = False, **kwargs) -> GemJar:
|
|
46
45
|
"""
|
|
47
46
|
Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
|
|
@@ -78,24 +77,32 @@ class GemMixin:
|
|
|
78
77
|
)
|
|
79
78
|
|
|
80
79
|
try:
|
|
81
|
-
response_json =
|
|
80
|
+
response_json = extract_json_from_response(response.text)
|
|
82
81
|
|
|
83
82
|
predefined_gems, custom_gems = [], []
|
|
84
83
|
|
|
85
84
|
for part in response_json:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
85
|
+
try:
|
|
86
|
+
identifier = get_nested_value(part, [-1])
|
|
87
|
+
part_body_str = get_nested_value(part, [2])
|
|
88
|
+
if not part_body_str:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
part_body = json.loads(part_body_str)
|
|
92
|
+
if identifier == "system":
|
|
93
|
+
predefined_gems = get_nested_value(part_body, [2], [])
|
|
94
|
+
elif identifier == "custom":
|
|
95
|
+
custom_gems = get_nested_value(part_body, [2], [])
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
continue
|
|
91
98
|
|
|
92
99
|
if not predefined_gems and not custom_gems:
|
|
93
100
|
raise Exception
|
|
94
101
|
except Exception:
|
|
95
102
|
await self.close()
|
|
96
|
-
logger.debug(f"
|
|
103
|
+
logger.debug(f"Unexpected response data structure: {response.text}")
|
|
97
104
|
raise APIError(
|
|
98
|
-
"Failed to fetch gems.
|
|
105
|
+
"Failed to fetch gems. Unexpected response data structure. Client will try to re-initialize on next request."
|
|
99
106
|
)
|
|
100
107
|
|
|
101
108
|
self._gems = GemJar(
|
|
@@ -131,7 +138,6 @@ class GemMixin:
|
|
|
131
138
|
|
|
132
139
|
return self._gems
|
|
133
140
|
|
|
134
|
-
@running(retry=2)
|
|
135
141
|
async def create_gem(self, name: str, prompt: str, description: str = "") -> Gem:
|
|
136
142
|
"""
|
|
137
143
|
Create a new custom gem.
|
|
@@ -175,19 +181,26 @@ class GemMixin:
|
|
|
175
181
|
[],
|
|
176
182
|
]
|
|
177
183
|
]
|
|
178
|
-
).decode(),
|
|
184
|
+
).decode("utf-8"),
|
|
179
185
|
)
|
|
180
186
|
]
|
|
181
187
|
)
|
|
182
188
|
|
|
183
189
|
try:
|
|
184
|
-
response_json =
|
|
185
|
-
|
|
190
|
+
response_json = extract_json_from_response(response.text)
|
|
191
|
+
part_body_str = get_nested_value(response_json, [0, 2], verbose=True)
|
|
192
|
+
if not part_body_str:
|
|
193
|
+
raise Exception
|
|
194
|
+
|
|
195
|
+
part_body = json.loads(part_body_str)
|
|
196
|
+
gem_id = get_nested_value(part_body, [0], verbose=True)
|
|
197
|
+
if not gem_id:
|
|
198
|
+
raise Exception
|
|
186
199
|
except Exception:
|
|
187
200
|
await self.close()
|
|
188
|
-
logger.debug(f"
|
|
201
|
+
logger.debug(f"Unexpected response data structure: {response.text}")
|
|
189
202
|
raise APIError(
|
|
190
|
-
"Failed to create gem.
|
|
203
|
+
"Failed to create gem. Unexpected response data structure. Client will try to re-initialize on next request."
|
|
191
204
|
)
|
|
192
205
|
|
|
193
206
|
return Gem(
|
|
@@ -198,7 +211,6 @@ class GemMixin:
|
|
|
198
211
|
predefined=False,
|
|
199
212
|
)
|
|
200
213
|
|
|
201
|
-
@running(retry=2)
|
|
202
214
|
async def update_gem(
|
|
203
215
|
self, gem: Gem | str, name: str, prompt: str, description: str = ""
|
|
204
216
|
) -> Gem:
|
|
@@ -253,7 +265,7 @@ class GemMixin:
|
|
|
253
265
|
0,
|
|
254
266
|
],
|
|
255
267
|
]
|
|
256
|
-
).decode(),
|
|
268
|
+
).decode("utf-8"),
|
|
257
269
|
)
|
|
258
270
|
]
|
|
259
271
|
)
|
|
@@ -266,7 +278,6 @@ class GemMixin:
|
|
|
266
278
|
predefined=False,
|
|
267
279
|
)
|
|
268
280
|
|
|
269
|
-
@running(retry=2)
|
|
270
281
|
async def delete_gem(self, gem: Gem | str, **kwargs) -> None:
|
|
271
282
|
"""
|
|
272
283
|
Delete a custom gem.
|
|
@@ -283,6 +294,10 @@ class GemMixin:
|
|
|
283
294
|
gem_id = gem
|
|
284
295
|
|
|
285
296
|
await self._batch_execute(
|
|
286
|
-
[
|
|
297
|
+
[
|
|
298
|
+
RPCData(
|
|
299
|
+
rpcid=GRPC.DELETE_GEM, payload=json.dumps([gem_id]).decode("utf-8")
|
|
300
|
+
)
|
|
301
|
+
],
|
|
287
302
|
**kwargs,
|
|
288
303
|
)
|
gemini_webapi/constants.py
CHANGED
|
@@ -25,6 +25,9 @@ class GRPC(StrEnum):
|
|
|
25
25
|
UPDATE_GEM = "kHv0Vd"
|
|
26
26
|
DELETE_GEM = "UXcSJb"
|
|
27
27
|
|
|
28
|
+
# Activity methods
|
|
29
|
+
BARD_ACTIVITY = "ESY5D"
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
class Headers(Enum):
|
|
30
33
|
GEMINI = {
|
|
@@ -32,7 +35,7 @@ class Headers(Enum):
|
|
|
32
35
|
"Host": "gemini.google.com",
|
|
33
36
|
"Origin": "https://gemini.google.com",
|
|
34
37
|
"Referer": "https://gemini.google.com/",
|
|
35
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
38
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
36
39
|
"X-Same-Domain": "1",
|
|
37
40
|
}
|
|
38
41
|
ROTATE_COOKIES = {
|
|
@@ -46,21 +49,21 @@ class Model(Enum):
|
|
|
46
49
|
G_3_0_PRO = (
|
|
47
50
|
"gemini-3.0-pro",
|
|
48
51
|
{
|
|
49
|
-
"x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]'
|
|
52
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4],null,null,1]'
|
|
50
53
|
},
|
|
51
54
|
False,
|
|
52
55
|
)
|
|
53
|
-
|
|
54
|
-
"gemini-
|
|
56
|
+
G_3_0_FLASH = (
|
|
57
|
+
"gemini-3.0-flash",
|
|
55
58
|
{
|
|
56
|
-
"x-goog-ext-525001261-jspb": '[1,null,null,null,"
|
|
59
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]'
|
|
57
60
|
},
|
|
58
61
|
False,
|
|
59
62
|
)
|
|
60
|
-
|
|
61
|
-
"gemini-
|
|
63
|
+
G_3_0_FLASH_THINKING = (
|
|
64
|
+
"gemini-3.0-flash-thinking",
|
|
62
65
|
{
|
|
63
|
-
"x-goog-ext-525001261-jspb": '[1,null,null,null,"
|
|
66
|
+
"x-goog-ext-525001261-jspb": '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]'
|
|
64
67
|
},
|
|
65
68
|
False,
|
|
66
69
|
)
|
|
@@ -103,6 +106,7 @@ class ErrorCode(IntEnum):
|
|
|
103
106
|
Known error codes returned from server.
|
|
104
107
|
"""
|
|
105
108
|
|
|
109
|
+
TEMPORARY_ERROR_1013 = 1013 # Randomly raised when generating with certain models, but disappears soon after
|
|
106
110
|
USAGE_LIMIT_EXCEEDED = 1037
|
|
107
111
|
MODEL_INCONSISTENT = 1050
|
|
108
112
|
MODEL_HEADER_INVALID = 1052
|
gemini_webapi/types/candidate.py
CHANGED
gemini_webapi/types/image.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
|
-
from httpx import AsyncClient, HTTPError
|
|
6
|
+
from httpx import AsyncClient, Cookies, HTTPError
|
|
6
7
|
from pydantic import BaseModel, field_validator
|
|
7
8
|
|
|
8
9
|
from ..utils import logger
|
|
@@ -39,7 +40,7 @@ class Image(BaseModel):
|
|
|
39
40
|
self,
|
|
40
41
|
path: str = "temp",
|
|
41
42
|
filename: str | None = None,
|
|
42
|
-
cookies: dict | None = None,
|
|
43
|
+
cookies: dict | Cookies | None = None,
|
|
43
44
|
verbose: bool = False,
|
|
44
45
|
skip_invalid_filename: bool = False,
|
|
45
46
|
) -> str | None:
|
|
@@ -121,16 +122,16 @@ class GeneratedImage(Image):
|
|
|
121
122
|
|
|
122
123
|
Parameters
|
|
123
124
|
----------
|
|
124
|
-
cookies: `dict`
|
|
125
|
+
cookies: `dict | httpx.Cookies`
|
|
125
126
|
Cookies used for requesting the content of the generated image, inherit from GeminiClient object or manually set.
|
|
126
127
|
Should contain valid "__Secure-1PSID" and "__Secure-1PSIDTS" values.
|
|
127
128
|
"""
|
|
128
129
|
|
|
129
|
-
cookies:
|
|
130
|
+
cookies: Any
|
|
130
131
|
|
|
131
132
|
@field_validator("cookies")
|
|
132
133
|
@classmethod
|
|
133
|
-
def validate_cookies(cls, v:
|
|
134
|
+
def validate_cookies(cls, v: Any) -> Any:
|
|
134
135
|
if len(v) == 0:
|
|
135
136
|
raise ValueError(
|
|
136
137
|
"GeneratedImage is designed to be initialized with same cookies as GeminiClient."
|
|
@@ -142,7 +143,7 @@ class GeneratedImage(Image):
|
|
|
142
143
|
self,
|
|
143
144
|
path: str = "temp",
|
|
144
145
|
filename: str | None = None,
|
|
145
|
-
cookies: dict | None = None,
|
|
146
|
+
cookies: dict | Cookies | None = None,
|
|
146
147
|
verbose: bool = False,
|
|
147
148
|
skip_invalid_filename: bool = False,
|
|
148
149
|
full_size: bool = True,
|
|
@@ -32,10 +32,18 @@ class ModelOutput(BaseModel):
|
|
|
32
32
|
def text(self) -> str:
|
|
33
33
|
return self.candidates[self.chosen].text
|
|
34
34
|
|
|
35
|
+
@property
|
|
36
|
+
def text_delta(self) -> str:
|
|
37
|
+
return self.candidates[self.chosen].text_delta or ""
|
|
38
|
+
|
|
35
39
|
@property
|
|
36
40
|
def thoughts(self) -> str | None:
|
|
37
41
|
return self.candidates[self.chosen].thoughts
|
|
38
42
|
|
|
43
|
+
@property
|
|
44
|
+
def thoughts_delta(self) -> str:
|
|
45
|
+
return self.candidates[self.chosen].thoughts_delta or ""
|
|
46
|
+
|
|
39
47
|
@property
|
|
40
48
|
def images(self) -> list[Image]:
|
|
41
49
|
return self.candidates[self.chosen].images
|
gemini_webapi/utils/__init__.py
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
# flake8: noqa
|
|
2
2
|
|
|
3
|
-
from asyncio import Task
|
|
4
|
-
|
|
5
3
|
from .decorators import running
|
|
6
4
|
from .get_access_token import get_access_token
|
|
7
5
|
from .load_browser_cookies import load_browser_cookies
|
|
8
6
|
from .logger import logger, set_log_level
|
|
9
|
-
from .parsing import extract_json_from_response, get_nested_value
|
|
7
|
+
from .parsing import extract_json_from_response, get_nested_value, parse_stream_frames
|
|
10
8
|
from .rotate_1psidts import rotate_1psidts
|
|
11
9
|
from .upload_file import upload_file, parse_file_name
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
rotate_tasks: dict[str, Task] = {}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import functools
|
|
3
|
+
import inspect
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
|
|
5
|
-
from ..exceptions import APIError
|
|
6
|
+
from ..exceptions import APIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DELAY_FACTOR = 5
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
def running(retry: int = 0) -> Callable:
|
|
9
13
|
"""
|
|
10
14
|
Decorator to check if GeminiClient is running before making a request.
|
|
15
|
+
Supports both regular async functions and async generators.
|
|
11
16
|
|
|
12
17
|
Parameters
|
|
13
18
|
----------
|
|
@@ -16,38 +21,78 @@ def running(retry: int = 0) -> Callable:
|
|
|
16
21
|
"""
|
|
17
22
|
|
|
18
23
|
def decorator(func):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
if inspect.isasyncgenfunction(func):
|
|
25
|
+
|
|
26
|
+
@functools.wraps(func)
|
|
27
|
+
async def wrapper(client, *args, current_retry=None, **kwargs):
|
|
28
|
+
if current_retry is None:
|
|
29
|
+
current_retry = retry
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
if not client._running:
|
|
33
|
+
await client.init(
|
|
34
|
+
timeout=client.timeout,
|
|
35
|
+
auto_close=client.auto_close,
|
|
36
|
+
close_delay=client.close_delay,
|
|
37
|
+
auto_refresh=client.auto_refresh,
|
|
38
|
+
refresh_interval=client.refresh_interval,
|
|
39
|
+
verbose=client.verbose,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not client._running:
|
|
43
|
+
raise APIError(
|
|
44
|
+
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async for item in func(client, *args, **kwargs):
|
|
48
|
+
yield item
|
|
49
|
+
except APIError:
|
|
50
|
+
if current_retry > 0:
|
|
51
|
+
delay = (retry - current_retry + 1) * DELAY_FACTOR
|
|
52
|
+
await asyncio.sleep(delay)
|
|
53
|
+
async for item in wrapper(
|
|
54
|
+
client, *args, current_retry=current_retry - 1, **kwargs
|
|
55
|
+
):
|
|
56
|
+
yield item
|
|
57
|
+
else:
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
else:
|
|
62
|
+
|
|
63
|
+
@functools.wraps(func)
|
|
64
|
+
async def wrapper(client, *args, current_retry=None, **kwargs):
|
|
65
|
+
if current_retry is None:
|
|
66
|
+
current_retry = retry
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
if not client._running:
|
|
70
|
+
await client.init(
|
|
71
|
+
timeout=client.timeout,
|
|
72
|
+
auto_close=client.auto_close,
|
|
73
|
+
close_delay=client.close_delay,
|
|
74
|
+
auto_refresh=client.auto_refresh,
|
|
75
|
+
refresh_interval=client.refresh_interval,
|
|
76
|
+
verbose=client.verbose,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not client._running:
|
|
80
|
+
raise APIError(
|
|
81
|
+
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
82
|
+
)
|
|
83
|
+
|
|
39
84
|
return await func(client, *args, **kwargs)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
85
|
+
except APIError:
|
|
86
|
+
if current_retry > 0:
|
|
87
|
+
delay = (retry - current_retry + 1) * DELAY_FACTOR
|
|
88
|
+
await asyncio.sleep(delay)
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
90
|
+
return await wrapper(
|
|
91
|
+
client, *args, current_retry=current_retry - 1, **kwargs
|
|
92
|
+
)
|
|
48
93
|
|
|
49
|
-
|
|
94
|
+
raise
|
|
50
95
|
|
|
51
|
-
|
|
96
|
+
return wrapper
|
|
52
97
|
|
|
53
98
|
return decorator
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
from asyncio import Task
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from httpx import AsyncClient, Response
|
|
7
|
+
from httpx import AsyncClient, Cookies, Response
|
|
8
8
|
|
|
9
9
|
from ..constants import Endpoint, Headers
|
|
10
10
|
from ..exceptions import AuthError
|
|
@@ -13,8 +13,8 @@ from .logger import logger
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
async def send_request(
|
|
16
|
-
cookies: dict, proxy: str | None = None
|
|
17
|
-
) -> tuple[Response | None,
|
|
16
|
+
cookies: dict | Cookies, proxy: str | None = None
|
|
17
|
+
) -> tuple[Response | None, Cookies]:
|
|
18
18
|
"""
|
|
19
19
|
Send http request with provided cookies.
|
|
20
20
|
"""
|
|
@@ -25,16 +25,18 @@ async def send_request(
|
|
|
25
25
|
headers=Headers.GEMINI.value,
|
|
26
26
|
cookies=cookies,
|
|
27
27
|
follow_redirects=True,
|
|
28
|
-
verify=False,
|
|
29
28
|
) as client:
|
|
30
|
-
response = await client.get(Endpoint.INIT
|
|
29
|
+
response = await client.get(Endpoint.INIT)
|
|
31
30
|
response.raise_for_status()
|
|
32
|
-
return response, cookies
|
|
31
|
+
return response, client.cookies
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
async def get_access_token(
|
|
36
|
-
base_cookies: dict
|
|
37
|
-
|
|
35
|
+
base_cookies: dict | Cookies,
|
|
36
|
+
proxy: str | None = None,
|
|
37
|
+
verbose: bool = False,
|
|
38
|
+
verify: bool = True,
|
|
39
|
+
) -> tuple[str, Cookies, str | None, str | None]:
|
|
38
40
|
"""
|
|
39
41
|
Send a get request to gemini.google.com for each group of available cookies and return
|
|
40
42
|
the value of "SNlM0e" as access token on the first successful request.
|
|
@@ -46,19 +48,19 @@ async def get_access_token(
|
|
|
46
48
|
|
|
47
49
|
Parameters
|
|
48
50
|
----------
|
|
49
|
-
base_cookies : `dict`
|
|
51
|
+
base_cookies : `dict | httpx.Cookies`
|
|
50
52
|
Base cookies to be used in the request.
|
|
51
53
|
proxy: `str`, optional
|
|
52
54
|
Proxy URL.
|
|
53
55
|
verbose: `bool`, optional
|
|
54
56
|
If `True`, will print more infomation in logs.
|
|
57
|
+
verify: `bool`, optional
|
|
58
|
+
Whether to verify SSL certificates.
|
|
55
59
|
|
|
56
60
|
Returns
|
|
57
61
|
-------
|
|
58
|
-
`str`
|
|
59
|
-
|
|
60
|
-
`dict`
|
|
61
|
-
Cookies of the successful request.
|
|
62
|
+
`tuple[str, str | None, str | None, Cookies]`
|
|
63
|
+
By order: access token; build label; session id; cookies of the successful request.
|
|
62
64
|
|
|
63
65
|
Raises
|
|
64
66
|
------
|
|
@@ -67,22 +69,22 @@ async def get_access_token(
|
|
|
67
69
|
"""
|
|
68
70
|
|
|
69
71
|
async with AsyncClient(
|
|
70
|
-
http2=True,
|
|
71
|
-
proxy=proxy,
|
|
72
|
-
follow_redirects=True,
|
|
73
|
-
verify=False,
|
|
72
|
+
http2=True, proxy=proxy, follow_redirects=True, verify=verify
|
|
74
73
|
) as client:
|
|
75
|
-
response = await client.get(Endpoint.GOOGLE
|
|
74
|
+
response = await client.get(Endpoint.GOOGLE)
|
|
76
75
|
|
|
77
|
-
extra_cookies =
|
|
76
|
+
extra_cookies = Cookies()
|
|
78
77
|
if response.status_code == 200:
|
|
79
78
|
extra_cookies = response.cookies
|
|
80
79
|
|
|
81
80
|
tasks = []
|
|
82
81
|
|
|
83
82
|
# Base cookies passed directly on initializing client
|
|
83
|
+
# We use a Jar to merge extra_cookies and base_cookies safely (preserving domains)
|
|
84
84
|
if "__Secure-1PSID" in base_cookies and "__Secure-1PSIDTS" in base_cookies:
|
|
85
|
-
|
|
85
|
+
jar = Cookies(extra_cookies)
|
|
86
|
+
jar.update(base_cookies)
|
|
87
|
+
tasks.append(Task(send_request(jar, proxy=proxy)))
|
|
86
88
|
elif verbose:
|
|
87
89
|
logger.debug(
|
|
88
90
|
"Skipping loading base cookies. Either __Secure-1PSID or __Secure-1PSIDTS is not provided."
|
|
@@ -94,18 +96,25 @@ async def get_access_token(
|
|
|
94
96
|
and Path(GEMINI_COOKIE_PATH)
|
|
95
97
|
or (Path(__file__).parent / "temp")
|
|
96
98
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
|
|
100
|
+
# Safely get __Secure-1PSID value
|
|
101
|
+
if isinstance(base_cookies, Cookies):
|
|
102
|
+
secure_1psid = base_cookies.get(
|
|
103
|
+
"__Secure-1PSID", domain=".google.com"
|
|
104
|
+
) or base_cookies.get("__Secure-1PSID")
|
|
105
|
+
else:
|
|
106
|
+
secure_1psid = base_cookies.get("__Secure-1PSID")
|
|
107
|
+
|
|
108
|
+
if secure_1psid:
|
|
109
|
+
filename = f".cached_1psidts_{secure_1psid}.txt"
|
|
99
110
|
cache_file = cache_dir / filename
|
|
100
111
|
if cache_file.is_file():
|
|
101
112
|
cached_1psidts = cache_file.read_text()
|
|
102
113
|
if cached_1psidts:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
tasks.append(Task(send_request(cached_cookies, proxy=proxy)))
|
|
114
|
+
jar = Cookies(extra_cookies)
|
|
115
|
+
jar.update(base_cookies)
|
|
116
|
+
jar.set("__Secure-1PSIDTS", cached_1psidts, domain=".google.com")
|
|
117
|
+
tasks.append(Task(send_request(jar, proxy=proxy)))
|
|
109
118
|
elif verbose:
|
|
110
119
|
logger.debug("Skipping loading cached cookies. Cache file is empty.")
|
|
111
120
|
elif verbose:
|
|
@@ -116,12 +125,11 @@ async def get_access_token(
|
|
|
116
125
|
for cache_file in cache_files:
|
|
117
126
|
cached_1psidts = cache_file.read_text()
|
|
118
127
|
if cached_1psidts:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
tasks.append(Task(send_request(cached_cookies, proxy=proxy)))
|
|
128
|
+
jar = Cookies(extra_cookies)
|
|
129
|
+
psid = cache_file.stem[16:]
|
|
130
|
+
jar.set("__Secure-1PSID", psid, domain=".google.com")
|
|
131
|
+
jar.set("__Secure-1PSIDTS", cached_1psidts, domain=".google.com")
|
|
132
|
+
tasks.append(Task(send_request(jar, proxy=proxy)))
|
|
125
133
|
valid_caches += 1
|
|
126
134
|
|
|
127
135
|
if valid_caches == 0 and verbose:
|
|
@@ -180,13 +188,20 @@ async def get_access_token(
|
|
|
180
188
|
for i, future in enumerate(asyncio.as_completed(tasks)):
|
|
181
189
|
try:
|
|
182
190
|
response, request_cookies = await future
|
|
183
|
-
|
|
184
|
-
if
|
|
191
|
+
snlm0e = re.search(r'"SNlM0e":\s*"(.*?)"', response.text)
|
|
192
|
+
if snlm0e:
|
|
193
|
+
cfb2h = re.search(r'"cfb2h":\s*"(.*?)"', response.text)
|
|
194
|
+
fdrfje = re.search(r'"FdrFJe":\s*"(.*?)"', response.text)
|
|
185
195
|
if verbose:
|
|
186
196
|
logger.debug(
|
|
187
197
|
f"Init attempt ({i + 1}/{len(tasks)}) succeeded. Initializing client..."
|
|
188
198
|
)
|
|
189
|
-
return
|
|
199
|
+
return (
|
|
200
|
+
snlm0e.group(1),
|
|
201
|
+
cfb2h.group(1) if cfb2h else None,
|
|
202
|
+
fdrfje.group(1) if fdrfje else None,
|
|
203
|
+
request_cookies,
|
|
204
|
+
)
|
|
190
205
|
elif verbose:
|
|
191
206
|
logger.debug(
|
|
192
207
|
f"Init attempt ({i + 1}/{len(tasks)}) failed. Cookies invalid."
|