diff --git a/gui.py b/gui.py index a8b5abff..c0e38fb9 100644 --- a/gui.py +++ b/gui.py @@ -1302,6 +1302,18 @@ def _on_tab_switched(self, event: tk.Event[ttk.Notebook]) -> None: # refresh only if we're switching to the tab self.refresh() + def get_status(self, campaign: DropsCampaign) -> tuple[str, str]: + if campaign.active: + status_text: str = _("gui", "inventory", "status", "active") + status_color: str = "green" + elif campaign.upcoming: + status_text = _("gui", "inventory", "status", "upcoming") + status_color = "goldenrod" + else: + status_text = _("gui", "inventory", "status", "expired") + status_color = "red" + return (status_text, status_color) + def refresh(self): for campaign in self._campaigns: # status @@ -1341,6 +1353,13 @@ async def add_campaign(self, campaign: DropsCampaign) -> None: campaign_frame, text=status_text, takefocus=False, foreground=status_color ) status_label.grid(column=1, row=1, sticky="w", padx=4) + # NOTE: We have to save the campaign's frame and status before any awaits happen, + # otherwise the len(self._campaigns) call may overwrite an existing frame, + # if the campaigns are added concurrently. + self._campaigns[campaign] = { + "frame": campaign_frame, + "status": status_label, + } # Starts / Ends MouseOverLabel( campaign_frame, @@ -1415,10 +1434,6 @@ async def add_campaign(self, campaign: DropsCampaign) -> None: self._drops[drop.id] = label = MouseOverLabel(drop_frame) self.update_progress(drop, label) label.grid(column=0, row=1) - self._campaigns[campaign] = { - "frame": campaign_frame, - "status": status_label, - } if self._manager.tabs.current_tab() == 1: self._update_visibility(campaign) self._canvas_update() @@ -1429,18 +1444,6 @@ def clear(self) -> None: self._drops.clear() self._campaigns.clear() - def get_status(self, campaign: DropsCampaign) -> tuple[str, str]: - if campaign.active: - status_text: str = _("gui", "inventory", "status", "active") - status_color: str = "green" - elif campaign.upcoming: - status_text = _("gui", "inventory", "status", "upcoming") - status_color = "goldenrod" - else: - status_text = _("gui", "inventory", "status", "expired") - status_color = "red" - return (status_text, status_color) - def update_progress(self, drop: TimedDrop, label: MouseOverLabel) -> None: # Returns: main text, alt text, text color alt_text: str = '' diff --git a/twitch.py b/twitch.py index 789a86fa..d87576ce 100644 --- a/twitch.py +++ b/twitch.py @@ -1462,25 +1462,37 @@ async def fetch_inventory(self) -> None: campaigns.sort(key=lambda c: c.active, reverse=True) campaigns.sort(key=lambda c: c.upcoming and c.starts_at or c.ends_at) campaigns.sort(key=lambda c: c.linked, reverse=True) + self._drops.clear() self.gui.inv.clear() self.inventory.clear() + self._mnt_triggers.clear() switch_triggers: set[datetime] = set() next_hour = datetime.now(timezone.utc) + timedelta(hours=1) - for i, campaign in enumerate(campaigns, start=1): - status_update( - _("gui", "status", "adding_campaigns").format(counter=f"({i}/{len(campaigns)})") - ) + # add the campaigns to the internal inventory + for campaign in campaigns: self._drops.update({drop.id: drop for drop in campaign.drops}) if campaign.can_earn_within(next_hour): switch_triggers.update(campaign.time_triggers) - # NOTE: this fetches pictures from the CDN, so might be slow without a cache - await self.gui.inv.add_campaign(campaign) - # this is needed here explicitly, because images aren't always fetched + self.inventory.append(campaign) + # concurrently add the campaigns into the GUI + # NOTE: this fetches pictures from the CDN, so might be slow without a cache + for i, coro in enumerate( + asyncio.as_completed( + [ + asyncio.create_task(self.gui.inv.add_campaign(campaign)) + for campaign in campaigns + ] + ), + start=1, + ): + status_update( + _("gui", "status", "adding_campaigns").format(counter=f"({i}/{len(campaigns)})") + ) + await coro + # this is needed here explicitly, because cache reads from disk don't raise this if self.gui.close_requested: raise ExitRequest() - self.inventory.append(campaign) - self._mnt_triggers.clear() self._mnt_triggers.extend(sorted(switch_triggers)) # trim out all triggers that we're already past now = datetime.now(timezone.utc)