Streamline connection retry code
This commit is contained in:
1 parent
038fac48f9
commit
d90d20a42c
2 files changed
+57
-39
No files matched your search
+12
-18
@@ -9,10 +9,8 @@ from functools import cached_property
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from exceptions import MinerException
|
|
||||||
from utils import Game, invalidate_cache
|
from utils import Game, invalidate_cache
|
||||||
|
from exceptions import MinerException, RequestException
|
||||||
from constants import JsonType, BASE_URL, GQL_OPERATIONS, ONLINE_DELAY, DROPS_ENABLED_TAG
|
from constants import JsonType, BASE_URL, GQL_OPERATIONS, ONLINE_DELAY, DROPS_ENABLED_TAG
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -195,9 +193,8 @@ class Channel:
|
|||||||
To get this monstrous thing, you have to walk a chain of requests.
|
To get this monstrous thing, you have to walk a chain of requests.
|
||||||
Streamer page (HTML) --parse-> Streamer Settings (JavaScript) --parse-> Spade URL
|
Streamer page (HTML) --parse-> Streamer Settings (JavaScript) --parse-> Spade URL
|
||||||
"""
|
"""
|
||||||
assert self._twitch._session is not None
|
async with self._twitch.request("GET", self.url) as response:
|
||||||
async with self._twitch._session.get(self.url) as response:
|
streamer_html: str = await response.text(encoding="utf8")
|
||||||
streamer_html = await response.text(encoding="utf8")
|
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r'src="(https://static\.twitchcdn\.net/config/settings\.[0-9a-f]{32}\.js)"',
|
r'src="(https://static\.twitchcdn\.net/config/settings\.[0-9a-f]{32}\.js)"',
|
||||||
streamer_html,
|
streamer_html,
|
||||||
@@ -206,8 +203,8 @@ class Channel:
|
|||||||
if not match:
|
if not match:
|
||||||
raise MinerException("Error while spade_url extraction: step #1")
|
raise MinerException("Error while spade_url extraction: step #1")
|
||||||
streamer_settings = match.group(1)
|
streamer_settings = match.group(1)
|
||||||
async with self._twitch._session.get(streamer_settings) as response:
|
async with self._twitch.request("GET", streamer_settings) as response:
|
||||||
settings_js = await response.text(encoding="utf8")
|
settings_js: str = await response.text(encoding="utf8")
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r'"spade_url": ?"(https://video-edge-[.\w\-/]+\.ts)"', settings_js, re.I
|
r'"spade_url": ?"(https://video-edge-[.\w\-/]+\.ts)"', settings_js, re.I
|
||||||
)
|
)
|
||||||
@@ -322,16 +319,13 @@ class Channel:
|
|||||||
"""
|
"""
|
||||||
if not self.online:
|
if not self.online:
|
||||||
return False
|
return False
|
||||||
session = self._twitch._session
|
|
||||||
if session is None:
|
|
||||||
return False
|
|
||||||
if self._spade_url is None:
|
if self._spade_url is None:
|
||||||
self._spade_url = await self.get_spade_url()
|
self._spade_url = await self.get_spade_url()
|
||||||
logger.debug(f"Sending minute-watched to {self.name}")
|
logger.debug(f"Sending minute-watched to {self.name}")
|
||||||
for attempt in range(5):
|
try:
|
||||||
try:
|
async with self._twitch.request(
|
||||||
async with session.post(self._spade_url, data=self._payload) as response:
|
"POST", self._spade_url, data=self._payload
|
||||||
return response.status == 204
|
) as response:
|
||||||
except (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError):
|
return response.status == 204
|
||||||
continue
|
except RequestException:
|
||||||
return False
|
return False
|
||||||
@@ -8,9 +8,19 @@ from time import time
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from contextlib import suppress
|
from contextlib import suppress, asynccontextmanager
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional, Union, List, Dict, Set, OrderedDict, Callable, Iterable, cast, TYPE_CHECKING
|
Optional,
|
||||||
|
Union,
|
||||||
|
List,
|
||||||
|
Dict,
|
||||||
|
Set,
|
||||||
|
OrderedDict,
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
AsyncIterator,
|
||||||
|
cast,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -22,7 +32,7 @@ from gui import GUIManager
|
|||||||
from channel import Channel
|
from channel import Channel
|
||||||
from websocket import WebsocketPool
|
from websocket import WebsocketPool
|
||||||
from inventory import DropsCampaign, TimedDrop
|
from inventory import DropsCampaign, TimedDrop
|
||||||
from exceptions import LoginException, CaptchaRequired
|
from exceptions import RequestException, LoginException, CaptchaRequired
|
||||||
from utils import task_wrapper, timestamp, Game, AwaitableValue, OrderedSet
|
from utils import task_wrapper, timestamp, Game, AwaitableValue, OrderedSet
|
||||||
from constants import (
|
from constants import (
|
||||||
GQL_URL,
|
GQL_URL,
|
||||||
@@ -153,13 +163,11 @@ class Twitch:
|
|||||||
• Selecting a stream to watch, and watching it
|
• Selecting a stream to watch, and watching it
|
||||||
• Changing the stream that's being watched if necessary
|
• Changing the stream that's being watched if necessary
|
||||||
"""
|
"""
|
||||||
if self._session is None:
|
|
||||||
await self.initialize()
|
|
||||||
self.gui.start()
|
self.gui.start()
|
||||||
|
await self.check_login()
|
||||||
if self._watching_task is not None:
|
if self._watching_task is not None:
|
||||||
self._watching_task.cancel()
|
self._watching_task.cancel()
|
||||||
self._watching_task = asyncio.create_task(self._watch_loop())
|
self._watching_task = asyncio.create_task(self._watch_loop())
|
||||||
await self.check_login()
|
|
||||||
# Add default topics
|
# Add default topics
|
||||||
assert self._user_id is not None
|
assert self._user_id is not None
|
||||||
self.websocket.add_topics([
|
self.websocket.add_topics([
|
||||||
@@ -606,7 +614,6 @@ class Twitch:
|
|||||||
self.gui.print(f"Claimed bonus points: {points}")
|
self.gui.print(f"Claimed bonus points: {points}")
|
||||||
|
|
||||||
async def _login(self) -> str:
|
async def _login(self) -> str:
|
||||||
assert self._session is not None
|
|
||||||
logger.debug("Login flow started")
|
logger.debug("Login flow started")
|
||||||
login_form: LoginForm = self.gui.login
|
login_form: LoginForm = self.gui.login
|
||||||
|
|
||||||
@@ -624,7 +631,7 @@ class Twitch:
|
|||||||
payload.pop("authy_token", None)
|
payload.pop("authy_token", None)
|
||||||
payload.pop("twitchguard_code", None)
|
payload.pop("twitchguard_code", None)
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
async with self._session.post(f"{AUTH_URL}/login", json=payload) as response:
|
async with self.request("POST", f"{AUTH_URL}/login", json=payload) as response:
|
||||||
login_response: JsonType = await response.json()
|
login_response: JsonType = await response.json()
|
||||||
|
|
||||||
# Feed this back in to avoid running into CAPTCHA if possible
|
# Feed this back in to avoid running into CAPTCHA if possible
|
||||||
@@ -690,10 +697,12 @@ class Twitch:
|
|||||||
# we're all good
|
# we're all good
|
||||||
return
|
return
|
||||||
# looks like we're missing something
|
# looks like we're missing something
|
||||||
assert self._session is not None
|
|
||||||
login_form: LoginForm = self.gui.login
|
login_form: LoginForm = self.gui.login
|
||||||
logger.debug("Checking login")
|
logger.debug("Checking login")
|
||||||
login_form.update("Logging in...", None)
|
login_form.update("Logging in...", None)
|
||||||
|
if self._session is None:
|
||||||
|
await self.initialize()
|
||||||
|
assert self._session is not None
|
||||||
jar = cast(aiohttp.CookieJar, self._session.cookie_jar)
|
jar = cast(aiohttp.CookieJar, self._session.cookie_jar)
|
||||||
for attempt in range(2):
|
for attempt in range(2):
|
||||||
cookie = jar.filter_cookies("https://twitch.tv") # type: ignore
|
cookie = jar.filter_cookies("https://twitch.tv") # type: ignore
|
||||||
@@ -707,7 +716,8 @@ class Twitch:
|
|||||||
self._access_token = cookie["auth-token"].value
|
self._access_token = cookie["auth-token"].value
|
||||||
logger.debug("Restoring session from cookie")
|
logger.debug("Restoring session from cookie")
|
||||||
# validate our access token, by obtaining user_id
|
# validate our access token, by obtaining user_id
|
||||||
async with self._session.get(
|
async with self.request(
|
||||||
|
"GET",
|
||||||
"https://id.twitch.tv/oauth2/validate",
|
"https://id.twitch.tv/oauth2/validate",
|
||||||
headers={"Authorization": f"OAuth {self._access_token}"}
|
headers={"Authorization": f"OAuth {self._access_token}"}
|
||||||
) as response:
|
) as response:
|
||||||
@@ -731,23 +741,37 @@ class Twitch:
|
|||||||
jar.update_cookies(cookie, URL("https://twitch.tv"))
|
jar.update_cookies(cookie, URL("https://twitch.tv"))
|
||||||
jar.save(COOKIES_PATH)
|
jar.save(COOKIES_PATH)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def request(
|
||||||
|
self, method: str, url: str, **kwargs
|
||||||
|
) -> AsyncIterator[aiohttp.ClientResponse]:
|
||||||
|
if self._session is None:
|
||||||
|
await self.initialize()
|
||||||
|
session = self._session
|
||||||
|
assert session is not None
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
async with session.request(method, url, **kwargs) as response:
|
||||||
|
yield response
|
||||||
|
break
|
||||||
|
except (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError):
|
||||||
|
await asyncio.sleep(0.1 * attempt)
|
||||||
|
else:
|
||||||
|
raise RequestException(
|
||||||
|
"Ran out of attempts while handling a request: "
|
||||||
|
f"(method={method.upper()}, {url=}, {kwargs=})"
|
||||||
|
)
|
||||||
|
|
||||||
async def gql_request(self, op: GQLOperation) -> JsonType:
|
async def gql_request(self, op: GQLOperation) -> JsonType:
|
||||||
await self.check_login()
|
|
||||||
assert self._session is not None
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"OAuth {self._access_token}",
|
"Authorization": f"OAuth {self._access_token}",
|
||||||
"Client-Id": CLIENT_ID,
|
"Client-Id": CLIENT_ID,
|
||||||
}
|
}
|
||||||
gql_logger.debug(f"GQL Request: {op}")
|
gql_logger.debug(f"GQL Request: {op}")
|
||||||
for attempt in range(5):
|
async with self.request("POST", GQL_URL, json=op, headers=headers) as response:
|
||||||
try:
|
response_json: JsonType = await response.json()
|
||||||
async with self._session.post(GQL_URL, json=op, headers=headers) as response:
|
gql_logger.debug(f"GQL Response: {response_json}")
|
||||||
response_json = await response.json()
|
return response_json
|
||||||
gql_logger.debug(f"GQL Response: {response_json}")
|
|
||||||
return response_json
|
|
||||||
except (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError):
|
|
||||||
continue
|
|
||||||
raise RuntimeError(f"Ran out of attempts while handling a GQL request: {op}")
|
|
||||||
|
|
||||||
async def fetch_campaign(
|
async def fetch_campaign(
|
||||||
self, campaign_id: str, claimed_benefits: Dict[str, datetime]
|
self, campaign_id: str, claimed_benefits: Dict[str, datetime]
|
||||||
|
|||||||
Reference in new issue
Block a user