Streamline connection retry code

This commit is contained in:
DevilXD committed 2022-02-09 11:59:08 +01:00
1 parent 038fac48f9
commit d90d20a42c
2 files changed
+57 -39

No files matched your search

+12 -18
View File
@@ -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
+45 -21
View File
@@ -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]