webtap-tool 0.1.5__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webtap-tool might be problematic. Click here for more details.

@@ -1,143 +1,27 @@
1
- """Desktop entry setup functionality for WebTap."""
1
+ """Desktop/Application launcher setup (cross-platform)."""
2
2
 
3
- import re
4
3
  import logging
5
- from pathlib import Path
6
4
  from typing import Dict, Any
7
5
 
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- def install_desktop_entry(force: bool = False) -> Dict[str, Any]:
12
- """Install desktop entry to override system Chrome launcher.
13
-
14
- Creates a .desktop file that makes Chrome launch with our debug wrapper
15
- when started from GUI (app launcher, dock, etc).
16
-
17
- Args:
18
- force: Overwrite existing desktop entry
19
-
20
- Returns:
21
- Dict with success, message, path, details
22
- """
23
- # Check if wrapper exists first
24
- wrapper_path = Path.home() / ".local" / "bin" / "wrappers" / "google-chrome-stable"
25
- if not wrapper_path.exists():
26
- return {
27
- "success": False,
28
- "message": "Chrome wrapper not found. Run 'setup-chrome' first",
29
- "path": None,
30
- "details": f"Expected wrapper at {wrapper_path}",
31
- }
32
-
33
- # Desktop entry location (user override of system entry)
34
- desktop_path = Path.home() / ".local" / "share" / "applications" / "google-chrome.desktop"
6
+ from .platform import get_platform_info
35
7
 
36
- if desktop_path.exists() and not force:
37
- return {
38
- "success": False,
39
- "message": "Desktop entry already exists",
40
- "path": str(desktop_path),
41
- "details": "Use --force to overwrite",
42
- }
8
+ logger = logging.getLogger(__name__)
43
9
 
44
- # Get system Chrome desktop file for reference
45
- system_desktop = _find_system_desktop_file()
46
-
47
- if system_desktop:
48
- desktop_content = _create_from_system_desktop(system_desktop, wrapper_path)
49
- source_info = system_desktop.name
50
- else:
51
- desktop_content = _create_minimal_desktop(wrapper_path)
52
- source_info = "minimal template"
53
-
54
- # Create directory and save
55
- desktop_path.parent.mkdir(parents=True, exist_ok=True)
56
- desktop_path.write_text(desktop_content)
57
- desktop_path.chmod(0o644) # Standard permissions for desktop files
58
-
59
- logger.info(f"Installed desktop entry to {desktop_path}")
60
-
61
- return {
62
- "success": True,
63
- "message": "Installed Chrome desktop entry with debug wrapper",
64
- "path": str(desktop_path),
65
- "details": f"Based on {source_info}",
66
- }
67
-
68
-
69
- def _find_system_desktop_file() -> Path | None:
70
- """Find the system Chrome desktop file."""
71
- for system_path in [
72
- Path("/usr/share/applications/google-chrome.desktop"),
73
- Path("/usr/share/applications/google-chrome-stable.desktop"),
74
- Path("/usr/local/share/applications/google-chrome.desktop"),
75
- ]:
76
- if system_path.exists():
77
- return system_path
78
- return None
79
-
80
-
81
- def _create_from_system_desktop(system_desktop: Path, wrapper_path: Path) -> str:
82
- """Create desktop content by modifying the system desktop file."""
83
- try:
84
- system_content = system_desktop.read_text()
85
-
86
- # Replace all Exec= lines to use our wrapper
87
- # Match Exec= lines that point to chrome executables
88
- exec_pattern = re.compile(
89
- r"^Exec=.*?(?:google-chrome-stable|google-chrome|chromium|chrome)(?:\s|$)", re.MULTILINE
90
- )
91
-
92
- # Replace with our wrapper, preserving arguments
93
- def replace_exec(match):
94
- line = match.group(0)
95
- # Find where the executable ends and arguments begin
96
- # Look for common chrome executables
97
- for exe in ["google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"]:
98
- if exe in line:
99
- # Split at the executable name
100
- parts = line.split(exe, 1)
101
- if len(parts) > 1:
102
- # Has arguments after executable
103
- return f"Exec={wrapper_path}{parts[1]}"
104
- else:
105
- # No arguments
106
- return f"Exec={wrapper_path}"
107
- # Fallback - shouldn't happen
108
- return f"Exec={wrapper_path}"
109
-
110
- desktop_content = exec_pattern.sub(replace_exec, system_content)
111
-
112
- # Also handle TryExec if present (used to check if program exists)
113
- tryexec_pattern = re.compile(
114
- r"^TryExec=.*?(?:google-chrome-stable|google-chrome|chromium|chrome)", re.MULTILINE
115
- )
116
- desktop_content = tryexec_pattern.sub(f"TryExec={wrapper_path}", desktop_content)
117
-
118
- return desktop_content
119
-
120
- except Exception as e:
121
- logger.warning(f"Failed to parse system desktop file, using minimal entry: {e}")
122
- return _create_minimal_desktop(wrapper_path)
123
-
124
-
125
- def _create_minimal_desktop(wrapper_path: Path) -> str:
126
- """Create a minimal desktop entry from scratch."""
127
- return f"""[Desktop Entry]
10
+ # Desktop entry template for Linux
11
+ LINUX_DESKTOP_ENTRY = """[Desktop Entry]
128
12
  Version=1.0
