Conversation
…d fields and implement OAuth flow for token retrieval
…ADME for new setup instructions
… UI and configuration model
…eprecated spotify_secret from configuration and update UI for connecting to Spotify.
…nt ID, deriving channel from broadcaster token, and updating settings UI for improved user experience. Refactor connection handling for both Twitch and Spotify accounts.
…hutdown process in BotController. Update Twitch connection handling to specify role (bot/broadcaster) and adjust Spotify redirect URI for consistency.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request introduces OAuth authentication refactoring, moving from deprecated OAuth patterns to PKCE-based Spotify authentication and Twitch implicit grant flow. It adds Channel Points integration via Twitch EventSub, updates UI components to support new OAuth connection workflows, removes Discord webhook models, and updates the CI/CD pipeline alongside a new local build script. Version bumped to 0.1.9_4. Changes
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/build-and-sign.yml (1)
36-55:⚠️ Potential issue | 🟠 MajorPin Nuitka action and version to stable releases instead of
@main.Lines 38 and 52 use
Nuitka/Nuitka-Action@main, and lines 40 and 54 specifynuitka-version: main. These unpinned targets introduce supply-chain drift and make builds non-reproducible. Pin to stable releases:Nuitka/Nuitka-Action@v1.4andnuitka-version: 4.0.🔧 Proposed fix
- name: Build (PR to pre with console) if: github.event_name == 'pull_request' && github.base_ref == 'pre' - uses: Nuitka/Nuitka-Action@main + uses: Nuitka/Nuitka-Action@v1.4 with: - nuitka-version: main + nuitka-version: 4.0 script-name: main.py disable-console: false enable-plugins: tk-inter windows-icon-from-ico: icon.ico output-dir: build output-file: ScryptTunes.exe include-data-files: icon.ico=icon.ico mode: standalone - name: Build (main push, console disabled) if: github.ref == 'refs/heads/main' - uses: Nuitka/Nuitka-Action@main + uses: Nuitka/Nuitka-Action@v1.4 with: - nuitka-version: main + nuitka-version: 4.0 script-name: main.py
🤖 Fix all issues with AI agents
In `@bot/scrypt_tunes.py`:
- Around line 542-545: The ctx.send() call inside the mobile-link branch (the
block starting with "if '.link/' in song_uri:") is missing an await and should
be awaited like the other sends; update the call to await ctx.send(...) so the
coroutine is executed (keep the same message text and surrounding logic where
req.get(...) follows).
- Around line 283-446: The code is doing blocking HTTP calls with req.get/post
inside async functions; modify the three locations (in
_ensure_channel_points_listener where req.get(...) is used to validate the
token, in _start_eventsub_channel_points nested _run() where req.post(...)
creates the EventSub subscription, and in chat_song_request where req.get(...)
is used) to offload the synchronous requests to a threadpool by wrapping them
with await asyncio.to_thread(...) (i.e., replace direct calls to req.get/post
with await asyncio.to_thread(req.get, <args...>) or await
asyncio.to_thread(req.post, <args...>)), preserving the same
headers/json/timeout handling and response checks so the logic using
resp.status_code/resp.json()/r.text continues to work; update imports if needed
(ensure asyncio is imported) and keep the surrounding async function structure
(_ensure_channel_points_listener, _start_eventsub_channel_points::_run,
chat_song_request) unchanged.
In `@README.md`:
- Around line 7-8: The "Dev Announcements" heading currently uses H3 (### Dev
Announcements) which breaks the heading hierarchy; update that heading to H2 by
changing "### Dev Announcements" to "## Dev Announcements" so the document
follows proper Markdown heading levels.
In `@ui/controllers/settings_controller.py`:
- Around line 255-262: The OAuth success callback is being invoked from the
background thread inside _shutdown which calls
controller_ref._oauth_on_success_cb(f"oauth:{access_token}"), but those
callbacks perform Tkinter UI work and must run on the main thread; change the
invocation to marshal to the Tk main loop by using root.after(0, lambda:
controller_ref._oauth_on_success_cb(f"oauth:{access_token}")) (or equivalent) so
the callback runs on the UI thread instead of directly in _shutdown after
controller_ref._oauth_server_httpd.shutdown().
In `@ui/frames/general_settings_frame.py`:
- Around line 51-56: The label for the Channel Points reward currently shows the
internal key; update the TextSettingRow initialization in
general_settings_frame.py so it supplies a user-friendly label/title (e.g.
"Channel Points Reward" or "Channel Points Reward Title") instead of the raw
setting_name. Locate the TextSettingRow call that sets
setting_name="channel_points_reward" and add or replace the label argument your
UI component supports (e.g. setting_label, label, or title) to present the
readable text while keeping setting_name and setting_description unchanged; if
TextSettingRow lacks a label prop, extend its constructor (and usages) to accept
and render a human-facing label.
| async def _ensure_channel_points_listener(self): | ||
| reward_name = (self.config.channel_points_reward or "").strip() | ||
| if not reward_name: | ||
| return | ||
| # Need bot token for chat; broadcaster token for channel points (fallback to bot if same) | ||
| if not self._twitch_access_token: | ||
| logging.warning("Channel points listener not started: missing Twitch access token") | ||
| return | ||
|
|
||
| token_for_validate = self._twitch_broadcaster_token or self._twitch_access_token | ||
| if not self._twitch_user_id: | ||
| resp = req.get( | ||
| "https://id.twitch.tv/oauth2/validate", | ||
| headers={"Authorization": f"Bearer {token_for_validate}"}, | ||
| timeout=10, | ||
| ) | ||
| if resp.status_code != 200: | ||
| logging.warning(f"Token validate failed ({resp.status_code}): {resp.text}") | ||
| return | ||
| data = resp.json() | ||
| self._twitch_user_id = data.get("user_id") | ||
| self._twitch_client_id = data.get("client_id") or self._twitch_client_id | ||
| user_login = (data.get("login") or "").lower() | ||
| scopes = set(data.get("scopes", []) or []) | ||
| logging.info( | ||
| "Validated Twitch token for login='%s' user_id=%s with scopes=%s", | ||
| user_login, | ||
| self._twitch_user_id, | ||
| ",".join(sorted(scopes)) or "<none>", | ||
| ) | ||
| required = {"channel:read:redemptions"} | ||
| if not required.issubset(scopes): | ||
| logging.warning("Twitch token missing required scope 'channel:read:redemptions'. Please connect Broadcaster with that scope.") | ||
| return | ||
| if not self._twitch_user_id: | ||
| logging.warning("Could not determine user_id from token validate response") | ||
| return | ||
|
|
||
| configured_channel = (self.config.channel or "").lower() | ||
| if configured_channel and configured_channel != user_login: | ||
| logging.error( | ||
| "Channel Points require the broadcaster token. Configured channel='%s', but authorized as '%s'. " | ||
| "Either Connect Broadcaster in Settings, or change the Watch Channel to '%s'.", | ||
| configured_channel, | ||
| user_login, | ||
| user_login, | ||
| ) | ||
| return | ||
|
|
||
| await self._start_eventsub_channel_points() | ||
|
|
||
| async def _start_eventsub_channel_points(self): | ||
| reward_name = (self.config.channel_points_reward or "").strip() | ||
| if not reward_name: | ||
| return | ||
| if not self._twitch_access_token or not self._twitch_user_id: | ||
| return | ||
| from constants import TWITCH_CLIENT_ID as DEFAULT_TWITCH_CLIENT_ID | ||
| client_id = os.getenv("TWITCH_CLIENT_ID") or getattr(self.config, "client_id", None) or self._twitch_client_id or DEFAULT_TWITCH_CLIENT_ID | ||
| if not client_id: | ||
| logging.error("EventSub requires a Client ID. Set TWITCH_CLIENT_ID env or add client_id to config.json.") | ||
| return | ||
|
|
||
| if self._eventsub_task and not self._eventsub_task.done(): | ||
| return | ||
|
|
||
| async def _run(): | ||
| try: | ||
| self._eventsub_session = aiohttp.ClientSession() | ||
| ws = await self._eventsub_session.ws_connect("wss://eventsub.wss.twitch.tv/ws") | ||
| self._eventsub_ws = ws | ||
| first = await ws.receive_json() | ||
| meta = first.get("metadata", {}) | ||
| if meta.get("message_type") != "session_welcome": | ||
| logging.error(f"EventSub unexpected first message: {first}") | ||
| return | ||
| session_id = first.get("payload", {}).get("session", {}).get("id") | ||
| if not session_id: | ||
| logging.error("EventSub missing session id in welcome") | ||
| return | ||
| auth_token = self._twitch_broadcaster_token or self._twitch_access_token | ||
| headers = { | ||
| "Authorization": f"Bearer {auth_token}", | ||
| "Client-ID": client_id, | ||
| "Content-Type": "application/json", | ||
| } | ||
| body = { | ||
| "type": "channel.channel_points_custom_reward_redemption.add", | ||
| "version": "1", | ||
| "condition": {"broadcaster_user_id": str(self._twitch_user_id)}, | ||
| "transport": {"method": "websocket", "session_id": session_id}, | ||
| } | ||
| r = req.post("https://api.twitch.tv/helix/eventsub/subscriptions", headers=headers, json=body, timeout=10) | ||
| if r.status_code not in (200, 202): | ||
| logging.error("EventSub subscription create failed (status=%s): %s", r.status_code, r.text) | ||
| return | ||
| logging.info("EventSub subscription created for Channel Points redemptions") | ||
|
|
||
| async for msg in ws: | ||
| if msg.type == aiohttp.WSMsgType.TEXT: | ||
| try: | ||
| data = msg.json() if hasattr(msg, "json") else json.loads(msg.data) | ||
| except Exception: | ||
| try: | ||
| data = json.loads(msg.data) | ||
| except Exception: | ||
| logging.debug(f"EventSub non-JSON: {msg.data}") | ||
| continue | ||
| mtype = data.get("metadata", {}).get("message_type") | ||
| if mtype == "notification": | ||
| payload = data.get("payload", {}) | ||
| sub_type = payload.get("subscription", {}).get("type") | ||
| if sub_type == "channel.channel_points_custom_reward_redemption.add": | ||
| event = payload.get("event", {}) | ||
| title = (event.get("reward", {}).get("title") or "").strip().lower() | ||
| expected = reward_name.lower() | ||
| if title == expected: | ||
|
|
||
| try: | ||
| data_sp = self.sp.currently_playing() | ||
| if data_sp and data_sp.get("item"): | ||
| song_artists = data_sp["item"]["artists"] | ||
| song_artists_names = [a["name"] for a in song_artists] | ||
| min_through = int(data_sp["progress_ms"] / (1000 * 60) % 60) | ||
| sec_through = int(data_sp["progress_ms"] / (1000) % 60) | ||
| time_through = f"{min_through} mins, {sec_through} secs" | ||
| min_total = int(data_sp["item"]["duration_ms"] / (1000 * 60) % 60) | ||
| sec_total = int(data_sp["item"]["duration_ms"] / (1000) % 60) | ||
| time_total = f"{min_total} mins, {sec_total} secs" | ||
| message = ( | ||
| f"Now Playing - {data_sp['item']['name']} by {', '.join(song_artists_names)} | " | ||
| f"Link: {data_sp['item']['external_urls']['spotify']} | {time_through} - {time_total}" | ||
| ) | ||
| channel = self.get_channel(self.config.channel) | ||
| if channel: | ||
| await channel.send(message) | ||
| except Exception: | ||
| pass | ||
| elif mtype == "session_keepalive": | ||
| continue | ||
| elif mtype == "session_reconnect": | ||
| reconnect_url = data.get("payload", {}).get("session", {}).get("reconnect_url") | ||
| logging.warning(f"EventSub reconnect requested: {reconnect_url}") | ||
| break | ||
| elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR): | ||
| break | ||
| except Exception as e: | ||
| logging.error(f"EventSub error: {e}") | ||
| finally: | ||
| try: | ||
| if self._eventsub_ws: | ||
| await self._eventsub_ws.close() | ||
| except Exception: | ||
| pass | ||
| try: | ||
| if self._eventsub_session: | ||
| await self._eventsub_session.close() | ||
| except Exception: | ||
| pass | ||
| self._eventsub_ws = None | ||
| self._eventsub_session = None | ||
| self._eventsub_task = None | ||
|
|
||
| self._eventsub_task = self.loop.create_task(_run()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the import statement for 'req' in bot/scrypt_tunes.py
head -50 bot/scrypt_tunes.py | grep -n "import\|req"Repository: StuxMirai/ScryptTunes
Length of output: 661
🏁 Script executed:
# Verify the exact lines of the HTTP calls mentioned in the review
sed -n '280,450p' bot/scrypt_tunes.py | grep -n "req\." -B 2 -A 2Repository: StuxMirai/ScryptTunes
Length of output: 771
🏁 Script executed:
# Check if there are any other blocking requests in the file
rg "req\.(get|post|put|delete|patch)" bot/scrypt_tunes.py -nRepository: StuxMirai/ScryptTunes
Length of output: 271
🏁 Script executed:
# Check the Python version to ensure asyncio.to_thread is available (requires Python 3.9+)
cat pyproject.toml | grep -i python || cat setup.py | grep -i python || cat requirements.txt | head -20Repository: StuxMirai/ScryptTunes
Length of output: 302
🏁 Script executed:
# Check Python version requirement in repository
find . -type f \( -name "*.toml" -o -name "*.cfg" -o -name ".python-version" -o -name "README*" \) -exec grep -l "python" {} \; 2>/dev/null | head -5Repository: StuxMirai/ScryptTunes
Length of output: 76
🏁 Script executed:
# Get more context around line 545 to verify if it's in async context
sed -n '540,560p' bot/scrypt_tunes.pyRepository: StuxMirai/ScryptTunes
Length of output: 1150
🏁 Script executed:
# Verify the function signatures for the async functions containing blocking calls
sed -n '283,295p' bot/scrypt_tunes.py && echo "---" && sed -n '320,330p' bot/scrypt_tunes.pyRepository: StuxMirai/ScryptTunes
Length of output: 1242
🏁 Script executed:
# Check Python version requirement in README.md
grep -i "python" README.md | head -10Repository: StuxMirai/ScryptTunes
Length of output: 226
🏁 Script executed:
# Also check if there are any GitHub Actions or other CI files that specify Python version
find . -type f \( -name "*.yml" -o -name "*.yaml" \) -exec grep -l "python" {} \; 2>/dev/null | head -3Repository: StuxMirai/ScryptTunes
Length of output: 103
🏁 Script executed:
# Check if asyncio.to_thread is already used elsewhere in the codebase
rg "asyncio\.to_thread" bot/ --type pyRepository: StuxMirai/ScryptTunes
Length of output: 47
🏁 Script executed:
# Verify the context of line 545 - check the function it's in
sed -n '520,550p' bot/scrypt_tunes.py | head -40Repository: StuxMirai/ScryptTunes
Length of output: 1796
🏁 Script executed:
# Check GitHub Actions for Python version
cat ./.github/workflows/build-and-sign.ymlRepository: StuxMirai/ScryptTunes
Length of output: 3429
🏁 Script executed:
# Verify the full signature of chat_song_request to confirm it's async
sed -n '470,475p' bot/scrypt_tunes.pyRepository: StuxMirai/ScryptTunes
Length of output: 282
🏁 Script executed:
# Double-check if there are any try/except blocks or threading already used to handle blocking calls
rg "ThreadPoolExecutor|Thread\(|to_thread" bot/scrypt_tunes.pyRepository: StuxMirai/ScryptTunes
Length of output: 47
Blocking synchronous HTTP calls in async functions will block the event loop.
Three requests.get/post calls are blocking the event loop in async functions:
- Line 294:
req.get()in_ensure_channel_points_listener() - Line 375:
req.post()in nested_run()within_start_eventsub_channel_points() - Line 545:
req.get()inchat_song_request()
This blocks chat handling and EventSub processing. Use asyncio.to_thread() to offload these to a thread pool (Python 3.12 supports this):
- resp = req.get(
- "https://id.twitch.tv/oauth2/validate",
- headers={"Authorization": f"Bearer {token_for_validate}"},
- timeout=10,
- )
+ resp = await asyncio.to_thread(
+ req.get,
+ "https://id.twitch.tv/oauth2/validate",
+ headers={"Authorization": f"Bearer {token_for_validate}"},
+ timeout=10,
+ )Apply the same pattern to the other two blocking calls.
🧰 Tools
🪛 Ruff (0.15.0)
[warning] 385-385: Do not catch blind exception: Exception
(BLE001)
[warning] 388-388: Do not catch blind exception: Exception
(BLE001)
[error] 419-420: try-except-pass detected, consider logging the exception
(S110)
[warning] 419-419: Do not catch blind exception: Exception
(BLE001)
[warning] 429-429: Do not catch blind exception: Exception
(BLE001)
[warning] 430-430: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
[error] 435-436: try-except-pass detected, consider logging the exception
(S110)
[warning] 435-435: Do not catch blind exception: Exception
(BLE001)
[error] 440-441: try-except-pass detected, consider logging the exception
(S110)
[warning] 440-440: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In `@bot/scrypt_tunes.py` around lines 283 - 446, The code is doing blocking HTTP
calls with req.get/post inside async functions; modify the three locations (in
_ensure_channel_points_listener where req.get(...) is used to validate the
token, in _start_eventsub_channel_points nested _run() where req.post(...)
creates the EventSub subscription, and in chat_song_request where req.get(...)
is used) to offload the synchronous requests to a threadpool by wrapping them
with await asyncio.to_thread(...) (i.e., replace direct calls to req.get/post
with await asyncio.to_thread(req.get, <args...>) or await
asyncio.to_thread(req.post, <args...>)), preserving the same
headers/json/timeout handling and response checks so the logic using
resp.status_code/resp.json()/r.text continues to work; update imports if needed
(ensure asyncio is imported) and keep the surrounding async function structure
(_ensure_channel_points_listener, _start_eventsub_channel_points::_run,
chat_song_request) unchanged.
| if '.link/' in song_uri: | ||
| ctx.send( | ||
| f'@{ctx.author.name} Mobile link detected, attempting to get full url.') # todo: verify this is sending????? | ||
| f'@{ctx.author.name} Mobile link detected, attempting to get full url.') | ||
| req_data = req.get( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, examine the exact code at lines 542-545 and surrounding context
sed -n '535,555p' bot/scrypt_tunes.py | cat -nRepository: StuxMirai/ScryptTunes
Length of output: 1187
🏁 Script executed:
#!/bin/bash
# Search for all ctx.send calls in the file to understand the pattern
rg -n "ctx\.send" bot/scrypt_tunes.py | head -20Repository: StuxMirai/ScryptTunes
Length of output: 1656
🏁 Script executed:
#!/bin/bash
# Check if the file uses async/await patterns
rg -n "async def|await" bot/scrypt_tunes.py | head -20Repository: StuxMirai/ScryptTunes
Length of output: 1490
🏁 Script executed:
#!/bin/bash
# Get the function definition containing line 543
sed -n '500,545p' bot/scrypt_tunes.py | grep -B 20 "ctx.send" | head -30Repository: StuxMirai/ScryptTunes
Length of output: 1873
🏁 Script executed:
#!/bin/bash
# Get the exact function definition and the problematic ctx.send call
sed -n '520,560p' bot/scrypt_tunes.py | cat -nRepository: StuxMirai/ScryptTunes
Length of output: 2511
Add await to ctx.send() call.
The ctx.send() call at line 543 is a coroutine but lacks await. Without it, the message will not be sent and a RuntimeWarning will be raised. All other ctx.send() calls in the file use await; this one should too.
Fix
- ctx.send(
+ await ctx.send(
f'@{ctx.author.name} Mobile link detected, attempting to get full url.')📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if '.link/' in song_uri: | |
| ctx.send( | |
| f'@{ctx.author.name} Mobile link detected, attempting to get full url.') # todo: verify this is sending????? | |
| f'@{ctx.author.name} Mobile link detected, attempting to get full url.') | |
| req_data = req.get( | |
| if '.link/' in song_uri: | |
| await ctx.send( | |
| f'@{ctx.author.name} Mobile link detected, attempting to get full url.') | |
| req_data = req.get( |
🧰 Tools
🪛 Ruff (0.15.0)
[error] 545-545: Probable use of requests call without timeout
(S113)
🤖 Prompt for AI Agents
In `@bot/scrypt_tunes.py` around lines 542 - 545, The ctx.send() call inside the
mobile-link branch (the block starting with "if '.link/' in song_uri:") is
missing an await and should be awaited like the other sends; update the call to
await ctx.send(...) so the coroutine is executed (keep the same message text and
surrounding logic where req.get(...) follows).
| ### Dev Announcements | ||
| - New: Built‑in Twitch OAuth (Implicit Grant) and Channel Points via EventSub WebSocket. No Twitch client secret required. Spotify now uses PKCE (no client secret). |
There was a problem hiding this comment.
Fix heading level jump for “Dev Announcements”.
Line 7 jumps from H1 to H3; change it to H2 to satisfy heading hierarchy (MD001).
✏️ Suggested fix
-### Dev Announcements
+## Dev Announcements📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Dev Announcements | |
| - New: Built‑in Twitch OAuth (Implicit Grant) and Channel Points via EventSub WebSocket. No Twitch client secret required. Spotify now uses PKCE (no client secret). | |
| ## Dev Announcements | |
| - New: Built‑in Twitch OAuth (Implicit Grant) and Channel Points via EventSub WebSocket. No Twitch client secret required. Spotify now uses PKCE (no client secret). |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 7-7: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🤖 Prompt for AI Agents
In `@README.md` around lines 7 - 8, The "Dev Announcements" heading currently uses
H3 (### Dev Announcements) which breaks the heading hierarchy; update that
heading to H2 by changing "### Dev Announcements" to "## Dev Announcements" so
the document follows proper Markdown heading levels.
| def _shutdown(): | ||
| try: | ||
| controller_ref._oauth_server_httpd.shutdown() | ||
| except Exception: | ||
| pass | ||
| if controller_ref._oauth_on_success_cb: | ||
| controller_ref._oauth_on_success_cb(f"oauth:{access_token}") | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file and understand the context around lines 255-262
cat -n ui/controllers/settings_controller.py | sed -n '240,280p'Repository: StuxMirai/ScryptTunes
Length of output: 1989
🏁 Script executed:
# Search for _oauth_on_success_cb to understand how it's defined and used
rg "_oauth_on_success_cb" -B 3 -A 3Repository: StuxMirai/ScryptTunes
Length of output: 1875
🏁 Script executed:
# Search for _shutdown context and where it's called from
rg "_shutdown" -B 5 -A 10Repository: StuxMirai/ScryptTunes
Length of output: 4487
🌐 Web query:
Tkinter thread-safety messagebox widget calls main thread requirements
💡 Result:
Tkinter (including tkinter.messagebox) must be interacted with from the same thread that owns the Tcl/Tk interpreter—normally the main thread running root.mainloop(). If you call messagebox.askyesno(), showinfo(), etc. from a worker thread, it may hang, do nothing, or raise errors like “main thread is not in main loop”, because Tk activity is expected to happen on the interpreter/event-loop thread. [1][2]
What to do instead (recommended pattern):
- Do work in background threads.
- Send a “please show a dialog” request to the GUI thread (via
queue.Queue), and have the GUI thread callmessagebox.... - Or schedule the dialog from the GUI thread using
root.after(...).
This “all tkinter calls on the main thread; worker threads communicate via a queue; GUI polls or is scheduled with after” approach is the commonly recommended safe method. [3][4]
Minimal example (worker → main thread → messagebox):
import tkinter as tk
from tkinter import messagebox
import threading, queue
q = queue.Queue()
def worker():
# ... do background work ...
q.put(("done", "Background task finished"))
def poll():
try:
kind, text = q.get_nowait()
except queue.Empty:
pass
else:
if kind == "done":
messagebox.showinfo("Info", text) # runs on main thread
root.after(50, poll)
root = tk.Tk()
tk.Button(root, text="Start", command=lambda: threading.Thread(target=worker, daemon=True).start()).pack()
root.after(50, poll)
root.mainloop()Notes
- Even though some
_tkintercalls may be internally queued to the interpreter thread, the safe rule remains: keep GUI operations (including dialogs) on the GUI thread, and ensure the event loop is actually running/processing events. [1][3]
Sources
- Python docs (
tkinter) on threading/event-loop constraints. [1] - Stack Overflow: messageboxes/dialogs can’t be created in a thread other than the one that created Tk. [2]
- Python tracker discussion: safest method is all tkinter calls in main thread +
queue.Queue. [3] - Stack Overflow: use
afterinstead of timers/threads to trigger GUI actions. [4]
References:
[1] (docs.python.org)
[2] (stackoverflow.com)
[3] (bugs.python.org)
[4] (stackoverflow.com)
Citations:
- 1: https://docs.python.org/uk/dev/library/tkinter.html?utm_source=openai
- 2: https://stackoverflow.com/questions/17480637/tkinter-cannnot-create-messagebox-in-a-thread?utm_source=openai
- 3: https://bugs.python.org/msg316447?utm_source=openai
- 4: https://stackoverflow.com/questions/25013671/tkinter-issues-in-python?utm_source=openai
🏁 Script executed:
# Search for start_oauth_flow calls to see what callback is passed as on_success
rg "start_oauth_flow" -B 2 -A 2Repository: StuxMirai/ScryptTunes
Length of output: 47
🏁 Script executed:
# Look for method definitions of callbacks that might be passed as on_success
rg "def.*on_success|on_success.*=" -B 3 -A 5Repository: StuxMirai/ScryptTunes
Length of output: 5165
🏁 Script executed:
# Search for messagebox calls in the codebase to understand UI patterns
rg "messagebox|showinfo|askyesno|showwarning" -B 2 -A 2Repository: StuxMirai/ScryptTunes
Length of output: 5369
🏁 Script executed:
# Check if controller_ref has a 'root' attribute to validate the proposed fix
rg "self\.root\s*=" ui/controllers/settings_controller.py -B 2 -A 2Repository: StuxMirai/ScryptTunes
Length of output: 216
🏁 Script executed:
# Also check the class definition to understand the controller structure
head -50 ui/controllers/settings_controller.pyRepository: StuxMirai/ScryptTunes
Length of output: 1733
Marshal OAuth success callbacks to the UI thread.
The callback invoked on line 261 runs on a background thread (spawned at line 263), but the registered callbacks (_on_twitch_connected, _on_broadcaster_connected, _on_spotify_connected) all trigger messagebox.showinfo() or messagebox.showerror() calls, which must execute on the main Tk thread. Calling Tkinter operations from a worker thread can cause hangs, freezes, or errors.
Use root.after() to marshal the callback back to the main event loop:
Proposed fix
if controller_ref._oauth_on_success_cb:
- controller_ref._oauth_on_success_cb(f"oauth:{access_token}")
+ controller_ref.root.after(
+ 0, lambda: controller_ref._oauth_on_success_cb(f"oauth:{access_token}")
+ )🧰 Tools
🪛 Ruff (0.15.0)
[error] 258-259: try-except-pass detected, consider logging the exception
(S110)
[warning] 258-258: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
In `@ui/controllers/settings_controller.py` around lines 255 - 262, The OAuth
success callback is being invoked from the background thread inside _shutdown
which calls controller_ref._oauth_on_success_cb(f"oauth:{access_token}"), but
those callbacks perform Tkinter UI work and must run on the main thread; change
the invocation to marshal to the Tk main loop by using root.after(0, lambda:
controller_ref._oauth_on_success_cb(f"oauth:{access_token}")) (or equivalent) so
the callback runs on the UI thread instead of directly in _shutdown after
controller_ref._oauth_server_httpd.shutdown().
| self.channel_points_reward = TextSettingRow( | ||
| self, | ||
| setting_name="Token", | ||
| setting_description="Twitch OAuth Token", | ||
| initial_value=settings_controller.get("token"), | ||
| hidden=True, | ||
| setting_name="channel_points_reward", | ||
| setting_description="(Optional) Title of your Channel Points reward to trigger Now Playing", | ||
| initial_value=settings_controller.get("channel_points_reward"), | ||
| ) |
There was a problem hiding this comment.
Use a user‑friendly label for the Channel Points reward field.
The label currently shows the internal key; consider a readable title.
💡 Proposed fix
- setting_name="channel_points_reward",
+ setting_name="Channel Points Reward",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| self.channel_points_reward = TextSettingRow( | |
| self, | |
| setting_name="Token", | |
| setting_description="Twitch OAuth Token", | |
| initial_value=settings_controller.get("token"), | |
| hidden=True, | |
| setting_name="channel_points_reward", | |
| setting_description="(Optional) Title of your Channel Points reward to trigger Now Playing", | |
| initial_value=settings_controller.get("channel_points_reward"), | |
| ) | |
| self.channel_points_reward = TextSettingRow( | |
| self, | |
| setting_name="Channel Points Reward", | |
| setting_description="(Optional) Title of your Channel Points reward to trigger Now Playing", | |
| initial_value=settings_controller.get("channel_points_reward"), | |
| ) |
🤖 Prompt for AI Agents
In `@ui/frames/general_settings_frame.py` around lines 51 - 56, The label for the
Channel Points reward currently shows the internal key; update the
TextSettingRow initialization in general_settings_frame.py so it supplies a
user-friendly label/title (e.g. "Channel Points Reward" or "Channel Points
Reward Title") instead of the raw setting_name. Locate the TextSettingRow call
that sets setting_name="channel_points_reward" and add or replace the label
argument your UI component supports (e.g. setting_label, label, or title) to
present the readable text while keeping setting_name and setting_description
unchanged; if TextSettingRow lacks a label prop, extend its constructor (and
usages) to accept and render a human-facing label.
…ning label with a link for more information. Adjust layout for new elements and ensure settings are saved correctly.
Fix spotify auth
|
TODO:
|
|
closing for now to do the above refactors ^ |
@StuxMirai dont forget to bump version number before finishing
still problems. spotify required registering users with the api application. limited to 5 unless im some multi-million dollar org
https://community.spotify.com/t5/Spotify-for-Developers/Updating-the-Criteria-for-Web-API-Extended-Access/td-p/6920661