Redesign the watch loop with awaitable channel value

This commit is contained in:
DevilXD committed 2022-01-11 15:37:11 +01:00
1 parent 215f02bb87
commit e0868c5fa7
2 files changed
+65 -35

No files matched your search

+3 -9
View File
@@ -489,7 +489,6 @@ class ConsoleOutput:
class Buttons(TypedDict): class Buttons(TypedDict):
frame: ttk.Frame frame: ttk.Frame
cleanup: ttk.Button
switch: ttk.Button switch: ttk.Button
load_points: ttk.Button load_points: ttk.Button
@@ -506,11 +505,6 @@ class ChannelList:
buttons_frame = ttk.Frame(frame) buttons_frame = ttk.Frame(frame)
self._buttons: Buttons = { self._buttons: Buttons = {
"frame": buttons_frame, "frame": buttons_frame,
"cleanup": ttk.Button(
buttons_frame,
text="Cleanup",
command=manager._twitch.state_change(State.CHANNEL_CLEANUP),
),
"switch": ttk.Button( "switch": ttk.Button(
buttons_frame, buttons_frame,
text="Switch", text="Switch",
@@ -522,9 +516,8 @@ class ChannelList:
), ),
} }
buttons_frame.grid(column=0, row=0, columnspan=2) buttons_frame.grid(column=0, row=0, columnspan=2)
self._buttons["cleanup"].grid(column=0, row=0) self._buttons["switch"].grid(column=0, row=0)
self._buttons["switch"].grid(column=1, row=0) self._buttons["load_points"].grid(column=1, row=0)
self._buttons["load_points"].grid(column=2, row=0)
scroll = ttk.Scrollbar(frame, orient="vertical") scroll = ttk.Scrollbar(frame, orient="vertical")
self._table = table = ttk.Treeview( self._table = table = ttk.Treeview(
frame, frame,
@@ -601,6 +594,7 @@ class ChannelList:
# causes the columns to shrink back after long values have been removed from it # causes the columns to shrink back after long values have been removed from it
columns = self._table.cget("columns") columns = self._table.cget("columns")
iids = self._table.get_children() iids = self._table.get_children()
if iids: # table needs to have at least one item
for column in columns: for column in columns:
width = max(self._measure(self._table.set(i, column)) for i in iids) width = max(self._measure(self._table.set(i, column)) for i in iids)
self._table.column(column, minwidth=width, width=width) self._table.column(column, minwidth=width, width=width)
+62 -26
View File
@@ -8,7 +8,9 @@ from yarl import URL
from time import time from time import time
from itertools import chain from itertools import chain
from functools import partial from functools import partial
from typing import Any, Callable, Iterable, Optional, Union, List, Dict, cast, TYPE_CHECKING from typing import (
Callable, Iterable, Optional, Union, List, Dict, Generic, TypeVar, cast, TYPE_CHECKING
)
try: try:
import aiohttp import aiohttp
@@ -43,6 +45,35 @@ logger = logging.getLogger("TwitchDrops")
gql_logger = logging.getLogger("TwitchDrops.gql") gql_logger = logging.getLogger("TwitchDrops.gql")
_V = TypeVar("_V")
_D = TypeVar("_D")
class _AwaitableValue(Generic[_V]):
def __init__(self):
self._value: _V
self._event = asyncio.Event()
def has_value(self) -> bool:
return self._event.is_set()
def get_with_default(self, default: _D) -> Union[_D, _V]:
if self._event.is_set():
return self._value
return default
async def get(self) -> _V:
await self._event.wait()
return self._value
def set(self, value: _V) -> None:
self._value = value
self._event.set()
def clear(self) -> None:
self._event.clear()
class Twitch: class Twitch:
def __init__(self, options: ParsedArgs): def __init__(self, options: ParsedArgs):
self._options = options self._options = options
@@ -66,9 +97,9 @@ class Twitch:
self.inventory: List[DropsCampaign] = [] # inventory self.inventory: List[DropsCampaign] = [] # inventory
# Storing and watching channels # Storing and watching channels
self.channels: Dict[int, Channel] = {} self.channels: Dict[int, Channel] = {}
self._watching_channel: Optional[Channel] = None self._watching_channel: _AwaitableValue[Channel] = _AwaitableValue()
self._watching_task: Optional[asyncio.Task[Any]] = None self._watching_task: Optional[asyncio.Task[None]] = None
self._last_watch = time() - 60 self._watching_restart = asyncio.Event()
self._drop_update: Optional[asyncio.Future[bool]] = None self._drop_update: Optional[asyncio.Future[bool]] = None
# Websocket # Websocket
self.websocket = WebsocketPool(self) self.websocket = WebsocketPool(self)
@@ -137,6 +168,9 @@ class Twitch:
start_time = time() start_time = time()
self.gui.print("Exiting...") self.gui.print("Exiting...")
self.stop_watching() self.stop_watching()
if self._watching_task is not None:
self._watching_task.cancel()
self._watching_task = None
self._session.cookie_jar.save(COOKIES_PATH) # type: ignore self._session.cookie_jar.save(COOKIES_PATH) # type: ignore
await self._session.close() await self._session.close()
await self.websocket.stop() await self.websocket.stop()
@@ -157,6 +191,7 @@ class Twitch:
• Changing the stream that's being watched if necessary • Changing the stream that's being watched if necessary
""" """
self.gui.start() self.gui.start()
self._watching_task = asyncio.create_task(self._watch_loop())
await self.check_login() await self.check_login()
# Add default topics # Add default topics
assert self._user_id is not None assert self._user_id is not None
@@ -271,16 +306,13 @@ class Twitch:
self.change_state(State.CHANNEL_CLEANUP) self.change_state(State.CHANNEL_CLEANUP)
await self._state_change.wait() await self._state_change.wait()
async def _watch_loop(self, channel: Channel): async def _watch_loop(self) -> None:
# last_watch is a timestamp of the last time we've sent a watch payload
# We need this because watch_loop can be cancelled and rescheduled multiple times
# in quick succession, and apparently Twitch doesn't like that very much
interval = WATCH_INTERVAL.total_seconds() interval = WATCH_INTERVAL.total_seconds()
await asyncio.sleep(self._last_watch + interval - time()) i = 1
i = 0
while True: while True:
channel = await self._watching_channel.get()
await channel.send_watch() await channel.send_watch()
self._last_watch = time() last_watch = time()
self._drop_update = asyncio.Future() self._drop_update = asyncio.Future()
use_active = False use_active = False
try: try:
@@ -317,38 +349,42 @@ class Twitch:
drop.display() drop.display()
else: else:
logger.error("Active drop search failed") logger.error("Active drop search failed")
if i == 0: if i % 30 == 1:
# ensure every 30 minutes that we don't have unclaimed points bonus # ensure every 30 minutes that we don't have unclaimed points bonus
await channel.claim_bonus() await channel.claim_bonus()
i = (i + 1) % 30 if i % 60 == 0:
await asyncio.sleep(self._last_watch + interval - time()) # refresh inventory every hour
self.change_state(State.INVENTORY_FETCH)
i = (i + 1) % 3600
self._watching_restart.clear()
try:
await asyncio.wait_for(
self._watching_restart.wait(), timeout=last_watch + interval - time()
)
except asyncio.TimeoutError:
pass
def watch(self, channel: Channel): def watch(self, channel: Channel):
if self.is_watching(channel): if self.is_watching(channel):
# we're already watching the same channel, so there's no point switching # we're already watching the same channel, so there's no point switching
return return
if self._watching_task is not None:
self._watching_task.cancel()
self.gui.channels.set_watching(channel) self.gui.channels.set_watching(channel)
self._watching_channel = channel self._watching_channel.set(channel)
self._watching_task = asyncio.create_task(self._watch_loop(channel))
def stop_watching(self): def stop_watching(self):
self.gui.progress.stop_timer() self.gui.progress.stop_timer()
self.gui.channels.clear_watching() self.gui.channels.clear_watching()
if self._watching_task is not None: self._watching_channel.clear()
self._watching_task.cancel()
self._watching_task = None
self._watching_channel = None
def restart_watching(self, channel: Optional[Channel] = None): def restart_watching(self, channel: Optional[Channel] = None):
# this forcibly re-sends the watching payload to the specified or currently watched channel # this forcibly re-sends the watching payload to the specified or currently watched channel
if channel is None: if channel is None:
channel = self._watching_channel channel = self._watching_channel.get_with_default(None)
if channel is not None: if channel is not None:
self.stop_watching() self.gui.progress.stop_timer()
self._last_watch = time() - 60 self._watching_channel.set(channel)
self.watch(channel) self.gui.channels.set_watching(channel)
self._watching_restart.set()
async def process_stream_state(self, channel_id: int, message: JsonType): async def process_stream_state(self, channel_id: int, message: JsonType):
msg_type = message["type"] msg_type = message["type"]