129
13
  Type=Application
130
- Name=Google Chrome
131
- GenericName=Web Browser
132
- Comment=Access the Internet
14
+ Name=Chrome Debug
15
+ GenericName=Web Browser (Debug Mode)
16
+ Comment=Chrome with remote debugging enabled
133
17
  Icon=google-chrome
134
- Categories=Network;WebBrowser;
18
+ Categories=Development;WebBrowser;
135
19
  MimeType=application/pdf;application/rdf+xml;application/rss+xml;application/xhtml+xml;application/xhtml_xml;application/xml;image/gif;image/jpeg;image/png;image/webp;text/html;text/xml;x-scheme-handler/ftp;x-scheme-handler/http;x-scheme-handler/https;
136
20
  StartupWMClass=Google-chrome
137
21
  StartupNotify=true
138
22
  Terminal=false
139
23
  Exec={wrapper_path} %U
140
- Actions=new-window;new-private-window;
24
+ Actions=new-window;new-private-window;temp-profile;
141
25
 
142
26
  [Desktop Action new-window]
143
27
  Name=New Window
@@ -148,4 +32,197 @@ Exec={wrapper_path}
148
32
  Name=New Incognito Window
149
33
  StartupWMClass=Google-chrome
150
34
  Exec={wrapper_path} --incognito
