diff --git a/channel.py b/channel.py index 789cc8c..edcd6b7 100644 --- a/channel.py +++ b/channel.py @@ -37,7 +37,7 @@ class Stream: self.channel: Channel = channel self.broadcast_id = int(id) self.viewers: int = viewers - self.drops_enabled: bool = True + self.drops_enabled: bool = not channel._twitch.settings.available_drops_check self.game: Game | None = Game(game) if game else None self.title: str = title self._stream_url: URLType | None = None diff --git a/gui.py b/gui.py index e3cd47c..7b70dcf 100644 --- a/gui.py +++ b/gui.py @@ -1553,6 +1553,8 @@ class _SettingsVars(TypedDict): language: StringVar priority_mode: StringVar tray_notifications: IntVar + enable_badges_emotes: IntVar + available_drops_check: IntVar class SettingsPanel: @@ -1586,6 +1588,12 @@ class SettingsPanel: "dark_mode": IntVar(master, int(self._settings.dark_mode)), "priority_mode": StringVar(master, self.PRIORITY_MODES[priority_mode]), "tray_notifications": IntVar(master, self._settings.tray_notifications), + "enable_badges_emotes": IntVar( + master, int(self._settings.enable_badges_emotes) + ), + "available_drops_check": IntVar( + master, int(self._settings.available_drops_check) + ), } self._game_names: set[str] = set() master.rowconfigure(0, weight=1) @@ -1593,6 +1601,7 @@ class SettingsPanel: # use a frame to center the content within the tab center_frame = ttk.Frame(master) center_frame.grid(column=0, row=0) + # General section general_frame = ttk.LabelFrame( center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "general", "name") @@ -1602,11 +1611,11 @@ class SettingsPanel: # NOTE: this can be adjusted or removed later on if more options were to be added general_frame.rowconfigure(0, weight=1) general_frame.columnconfigure(0, weight=1) - center_frame2 = ttk.Frame(general_frame) - center_frame2.grid(column=0, row=0) + general_center = ttk.Frame(general_frame) + general_center.grid(column=0, row=0) # language frame - language_frame = ttk.Frame(center_frame2) + language_frame = ttk.Frame(general_center) language_frame.grid(column=0, row=0) ttk.Label(language_frame, text="Language 🌐 (requires restart): ").grid(column=0, row=0) SelectCombobox( @@ -1617,7 +1626,7 @@ class SettingsPanel: ).grid(column=1, row=0) # checkboxes frame - checkboxes_frame = ttk.Frame(center_frame2) + checkboxes_frame = ttk.Frame(general_center) checkboxes_frame.grid(column=0, row=1) ttk.Label( checkboxes_frame, text=_("gui", "settings", "general", "autostart") @@ -1637,7 +1646,9 @@ class SettingsPanel: ttk.Checkbutton( checkboxes_frame, variable=self._vars["tray_notifications"], - command=self.update_notifications, + command=lambda: setattr( + self._settings, "tray_notifications", bool(self._vars["tray_notifications"].get()) + ), ).grid(column=1, row=irow, sticky="w") ttk.Label( checkboxes_frame, text=_("gui", "settings", "general", "dark_mode") @@ -1658,7 +1669,7 @@ class SettingsPanel: ).grid(column=1, row=irow, sticky="w") # proxy frame - proxy_frame = ttk.Frame(center_frame2) + proxy_frame = ttk.Frame(general_center) proxy_frame.grid(column=0, row=2) ttk.Label(proxy_frame, text=_("gui", "settings", "general", "proxy")).grid(column=0, row=0) self._proxy = PlaceholderEntry( @@ -1671,11 +1682,48 @@ class SettingsPanel: ) self._proxy.config(validatecommand=partial(proxy_validate, self._proxy, self._settings)) self._proxy.grid(column=0, row=1) + + # Advanced section + advanced_frame = ttk.LabelFrame( + center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "advanced", "name") + ) + advanced_frame.grid(column=0, row=1, sticky="nsew") + advanced_frame.columnconfigure(0, weight=1) + advanced_frame.rowconfigure(0, weight=1) + advanced_center = ttk.Frame(advanced_frame) + advanced_center.grid(column=0, row=0) + + # Toggles for badges and emotes, and available drops check + ttk.Label( + advanced_center, text=_("gui", "settings", "advanced", "enable_badges_emotes") + ).grid(column=0, row=(irow := 0), sticky="e") + ttk.Checkbutton( + advanced_center, + variable=self._vars["enable_badges_emotes"], + command=lambda: setattr( + self._settings, + "enable_badges_emotes", + bool(self._vars["enable_badges_emotes"].get()), + ), + ).grid(column=1, row=irow, sticky="w") + ttk.Label( + advanced_center, text=_("gui", "settings", "advanced", "available_drops_check") + ).grid(column=0, row=(irow := irow + 1), sticky="e") + ttk.Checkbutton( + advanced_center, + variable=self._vars["available_drops_check"], + command=lambda: setattr( + self._settings, + "available_drops_check", + bool(self._vars["available_drops_check"].get()), + ), + ).grid(column=1, row=irow, sticky="w") + # Priority section priority_frame = ttk.LabelFrame( center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "priority") ) - priority_frame.grid(column=1, row=0, sticky="nsew") + priority_frame.grid(column=1, row=0, rowspan=2, sticky="nsew") self._priority_entry = PlaceholderCombobox( priority_frame, placeholder=_("gui", "settings", "game_name"), width=30 ) @@ -1686,7 +1734,7 @@ class SettingsPanel: ).grid(column=1, row=0) self._priority_list = PaddedListbox( priority_frame, - height=10, + height=12, padding=(1, 0), activestyle="none", selectmode="single", @@ -1715,11 +1763,12 @@ class SettingsPanel: priority_frame, text="❌", command=self.priority_delete, width=3, style="Large.TButton" ).grid(column=1, row=3, sticky="ns") priority_frame.rowconfigure(3, weight=1) + # Exclude section exclude_frame = ttk.LabelFrame( center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "exclude") ) - exclude_frame.grid(column=2, row=0, sticky="nsew") + exclude_frame.grid(column=2, row=0, rowspan=2, sticky="nsew") self._exclude_entry = PlaceholderCombobox( exclude_frame, placeholder=_("gui", "settings", "game_name"), width=26 ) @@ -1729,7 +1778,7 @@ class SettingsPanel: ).grid(column=1, row=0) self._exclude_list = PaddedListbox( exclude_frame, - height=10, + height=12, padding=(1, 0), activestyle="none", selectmode="single", @@ -1743,9 +1792,10 @@ class SettingsPanel: ttk.Button( exclude_frame, text="❌", command=self.exclude_delete, width=3, style="Large.TButton" ).grid(column=0, row=2, columnspan=2, sticky="ew") + # Reload button reload_frame = ttk.Frame(center_frame) - reload_frame.grid(column=0, row=1, columnspan=3, pady=4) + reload_frame.grid(column=0, row=2, columnspan=3, pady=4) ttk.Label(reload_frame, text=_("gui", "settings", "reload_text")).grid(column=0, row=0) ttk.Button( reload_frame, @@ -1761,12 +1811,8 @@ class SettingsPanel: def update_dark_mode(self) -> None: self._settings.dark_mode = bool(self._vars["dark_mode"].get()) - self._settings.alter() self._manager.apply_theme(self._settings.dark_mode) - def update_notifications(self) -> None: - self._settings.tray_notifications = bool(self._vars["tray_notifications"].get()) - def _get_self_path(self) -> str: # NOTE: we need double quotes in case the path contains spaces return f'"{SELF_PATH.resolve()!s}"' @@ -2629,6 +2675,8 @@ if __name__ == "__main__": autostart_tray=False, exclude={"Lit Game"}, tray_notifications=True, + enable_badges_emotes=False, + available_drops_check=False, logging_level=LOGGING_LEVELS[0], priority_mode=PriorityMode.PRIORITY_ONLY, ) diff --git a/inventory.py b/inventory.py index 1727353..11e162a 100644 --- a/inventory.py +++ b/inventory.py @@ -395,7 +395,9 @@ class DropsCampaign: @property def eligible(self) -> bool: - return self.linked or self.has_badge_or_emote + return self.linked or ( + self._twitch.settings.enable_badges_emotes and self.has_badge_or_emote + ) @cached_property def has_badge_or_emote(self) -> bool: diff --git a/settings.py b/settings.py index 910666d..1eb8f9e 100644 --- a/settings.py +++ b/settings.py @@ -20,6 +20,8 @@ class SettingsFile(TypedDict): autostart_tray: bool connection_quality: int tray_notifications: bool + enable_badges_emotes: bool + available_drops_check: bool priority_mode: PriorityMode @@ -32,6 +34,8 @@ default_settings: SettingsFile = { "connection_quality": 1, "language": DEFAULT_LANG, "tray_notifications": True, + "enable_badges_emotes": False, + "available_drops_check": False, "priority_mode": PriorityMode.PRIORITY_ONLY, } @@ -54,6 +58,8 @@ class Settings: autostart_tray: bool connection_quality: int tray_notifications: bool + enable_badges_emotes: bool + available_drops_check: bool priority_mode: PriorityMode PASSTHROUGH = ("_settings", "_args", "_altered") diff --git a/translate.py b/translate.py index 33a1b7d..4c0335e 100644 --- a/translate.py +++ b/translate.py @@ -166,6 +166,12 @@ class GUISettingsGeneral(TypedDict): proxy: str +class GUISettingsAdvanced(TypedDict): + name: str + enable_badges_emotes: str + available_drops_check: str + + class GUIPriorityModes(TypedDict): priority_only: str ending_soonest: str @@ -174,6 +180,7 @@ class GUIPriorityModes(TypedDict): class GUISettings(TypedDict): general: GUISettingsGeneral + advanced: GUISettingsAdvanced priority_modes: GUIPriorityModes game_name: str priority: str @@ -366,6 +373,11 @@ default_translation: Translation = { "priority_mode": "Priority mode: ", "proxy": "Proxy (requires restart):", }, + "advanced": { + "name": "Advanced", + "enable_badges_emotes": "Enable partial support for badges and emotes: ", + "available_drops_check": "Enable extra available drops check: ", + }, "priority_modes": { "priority_only": "Priority list only", "ending_soonest": "Ending soonest", diff --git a/twitch.py b/twitch.py index 97ee9da..dd02a62 100644 --- a/twitch.py +++ b/twitch.py @@ -1562,7 +1562,7 @@ class Twitch: async def bulk_check_online(self, channels: abc.Iterable[Channel]): """ Utilize batch GQL requests to check ONLINE status for a lot of channels at once. - Also handles the drops_enabled check. + Also handles the drops_enabled check (if enabled). """ acl_streams_map: dict[int, JsonType] = {} stream_gql_ops: list[GQLOperation] = [channel.stream_gql for channel in channels] @@ -1587,29 +1587,30 @@ class Twitch: task.cancel() raise # for all channels with an active stream, check the available drops as well - # acl_available_drops_map: dict[int, list[JsonType]] = {} - # available_gql_ops: list[GQLOperation] = [ - # GQL_OPERATIONS["AvailableDrops"].with_variables({"channelID": str(channel_id)}) - # for channel_id, channel_data in acl_streams_map.items() - # if channel_data["stream"] is not None # only do this for ONLINE channels - # ] - # available_gql_tasks: list[asyncio.Task[list[JsonType]]] = [ - # asyncio.create_task(self.gql_request(available_gql_chunk)) - # for available_gql_chunk in chunk(available_gql_ops, 20) - # ] - # try: - # for coro in asyncio.as_completed(available_gql_tasks): - # response_list = await coro - # for response_json in response_list: - # available_info: JsonType = response_json["data"]["channel"] - # acl_available_drops_map[int(available_info["id"])] = ( - # available_info["viewerDropCampaigns"] or [] - # ) - # except Exception: - # # asyncio.as_completed doesn't cancel tasks on errors - # for task in available_gql_tasks: - # task.cancel() - # raise + acl_available_drops_map: dict[int, list[JsonType]] = {} + if self.settings.available_drops_check: + available_gql_ops: list[GQLOperation] = [ + GQL_OPERATIONS["AvailableDrops"].with_variables({"channelID": str(channel_id)}) + for channel_id, channel_data in acl_streams_map.items() + if channel_data["stream"] is not None # only do this for ONLINE channels + ] + available_gql_tasks: list[asyncio.Task[list[JsonType]]] = [ + asyncio.create_task(self.gql_request(available_gql_chunk)) + for available_gql_chunk in chunk(available_gql_ops, 20) + ] + try: + for coro in asyncio.as_completed(available_gql_tasks): + response_list = await coro + for response_json in response_list: + available_info: JsonType = response_json["data"]["channel"] + acl_available_drops_map[int(available_info["id"])] = ( + available_info["viewerDropCampaigns"] or [] + ) + except Exception: + # asyncio.as_completed doesn't cancel tasks on errors + for task in available_gql_tasks: + task.cancel() + raise for channel in channels: channel_id = channel.id if channel_id not in acl_streams_map: @@ -1617,6 +1618,5 @@ class Twitch: channel_data = acl_streams_map[channel_id] if channel_data["stream"] is None: continue - # available_drops: list[JsonType] = acl_available_drops_map[channel_id] - # channel.external_update(channel_data, available_drops) - channel.external_update(channel_data, []) + available_drops: list[JsonType] = acl_available_drops_map.get(channel_id, []) + channel.external_update(channel_data, available_drops)