diff --git a/CLAUDE.md b/CLAUDE.md index b0459ee..e18fd7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -300,7 +300,7 @@ The project does not include a test suite. Manual testing workflow: 1. Run with `-vvv` for maximum verbosity (levels: -v, -vv, -vvv, -vvvv) 2. Use `--dump` to generate debug data dumps -3. Check timestamped log files in `./logs/` directory (always created as `TDM.TIMESTAMP.log`) +3. Check log files in `./logs/` directory 4. Use `--debug-ws` for websocket debug logging 5. Use `--debug-gql` for GraphQL debug logging 6. Monitor web GUI console output and browser developer tools diff --git a/add_language_names.py b/add_language_names.py index c372294..c2ede50 100644 --- a/add_language_names.py +++ b/add_language_names.py @@ -4,8 +4,10 @@ import json from pathlib import Path + LANG_PATH = Path(__file__).parent / "lang" + def add_language_names(): """Add language_name field to each translation file based on filename.""" for filepath in LANG_PATH.glob("*.json"): @@ -15,7 +17,7 @@ def add_language_names(): print(f"Processing {filepath.name}...") # Read the JSON file - with open(filepath, 'r', encoding='utf-8') as f: + with open(filepath, encoding="utf-8") as f: data = json.load(f) # Add language_name at the beginning @@ -23,11 +25,12 @@ def add_language_names(): updated_data.update(data) # Write back to file with proper formatting - with open(filepath, 'w', encoding='utf-8') as f: + with open(filepath, "w", encoding="utf-8") as f: json.dump(updated_data, f, ensure_ascii=False, indent=4) print(f" ✓ Added language_name: {language_name}") + if __name__ == "__main__": add_language_names() print("\n✓ All translation files updated!") diff --git a/add_more_translations.py b/add_more_translations.py index 29ab795..dcca6fe 100644 --- a/add_more_translations.py +++ b/add_more_translations.py @@ -5,9 +5,9 @@ Adds English text as placeholders where translations are missing. """ import json -import os from pathlib import Path + # Translations to add/update for each language # Format: {language_code: {key_path: translation}} TRANSLATIONS = { @@ -36,10 +36,10 @@ TRANSLATIONS = { "gui.help.how_to_use": "使用方法", "gui.help.how_to_use_items": [ "使用您的 Twitch 账号登录(OAuth 设备代码流程)", - "在 twitch.tv/drops/campaigns 关联您的账号", + '在 twitch.tv/drops/campaigns 关联您的账号', "矿工将自动发现活动并开始挖掘", "在设置中配置优先游戏以关注您想要的内容", - "在主界面和库存选项卡中监控进度" + "在主界面和库存选项卡中监控进度", ], "gui.help.features": "功能", "gui.help.features_items": [ @@ -47,13 +47,13 @@ TRANSLATIONS = { "游戏优先级和排除列表", "同时跟踪最多 199 个频道", "自动切换频道", - "实时进度跟踪" + "实时进度跟踪", ], "gui.help.important_notes": "重要提示", "gui.help.important_notes_items": [ "挖掘时请勿在同一账号上观看流", "保护好您的 cookies.jar 文件", - "需要关联游戏账号才能掉宝" + "需要关联游戏账号才能掉宝", ], "gui.help.github_repo": "GitHub 仓库", "gui.header.language": "语言:", @@ -88,10 +88,10 @@ DEFAULT_TRANSLATIONS = { "gui.help.how_to_use": "How to Use", "gui.help.how_to_use_items": [ "Login using your Twitch account (OAuth device code flow)", - "Link your accounts at twitch.tv/drops/campaigns", + 'Link your accounts at twitch.tv/drops/campaigns', "The miner will automatically discover campaigns and start mining", "Configure priority games in Settings to focus on what you want", - "Monitor progress in the Main and Inventory tabs" + "Monitor progress in the Main and Inventory tabs", ], "gui.help.features": "Features", "gui.help.features_items": [ @@ -99,13 +99,13 @@ DEFAULT_TRANSLATIONS = { "Game priority and exclusion lists", "Tracks up to 199 channels simultaneously", "Automatic channel switching", - "Real-time progress tracking" + "Real-time progress tracking", ], "gui.help.important_notes": "Important Notes", "gui.help.important_notes_items": [ "Do not watch streams on the same account while mining", "Keep your cookies.jar file secure", - "Requires linked game accounts for drops" + "Requires linked game accounts for drops", ], "gui.help.github_repo": "GitHub Repository", "gui.header.language": "Language:", @@ -139,7 +139,7 @@ def get_nested_value(data, key_path, default=None): def update_language_file(file_path): """Update a language file with missing translations.""" - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, encoding="utf-8") as f: data = json.load(f) language_name = data.get("english_name", "Unknown") @@ -164,7 +164,7 @@ def update_language_file(file_path): if updated: # Write back with proper formatting - with open(file_path, 'w', encoding='utf-8') as f: + with open(file_path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=4) print(f" ✓ Saved {language_name}") else: diff --git a/src/core/client.py b/src/core/client.py index aa015f8..09f1e5e 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -6,7 +6,7 @@ from collections import OrderedDict, abc, deque from datetime import datetime, timedelta, timezone from functools import partial from time import time -from typing import TYPE_CHECKING, Any, Final, Literal, NoReturn +from typing import TYPE_CHECKING, Any, Final, Literal import aiohttp @@ -32,7 +32,6 @@ from src.services.message_handlers import MessageHandlerService from src.services.watch_service import WatchService from src.utils import ( AwaitableValue, - task_wrapper, ) from src.websocket import WebsocketPool @@ -209,9 +208,14 @@ class Twitch: # Add default topics self.websocket.add_topics( [ - WebsocketTopic("User", "Drops", auth_state.user_id, self._message_handler_service.process_drops), WebsocketTopic( - "User", "Notifications", auth_state.user_id, self._message_handler_service.process_notifications + "User", "Drops", auth_state.user_id, self._message_handler_service.process_drops + ), + WebsocketTopic( + "User", + "Notifications", + auth_state.user_id, + self._message_handler_service.process_notifications, ), ] ) @@ -433,12 +437,18 @@ class Twitch: for channel_id in channels: to_add_topics.append( WebsocketTopic( - "Channel", "StreamState", channel_id, self._message_handler_service.process_stream_state + "Channel", + "StreamState", + channel_id, + self._message_handler_service.process_stream_state, ) ) to_add_topics.append( WebsocketTopic( - "Channel", "StreamUpdate", channel_id, self._message_handler_service.process_stream_update + "Channel", + "StreamUpdate", + channel_id, + self._message_handler_service.process_stream_update, ) ) self.websocket.add_topics(to_add_topics) @@ -512,7 +522,9 @@ class Twitch: self.exit_manual_mode("No channels available for manual game") # Auto-select best channel based on priority else: - for channel in sorted(channels.values(), key=self._channel_service.get_priority): + for channel in sorted( + channels.values(), key=self._channel_service.get_priority + ): if self.can_watch(channel) and self.should_switch(channel): new_watching = channel break @@ -531,7 +543,9 @@ class Twitch: if self.is_manual_mode() and self._manual_target_game: status_text = f"🎯 Manual Mode: Watching {watching_channel.name} for {self._manual_target_game.name}" else: - status_text = _.t["status"]["watching"].format(channel=watching_channel.name) + status_text = _.t["status"]["watching"].format( + channel=watching_channel.name + ) self.gui.status.update(status_text) self._state_change.clear() else: diff --git a/src/i18n/translator.py b/src/i18n/translator.py index 44d487d..83fb289 100644 --- a/src/i18n/translator.py +++ b/src/i18n/translator.py @@ -6,6 +6,7 @@ from typing import TypedDict, cast from src.config import DEFAULT_LANG, LANG_PATH + class StatusMessages(TypedDict): terminated: str watching: str @@ -213,13 +214,14 @@ class Translator: self.t: Translation # load available languages from JSON files by reading language_name field for filepath in LANG_PATH.glob("*.json"): - try: - loaded_translation: Translation = json.load(open(filepath, "r")) - self._langs[loaded_translation["language_name"]] = loaded_translation - except Exception as e: - # if we can't read the file, skip it - self.logger.warning(f"Failed to load language file {filepath}: {e}") - continue + with filepath.open("r", encoding="utf-8") as json_file: + try: + loaded_translation: Translation = json.load(json_file) + self._langs[loaded_translation["language_name"]] = loaded_translation + except Exception as e: + # if we can't read the file, skip it + self.logger.warning(f"Failed to load language file {filepath}: {e}") + continue self._langs = dict(sorted(self._langs.items())) self.set_language(DEFAULT_LANG) @@ -233,4 +235,5 @@ class Translator: self.current_language = language self.t = cast(Translation, self._langs.get(language)) + _ = Translator() diff --git a/src/services/channel_service.py b/src/services/channel_service.py index 74d79ef..d0d7b97 100644 --- a/src/services/channel_service.py +++ b/src/services/channel_service.py @@ -150,14 +150,21 @@ class ChannelService: # NOTE: Have to do this here, because "channels" can be any iterable return - stream_gql_tasks: list[asyncio.Task[list[JsonType]]] = [ + # gql_request may return either a single JsonType or a list[JsonType], + # so accept the union in the Task type. + stream_gql_tasks: list[asyncio.Task[JsonType | list[JsonType]]] = [ asyncio.create_task(self._twitch.gql_request(stream_gql_chunk)) for stream_gql_chunk in chunk(stream_gql_ops, 20) ] try: for coro in asyncio.as_completed(stream_gql_tasks): - response_list: list[JsonType] = await coro + response = await coro + # Normalize response to a list for uniform processing + if isinstance(response, list): + response_list: list[JsonType] = response + else: + response_list = [response] for response_json in response_list: channel_data: JsonType = response_json["data"]["user"] if channel_data is not None: diff --git a/src/services/inventory_service.py b/src/services/inventory_service.py index d582c81..783e2ff 100644 --- a/src/services/inventory_service.py +++ b/src/services/inventory_service.py @@ -185,7 +185,9 @@ class InventoryService: for i, coro in enumerate(asyncio.as_completed(add_campaign_tasks), start=1): await coro status_update( - _.t["gui"]["status"]["adding_campaigns"].format(counter=f"({i}/{len(campaigns)})") + _.t["gui"]["status"]["adding_campaigns"].format( + counter=f"({i}/{len(campaigns)})" + ) ) # this is needed here explicitly, because cache reads from disk don't raise this from src.config import State diff --git a/src/web/managers/login.py b/src/web/managers/login.py index 8a57cdf..4695aba 100644 --- a/src/web/managers/login.py +++ b/src/web/managers/login.py @@ -105,7 +105,7 @@ class LoginFormManager: Returns: Dictionary with status, user_id, and optional oauth_pending data """ - result = {"status": self._status, "user_id": self._user_id} + result: dict[str, Any] = {"status": self._status, "user_id": self._user_id} # Include OAuth code if pending if self._oauth_pending: result["oauth_pending"] = self._oauth_pending diff --git a/src/web/managers/settings.py b/src/web/managers/settings.py index 4b1459d..05b836a 100644 --- a/src/web/managers/settings.py +++ b/src/web/managers/settings.py @@ -69,10 +69,13 @@ class SettingsManager: _.set_language(language) self._settings.language = language # Notify clients that translations need to be reloaded - asyncio.create_task(self._broadcaster.emit("language_changed", {"language": language})) + asyncio.create_task( + self._broadcaster.emit("language_changed", {"language": language}) + ) except ValueError as e: # Invalid language, log warning import logging + logging.warning(f"Invalid language '{language}': {e}") if "connection_quality" in settings_data: self._settings.connection_quality = settings_data["connection_quality"]