35
+
36
+ [Desktop Action temp-profile]
37
+ Name=New Window (Temp Profile)
38
+ StartupWMClass=Google-chrome
39
+ Exec={wrapper_path} --temp
40
+ """
41
+
42
+ # Info.plist template for macOS app bundle
43
+ MACOS_INFO_PLIST = """<?xml version="1.0" encoding="UTF-8"?>
44
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
45
+ <plist version="1.0">
46
+ <dict>
47
+ <key>CFBundleExecutable</key>
48
+ <string>Chrome Debug</string>
49
+ <key>CFBundleIdentifier</key>
50
+ <string>com.webtap.chrome-debug</string>
51
+ <key>CFBundleName</key>
52
+ <string>Chrome Debug</string>
53
+ <key>CFBundleDisplayName</key>
54
+ <string>Chrome Debug</string>
55
+ <key>CFBundleVersion</key>
56
+ <string>1.0</string>
57
+ <key>CFBundleShortVersionString</key>
58
+ <string>1.0</string>
59
+ <key>CFBundlePackageType</key>
60
+ <string>APPL</string>
61
+ <key>CFBundleSignature</key>
62
+ <string>????</string>
63
+ <key>LSMinimumSystemVersion</key>
64
+ <string>10.12</string>
65
+ <key>CFBundleDocumentTypes</key>
66
+ <array>
67
+ <dict>
68
+ <key>CFBundleTypeName</key>
69
+ <string>HTML Document</string>
70
+ <key>CFBundleTypeRole</key>
71
+ <string>Viewer</string>
72
+ <key>LSItemContentTypes</key>
73
+ <array>
74
+ <string>public.html</string>
75
+ </array>
76
+ </dict>
77
+ <dict>
78
+ <key>CFBundleTypeName</key>
79
+ <string>Web Location</string>
80
+ <key>CFBundleTypeRole</key>
81
+ <string>Viewer</string>
82
+ <key>LSItemContentTypes</key>
83
+ <array>
84
+ <string>public.url</string>
85
+ </array>
86
+ </dict>
87
+ </array>
88
+ <key>CFBundleURLTypes</key>
89
+ <array>
90
+ <dict>
91
+ <key>CFBundleURLName</key>
92
+ <string>Web site URL</string>
93
+ <key>CFBundleURLSchemes</key>
94
+ <array>
95
+ <string>http</string>
96
+ <string>https</string>
97
+ </array>
98
+ </dict>
99
+ </array>
100
+ </dict>
101
+ </plist>"""
102
+
103
+
104
+ class DesktopSetupService:
105
+ """Platform-appropriate GUI launcher setup."""
106
+
107
+ def __init__(self):
108
+ self.info = get_platform_info()
109
+ self.paths = self.info["paths"]
110
+ self.chrome = self.info["chrome"]
111
+
112
+ # Unified wrapper path: ~/.local/bin/chrome-debug
113
+ self.wrapper_name = self.chrome["wrapper_name"] # chrome-debug
114
+ self.wrapper_path = self.paths["bin_dir"] / self.wrapper_name
115
+
116
+ def install_launcher(self, force: bool = False) -> Dict[str, Any]:
117
+ """Install platform-appropriate launcher.
118
+
119
+ Args:
120
+ force: Overwrite existing launcher
121
+
122
+ Returns:
123
+ Installation result
124
+ """
125
+ # Check if wrapper exists first
126
+ if not self.wrapper_path.exists():
127
+ return {
128
+ "success": False,
129
+ "message": "Chrome wrapper 'chrome-debug' not found. Run 'setup-chrome' first",
130
+ "path": None,
131
+ "details": f"Expected wrapper at {self.wrapper_path}",
132
+ }
133
+
134
+ if self.info["is_macos"]:
135
+ return self._install_macos_app(force)
136
+ else:
137
+ return self._install_linux_desktop(force)
138
+
139
+ def _install_macos_app(self, force: bool) -> Dict[str, Any]:
140
+ """Create .app bundle for macOS.
141
+
142
+ Args:
143
+ force: Overwrite existing app
144
+
145
+ Returns:
146
+ Installation result
147
+ """
148
+ app_path = self.paths["applications_dir"] / "Chrome Debug.app"
149
+
150
+ if app_path.exists() and not force:
151
+ return {
152
+ "success": False,
153
+ "message": "Chrome Debug app already exists",
154
+ "path": str(app_path),
155
+ "details": "Use --force to overwrite",
156
+ }
157
+
158
+ # Create app structure
159
+ contents_dir = app_path / "Contents"
160
+ macos_dir = contents_dir / "MacOS"
161
+ macos_dir.mkdir(parents=True, exist_ok=True)
162
+
163
+ # Create launcher script
164
+ launcher_path = macos_dir / "Chrome Debug"
165
+ launcher_content = f"""#!/bin/bash
166
+ # Chrome Debug app launcher
167
+ # Uses absolute path to wrapper
168
+ exec "{self.wrapper_path.expanduser()}" "$@"
151
169
  """
170
+ launcher_path.write_text(launcher_content)
171
+ launcher_path.chmod(0o755)
172
+
173
+ # Create Info.plist
174
+ plist_path = contents_dir / "Info.plist"
175
+ plist_path.write_text(MACOS_INFO_PLIST)
176
+
177
+ logger.info(f"Created Chrome Debug app at {app_path}")
178
+
179
+ return {
180
+ "success": True,
181
+ "message": "Chrome Debug app created successfully",
182
+ "path": str(app_path),
183
+ "details": "Available in Launchpad and Spotlight search",
184
+ }
185
+
186
+ def _install_linux_desktop(self, force: bool) -> Dict[str, Any]:
187
+ """Install .desktop file for Linux.
188
+
189
+ Args:
190
+ force: Overwrite existing desktop entry
191
+
192
+ Returns:
193
+ Installation result
194
+ """
195
+ # Create separate Chrome Debug desktop entry (doesn't override system Chrome)
196
+ desktop_path = self.paths["applications_dir"] / "chrome-debug.desktop"
197
+
198
+ if desktop_path.exists() and not force:
199
+ return {
200
+ "success": False,
201
+ "message": "Desktop entry already exists",
202
+ "path": str(desktop_path),
203
+ "details": "Use --force to overwrite",
204
+ }
205
+
206
+ # Create Chrome Debug desktop entry
207
+ desktop_content = self._create_chrome_debug_desktop()
208
+
209
+ # Create directory and save
210
+ desktop_path.parent.mkdir(parents=True, exist_ok=True)
211
+ desktop_path.write_text(desktop_content)
212
+ desktop_path.chmod(0o644) # Standard permissions for desktop files
213
+
214
+ logger.info(f"Installed desktop entry to {desktop_path}")
215
+
216
+ return {
217
+ "success": True,
218
+ "message": "Installed Chrome Debug desktop entry",
219
+ "path": str(desktop_path),
220
+ "details": "Available in application menu as 'Chrome Debug'",
221
+ }
222
+
223
+ def _create_chrome_debug_desktop(self) -> str:
224
+ """Create Chrome Debug desktop entry with absolute paths."""
225
+ # Use absolute expanded path for Exec lines
226
+ wrapper_abs_path = self.wrapper_path.expanduser()
227
+
228
+ return LINUX_DESKTOP_ENTRY.format(wrapper_path=wrapper_abs_path)
@@ -1,12 +1,13 @@
1
- """Chrome extension setup functionality for WebTap."""
1
+ """Chrome extension setup service (cross-platform)."""
2
2
 
3
3
  import json
4
4
  import logging
5
- from pathlib import Path
6
5
  from typing import Dict, Any
7
6
 
8
7
  import requests
9
8
 
9
+ from .platform import get_platform_info, ensure_directories
10
+
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
13
  # GitHub URLs for extension files
@@ -14,75 +15,87 @@ EXTENSION_BASE_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/
14
15
  EXTENSION_FILES = ["manifest.json", "popup.html", "popup.js"]
15
16
 
16
17
 
17
- def install_extension(force: bool = False) -> Dict[str, Any]:
18
- """Install Chrome extension to ~/.config/webtap/extension/.
19
-
20
- Args:
21
- force: Overwrite existing files
22
-
23
- Returns:
24
- Dict with success, message, path, details
25
- """
26
- # XDG config directory for Linux
27
- target_dir = Path.home() / ".config" / "webtap" / "extension"
18
+ class ExtensionSetupService:
19
+ """Chrome extension installation service."""
20
+
21
+ def __init__(self):
22
+ self.info = get_platform_info()
23
+ self.paths = self.info["paths"]
24
+
25
+ # Extension goes in data directory (persistent app data)
26
+ self.extension_dir = self.paths["data_dir"] / "extension"
27
+
28
+ def install_extension(self, force: bool = False) -> Dict[str, Any]:
29
+ """Install Chrome extension to platform-appropriate location.
30
+
31
+ Args:
32
+ force: Overwrite existing files
33
+
34
+ Returns:
35
+ Installation result
36
+ """
37
+ # Check if exists (manifest.json is required file)
38
+ if (self.extension_dir / "manifest.json").exists() and not force:
39
+ return {
40
+ "success": False,
41
+ "message": f"Extension already exists at {self.extension_dir}",
42
+ "path": str(self.extension_dir),
43
+ "details": "Use --force to overwrite",
44
+ }
45
+
46
+ # Ensure base directories exist
47
+ ensure_directories()
48
+
49
+ # Create extension directory
50
+ self.extension_dir.mkdir(parents=True, exist_ok=True)
51
+
52
+ # Download each file
53
+ downloaded = []
54
+ failed = []
55
+
56
+ for filename in EXTENSION_FILES:
57
+ url = f"{EXTENSION_BASE_URL}/{filename}"
58
+ target_file = self.extension_dir / filename
59
+
60
+ try:
61
+ logger.info(f"Downloading {filename}")
62
+ response = requests.get(url, timeout=10)
63
+ response.raise_for_status()
64
+
65
+ # For manifest.json, validate it's proper JSON
66
+ if filename == "manifest.json":
67
+ json.loads(response.text)
68
+
69
+ target_file.write_text(response.text)
70
+ downloaded.append(filename)
71
+
72
+ except Exception as e:
73
+ logger.error(f"Failed to download {filename}: {e}")
74
+ failed.append(filename)
75
+
76
+ # Determine success level
77
+ if not downloaded:
78
+ return {
79
+ "success": False,
80
+ "message": "Failed to download any extension files",
81
+ "path": None,
82
+ "details": "Check network connection and try again",
83
+ }
84
+
85
+ if failed:
86
+ # Partial success - some files downloaded
87
+ return {
88
+ "success": True, # Partial is still success
89
+ "message": f"Downloaded {len(downloaded)}/{len(EXTENSION_FILES)} files",
90
+ "path": str(self.extension_dir),
91
+ "details": f"Failed: {', '.join(failed)}",
92
+ }
93
+
94
+ logger.info(f"Extension installed to {self.extension_dir}")
28
95
 
29
- # Check if exists (manifest.json is required file)
30
- if (target_dir / "manifest.json").exists() and not force:
31
96
  return {
32
- "success": False,
33
- "message": f"Extension already exists at {target_dir}",
34
- "path": str(target_dir),
35
- "details": "Use --force to overwrite",
97
+ "success": True,
98
+ "message": "Downloaded Chrome extension",
99
+ "path": str(self.extension_dir),
100
+ "details": f"Files: {', '.join(downloaded)}",
36
101
  }
37
-
38
- # Create directory
39
- target_dir.mkdir(parents=True, exist_ok=True)
40
-
41
- # Download each file
42
- downloaded = []
43
- failed = []
44
-
45
- for filename in EXTENSION_FILES:
46
- url = f"{EXTENSION_BASE_URL}/{filename}"
47
- target_file = target_dir / filename
48
-
49
- try:
50
- logger.info(f"Downloading {filename}")
51
- response = requests.get(url, timeout=10)
52
- response.raise_for_status()
53
-
54
- # For manifest.json, validate it's proper JSON
55
- if filename == "manifest.json":
56
- json.loads(response.text)
57
-
58
- target_file.write_text(response.text)
59
- downloaded.append(filename)
60
-
61
- except Exception as e:
62
- logger.error(f"Failed to download {filename}: {e}")
63
- failed.append(filename)
64
-
65
- # Determine success level
66
- if not downloaded:
67
- return {
68
- "success": False,
69
- "message": "Failed to download any extension files",
70
- "path": None,
71
- "details": "Check network connection and try again",
72
- }
73
-
74
- if failed:
75
- # Partial success - some files downloaded
76
- return {
77
- "success": True, # Partial is still success
78
- "message": f"Downloaded {len(downloaded)}/{len(EXTENSION_FILES)} files",
79
- "path": str(target_dir),
80
- "details": f"Failed: {', '.join(failed)}",
81
- }
82
-
83
- return {
84
- "success": True,
85
- "message": "Downloaded Chrome extension",
86
- "path": str(target_dir),
87
- "details": f"Files: {', '.join(downloaded)}",
88
- }
@@ -1,4 +1,4 @@
1
- """Filter setup functionality for WebTap."""
1
+ """Filter setup service (cross-platform)."""
2
2
 
3
3
  import json
4
4
  import logging
@@ -7,73 +7,83 @@ from typing import Dict, Any
7
7
 
8
8
  import requests
9
9
 
10
+ from .platform import get_platform_info
11
+
10
12
  logger = logging.getLogger(__name__)
11
13
 
12
14
  # GitHub URL for filters
13
15
  FILTERS_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/data/filters.json"
14
16
 
15
17
 
16
- def install_filters(force: bool = False) -> Dict[str, Any]:
17
- """Install filters to .webtap/filters.json.
18
-
19
- Args:
20
- force: Overwrite existing file
18
+ class FilterSetupService:
19
+ """Filter configuration installation service."""
21
20
 
