talks-reducer 0.7.1__py3-none-any.whl → 0.8.0__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.
- talks_reducer/__about__.py +1 -1
- talks_reducer/cli.py +225 -181
- talks_reducer/discovery.py +78 -22
- talks_reducer/gui/__init__.py +17 -1546
- talks_reducer/gui/__main__.py +1 -1
- talks_reducer/gui/app.py +1385 -0
- talks_reducer/gui/discovery.py +1 -1
- talks_reducer/gui/layout.py +18 -31
- talks_reducer/gui/progress.py +80 -0
- talks_reducer/gui/remote.py +11 -3
- talks_reducer/gui/startup.py +202 -0
- talks_reducer/icons.py +123 -0
- talks_reducer/pipeline.py +65 -31
- talks_reducer/server.py +111 -47
- talks_reducer/server_tray.py +192 -236
- talks_reducer/service_client.py +77 -14
- {talks_reducer-0.7.1.dist-info → talks_reducer-0.8.0.dist-info}/METADATA +24 -2
- talks_reducer-0.8.0.dist-info/RECORD +33 -0
- talks_reducer-0.7.1.dist-info/RECORD +0 -29
- {talks_reducer-0.7.1.dist-info → talks_reducer-0.8.0.dist-info}/WHEEL +0 -0
- {talks_reducer-0.7.1.dist-info → talks_reducer-0.8.0.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.7.1.dist-info → talks_reducer-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.7.1.dist-info → talks_reducer-0.8.0.dist-info}/top_level.txt +0 -0
talks_reducer/gui/discovery.py
CHANGED
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, List
|
|
8
8
|
from ..discovery import discover_servers as core_discover_servers
|
9
9
|
|
10
10
|
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
11
|
-
from . import TalksReducerGUI
|
11
|
+
from .app import TalksReducerGUI
|
12
12
|
|
13
13
|
|
14
14
|
def start_discovery(gui: "TalksReducerGUI") -> None:
|
talks_reducer/gui/layout.py
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import sys
|
6
|
-
from pathlib import Path
|
7
6
|
from typing import TYPE_CHECKING, Callable
|
8
7
|
|
8
|
+
from ..icons import find_icon_path
|
9
9
|
from ..models import default_temp_folder
|
10
10
|
|
11
11
|
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
12
12
|
import tkinter as tk
|
13
13
|
|
14
|
-
from . import TalksReducerGUI
|
14
|
+
from .app import TalksReducerGUI
|
15
15
|
|
16
16
|
|
17
17
|
def build_layout(gui: "TalksReducerGUI") -> None:
|
@@ -454,37 +454,24 @@ def reset_basic_defaults(gui: "TalksReducerGUI") -> None:
|
|
454
454
|
def apply_window_icon(gui: "TalksReducerGUI") -> None:
|
455
455
|
"""Configure the application icon when the asset is available."""
|
456
456
|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
icon_candidates.append(
|
462
|
-
(
|
463
|
-
base_path / "talks_reducer" / "resources" / "icons" / "icon.ico",
|
464
|
-
"ico",
|
465
|
-
)
|
466
|
-
)
|
467
|
-
icon_candidates.append(
|
468
|
-
(
|
469
|
-
base_path / "talks_reducer" / "resources" / "icons" / "icon.png",
|
470
|
-
"png",
|
471
|
-
)
|
457
|
+
icon_filenames = (
|
458
|
+
("app.ico", "app.png")
|
459
|
+
if sys.platform.startswith("win")
|
460
|
+
else ("app.png", "app.ico")
|
472
461
|
)
|
462
|
+
icon_path = find_icon_path(filenames=icon_filenames)
|
463
|
+
if icon_path is None:
|
464
|
+
return
|
473
465
|
|
474
|
-
|
475
|
-
if
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
gui.root.iconphoto(False, gui.tk.PhotoImage(file=str(icon_path)))
|
484
|
-
return
|
485
|
-
except (gui.tk.TclError, Exception):
|
486
|
-
# Missing Tk image support or invalid icon format - try next candidate
|
487
|
-
continue
|
466
|
+
try:
|
467
|
+
if icon_path.suffix.lower() == ".ico" and sys.platform.startswith("win"):
|
468
|
+
# On Windows, iconbitmap works better without the 'default' parameter.
|
469
|
+
gui.root.iconbitmap(str(icon_path))
|
470
|
+
else:
|
471
|
+
gui.root.iconphoto(False, gui.tk.PhotoImage(file=str(icon_path)))
|
472
|
+
except (gui.tk.TclError, Exception):
|
473
|
+
# Missing Tk image support or invalid icon format - fail silently.
|
474
|
+
return
|
488
475
|
|
489
476
|
|
490
477
|
def apply_window_size(gui: "TalksReducerGUI", *, simple: bool) -> None:
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"""Progress helpers that bridge the pipeline with the Tkinter GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Callable, Optional
|
6
|
+
|
7
|
+
from ..progress import ProgressHandle, SignalProgressReporter
|
8
|
+
|
9
|
+
|
10
|
+
class _GuiProgressHandle(ProgressHandle):
|
11
|
+
"""Simple progress handle that records totals but only logs milestones."""
|
12
|
+
|
13
|
+
def __init__(self, log_callback: Callable[[str], None], desc: str) -> None:
|
14
|
+
self._log_callback = log_callback
|
15
|
+
self._desc = desc
|
16
|
+
self._current = 0
|
17
|
+
self._total: Optional[int] = None
|
18
|
+
if desc:
|
19
|
+
self._log_callback(f"{desc} started")
|
20
|
+
|
21
|
+
@property
|
22
|
+
def current(self) -> int:
|
23
|
+
return self._current
|
24
|
+
|
25
|
+
def ensure_total(self, total: int) -> None:
|
26
|
+
if self._total is None or total > self._total:
|
27
|
+
self._total = total
|
28
|
+
|
29
|
+
def advance(self, amount: int) -> None:
|
30
|
+
if amount > 0:
|
31
|
+
self._current += amount
|
32
|
+
|
33
|
+
def finish(self) -> None:
|
34
|
+
if self._total is not None:
|
35
|
+
self._current = self._total
|
36
|
+
if self._desc:
|
37
|
+
self._log_callback(f"{self._desc} completed")
|
38
|
+
|
39
|
+
def __enter__(self) -> "_GuiProgressHandle":
|
40
|
+
return self
|
41
|
+
|
42
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
43
|
+
if exc_type is None:
|
44
|
+
self.finish()
|
45
|
+
return False
|
46
|
+
|
47
|
+
|
48
|
+
class _TkProgressReporter(SignalProgressReporter):
|
49
|
+
"""Progress reporter that forwards updates to the GUI thread."""
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
log_callback: Callable[[str], None],
|
54
|
+
process_callback: Optional[Callable] = None,
|
55
|
+
*,
|
56
|
+
stop_callback: Optional[Callable[[], bool]] = None,
|
57
|
+
) -> None:
|
58
|
+
self._log_callback = log_callback
|
59
|
+
self.process_callback = process_callback
|
60
|
+
self._stop_callback = stop_callback
|
61
|
+
|
62
|
+
def log(self, message: str) -> None:
|
63
|
+
self._log_callback(message)
|
64
|
+
print(message, flush=True)
|
65
|
+
|
66
|
+
def task(
|
67
|
+
self, *, desc: str = "", total: Optional[int] = None, unit: str = ""
|
68
|
+
) -> _GuiProgressHandle:
|
69
|
+
del total, unit
|
70
|
+
return _GuiProgressHandle(self._log_callback, desc)
|
71
|
+
|
72
|
+
def stop_requested(self) -> bool:
|
73
|
+
"""Return ``True`` when the GUI has asked to cancel processing."""
|
74
|
+
|
75
|
+
if self._stop_callback is None:
|
76
|
+
return False
|
77
|
+
return bool(self._stop_callback())
|
78
|
+
|
79
|
+
|
80
|
+
__all__ = ["_GuiProgressHandle", "_TkProgressReporter"]
|
talks_reducer/gui/remote.py
CHANGED
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
|
14
14
|
from ..pipeline import ProcessingAborted
|
15
15
|
|
16
16
|
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
17
|
-
from . import TalksReducerGUI
|
17
|
+
from .app import TalksReducerGUI
|
18
18
|
|
19
19
|
|
20
20
|
def normalize_server_url(server_url: str) -> str:
|
@@ -219,6 +219,12 @@ def check_remote_server_for_gui(
|
|
219
219
|
)
|
220
220
|
|
221
221
|
|
222
|
+
def _load_service_client() -> object:
|
223
|
+
"""Return the Talks Reducer service client module."""
|
224
|
+
|
225
|
+
return importlib.import_module("talks_reducer.service_client")
|
226
|
+
|
227
|
+
|
222
228
|
def process_files_via_server(
|
223
229
|
gui: "TalksReducerGUI",
|
224
230
|
files: List[str],
|
@@ -228,6 +234,8 @@ def process_files_via_server(
|
|
228
234
|
open_after_convert: bool,
|
229
235
|
default_remote_destination: Callable[[Path, bool], Path],
|
230
236
|
parse_summary: Callable[[str], tuple[Optional[float], Optional[float]]],
|
237
|
+
load_service_client: Callable[[], object] = _load_service_client,
|
238
|
+
check_server: Callable[..., bool] = check_remote_server_for_gui,
|
231
239
|
) -> bool:
|
232
240
|
"""Send *files* to the configured server for processing."""
|
233
241
|
|
@@ -236,7 +244,7 @@ def process_files_via_server(
|
|
236
244
|
raise ProcessingAborted("Remote processing cancelled by user.")
|
237
245
|
|
238
246
|
try:
|
239
|
-
service_module =
|
247
|
+
service_module = load_service_client()
|
240
248
|
except ModuleNotFoundError as exc:
|
241
249
|
gui._append_log(f"Server client unavailable: {exc}")
|
242
250
|
gui._schedule_on_ui_thread(
|
@@ -253,7 +261,7 @@ def process_files_via_server(
|
|
253
261
|
lambda: gui._set_status("waiting", f"Waiting server {host_label}...")
|
254
262
|
)
|
255
263
|
|
256
|
-
available =
|
264
|
+
available = check_server(
|
257
265
|
gui,
|
258
266
|
server_url,
|
259
267
|
success_status="waiting",
|
@@ -0,0 +1,202 @@
|
|
1
|
+
"""Startup utilities for launching the Talks Reducer GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import importlib
|
7
|
+
import json
|
8
|
+
import subprocess
|
9
|
+
import sys
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Optional, Sequence, Tuple
|
12
|
+
|
13
|
+
from ..cli import main as cli_main
|
14
|
+
from .app import TalksReducerGUI
|
15
|
+
|
16
|
+
|
17
|
+
def _check_tkinter_available() -> Tuple[bool, str]:
|
18
|
+
"""Check if tkinter can create windows without importing it globally."""
|
19
|
+
|
20
|
+
# Test in a subprocess to avoid crashing the main process
|
21
|
+
test_code = """
|
22
|
+
import json
|
23
|
+
|
24
|
+
def run_check():
|
25
|
+
try:
|
26
|
+
import tkinter as tk # noqa: F401 - imported for side effect
|
27
|
+
except Exception as exc: # pragma: no cover - runs in subprocess
|
28
|
+
return {
|
29
|
+
"status": "import_error",
|
30
|
+
"error": f"{exc.__class__.__name__}: {exc}",
|
31
|
+
}
|
32
|
+
|
33
|
+
try:
|
34
|
+
import tkinter as tk
|
35
|
+
|
36
|
+
root = tk.Tk()
|
37
|
+
root.destroy()
|
38
|
+
except Exception as exc: # pragma: no cover - runs in subprocess
|
39
|
+
return {
|
40
|
+
"status": "init_error",
|
41
|
+
"error": f"{exc.__class__.__name__}: {exc}",
|
42
|
+
}
|
43
|
+
|
44
|
+
return {"status": "ok"}
|
45
|
+
|
46
|
+
|
47
|
+
if __name__ == "__main__":
|
48
|
+
print(json.dumps(run_check()))
|
49
|
+
"""
|
50
|
+
|
51
|
+
try:
|
52
|
+
result = subprocess.run(
|
53
|
+
[sys.executable, "-c", test_code], capture_output=True, text=True, timeout=5
|
54
|
+
)
|
55
|
+
|
56
|
+
output = result.stdout.strip() or result.stderr.strip()
|
57
|
+
|
58
|
+
if not output:
|
59
|
+
return False, "Window creation failed"
|
60
|
+
|
61
|
+
try:
|
62
|
+
payload = json.loads(output)
|
63
|
+
except json.JSONDecodeError:
|
64
|
+
return False, output
|
65
|
+
|
66
|
+
status = payload.get("status")
|
67
|
+
|
68
|
+
if status == "ok":
|
69
|
+
return True, ""
|
70
|
+
|
71
|
+
if status == "import_error":
|
72
|
+
return (
|
73
|
+
False,
|
74
|
+
f"tkinter is not installed ({payload.get('error', 'unknown error')})",
|
75
|
+
)
|
76
|
+
|
77
|
+
if status == "init_error":
|
78
|
+
return (
|
79
|
+
False,
|
80
|
+
f"tkinter could not open a window ({payload.get('error', 'unknown error')})",
|
81
|
+
)
|
82
|
+
|
83
|
+
return False, output
|
84
|
+
except Exception as e: # pragma: no cover - defensive fallback
|
85
|
+
return False, f"Error testing tkinter: {e}"
|
86
|
+
|
87
|
+
|
88
|
+
def main(argv: Optional[Sequence[str]] = None) -> bool:
|
89
|
+
"""Launch the GUI when run without arguments, otherwise defer to the CLI."""
|
90
|
+
|
91
|
+
if argv is None:
|
92
|
+
argv = sys.argv[1:]
|
93
|
+
|
94
|
+
parser = argparse.ArgumentParser(add_help=False)
|
95
|
+
parser.add_argument(
|
96
|
+
"--server",
|
97
|
+
action="store_true",
|
98
|
+
help="Launch the Talks Reducer server tray instead of the desktop GUI.",
|
99
|
+
)
|
100
|
+
parser.add_argument(
|
101
|
+
"--no-tray",
|
102
|
+
action="store_true",
|
103
|
+
help="Deprecated: the GUI no longer starts the server tray automatically.",
|
104
|
+
)
|
105
|
+
|
106
|
+
parsed_args, remaining = parser.parse_known_args(argv)
|
107
|
+
if parsed_args.server:
|
108
|
+
package_name = __package__ or "talks_reducer"
|
109
|
+
module_name = f"{package_name}.server_tray"
|
110
|
+
try:
|
111
|
+
tray_module = importlib.import_module(module_name)
|
112
|
+
except ModuleNotFoundError as exc:
|
113
|
+
if exc.name != module_name:
|
114
|
+
raise
|
115
|
+
root_package = package_name.split(".")[0] or "talks_reducer"
|
116
|
+
tray_module = importlib.import_module(f"{root_package}.server_tray")
|
117
|
+
tray_main = getattr(tray_module, "main")
|
118
|
+
tray_main(remaining)
|
119
|
+
return False
|
120
|
+
if parsed_args.no_tray:
|
121
|
+
sys.stderr.write(
|
122
|
+
"Warning: --no-tray is deprecated; the GUI no longer starts the server tray automatically.\n"
|
123
|
+
)
|
124
|
+
argv = remaining
|
125
|
+
|
126
|
+
if argv:
|
127
|
+
launch_gui = False
|
128
|
+
if sys.platform == "win32" and not any(arg.startswith("-") for arg in argv):
|
129
|
+
if any(Path(arg).exists() for arg in argv if arg):
|
130
|
+
launch_gui = True
|
131
|
+
|
132
|
+
if launch_gui:
|
133
|
+
try:
|
134
|
+
app = TalksReducerGUI(argv, auto_run=True)
|
135
|
+
app.run()
|
136
|
+
return True
|
137
|
+
except Exception:
|
138
|
+
# Fall back to the CLI if the GUI cannot be started.
|
139
|
+
pass
|
140
|
+
|
141
|
+
cli_main(argv)
|
142
|
+
return False
|
143
|
+
|
144
|
+
is_frozen = getattr(sys, "frozen", False)
|
145
|
+
|
146
|
+
if not is_frozen:
|
147
|
+
tkinter_available, error_msg = _check_tkinter_available()
|
148
|
+
|
149
|
+
if not tkinter_available:
|
150
|
+
try:
|
151
|
+
print("Talks Reducer GUI")
|
152
|
+
print("=" * 50)
|
153
|
+
print("X GUI not available on this system")
|
154
|
+
print(f"Error: {error_msg}")
|
155
|
+
print()
|
156
|
+
print("! Alternative: Use the command-line interface")
|
157
|
+
print()
|
158
|
+
print("The CLI provides all the same functionality:")
|
159
|
+
print(" python3 -m talks_reducer <input_file> [options]")
|
160
|
+
print()
|
161
|
+
print("Examples:")
|
162
|
+
print(" python3 -m talks_reducer video.mp4")
|
163
|
+
print(" python3 -m talks_reducer video.mp4 --small")
|
164
|
+
print(" python3 -m talks_reducer video.mp4 -o output.mp4")
|
165
|
+
print()
|
166
|
+
print("Run 'python3 -m talks_reducer --help' for all options.")
|
167
|
+
print()
|
168
|
+
print("Troubleshooting tips:")
|
169
|
+
if sys.platform == "darwin":
|
170
|
+
print(
|
171
|
+
" - On macOS, install Python from python.org or ensure "
|
172
|
+
"Homebrew's python-tk package is present."
|
173
|
+
)
|
174
|
+
elif sys.platform.startswith("linux"):
|
175
|
+
print(
|
176
|
+
" - On Linux, install the Tk bindings for Python (for example, "
|
177
|
+
"python3-tk)."
|
178
|
+
)
|
179
|
+
else:
|
180
|
+
print(" - Ensure your Python installation includes Tk support.")
|
181
|
+
print(" - You can always fall back to the CLI workflow below.")
|
182
|
+
print()
|
183
|
+
print("The CLI interface works perfectly and is recommended.")
|
184
|
+
except UnicodeEncodeError:
|
185
|
+
sys.stderr.write("GUI not available. Use CLI mode instead.\n")
|
186
|
+
return False
|
187
|
+
|
188
|
+
try:
|
189
|
+
app = TalksReducerGUI()
|
190
|
+
app.run()
|
191
|
+
return True
|
192
|
+
except Exception as e:
|
193
|
+
import traceback
|
194
|
+
|
195
|
+
sys.stderr.write(f"Error starting GUI: {e}\n")
|
196
|
+
sys.stderr.write(traceback.format_exc())
|
197
|
+
sys.stderr.write("\nPlease use the CLI mode instead:\n")
|
198
|
+
sys.stderr.write(" python3 -m talks_reducer <input_file> [options]\n")
|
199
|
+
sys.exit(1)
|
200
|
+
|
201
|
+
|
202
|
+
__all__ = ["_check_tkinter_available", "main"]
|
talks_reducer/icons.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
"""Icon discovery helpers shared across Talks Reducer entry points."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import sys
|
7
|
+
from contextlib import suppress
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Iterator, Optional, Sequence
|
10
|
+
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
_ICON_RELATIVE_PATHS: Sequence[Path] = (
|
14
|
+
Path("resources") / "icons",
|
15
|
+
Path("talks_reducer") / "resources" / "icons",
|
16
|
+
Path("docs") / "assets",
|
17
|
+
)
|
18
|
+
_ICON_PATH_SUFFIXES: Sequence[Path] = (
|
19
|
+
Path(""),
|
20
|
+
Path("_internal"),
|
21
|
+
Path("Contents") / "Resources",
|
22
|
+
Path("Resources"),
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
def _iter_base_roots(module_file: Optional[Path | str] = None) -> Iterator[Path]:
|
27
|
+
"""Yield base directories where icon assets may live."""
|
28
|
+
|
29
|
+
module_path = Path(module_file or __file__).resolve()
|
30
|
+
package_root = module_path.parent
|
31
|
+
project_root = package_root.parent
|
32
|
+
|
33
|
+
seen: set[Path] = set()
|
34
|
+
|
35
|
+
def _yield(path: Optional[Path]) -> Iterator[Path]:
|
36
|
+
if path is None:
|
37
|
+
return iter(())
|
38
|
+
resolved = path.resolve()
|
39
|
+
if resolved in seen:
|
40
|
+
return iter(())
|
41
|
+
seen.add(resolved)
|
42
|
+
return iter((resolved,))
|
43
|
+
|
44
|
+
for root in (
|
45
|
+
package_root,
|
46
|
+
project_root,
|
47
|
+
Path.cwd(),
|
48
|
+
):
|
49
|
+
yield from _yield(root)
|
50
|
+
|
51
|
+
frozen_root: Optional[Path] = None
|
52
|
+
frozen_value = getattr(sys, "_MEIPASS", None)
|
53
|
+
if frozen_value:
|
54
|
+
with suppress(Exception):
|
55
|
+
frozen_root = Path(str(frozen_value))
|
56
|
+
if frozen_root is not None:
|
57
|
+
yield from _yield(frozen_root)
|
58
|
+
|
59
|
+
with suppress(Exception):
|
60
|
+
executable_root = Path(sys.executable).resolve().parent
|
61
|
+
yield from _yield(executable_root)
|
62
|
+
|
63
|
+
with suppress(Exception):
|
64
|
+
launcher_root = Path(sys.argv[0]).resolve().parent
|
65
|
+
yield from _yield(launcher_root)
|
66
|
+
|
67
|
+
|
68
|
+
def iter_icon_candidates(
|
69
|
+
*,
|
70
|
+
filenames: Sequence[str],
|
71
|
+
relative_paths: Sequence[Path] | None = None,
|
72
|
+
module_file: Optional[Path | str] = None,
|
73
|
+
) -> Iterator[Path]:
|
74
|
+
"""Yield possible icon paths ordered from most to least specific."""
|
75
|
+
|
76
|
+
if relative_paths is None:
|
77
|
+
relative_paths = _ICON_RELATIVE_PATHS
|
78
|
+
|
79
|
+
seen: set[Path] = set()
|
80
|
+
for base_root in _iter_base_roots(module_file=module_file):
|
81
|
+
for suffix in _ICON_PATH_SUFFIXES:
|
82
|
+
candidate_root = (base_root / suffix).resolve()
|
83
|
+
if candidate_root in seen:
|
84
|
+
continue
|
85
|
+
seen.add(candidate_root)
|
86
|
+
LOGGER.debug("Considering icon root: %s", candidate_root)
|
87
|
+
|
88
|
+
if not candidate_root.exists():
|
89
|
+
LOGGER.debug("Skipping missing icon root: %s", candidate_root)
|
90
|
+
continue
|
91
|
+
|
92
|
+
for relative in relative_paths:
|
93
|
+
candidate_base = (candidate_root / relative).resolve()
|
94
|
+
if not candidate_base.exists():
|
95
|
+
LOGGER.debug("Skipping missing icon directory: %s", candidate_base)
|
96
|
+
continue
|
97
|
+
for name in filenames:
|
98
|
+
candidate = (candidate_base / name).resolve()
|
99
|
+
LOGGER.debug("Checking icon candidate: %s", candidate)
|
100
|
+
yield candidate
|
101
|
+
|
102
|
+
|
103
|
+
def find_icon_path(
|
104
|
+
*,
|
105
|
+
filenames: Sequence[str],
|
106
|
+
relative_paths: Sequence[Path] | None = None,
|
107
|
+
module_file: Optional[Path | str] = None,
|
108
|
+
) -> Optional[Path]:
|
109
|
+
"""Return the first existing icon path matching *filenames* or ``None``."""
|
110
|
+
|
111
|
+
for candidate in iter_icon_candidates(
|
112
|
+
filenames=filenames,
|
113
|
+
relative_paths=relative_paths,
|
114
|
+
module_file=module_file,
|
115
|
+
):
|
116
|
+
if candidate.is_file():
|
117
|
+
LOGGER.info("Found icon at %s", candidate)
|
118
|
+
return candidate
|
119
|
+
LOGGER.warning("Unable to locate Talks Reducer icon; checked %s", filenames)
|
120
|
+
return None
|
121
|
+
|
122
|
+
|
123
|
+
__all__ = ["find_icon_path", "iter_icon_candidates"]
|