Implement campaign linking URL, status and filtering
This commit is contained in:
1 parent
fbf23f5b60
commit
2e1711cf05
3 files changed
+142
-45
No files matched your search
@@ -8,8 +8,8 @@ from math import log10, ceil
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from collections import abc, namedtuple
|
from collections import abc, namedtuple
|
||||||
from tkinter.font import Font, nametofont
|
from tkinter.font import Font, nametofont
|
||||||
from tkinter import Tk, ttk, StringVar, DoubleVar, IntVar
|
|
||||||
from typing import Any, TypedDict, NoReturn, TYPE_CHECKING
|
from typing import Any, TypedDict, NoReturn, TYPE_CHECKING
|
||||||
|
from tkinter import Tk, ttk, StringVar, DoubleVar, IntVar, PhotoImage
|
||||||
|
|
||||||
import pystray
|
import pystray
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
@@ -243,16 +243,18 @@ class MouseOverLabel(ttk.Label):
|
|||||||
|
|
||||||
class LinkLabel(ttk.Label):
|
class LinkLabel(ttk.Label):
|
||||||
def __init__(self, *args, link: str, **kwargs) -> None:
|
def __init__(self, *args, link: str, **kwargs) -> None:
|
||||||
if "foreground" not in kwargs:
|
self._link: str = link
|
||||||
kwargs["foreground"] = "blue"
|
# style provides font and foreground color
|
||||||
if "cursor" not in kwargs:
|
|
||||||
kwargs["cursor"] = "hand2"
|
|
||||||
if "style" not in kwargs:
|
if "style" not in kwargs:
|
||||||
kwargs["style"] = "Link.TLabel"
|
kwargs["style"] = "Link.TLabel"
|
||||||
|
elif not kwargs["style"]:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
return
|
||||||
|
if "cursor" not in kwargs:
|
||||||
|
kwargs["cursor"] = "hand2"
|
||||||
if "padding" not in kwargs:
|
if "padding" not in kwargs:
|
||||||
# W, N, E, S
|
# W, N, E, S
|
||||||
kwargs["padding"] = (0, 2, 0, 2)
|
kwargs["padding"] = (0, 2, 0, 2)
|
||||||
self._link: str = link
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.bind("<ButtonRelease-1>", self.webopen(self._link))
|
self.bind("<ButtonRelease-1>", self.webopen(self._link))
|
||||||
|
|
||||||
@@ -271,25 +273,24 @@ class WebsocketStatus:
|
|||||||
self._topics_var = StringVar(master)
|
self._topics_var = StringVar(master)
|
||||||
frame = ttk.LabelFrame(master, text="Websocket Status", padding=(4, 0, 4, 4))
|
frame = ttk.LabelFrame(master, text="Websocket Status", padding=(4, 0, 4, 4))
|
||||||
frame.grid(column=0, row=0, sticky="nsew", padx=2)
|
frame.grid(column=0, row=0, sticky="nsew", padx=2)
|
||||||
monospace_font = Font(frame, family="Courier New", size=10)
|
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
frame,
|
frame,
|
||||||
text='\n'.join(f"Websocket #{i}:" for i in range(1, MAX_WEBSOCKETS + 1)),
|
text='\n'.join(f"Websocket #{i}:" for i in range(1, MAX_WEBSOCKETS + 1)),
|
||||||
font=monospace_font,
|
style="MS.TLabel",
|
||||||
).grid(column=0, row=0)
|
).grid(column=0, row=0)
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
frame,
|
frame,
|
||||||
textvariable=self._status_var,
|
textvariable=self._status_var,
|
||||||
width=16,
|
width=16,
|
||||||
justify="left",
|
justify="left",
|
||||||
font=monospace_font,
|
style="MS.TLabel",
|
||||||
).grid(column=1, row=0)
|
).grid(column=1, row=0)
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
frame,
|
frame,
|
||||||
textvariable=self._topics_var,
|
textvariable=self._topics_var,
|
||||||
width=(digits * 2 + 1),
|
width=(digits * 2 + 1),
|
||||||
justify="right",
|
justify="right",
|
||||||
font=monospace_font,
|
style="MS.TLabel",
|
||||||
).grid(column=2, row=0)
|
).grid(column=2, row=0)
|
||||||
self._items: dict[int, _WSEntry | None] = {i: None for i in range(MAX_WEBSOCKETS)}
|
self._items: dict[int, _WSEntry | None] = {i: None for i in range(MAX_WEBSOCKETS)}
|
||||||
self._update()
|
self._update()
|
||||||
@@ -905,14 +906,36 @@ class Notebook:
|
|||||||
class InventoryOverview:
|
class InventoryOverview:
|
||||||
def __init__(self, manager: GUIManager, master: ttk.Widget):
|
def __init__(self, manager: GUIManager, master: ttk.Widget):
|
||||||
self._cache = manager._cache
|
self._cache = manager._cache
|
||||||
master.rowconfigure(0, weight=1)
|
self._filters = {
|
||||||
master.columnconfigure(0, weight=1)
|
"notlinked": IntVar(master, 0),
|
||||||
|
"expired": IntVar(master, 1),
|
||||||
|
"upcoming": IntVar(master, 1),
|
||||||
|
}
|
||||||
|
filter_frame = ttk.LabelFrame(master, text="Filter", padding=(4, 0, 4, 4))
|
||||||
|
LABEL_SPACING = 20
|
||||||
|
filter_frame.grid(column=0, row=0, columnspan=2, sticky="nsew")
|
||||||
|
ttk.Label(filter_frame, text="Show:", padding=(0, 0, 10, 0)).grid(column=0, row=0)
|
||||||
|
ttk.Checkbutton(filter_frame, variable=self._filters["notlinked"]).grid(column=1, row=0)
|
||||||
|
ttk.Label(
|
||||||
|
filter_frame, text="Not linked", padding=(0, 0, LABEL_SPACING, 0)
|
||||||
|
).grid(column=2, row=0)
|
||||||
|
ttk.Checkbutton(filter_frame, variable=self._filters["expired"]).grid(column=3, row=0)
|
||||||
|
ttk.Label(
|
||||||
|
filter_frame, text="Expired", padding=(0, 0, LABEL_SPACING, 0)
|
||||||
|
).grid(column=4, row=0)
|
||||||
|
ttk.Checkbutton(filter_frame, variable=self._filters["upcoming"]).grid(column=5, row=0)
|
||||||
|
ttk.Label(
|
||||||
|
filter_frame, text="Upcoming", padding=(0, 0, LABEL_SPACING, 0)
|
||||||
|
).grid(column=6, row=0)
|
||||||
|
ttk.Button(filter_frame, text="Refresh", command=self.refresh).grid(column=7, row=0)
|
||||||
self._canvas = tk.Canvas(master, scrollregion=(0, 0, 0, 0))
|
self._canvas = tk.Canvas(master, scrollregion=(0, 0, 0, 0))
|
||||||
self._canvas.grid(column=0, row=0, sticky="nsew")
|
self._canvas.grid(column=0, row=1, sticky="nsew")
|
||||||
|
master.rowconfigure(1, weight=1)
|
||||||
|
master.columnconfigure(0, weight=1)
|
||||||
xscroll = ttk.Scrollbar(master, orient="horizontal", command=self._canvas.xview)
|
xscroll = ttk.Scrollbar(master, orient="horizontal", command=self._canvas.xview)
|
||||||
xscroll.grid(column=0, row=1, sticky="ew")
|
xscroll.grid(column=0, row=2, sticky="ew")
|
||||||
yscroll = ttk.Scrollbar(master, orient="vertical", command=self._canvas.yview)
|
yscroll = ttk.Scrollbar(master, orient="vertical", command=self._canvas.yview)
|
||||||
yscroll.grid(column=1, row=0, sticky="ns")
|
yscroll.grid(column=1, row=1, sticky="ns")
|
||||||
self._canvas.configure(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
|
self._canvas.configure(xscrollcommand=xscroll.set, yscrollcommand=yscroll.set)
|
||||||
self._canvas.bind("<Configure>", self._canvas_update)
|
self._canvas.bind("<Configure>", self._canvas_update)
|
||||||
self._main_frame = ttk.Frame(self._canvas)
|
self._main_frame = ttk.Frame(self._canvas)
|
||||||
@@ -921,10 +944,31 @@ class InventoryOverview:
|
|||||||
)
|
)
|
||||||
self._canvas.bind("<Leave>", lambda e: self._canvas.unbind_all("<MouseWheel>"))
|
self._canvas.bind("<Leave>", lambda e: self._canvas.unbind_all("<MouseWheel>"))
|
||||||
self._canvas.create_window(0, 0, anchor="nw", window=self._main_frame)
|
self._canvas.create_window(0, 0, anchor="nw", window=self._main_frame)
|
||||||
self._campaigns: list[DropsCampaign] = []
|
self._campaigns: dict[DropsCampaign, ttk.Frame] = {}
|
||||||
self._drops: dict[str, ttk.Label] = {}
|
self._drops: dict[str, ttk.Label] = {}
|
||||||
|
|
||||||
|
def _update_visibility(self, campaign: DropsCampaign):
|
||||||
|
# True if the campaign is supposed to show, False makes it hidden.
|
||||||
|
frame = self._campaigns[campaign]
|
||||||
|
if (
|
||||||
|
(campaign.linked or self._filters["notlinked"].get())
|
||||||
|
and (
|
||||||
|
campaign.active
|
||||||
|
or self._filters["expired"].get() and campaign.expired
|
||||||
|
or self._filters["upcoming"].get() and campaign.upcoming
|
||||||
|
)
|
||||||
|
):
|
||||||
|
frame.grid()
|
||||||
|
else:
|
||||||
|
frame.grid_remove()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
for campaign in self._campaigns:
|
||||||
|
self._update_visibility(campaign)
|
||||||
|
self._canvas_update()
|
||||||
|
|
||||||
def _canvas_update(self, event: tk.Event[tk.Canvas] | None = None):
|
def _canvas_update(self, event: tk.Event[tk.Canvas] | None = None):
|
||||||
|
self._canvas.update_idletasks()
|
||||||
self._canvas.configure(scrollregion=self._canvas.bbox("all"))
|
self._canvas.configure(scrollregion=self._canvas.bbox("all"))
|
||||||
|
|
||||||
def _on_mousewheel(self, event: tk.Event[tk.Misc]):
|
def _on_mousewheel(self, event: tk.Event[tk.Misc]):
|
||||||
@@ -939,13 +983,16 @@ class InventoryOverview:
|
|||||||
async def add_campaign(self, campaign: DropsCampaign) -> None:
|
async def add_campaign(self, campaign: DropsCampaign) -> None:
|
||||||
campaign_frame = ttk.Frame(self._main_frame, relief="ridge", borderwidth=1, padding=4)
|
campaign_frame = ttk.Frame(self._main_frame, relief="ridge", borderwidth=1, padding=4)
|
||||||
campaign_frame.grid(column=0, row=len(self._campaigns), sticky="nsew", pady=3)
|
campaign_frame.grid(column=0, row=len(self._campaigns), sticky="nsew", pady=3)
|
||||||
self._campaigns.append(campaign)
|
self._campaigns[campaign] = campaign_frame
|
||||||
campaign_frame.rowconfigure(3, weight=1)
|
self._update_visibility(campaign)
|
||||||
|
campaign_frame.rowconfigure(4, weight=1)
|
||||||
campaign_frame.columnconfigure(1, weight=1)
|
campaign_frame.columnconfigure(1, weight=1)
|
||||||
campaign_frame.columnconfigure(3, weight=10000)
|
campaign_frame.columnconfigure(3, weight=10000)
|
||||||
|
# Name
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
campaign_frame, text=campaign.name, takefocus=False, width=45
|
campaign_frame, text=campaign.name, takefocus=False, width=45
|
||||||
).grid(column=0, row=0, columnspan=2, sticky="w")
|
).grid(column=0, row=0, columnspan=2, sticky="w")
|
||||||
|
# Status
|
||||||
if campaign.active:
|
if campaign.active:
|
||||||
status_text: str = "Active ✔"
|
status_text: str = "Active ✔"
|
||||||
status_color: tk._Color = "green"
|
status_color: tk._Color = "green"
|
||||||
@@ -958,6 +1005,7 @@ class InventoryOverview:
|
|||||||
ttk.Label(
|
ttk.Label(
|
||||||
campaign_frame, text=status_text, takefocus=False, foreground=status_color
|
campaign_frame, text=status_text, takefocus=False, foreground=status_color
|
||||||
).grid(column=1, row=1, sticky="w", padx=4)
|
).grid(column=1, row=1, sticky="w", padx=4)
|
||||||
|
# Starts / Ends
|
||||||
MouseOverLabel(
|
MouseOverLabel(
|
||||||
campaign_frame,
|
campaign_frame,
|
||||||
text=f"Ends: {campaign.ends_at.astimezone().replace(microsecond=0, tzinfo=None)}",
|
text=f"Ends: {campaign.ends_at.astimezone().replace(microsecond=0, tzinfo=None)}",
|
||||||
@@ -966,6 +1014,26 @@ class InventoryOverview:
|
|||||||
),
|
),
|
||||||
takefocus=False,
|
takefocus=False,
|
||||||
).grid(column=1, row=2, sticky="w", padx=4)
|
).grid(column=1, row=2, sticky="w", padx=4)
|
||||||
|
# Linking status
|
||||||
|
if campaign.linked:
|
||||||
|
link_kwargs = {
|
||||||
|
"style": '',
|
||||||
|
"text": "Linked ✔",
|
||||||
|
"foreground": "green",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
link_kwargs = {
|
||||||
|
"text": "Not linked ❌",
|
||||||
|
"foreground": "red",
|
||||||
|
}
|
||||||
|
LinkLabel(
|
||||||
|
campaign_frame,
|
||||||
|
link=campaign.link_url,
|
||||||
|
takefocus=False,
|
||||||
|
padding=0,
|
||||||
|
**link_kwargs,
|
||||||
|
).grid(column=1, row=3, sticky="w", padx=4)
|
||||||
|
# ACL channels
|
||||||
acl = campaign.allowed_channels
|
acl = campaign.allowed_channels
|
||||||
if acl:
|
if acl:
|
||||||
if len(acl) <= 5:
|
if len(acl) <= 5:
|
||||||
@@ -977,24 +1045,29 @@ class InventoryOverview:
|
|||||||
allowed_text = "All"
|
allowed_text = "All"
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
campaign_frame, text=f"Allowed channels:\n{allowed_text}", takefocus=False
|
campaign_frame, text=f"Allowed channels:\n{allowed_text}", takefocus=False
|
||||||
).grid(column=1, row=3, sticky="nw", padx=4)
|
).grid(column=1, row=4, sticky="nw", padx=4)
|
||||||
campaign_image = await self._cache.get(campaign.image_url, size=(96, 128))
|
# Image
|
||||||
ttk.Label(campaign_frame, image=campaign_image).grid(column=0, row=1, rowspan=3)
|
campaign_image = await self._cache.get(campaign.image_url, size=(108, 144))
|
||||||
|
ttk.Label(campaign_frame, image=campaign_image).grid(column=0, row=1, rowspan=4)
|
||||||
|
# Drops separator
|
||||||
ttk.Separator(
|
ttk.Separator(
|
||||||
campaign_frame, orient="vertical", takefocus=False
|
campaign_frame, orient="vertical", takefocus=False
|
||||||
).grid(column=2, row=0, rowspan=4, sticky="ns")
|
).grid(column=2, row=0, rowspan=5, sticky="ns")
|
||||||
|
# Drops display
|
||||||
drops_row = ttk.Frame(campaign_frame)
|
drops_row = ttk.Frame(campaign_frame)
|
||||||
drops_row.grid(column=3, row=0, rowspan=4, sticky="nsew", padx=4)
|
drops_row.grid(column=3, row=0, rowspan=5, sticky="nsew", padx=4)
|
||||||
drops_row.rowconfigure(0, weight=1)
|
drops_row.rowconfigure(0, weight=1)
|
||||||
for i, drop in enumerate(campaign.drops):
|
for i, drop in enumerate(campaign.drops):
|
||||||
drop_frame = ttk.Frame(drops_row, relief="ridge", borderwidth=1, padding=5)
|
drop_frame = ttk.Frame(drops_row, relief="ridge", borderwidth=1, padding=5)
|
||||||
drop_frame.grid(column=i, row=0, padx=4)
|
drop_frame.grid(column=i, row=0, padx=4)
|
||||||
benefits_frame = ttk.Frame(drop_frame)
|
benefits_frame = ttk.Frame(drop_frame)
|
||||||
benefits_frame.grid(column=0, row=0)
|
benefits_frame.grid(column=0, row=0)
|
||||||
for i, benefit in enumerate(drop.benefits):
|
benefit_images: list[PhotoImage] = await asyncio.gather(
|
||||||
benefit_image = await self._cache.get(benefit.image_url, (80, 80))
|
*(self._cache.get(benefit.image_url, (80, 80)) for benefit in drop.benefits)
|
||||||
|
)
|
||||||
|
for i, benefit, image in zip(range(len(drop.benefits)), drop.benefits, benefit_images):
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
benefits_frame, text=benefit.name, image=benefit_image, compound="bottom"
|
benefits_frame, text=benefit.name, image=image, compound="bottom"
|
||||||
).grid(column=i, row=0, padx=5)
|
).grid(column=i, row=0, padx=5)
|
||||||
progress_text, progress_color = self.get_progress(drop)
|
progress_text, progress_color = self.get_progress(drop)
|
||||||
self._drops[drop.id] = label = ttk.Label(
|
self._drops[drop.id] = label = ttk.Label(
|
||||||
@@ -1037,7 +1110,7 @@ def proxy_validate(entry: PlaceholderEntry, settings: Settings) -> bool:
|
|||||||
if valid:
|
if valid:
|
||||||
settings.proxy = url
|
settings.proxy = url
|
||||||
else:
|
else:
|
||||||
entry.delete(0, "end")
|
entry.clear()
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
|
|
||||||
@@ -1445,14 +1518,21 @@ class GUIManager:
|
|||||||
sublayout[1] = sublayout[1][1]["children"][0]
|
sublayout[1] = sublayout[1][1]["children"][0]
|
||||||
del original[0][1]["children"][1]
|
del original[0][1]["children"][1]
|
||||||
style.layout("TCheckbutton", original)
|
style.layout("TCheckbutton", original)
|
||||||
# adds a button style with a larger font
|
# label style - green, yellow and red text
|
||||||
|
style.configure("green.TLabel", foreground="green")
|
||||||
|
style.configure("yellow.TLabel", foreground="goldenrod")
|
||||||
|
style.configure("red.TLabel", foreground="red")
|
||||||
|
# label style with a monospace font
|
||||||
|
monospaced_font = Font(root, family="Courier New", size=10)
|
||||||
|
style.configure("MS.TLabel", font=monospaced_font)
|
||||||
|
# button style with a larger font
|
||||||
large_font = default_font.copy()
|
large_font = default_font.copy()
|
||||||
large_font.config(size=12)
|
large_font.config(size=12)
|
||||||
style.configure("Large.TButton", font=large_font)
|
style.configure("Large.TButton", font=large_font)
|
||||||
# adds a label style that mimics links
|
# label style that mimics links
|
||||||
link_font = default_font.copy()
|
link_font = default_font.copy()
|
||||||
link_font.config(underline=True)
|
link_font.config(underline=True)
|
||||||
style.configure("Link.TLabel", font=link_font)
|
style.configure("Link.TLabel", font=link_font, foreground="blue")
|
||||||
# end of style changes
|
# end of style changes
|
||||||
|
|
||||||
root_frame = ttk.Frame(root, padding=8)
|
root_frame = ttk.Frame(root, padding=8)
|
||||||
@@ -1607,6 +1687,9 @@ if __name__ == "__main__":
|
|||||||
return self._str__(self)
|
return self._str__(self)
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
|
class HashNamespace(SimpleNamespace):
|
||||||
|
__hash__ = object.__hash__ # type: ignore
|
||||||
|
|
||||||
def create_game(id: int, name: str):
|
def create_game(id: int, name: str):
|
||||||
return StrNamespace(name=name, id=id, _str__=lambda s: s.name)
|
return StrNamespace(name=name, id=id, _str__=lambda s: s.name)
|
||||||
|
|
||||||
@@ -1664,11 +1747,14 @@ if __name__ == "__main__":
|
|||||||
benefits = [SimpleNamespace(name=name, image_url=image_url) for name in rewards]
|
benefits = [SimpleNamespace(name=name, image_url=image_url) for name in rewards]
|
||||||
mock = SimpleNamespace(
|
mock = SimpleNamespace(
|
||||||
id="0",
|
id="0",
|
||||||
campaign=SimpleNamespace(
|
campaign=HashNamespace(
|
||||||
name=campaign_name,
|
name=campaign_name,
|
||||||
id="campaign",
|
id="campaign",
|
||||||
|
expired=False,
|
||||||
active=False,
|
active=False,
|
||||||
upcoming=True,
|
upcoming=True,
|
||||||
|
linked=False,
|
||||||
|
link_url="https://google.com",
|
||||||
image_url="https://static-cdn.jtvnw.net/ttv-boxart/460630-285x380.jpg",
|
image_url="https://static-cdn.jtvnw.net/ttv-boxart/460630-285x380.jpg",
|
||||||
allowed_channels=[],
|
allowed_channels=[],
|
||||||
starts_at=ref_stamp,
|
starts_at=ref_stamp,
|
||||||
|
|||||||
+4
-1
@@ -212,6 +212,8 @@ class DropsCampaign:
|
|||||||
self.id: str = data["id"]
|
self.id: str = data["id"]
|
||||||
self.name: str = data["name"]
|
self.name: str = data["name"]
|
||||||
self.game: Game = Game(data["game"])
|
self.game: Game = Game(data["game"])
|
||||||
|
self.linked: bool = data["self"]["isAccountConnected"]
|
||||||
|
self.link_url: str = data["accountLinkURL"]
|
||||||
# campaign's image actually comes from the game object
|
# campaign's image actually comes from the game object
|
||||||
# we use regex to get rid of the dimensions part (ex. ".../game_id-285x380.jpg")
|
# we use regex to get rid of the dimensions part (ex. ".../game_id-285x380.jpg")
|
||||||
self.image_url: URLType = remove_dimensions(data["game"]["boxArtURL"])
|
self.image_url: URLType = remove_dimensions(data["game"]["boxArtURL"])
|
||||||
@@ -279,7 +281,8 @@ class DropsCampaign:
|
|||||||
|
|
||||||
def _base_can_earn(self, channel: Channel | None = None) -> bool:
|
def _base_can_earn(self, channel: Channel | None = None) -> bool:
|
||||||
return (
|
return (
|
||||||
self.active # campaign is active
|
self.linked # account is connected
|
||||||
|
and self.active # campaign is active
|
||||||
# channel isn't specified, or there's no ACL, or the channel is in the ACL
|
# channel isn't specified, or there's no ACL, or the channel is in the ACL
|
||||||
and (channel is None or not self.allowed_channels or channel in self.allowed_channels)
|
and (channel is None or not self.allowed_channels or channel in self.allowed_channels)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -460,17 +460,22 @@ class Twitch:
|
|||||||
@task_wrapper
|
@task_wrapper
|
||||||
async def _maintenance_task(self) -> None:
|
async def _maintenance_task(self) -> None:
|
||||||
# NOTE: this task is started anew / restarted on every inventory fetch
|
# NOTE: this task is started anew / restarted on every inventory fetch
|
||||||
# short sleep to let the application sort out the starting sequence and watching channel
|
# sleep until the application sorts out the starting sequence and watching channel
|
||||||
await asyncio.sleep(30)
|
while self._state is State.INVENTORY_FETCH:
|
||||||
|
await asyncio.sleep(5)
|
||||||
# figure out the maximum sleep period
|
# figure out the maximum sleep period
|
||||||
# 1h at max, but can be shorter if there's an upcoming campaign earlier than that
|
# 1h at max, but can be shorter if there's a campaign state change earlier than that
|
||||||
# divide the period into up to two evenly spaced checks (usually ~15-30m)
|
# divide the period into up to two evenly spaced checks (usually ~15-30m)
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
one_hour = timedelta(hours=1)
|
one_hour = timedelta(hours=1)
|
||||||
period = min(
|
period = timedelta.max
|
||||||
(campaign.starts_at - now for campaign in self.inventory if campaign.starts_at > now),
|
for campaign in self.inventory:
|
||||||
default=one_hour,
|
if not campaign.linked:
|
||||||
)
|
break
|
||||||
|
if campaign.starts_at > now and (test_period := campaign.starts_at - now) < period:
|
||||||
|
period = test_period
|
||||||
|
elif campaign.ends_at > now and (test_period := campaign.ends_at - now) < period:
|
||||||
|
period = test_period
|
||||||
if period > one_hour:
|
if period > one_hour:
|
||||||
period = one_hour
|
period = one_hour
|
||||||
times = ceil(period / timedelta(minutes=30))
|
times = ceil(period / timedelta(minutes=30))
|
||||||
@@ -925,21 +930,24 @@ class Twitch:
|
|||||||
for c in available_list
|
for c in available_list
|
||||||
if (
|
if (
|
||||||
c["status"] in applicable_statuses # that are currently ACTIVE
|
c["status"] in applicable_statuses # that are currently ACTIVE
|
||||||
and c["self"]["isAccountConnected"] # and account is connected
|
|
||||||
and c["id"] not in existing_campaigns # and they aren't in the inventory already
|
and c["id"] not in existing_campaigns # and they aren't in the inventory already
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
# add campaigns that remained, that can be earned but are not in-progress yet
|
# add campaigns that remained, that can be earned but are not in-progress yet
|
||||||
for campaign_id, available_data in available_campaigns.items():
|
fetched_campaigns: list[DropsCampaign] = await asyncio.gather(
|
||||||
campaign = await self.fetch_campaign(campaign_id, available_data, claimed_benefits)
|
*(
|
||||||
if campaign.can_earn():
|
self.fetch_campaign(campaign_id, available_data, claimed_benefits)
|
||||||
campaigns.append(campaign)
|
for campaign_id, available_data in available_campaigns.items()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
campaigns.extend(fetched_campaigns)
|
||||||
campaigns.sort(key=lambda c: c.ends_at)
|
campaigns.sort(key=lambda c: c.ends_at)
|
||||||
|
campaigns.sort(key=lambda c: not c.linked)
|
||||||
self._drops.clear()
|
self._drops.clear()
|
||||||
self.gui.inv.clear()
|
self.gui.inv.clear()
|
||||||
self.inventory.clear()
|
self.inventory.clear()
|
||||||
for campaign in campaigns:
|
for campaign in campaigns:
|
||||||
self._drops.update((drop.id, drop) for drop in campaign.drops)
|
self._drops.update({drop.id: drop for drop in campaign.drops})
|
||||||
await self.gui.inv.add_campaign(campaign)
|
await self.gui.inv.add_campaign(campaign)
|
||||||
self.inventory.append(campaign)
|
self.inventory.append(campaign)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user