Implement simple proxy URL support
This commit is contained in:
1 parent
8d2fde9447
commit
e47bf30d5e
4 files changed
+94
-38
No files matched your search
@@ -11,6 +11,7 @@ from tkinter import Tk, ttk, StringVar, DoubleVar, IntVar
|
|||||||
from typing import Any, TypedDict, NoReturn, TYPE_CHECKING
|
from typing import Any, TypedDict, NoReturn, TYPE_CHECKING
|
||||||
|
|
||||||
import pystray
|
import pystray
|
||||||
|
from yarl import URL
|
||||||
from PIL import Image as Image_module
|
from PIL import Image as Image_module
|
||||||
|
|
||||||
from cache import ImageCache
|
from cache import ImageCache
|
||||||
@@ -22,6 +23,7 @@ from constants import SELF_PATH, FORMATTER, WS_TOPICS_LIMIT, MAX_WEBSOCKETS, WIN
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from twitch import Twitch
|
from twitch import Twitch
|
||||||
from channel import Channel
|
from channel import Channel
|
||||||
|
from settings import Settings
|
||||||
from inventory import DropsCampaign, TimedDrop
|
from inventory import DropsCampaign, TimedDrop
|
||||||
|
|
||||||
|
|
||||||
@@ -45,10 +47,12 @@ class PlaceholderEntry(ttk.Entry):
|
|||||||
master: ttk.Widget,
|
master: ttk.Widget,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
placeholder: str,
|
placeholder: str,
|
||||||
|
prefill: str = '',
|
||||||
placeholdercolor: str = "grey60",
|
placeholdercolor: str = "grey60",
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
):
|
):
|
||||||
super().__init__(master, *args, **kwargs)
|
super().__init__(master, *args, **kwargs)
|
||||||
|
self._prefill: str = prefill
|
||||||
self._show: str = kwargs.get("show", '')
|
self._show: str = kwargs.get("show", '')
|
||||||
self._text_color: str = kwargs.get("foreground", '')
|
self._text_color: str = kwargs.get("foreground", '')
|
||||||
self._ph_color: str = placeholdercolor
|
self._ph_color: str = placeholdercolor
|
||||||
@@ -58,33 +62,38 @@ class PlaceholderEntry(ttk.Entry):
|
|||||||
if isinstance(self, ttk.Combobox):
|
if isinstance(self, ttk.Combobox):
|
||||||
# only bind this for comboboxes
|
# only bind this for comboboxes
|
||||||
self.bind("<<ComboboxSelected>>", self._combobox_select)
|
self.bind("<<ComboboxSelected>>", self._combobox_select)
|
||||||
# start with a placeholder
|
self._ph: bool = False
|
||||||
self._ph: bool = True
|
self._insert_placeholder()
|
||||||
super().config(foreground=self._ph_color, show='')
|
|
||||||
super().insert(0, self._ph_text)
|
|
||||||
|
|
||||||
def _focus_in(self, event: tk.Event[PlaceholderEntry]) -> None:
|
def _insert_placeholder(self) -> None:
|
||||||
"""
|
"""
|
||||||
On focus in, if we've had a placeholder, clear the box and set normal text colour and show.
|
If we're empty, insert a placeholder, set placeholder text color and make sure it's shown.
|
||||||
"""
|
|
||||||
if self._ph:
|
|
||||||
self._ph = False
|
|
||||||
self.delete(0, "end")
|
|
||||||
super().config(foreground=self._text_color, show=self._show)
|
|
||||||
|
|
||||||
def _focus_out(self, event: tk.Event[PlaceholderEntry]) -> None:
|
|
||||||
"""
|
|
||||||
On focus out, if we're empty, insert a placeholder,
|
|
||||||
set placeholder text color and make sure it's shown.
|
|
||||||
If we're not empty, leave the box as is.
|
If we're not empty, leave the box as is.
|
||||||
"""
|
"""
|
||||||
if not super().get():
|
if not super().get():
|
||||||
self._ph = True
|
self._ph = True
|
||||||
super().config(foreground=self._ph_color, show='')
|
super().config(foreground=self._ph_color, show='')
|
||||||
super().insert(0, self._ph_text)
|
super().insert("end", self._ph_text)
|
||||||
|
|
||||||
|
def _remove_placeholder(self) -> None:
|
||||||
|
"""
|
||||||
|
If we've had a placeholder, clear the box and set normal text colour and show.
|
||||||
|
"""
|
||||||
|
if self._ph:
|
||||||
|
self._ph = False
|
||||||
|
super().delete(0, "end")
|
||||||
|
super().config(foreground=self._text_color, show=self._show)
|
||||||
|
if self._prefill:
|
||||||
|
super().insert("end", self._prefill)
|
||||||
|
|
||||||
|
def _focus_in(self, event: tk.Event[PlaceholderEntry]) -> None:
|
||||||
|
self._remove_placeholder()
|
||||||
|
|
||||||
|
def _focus_out(self, event: tk.Event[PlaceholderEntry]) -> None:
|
||||||
|
self._insert_placeholder()
|
||||||
|
|
||||||
def _combobox_select(self, event: tk.Event[PlaceholderEntry]):
|
def _combobox_select(self, event: tk.Event[PlaceholderEntry]):
|
||||||
# combobox clears and inserts the selected value internally, bypassing the insert method
|
# combobox clears and inserts the selected value internally, bypassing the insert method.
|
||||||
# disable the placeholder flag and set the color here, so _focus_in doesn't clear the entry
|
# disable the placeholder flag and set the color here, so _focus_in doesn't clear the entry
|
||||||
self._ph = False
|
self._ph = False
|
||||||
super().config(foreground=self._text_color, show=self._show)
|
super().config(foreground=self._text_color, show=self._show)
|
||||||
@@ -100,16 +109,17 @@ class PlaceholderEntry(ttk.Entry):
|
|||||||
setattr(self, attr, value)
|
setattr(self, attr, value)
|
||||||
|
|
||||||
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
options = {}
|
options: dict[str, Any] = {}
|
||||||
if args and args[0] is not None:
|
if args and args[0] is not None:
|
||||||
options.update(args[0])
|
options.update(args[0])
|
||||||
if kwargs:
|
if kwargs:
|
||||||
options.update(kwargs)
|
options.update(kwargs)
|
||||||
self._store_option(options, "show", "_show")
|
self._store_option(options, "show", "_show")
|
||||||
self._store_option(options, "placeholder", "_ph_text", remove=True)
|
|
||||||
self._store_option(options, "foreground", "_text_color")
|
self._store_option(options, "foreground", "_text_color")
|
||||||
|
self._store_option(options, "placeholder", "_ph_text", remove=True)
|
||||||
|
self._store_option(options, "prefill", "_prefill", remove=True)
|
||||||
self._store_option(options, "placeholdercolor", "_ph_color", remove=True)
|
self._store_option(options, "placeholdercolor", "_ph_color", remove=True)
|
||||||
return super().configure(*args, **kwargs)
|
return super().configure(**kwargs)
|
||||||
|
|
||||||
def config(self, *args: Any, **kwargs: Any) -> Any:
|
def config(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
# because 'config = configure' makes mypy complain
|
# because 'config = configure' makes mypy complain
|
||||||
@@ -122,17 +132,22 @@ class PlaceholderEntry(ttk.Entry):
|
|||||||
|
|
||||||
def insert(self, index: tk._EntryIndex, content: str) -> None:
|
def insert(self, index: tk._EntryIndex, content: str) -> None:
|
||||||
# when inserting into the entry externally, disable the placeholder flag
|
# when inserting into the entry externally, disable the placeholder flag
|
||||||
if self._ph:
|
if not content:
|
||||||
self._ph = False
|
# if an empty string was passed in
|
||||||
self.delete(0, "end")
|
return
|
||||||
super().config(foreground=self._text_color, show=self._show)
|
self._remove_placeholder()
|
||||||
return super().insert(index, content)
|
super().insert(index, content)
|
||||||
|
|
||||||
|
def delete(self, first: tk._EntryIndex, last: tk._EntryIndex | None = None) -> None:
|
||||||
|
super().delete(first, last)
|
||||||
|
self._insert_placeholder()
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self.delete(0, "end")
|
self.delete(0, "end")
|
||||||
self._ph = True
|
|
||||||
super().config(foreground=self._ph_color, show='')
|
def replace(self, content: str) -> None:
|
||||||
super().insert(0, self._ph_text)
|
super().delete(0, "end")
|
||||||
|
self.insert("end", content)
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderCombobox(PlaceholderEntry, ttk.Combobox):
|
class PlaceholderCombobox(PlaceholderEntry, ttk.Combobox):
|
||||||
@@ -979,8 +994,21 @@ class InventoryOverview:
|
|||||||
label.config(text=progress_text, foreground=progress_color)
|
label.config(text=progress_text, foreground=progress_color)
|
||||||
|
|
||||||
|
|
||||||
|
def proxy_validate(entry: PlaceholderEntry, settings: Settings) -> bool:
|
||||||
|
raw_url = entry.get().strip()
|
||||||
|
entry.replace(raw_url)
|
||||||
|
url = URL(raw_url)
|
||||||
|
valid = url.host is not None and url.port is not None
|
||||||
|
if valid:
|
||||||
|
settings.proxy = url
|
||||||
|
else:
|
||||||
|
entry.delete(0, "end")
|
||||||
|
return valid
|
||||||
|
|
||||||
|
|
||||||
class _SettingsVars(TypedDict):
|
class _SettingsVars(TypedDict):
|
||||||
tray: IntVar
|
tray: IntVar
|
||||||
|
proxy: StringVar
|
||||||
autostart: IntVar
|
autostart: IntVar
|
||||||
priority_only: IntVar
|
priority_only: IntVar
|
||||||
|
|
||||||
@@ -990,8 +1018,9 @@ class SettingsPanel:
|
|||||||
|
|
||||||
def __init__(self, manager: GUIManager, master: ttk.Widget):
|
def __init__(self, manager: GUIManager, master: ttk.Widget):
|
||||||
self._twitch = manager._twitch
|
self._twitch = manager._twitch
|
||||||
self._settings = manager._twitch.settings
|
self._settings: Settings = manager._twitch.settings
|
||||||
self._vars: _SettingsVars = {
|
self._vars: _SettingsVars = {
|
||||||
|
"proxy": StringVar(master, str(self._settings.proxy)),
|
||||||
"tray": IntVar(master, self._settings.autostart_tray),
|
"tray": IntVar(master, self._settings.autostart_tray),
|
||||||
"autostart": IntVar(master, self._settings.autostart),
|
"autostart": IntVar(master, self._settings.autostart),
|
||||||
"priority_only": IntVar(master, self._settings.priority_only),
|
"priority_only": IntVar(master, self._settings.priority_only),
|
||||||
@@ -1013,15 +1042,28 @@ class SettingsPanel:
|
|||||||
ttk.Label(center_frame2, text="Autostart: ").grid(column=0, row=0, sticky="e")
|
ttk.Label(center_frame2, text="Autostart: ").grid(column=0, row=0, sticky="e")
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
center_frame2, variable=self._vars["autostart"], command=self.update_autostart
|
center_frame2, variable=self._vars["autostart"], command=self.update_autostart
|
||||||
).grid(column=1, row=0)
|
).grid(column=1, row=0, sticky="w")
|
||||||
ttk.Label(center_frame2, text="Autostart into tray: ").grid(column=0, row=1, sticky="e")
|
ttk.Label(center_frame2, text="Autostart into tray: ").grid(column=0, row=1, sticky="e")
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
center_frame2, variable=self._vars["tray"], command=self.update_autostart
|
center_frame2, variable=self._vars["tray"], command=self.update_autostart
|
||||||
).grid(column=1, row=1)
|
).grid(column=1, row=1, sticky="w")
|
||||||
ttk.Label(center_frame2, text="Priority only: ").grid(column=0, row=2, sticky="e")
|
ttk.Label(center_frame2, text="Priority only: ").grid(column=0, row=2, sticky="e")
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
center_frame2, variable=self._vars["priority_only"], command=self.priority_only
|
center_frame2, variable=self._vars["priority_only"], command=self.priority_only
|
||||||
).grid(column=1, row=2)
|
).grid(column=1, row=2, sticky="w")
|
||||||
|
ttk.Label(
|
||||||
|
center_frame2, text="Proxy (requires restart):"
|
||||||
|
).grid(column=0, row=3, columnspan=2)
|
||||||
|
self._proxy = PlaceholderEntry(
|
||||||
|
center_frame2,
|
||||||
|
width=37,
|
||||||
|
validate="focusout",
|
||||||
|
prefill="http://",
|
||||||
|
textvariable=self._vars["proxy"],
|
||||||
|
placeholder="http://username:password@address:port",
|
||||||
|
)
|
||||||
|
self._proxy.config(validatecommand=partial(proxy_validate, self._proxy, self._settings))
|
||||||
|
self._proxy.grid(column=0, row=4, columnspan=2)
|
||||||
# Priority section
|
# Priority section
|
||||||
priority_frame = ttk.LabelFrame(center_frame, padding=(4, 0, 4, 4), text="Priority")
|
priority_frame = ttk.LabelFrame(center_frame, padding=(4, 0, 4, 4), text="Priority")
|
||||||
priority_frame.grid(column=1, row=0, sticky="nsew")
|
priority_frame.grid(column=1, row=0, sticky="nsew")
|
||||||
@@ -1490,6 +1532,7 @@ if __name__ == "__main__":
|
|||||||
"https://static-cdn.jtvnw.net/twitch-drops-assets-prod/"
|
"https://static-cdn.jtvnw.net/twitch-drops-assets-prod/"
|
||||||
"BENEFIT-81ab5665-b2f4-4179-96e6-74da5a82da28.jpeg"
|
"BENEFIT-81ab5665-b2f4-4179-96e6-74da5a82da28.jpeg"
|
||||||
),
|
),
|
||||||
|
can_claim=False,
|
||||||
is_claimed=False,
|
is_claimed=False,
|
||||||
preconditions=True,
|
preconditions=True,
|
||||||
rewards_text=lambda: rewards,
|
rewards_text=lambda: rewards,
|
||||||
@@ -1508,6 +1551,7 @@ if __name__ == "__main__":
|
|||||||
settings=SimpleNamespace(
|
settings=SimpleNamespace(
|
||||||
tray=False,
|
tray=False,
|
||||||
priority=[],
|
priority=[],
|
||||||
|
proxy=URL(),
|
||||||
autostart=False,
|
autostart=False,
|
||||||
priority_only=False,
|
priority_only=False,
|
||||||
autostart_tray=False,
|
autostart_tray=False,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, TypedDict, TYPE_CHECKING
|
from typing import Any, TypedDict, TYPE_CHECKING
|
||||||
|
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
from constants import SETTINGS_PATH
|
from constants import SETTINGS_PATH
|
||||||
from utils import json_load, json_save
|
from utils import json_load, json_save
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class SettingsFile(TypedDict):
|
class SettingsFile(TypedDict):
|
||||||
|
proxy: URL
|
||||||
autostart: bool
|
autostart: bool
|
||||||
exclude: set[str]
|
exclude: set[str]
|
||||||
priority: list[str]
|
priority: list[str]
|
||||||
@@ -18,6 +21,7 @@ class SettingsFile(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
default_settings: SettingsFile = {
|
default_settings: SettingsFile = {
|
||||||
|
"proxy": URL(),
|
||||||
"priority": [],
|
"priority": [],
|
||||||
"exclude": set(),
|
"exclude": set(),
|
||||||
"autostart": False,
|
"autostart": False,
|
||||||
@@ -36,6 +40,7 @@ class Settings:
|
|||||||
debug_gql: int
|
debug_gql: int
|
||||||
logging_level: int
|
logging_level: int
|
||||||
# from settings file
|
# from settings file
|
||||||
|
proxy: URL
|
||||||
autostart: bool
|
autostart: bool
|
||||||
exclude: set[str]
|
exclude: set[str]
|
||||||
priority: list[str]
|
priority: list[str]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from yarl import URL
|
|
||||||
from time import time
|
from time import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from collections import abc, OrderedDict
|
from collections import abc, OrderedDict
|
||||||
@@ -10,6 +9,7 @@ from contextlib import suppress, asynccontextmanager
|
|||||||
from typing import Final, NoReturn, cast, TYPE_CHECKING
|
from typing import Final, NoReturn, cast, TYPE_CHECKING
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
from gui import GUIManager
|
from gui import GUIManager
|
||||||
from channel import Channel
|
from channel import Channel
|
||||||
@@ -815,6 +815,8 @@ class Twitch:
|
|||||||
self.initialize()
|
self.initialize()
|
||||||
assert session is not None
|
assert session is not None
|
||||||
method = method.upper()
|
method = method.upper()
|
||||||
|
if self.settings.proxy and "proxy" not in kwargs:
|
||||||
|
kwargs["proxy"] = self.settings.proxy
|
||||||
cause: Exception | None = None
|
cause: Exception | None = None
|
||||||
for attempt in range(attempts):
|
for attempt in range(attempts):
|
||||||
logger.debug(f"Request: ({method=}, {url=}, {attempts=}, {kwargs=})")
|
logger.debug(f"Request: ({method=}, {url=}, {attempts=}, {kwargs=})")
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from typing import (
|
|||||||
Any, Literal, MutableSet, Callable, Generic, Mapping, TypeVar, cast, TYPE_CHECKING
|
Any, Literal, MutableSet, Callable, Generic, Mapping, TypeVar, cast, TYPE_CHECKING
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import yarl
|
||||||
|
|
||||||
from constants import WORKING_DIR, JsonType
|
from constants import WORKING_DIR, JsonType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -33,10 +35,6 @@ _P = ParamSpec("_P") # params
|
|||||||
_JSON_T = TypeVar("_JSON_T", bound=Mapping[Any, Any])
|
_JSON_T = TypeVar("_JSON_T", bound=Mapping[Any, Any])
|
||||||
logger = logging.getLogger("TwitchDrops")
|
logger = logging.getLogger("TwitchDrops")
|
||||||
NONCE_CHARS = string.ascii_letters + string.digits
|
NONCE_CHARS = string.ascii_letters + string.digits
|
||||||
serialize_env: dict[str, Callable[[Any], object]] = {
|
|
||||||
"set": set,
|
|
||||||
"datetime": lambda d: datetime.fromtimestamp(d, timezone.utc),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def resource_path(relative_path: Path | str) -> Path:
|
def resource_path(relative_path: Path | str) -> Path:
|
||||||
@@ -100,6 +98,8 @@ def _serialize(obj: Any) -> Any:
|
|||||||
# assume naive objects are UTC
|
# assume naive objects are UTC
|
||||||
obj = obj.replace(tzinfo=timezone.utc)
|
obj = obj.replace(tzinfo=timezone.utc)
|
||||||
d = obj.timestamp()
|
d = obj.timestamp()
|
||||||
|
elif isinstance(obj, yarl.URL):
|
||||||
|
d = str(obj)
|
||||||
else:
|
else:
|
||||||
raise TypeError(obj)
|
raise TypeError(obj)
|
||||||
# store with type
|
# store with type
|
||||||
@@ -110,6 +110,11 @@ def _serialize(obj: Any) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
_MISSING = object()
|
_MISSING = object()
|
||||||
|
serialize_env: dict[str, Callable[[Any], object]] = {
|
||||||
|
"set": set,
|
||||||
|
"datetime": lambda d: datetime.fromtimestamp(d, timezone.utc),
|
||||||
|
"URL": yarl.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _remove_missing(obj: JsonType) -> JsonType:
|
def _remove_missing(obj: JsonType) -> JsonType:
|
||||||
|
|||||||
Reference in new issue
Block a user