22
- Returns:
23
- Dict with success, message, path, details
24
- """
25
- # Same path that FilterManager uses
26
- target_path = Path.cwd() / ".webtap" / "filters.json"
21
+ def __init__(self):
22
+ self.info = get_platform_info()
23
+ self.paths = self.info["paths"]
27
24
 
28
- # Check if exists
29
- if target_path.exists() and not force:
30
- return {
31
- "success": False,
32
- "message": f"Filters already exist at {target_path}",
33
- "path": str(target_path),
34
- "details": "Use --force to overwrite",
35
- }
25
+ # Filters go in the current working directory's .webtap folder
26
+ # (project-specific, not global)
27
+ self.filters_path = Path.cwd() / ".webtap" / "filters.json"
36
28
 
37
- # Download from GitHub
38
- try:
39
- logger.info(f"Downloading filters from {FILTERS_URL}")
40
- response = requests.get(FILTERS_URL, timeout=10)
41
- response.raise_for_status()
29
+ def install_filters(self, force: bool = False) -> Dict[str, Any]:
30
+ """Install filters to .webtap/filters.json in current directory.
42
31
 
43
- # Validate it's proper JSON
44
- filters_data = json.loads(response.text)
32
+ Args:
33
+ force: Overwrite existing file
45
34
 
46
- # Quick validation - should have dict structure
47
- if not isinstance(filters_data, dict):
35
+ Returns:
36
+ Installation result
37
+ """
38
+ # Check if exists
39
+ if self.filters_path.exists() and not force:
48
40
  return {
49
41
  "success": False,
50
- "message": "Invalid filter format - expected JSON object",
51
- "path": None,
52
- "details": None,
42
+ "message": f"Filters already exist at {self.filters_path}",
43
+ "path": str(self.filters_path),
44
+ "details": "Use --force to overwrite",
45
+ }
46
+
47
+ # Download from GitHub
48
+ try:
49
+ logger.info(f"Downloading filters from {FILTERS_URL}")
50
+ response = requests.get(FILTERS_URL, timeout=10)
51
+ response.raise_for_status()
52
+
53
+ # Validate it's proper JSON
54
+ filters_data = json.loads(response.text)
55
+
56
+ # Quick validation - should have dict structure
57
+ if not isinstance(filters_data, dict):
58
+ return {
59
+ "success": False,
60
+ "message": "Invalid filter format - expected JSON object",
61
+ "path": None,
62
+ "details": None,
63
+ }
64
+
65
+ # Count categories for user feedback
66
+ category_count = len(filters_data)
67
+
68
+ # Create directory and save
69
+ self.filters_path.parent.mkdir(parents=True, exist_ok=True)
70
+ self.filters_path.write_text(response.text)
71
+
72
+ logger.info(f"Saved {category_count} filter categories to {self.filters_path}")
73
+
74
+ return {
75
+ "success": True,
76
+ "message": f"Downloaded {category_count} filter categories",
77
+ "path": str(self.filters_path),
78
+ "details": f"Categories: {', '.join(filters_data.keys())}",
53
79
  }
54
80
 
55
- # Count categories for user feedback
56
- category_count = len(filters_data)
57
-
58
- # Create directory and save
59
- target_path.parent.mkdir(parents=True, exist_ok=True)
60
- target_path.write_text(response.text)
61
-
62
- logger.info(f"Saved {category_count} filter categories to {target_path}")
63
-
64
- return {
65
- "success": True,
66
- "message": f"Downloaded {category_count} filter categories",
67
- "path": str(target_path),
68
- "details": f"Categories: {', '.join(filters_data.keys())}",
69
- }
70
-
71
- except requests.RequestException as e:
72
- logger.error(f"Network error downloading filters: {e}")
73
- return {"success": False, "message": f"Network error: {e}", "path": None, "details": None}
74
- except json.JSONDecodeError as e:
75
- logger.error(f"Invalid JSON in filters: {e}")
76
- return {"success": False, "message": f"Invalid JSON format: {e}", "path": None, "details": None}
77
- except Exception as e:
78
- logger.error(f"Unexpected error: {e}")
79
- return {"success": False, "message": f"Failed to download filters: {e}", "path": None, "details": None}
81
+ except requests.RequestException as e:
82
+ logger.error(f"Network error downloading filters: {e}")
83
+ return {"success": False, "message": f"Network error: {e}", "path": None, "details": None}
84
+ except json.JSONDecodeError as e:
85
+ logger.error(f"Invalid JSON in filters: {e}")
86
+ return {"success": False, "message": f"Invalid JSON format: {e}", "path": None, "details": None}
87
+ except Exception as e:
88
+ logger.error(f"Unexpected error: {e}")
89
+ return {"success": False, "message": f"Failed to download filters: {e}", "path": None, "details": None}