talks-reducer 0.7.2__py3-none-any.whl → 0.8.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.
- 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 +1 -1
- 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 +2 -0
- talks_reducer/pipeline.py +65 -31
- talks_reducer/resources/icons/app-256.png +0 -0
- talks_reducer/resources/icons/app.icns +0 -0
- talks_reducer/resources/icons/app.ico +0 -0
- talks_reducer/server.py +106 -41
- talks_reducer/server_tray.py +116 -39
- talks_reducer/service_client.py +77 -14
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/METADATA +2 -2
- talks_reducer-0.8.1.dist-info/RECORD +36 -0
- talks_reducer-0.7.2.dist-info/RECORD +0 -30
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/WHEEL +0 -0
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.7.2.dist-info → talks_reducer-0.8.1.dist-info}/top_level.txt +0 -0
talks_reducer/__about__.py
CHANGED
talks_reducer/cli.py
CHANGED
@@ -10,10 +10,9 @@ import sys
|
|
10
10
|
import time
|
11
11
|
from importlib import import_module
|
12
12
|
from pathlib import Path
|
13
|
-
from typing import Dict, List, Optional, Sequence, Tuple
|
13
|
+
from typing import Callable, Dict, List, Optional, Sequence, Tuple
|
14
14
|
|
15
15
|
from . import audio
|
16
|
-
|
17
16
|
from .ffmpeg import FFmpegNotFoundError
|
18
17
|
from .models import ProcessingOptions, default_temp_folder
|
19
18
|
from .pipeline import speed_up_video
|
@@ -141,116 +140,215 @@ def _print_total_time(start_time: float) -> None:
|
|
141
140
|
print(f"\nTime: {int(hours)}h {int(minutes)}m {seconds:.2f}s")
|
142
141
|
|
143
142
|
|
144
|
-
|
145
|
-
|
146
|
-
) -> Tuple[bool, Optional[str]]:
|
147
|
-
"""Upload *files* to the configured server and download the results.
|
143
|
+
class CliApplication:
|
144
|
+
"""Coordinator for CLI processing with dependency injection support."""
|
148
145
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
146
|
+
def __init__(
|
147
|
+
self,
|
148
|
+
*,
|
149
|
+
gather_files: Callable[[List[str]], List[str]],
|
150
|
+
send_video: Optional[Callable[..., Tuple[Path, str, str]]],
|
151
|
+
speed_up: Callable[[ProcessingOptions, object], object],
|
152
|
+
reporter_factory: Callable[[], object],
|
153
|
+
remote_error_message: Optional[str] = None,
|
154
|
+
) -> None:
|
155
|
+
self._gather_files = gather_files
|
156
|
+
self._send_video = send_video
|
157
|
+
self._speed_up = speed_up
|
158
|
+
self._reporter_factory = reporter_factory
|
159
|
+
self._remote_error_message = remote_error_message
|
153
160
|
|
154
|
-
|
155
|
-
|
156
|
-
except ImportError as exc: # pragma: no cover - optional dependency guard
|
157
|
-
return False, ("Server mode requires the gradio_client dependency. " f"({exc})")
|
158
|
-
|
159
|
-
server_url = parsed_args.server_url
|
160
|
-
if not server_url:
|
161
|
-
return False, "Server URL was not provided."
|
162
|
-
|
163
|
-
output_override: Optional[Path] = None
|
164
|
-
if parsed_args.output_file and len(files) == 1:
|
165
|
-
output_override = Path(parsed_args.output_file).expanduser()
|
166
|
-
elif parsed_args.output_file and len(files) > 1:
|
167
|
-
print(
|
168
|
-
"Warning: --output is ignored when processing multiple files via the server.",
|
169
|
-
file=sys.stderr,
|
170
|
-
)
|
161
|
+
def run(self, parsed_args: argparse.Namespace) -> Tuple[int, List[str]]:
|
162
|
+
"""Execute the CLI pipeline for *parsed_args*."""
|
171
163
|
|
172
|
-
|
173
|
-
|
174
|
-
remote_option_values["silent_threshold"] = float(parsed_args.silent_threshold)
|
175
|
-
if parsed_args.silent_speed is not None:
|
176
|
-
remote_option_values["silent_speed"] = float(parsed_args.silent_speed)
|
177
|
-
if parsed_args.sounded_speed is not None:
|
178
|
-
remote_option_values["sounded_speed"] = float(parsed_args.sounded_speed)
|
179
|
-
|
180
|
-
unsupported_options = []
|
181
|
-
for name in (
|
182
|
-
"frame_spreadage",
|
183
|
-
"sample_rate",
|
184
|
-
"temp_folder",
|
185
|
-
):
|
186
|
-
if getattr(parsed_args, name) is not None:
|
187
|
-
unsupported_options.append(f"--{name.replace('_', '-')}")
|
188
|
-
|
189
|
-
if unsupported_options:
|
190
|
-
print(
|
191
|
-
"Warning: the following options are ignored when using --url: "
|
192
|
-
+ ", ".join(sorted(unsupported_options)),
|
193
|
-
file=sys.stderr,
|
194
|
-
)
|
164
|
+
start_time = time.time()
|
165
|
+
files = self._gather_files(parsed_args.input_file)
|
195
166
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
167
|
+
args: Dict[str, object] = {
|
168
|
+
key: value for key, value in vars(parsed_args).items() if value is not None
|
169
|
+
}
|
170
|
+
del args["input_file"]
|
171
|
+
|
172
|
+
if "host" in args:
|
173
|
+
del args["host"]
|
174
|
+
|
175
|
+
if len(files) > 1 and "output_file" in args:
|
176
|
+
del args["output_file"]
|
177
|
+
|
178
|
+
error_messages: List[str] = []
|
179
|
+
reporter_logs: List[str] = []
|
180
|
+
|
181
|
+
if getattr(parsed_args, "server_url", None):
|
182
|
+
remote_success, remote_errors, fallback_logs = self._process_via_server(
|
183
|
+
files, parsed_args, start_time
|
184
|
+
)
|
185
|
+
error_messages.extend(remote_errors)
|
186
|
+
reporter_logs.extend(fallback_logs)
|
187
|
+
if remote_success:
|
188
|
+
return 0, error_messages
|
189
|
+
|
190
|
+
reporter = self._reporter_factory()
|
191
|
+
for message in reporter_logs:
|
192
|
+
reporter.log(message)
|
193
|
+
|
194
|
+
for index, file in enumerate(files):
|
195
|
+
print(
|
196
|
+
f"Processing file {index + 1}/{len(files)} '{os.path.basename(file)}'"
|
197
|
+
)
|
198
|
+
local_options = dict(args)
|
199
|
+
|
200
|
+
option_kwargs: Dict[str, object] = {"input_file": Path(file)}
|
201
|
+
|
202
|
+
if "output_file" in local_options:
|
203
|
+
option_kwargs["output_file"] = Path(local_options["output_file"])
|
204
|
+
if "temp_folder" in local_options:
|
205
|
+
option_kwargs["temp_folder"] = Path(local_options["temp_folder"])
|
206
|
+
if "silent_threshold" in local_options:
|
207
|
+
option_kwargs["silent_threshold"] = float(
|
208
|
+
local_options["silent_threshold"]
|
209
|
+
)
|
210
|
+
if "silent_speed" in local_options:
|
211
|
+
option_kwargs["silent_speed"] = float(local_options["silent_speed"])
|
212
|
+
if "sounded_speed" in local_options:
|
213
|
+
option_kwargs["sounded_speed"] = float(local_options["sounded_speed"])
|
214
|
+
if "frame_spreadage" in local_options:
|
215
|
+
option_kwargs["frame_spreadage"] = int(local_options["frame_spreadage"])
|
216
|
+
if "sample_rate" in local_options:
|
217
|
+
option_kwargs["sample_rate"] = int(local_options["sample_rate"])
|
218
|
+
if "small" in local_options:
|
219
|
+
option_kwargs["small"] = bool(local_options["small"])
|
220
|
+
options = ProcessingOptions(**option_kwargs)
|
221
|
+
|
222
|
+
try:
|
223
|
+
result = self._speed_up(options, reporter=reporter)
|
224
|
+
except FFmpegNotFoundError as exc:
|
225
|
+
message = str(exc)
|
226
|
+
return 1, [*error_messages, message]
|
227
|
+
|
228
|
+
reporter.log(f"Completed: {result.output_file}")
|
229
|
+
summary_parts: List[str] = []
|
230
|
+
time_ratio = getattr(result, "time_ratio", None)
|
231
|
+
size_ratio = getattr(result, "size_ratio", None)
|
232
|
+
if time_ratio is not None:
|
233
|
+
summary_parts.append(f"{time_ratio * 100:.0f}% time")
|
234
|
+
if size_ratio is not None:
|
235
|
+
summary_parts.append(f"{size_ratio * 100:.0f}% size")
|
236
|
+
if summary_parts:
|
237
|
+
reporter.log("Result: " + ", ".join(summary_parts))
|
238
|
+
|
239
|
+
_print_total_time(start_time)
|
240
|
+
return 0, error_messages
|
241
|
+
|
242
|
+
def _process_via_server(
|
243
|
+
self,
|
244
|
+
files: Sequence[str],
|
245
|
+
parsed_args: argparse.Namespace,
|
246
|
+
start_time: float,
|
247
|
+
) -> Tuple[bool, List[str], List[str]]:
|
248
|
+
"""Upload *files* to the configured server and download the results."""
|
249
|
+
|
250
|
+
if not self._send_video:
|
251
|
+
message = self._remote_error_message or "Server processing is unavailable."
|
252
|
+
fallback_notice = "Falling back to local processing pipeline."
|
253
|
+
return False, [message, fallback_notice], [message, fallback_notice]
|
254
|
+
|
255
|
+
server_url = parsed_args.server_url
|
256
|
+
if not server_url:
|
257
|
+
message = "Server URL was not provided."
|
258
|
+
fallback_notice = "Falling back to local processing pipeline."
|
259
|
+
return False, [message, fallback_notice], [message, fallback_notice]
|
260
|
+
|
261
|
+
output_override: Optional[Path] = None
|
262
|
+
if parsed_args.output_file and len(files) == 1:
|
263
|
+
output_override = Path(parsed_args.output_file).expanduser()
|
264
|
+
elif parsed_args.output_file and len(files) > 1:
|
265
|
+
print(
|
266
|
+
"Warning: --output is ignored when processing multiple files via the server.",
|
267
|
+
file=sys.stderr,
|
243
268
|
)
|
244
|
-
except Exception as exc: # pragma: no cover - network failure safeguard
|
245
|
-
return False, f"Failed to process {basename} via server: {exc}"
|
246
269
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
270
|
+
remote_option_values: Dict[str, float] = {}
|
271
|
+
if parsed_args.silent_threshold is not None:
|
272
|
+
remote_option_values["silent_threshold"] = float(
|
273
|
+
parsed_args.silent_threshold
|
274
|
+
)
|
275
|
+
if parsed_args.silent_speed is not None:
|
276
|
+
remote_option_values["silent_speed"] = float(parsed_args.silent_speed)
|
277
|
+
if parsed_args.sounded_speed is not None:
|
278
|
+
remote_option_values["sounded_speed"] = float(parsed_args.sounded_speed)
|
279
|
+
|
280
|
+
unsupported_options: List[str] = []
|
281
|
+
for name in ("frame_spreadage", "sample_rate", "temp_folder"):
|
282
|
+
if getattr(parsed_args, name) is not None:
|
283
|
+
unsupported_options.append(f"--{name.replace('_', '-')}")
|
284
|
+
|
285
|
+
if unsupported_options:
|
286
|
+
print(
|
287
|
+
"Warning: the following options are ignored when using --url: "
|
288
|
+
+ ", ".join(sorted(unsupported_options)),
|
289
|
+
file=sys.stderr,
|
290
|
+
)
|
251
291
|
|
252
|
-
|
253
|
-
|
292
|
+
for index, file in enumerate(files, start=1):
|
293
|
+
basename = os.path.basename(file)
|
294
|
+
print(
|
295
|
+
f"Processing file {index}/{len(files)} '{basename}' via server {server_url}"
|
296
|
+
)
|
297
|
+
printed_log_header = False
|
298
|
+
progress_state: dict[str, tuple[Optional[int], Optional[int], str]] = {}
|
299
|
+
stream_updates = bool(getattr(parsed_args, "server_stream", False))
|
300
|
+
|
301
|
+
def _stream_server_log(line: str) -> None:
|
302
|
+
nonlocal printed_log_header
|
303
|
+
if not printed_log_header:
|
304
|
+
print("\nServer log:", flush=True)
|
305
|
+
printed_log_header = True
|
306
|
+
print(line, flush=True)
|
307
|
+
|
308
|
+
def _stream_progress(
|
309
|
+
desc: str, current: Optional[int], total: Optional[int], unit: str
|
310
|
+
) -> None:
|
311
|
+
key = desc or "Processing"
|
312
|
+
state = (current, total, unit)
|
313
|
+
if progress_state.get(key) == state:
|
314
|
+
return
|
315
|
+
progress_state[key] = state
|
316
|
+
|
317
|
+
parts: List[str] = []
|
318
|
+
if current is not None and total and total > 0:
|
319
|
+
percent = (current / total) * 100
|
320
|
+
parts.append(f"{current}/{total}")
|
321
|
+
parts.append(f"{percent:.1f}%")
|
322
|
+
elif current is not None:
|
323
|
+
parts.append(str(current))
|
324
|
+
if unit:
|
325
|
+
parts.append(unit)
|
326
|
+
message = " ".join(parts).strip()
|
327
|
+
print(f"{key}: {message or 'update'}", flush=True)
|
328
|
+
|
329
|
+
try:
|
330
|
+
destination, summary, log_text = self._send_video(
|
331
|
+
input_path=Path(file),
|
332
|
+
output_path=output_override,
|
333
|
+
server_url=server_url,
|
334
|
+
small=bool(parsed_args.small),
|
335
|
+
**remote_option_values,
|
336
|
+
log_callback=_stream_server_log,
|
337
|
+
stream_updates=stream_updates,
|
338
|
+
progress_callback=_stream_progress if stream_updates else None,
|
339
|
+
)
|
340
|
+
except Exception as exc: # pragma: no cover - network failure safeguard
|
341
|
+
message = f"Failed to process {basename} via server: {exc}"
|
342
|
+
fallback_notice = "Falling back to local processing pipeline."
|
343
|
+
return False, [message, fallback_notice], [message, fallback_notice]
|
344
|
+
|
345
|
+
print(summary)
|
346
|
+
print(f"Saved processed video to {destination}")
|
347
|
+
if log_text.strip() and not printed_log_header:
|
348
|
+
print("\nServer log:\n" + log_text)
|
349
|
+
|
350
|
+
_print_total_time(start_time)
|
351
|
+
return True, [], []
|
254
352
|
|
255
353
|
|
256
354
|
def _launch_gui(argv: Sequence[str]) -> bool:
|
@@ -421,84 +519,30 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
421
519
|
if host_value:
|
422
520
|
parsed_args.server_url = f"http://{host_value}:9005"
|
423
521
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
del args["input_file"]
|
432
|
-
|
433
|
-
if "host" in args:
|
434
|
-
del args["host"]
|
435
|
-
|
436
|
-
if len(files) > 1 and "output_file" in args:
|
437
|
-
del args["output_file"]
|
438
|
-
|
439
|
-
fallback_messages: List[str] = []
|
440
|
-
if parsed_args.server_url:
|
441
|
-
server_success, error_message = _process_via_server(
|
442
|
-
files, parsed_args, start_time=start_time
|
522
|
+
send_video = None
|
523
|
+
remote_error_message: Optional[str] = None
|
524
|
+
try: # pragma: no cover - optional dependency guard
|
525
|
+
from . import service_client
|
526
|
+
except ImportError as exc:
|
527
|
+
remote_error_message = (
|
528
|
+
"Server mode requires the gradio_client dependency. " f"({exc})"
|
443
529
|
)
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
reporter = TqdmProgressReporter()
|
456
|
-
|
457
|
-
for message in fallback_messages:
|
458
|
-
reporter.log(message)
|
459
|
-
|
460
|
-
for index, file in enumerate(files):
|
461
|
-
print(f"Processing file {index + 1}/{len(files)} '{os.path.basename(file)}'")
|
462
|
-
local_options = dict(args)
|
463
|
-
|
464
|
-
option_kwargs: Dict[str, object] = {"input_file": Path(file)}
|
465
|
-
|
466
|
-
if "output_file" in local_options:
|
467
|
-
option_kwargs["output_file"] = Path(local_options["output_file"])
|
468
|
-
if "temp_folder" in local_options:
|
469
|
-
option_kwargs["temp_folder"] = Path(local_options["temp_folder"])
|
470
|
-
if "silent_threshold" in local_options:
|
471
|
-
option_kwargs["silent_threshold"] = float(local_options["silent_threshold"])
|
472
|
-
if "silent_speed" in local_options:
|
473
|
-
option_kwargs["silent_speed"] = float(local_options["silent_speed"])
|
474
|
-
if "sounded_speed" in local_options:
|
475
|
-
option_kwargs["sounded_speed"] = float(local_options["sounded_speed"])
|
476
|
-
if "frame_spreadage" in local_options:
|
477
|
-
option_kwargs["frame_spreadage"] = int(local_options["frame_spreadage"])
|
478
|
-
if "sample_rate" in local_options:
|
479
|
-
option_kwargs["sample_rate"] = int(local_options["sample_rate"])
|
480
|
-
if "small" in local_options:
|
481
|
-
option_kwargs["small"] = bool(local_options["small"])
|
482
|
-
options = ProcessingOptions(**option_kwargs)
|
483
|
-
|
484
|
-
try:
|
485
|
-
result = speed_up_video(options, reporter=reporter)
|
486
|
-
except FFmpegNotFoundError as exc:
|
487
|
-
print(str(exc), file=sys.stderr)
|
488
|
-
sys.exit(1)
|
530
|
+
else:
|
531
|
+
send_video = service_client.send_video
|
532
|
+
|
533
|
+
application = CliApplication(
|
534
|
+
gather_files=gather_input_files,
|
535
|
+
send_video=send_video,
|
536
|
+
speed_up=speed_up_video,
|
537
|
+
reporter_factory=TqdmProgressReporter,
|
538
|
+
remote_error_message=remote_error_message,
|
539
|
+
)
|
489
540
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
summary_parts.append(f"{time_ratio * 100:.0f}% time")
|
496
|
-
if size_ratio is not None:
|
497
|
-
summary_parts.append(f"{size_ratio * 100:.0f}% size")
|
498
|
-
if summary_parts:
|
499
|
-
reporter.log("Result: " + ", ".join(summary_parts))
|
500
|
-
|
501
|
-
_print_total_time(start_time)
|
541
|
+
exit_code, error_messages = application.run(parsed_args)
|
542
|
+
for message in error_messages:
|
543
|
+
print(message, file=sys.stderr)
|
544
|
+
if exit_code:
|
545
|
+
sys.exit(exit_code)
|
502
546
|
|
503
547
|
|
504
548
|
if __name__ == "__main__":
|
talks_reducer/discovery.py
CHANGED
@@ -7,7 +7,7 @@ import socket
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
8
8
|
from contextlib import closing
|
9
9
|
from http.client import HTTPConnection
|
10
|
-
from typing import Callable, Iterable, Iterator, List, Optional, Set
|
10
|
+
from typing import Callable, Iterable, Iterator, List, Optional, Sequence, Set
|
11
11
|
|
12
12
|
DEFAULT_PORT = 9005
|
13
13
|
DEFAULT_TIMEOUT = 0.4
|
@@ -15,6 +15,10 @@ DEFAULT_TIMEOUT = 0.4
|
|
15
15
|
_EXCLUDED_HOSTS = {"127.0.0.1", "localhost", "0.0.0.0"}
|
16
16
|
|
17
17
|
|
18
|
+
AddressSource = Callable[[], Iterable[str]]
|
19
|
+
ProbeHost = Callable[[str, int, float], Optional[str]]
|
20
|
+
|
21
|
+
|
18
22
|
def _should_include_host(host: Optional[str]) -> bool:
|
19
23
|
"""Return ``True`` when *host* should be scanned for discovery."""
|
20
24
|
|
@@ -23,39 +27,85 @@ def _should_include_host(host: Optional[str]) -> bool:
|
|
23
27
|
return host not in _EXCLUDED_HOSTS
|
24
28
|
|
25
29
|
|
26
|
-
def
|
27
|
-
"""
|
30
|
+
def _create_udp_socket(family: int, type_: int) -> socket.socket:
|
31
|
+
"""Return a UDP socket created with :func:`socket.socket`."""
|
28
32
|
|
29
|
-
|
33
|
+
return socket.socket(family, type_)
|
34
|
+
|
35
|
+
|
36
|
+
def _iter_getaddrinfo_addresses(
|
37
|
+
*,
|
38
|
+
hostname_resolver: Callable[[], str] = socket.gethostname,
|
39
|
+
getaddrinfo: Callable[..., Sequence[Sequence[object]]] = socket.getaddrinfo,
|
40
|
+
) -> Iterator[str]:
|
41
|
+
"""Yield IPv4 addresses discovered via ``getaddrinfo``."""
|
30
42
|
|
31
43
|
try:
|
32
|
-
hostname =
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
44
|
+
hostname = hostname_resolver()
|
45
|
+
except OSError:
|
46
|
+
return
|
47
|
+
|
48
|
+
try:
|
49
|
+
for info in getaddrinfo(hostname, None, family=socket.AF_INET):
|
50
|
+
try:
|
51
|
+
address = info[4][0] # type: ignore[index]
|
52
|
+
except (IndexError, TypeError):
|
53
|
+
continue
|
54
|
+
if address:
|
55
|
+
yield str(address)
|
38
56
|
except socket.gaierror:
|
39
|
-
|
57
|
+
return
|
58
|
+
|
40
59
|
|
41
|
-
|
60
|
+
def _iter_probe_addresses(
|
61
|
+
probes: Iterable[str] = ("8.8.8.8", "1.1.1.1"),
|
62
|
+
*,
|
63
|
+
socket_factory: Callable[[int, int], socket.socket] = _create_udp_socket,
|
64
|
+
) -> Iterator[str]:
|
65
|
+
"""Yield IPv4 addresses by opening UDP sockets to well-known hosts."""
|
66
|
+
|
67
|
+
for probe in probes:
|
42
68
|
try:
|
43
|
-
with closing(
|
69
|
+
with closing(socket_factory(socket.AF_INET, socket.SOCK_DGRAM)) as sock:
|
44
70
|
sock.connect((probe, 80))
|
45
71
|
address = sock.getsockname()[0]
|
46
|
-
if address
|
47
|
-
seen.add(address)
|
72
|
+
if address:
|
48
73
|
yield address
|
49
74
|
except OSError:
|
50
75
|
continue
|
51
76
|
|
52
77
|
|
53
|
-
def
|
78
|
+
def _iter_local_ipv4_addresses(
|
79
|
+
*, address_sources: Optional[Iterable[AddressSource]] = None
|
80
|
+
) -> Iterator[str]:
|
81
|
+
"""Yield IPv4 addresses that belong to the local machine."""
|
82
|
+
|
83
|
+
seen: Set[str] = set()
|
84
|
+
sources: Iterable[AddressSource]
|
85
|
+
if address_sources is None:
|
86
|
+
sources = (_iter_getaddrinfo_addresses, _iter_probe_addresses)
|
87
|
+
else:
|
88
|
+
sources = address_sources
|
89
|
+
|
90
|
+
for source in sources:
|
91
|
+
for address in source():
|
92
|
+
if not address:
|
93
|
+
continue
|
94
|
+
if address not in seen:
|
95
|
+
seen.add(address)
|
96
|
+
yield address
|
97
|
+
|
98
|
+
|
99
|
+
def _build_default_host_candidates(
|
100
|
+
prefix_length: int = 24,
|
101
|
+
*,
|
102
|
+
address_sources: Optional[Iterable[AddressSource]] = None,
|
103
|
+
) -> List[str]:
|
54
104
|
"""Return a list of host candidates based on detected local networks."""
|
55
105
|
|
56
106
|
hosts: Set[str] = set()
|
57
107
|
|
58
|
-
for address in _iter_local_ipv4_addresses():
|
108
|
+
for address in _iter_local_ipv4_addresses(address_sources=address_sources):
|
59
109
|
if _should_include_host(address):
|
60
110
|
hosts.add(address)
|
61
111
|
try:
|
@@ -104,6 +154,8 @@ def discover_servers(
|
|
104
154
|
timeout: float = DEFAULT_TIMEOUT,
|
105
155
|
hosts: Optional[Iterable[str]] = None,
|
106
156
|
progress_callback: Optional[ProgressCallback] = None,
|
157
|
+
address_sources: Optional[Iterable[AddressSource]] = None,
|
158
|
+
probe_host: Optional[ProbeHost] = None,
|
107
159
|
) -> List[str]:
|
108
160
|
"""Scan *hosts* for running Talks Reducer servers on *port*.
|
109
161
|
|
@@ -111,7 +163,9 @@ def discover_servers(
|
|
111
163
|
addresses are scanned. ``127.0.0.1``, ``localhost``, and ``0.0.0.0`` are
|
112
164
|
excluded to avoid duplicating local endpoints. The optional
|
113
165
|
*progress_callback* receives the number of scanned hosts and the total
|
114
|
-
candidate count whenever discovery advances.
|
166
|
+
candidate count whenever discovery advances. Supply *address_sources* to
|
167
|
+
override the IPv4 detection logic with custom iterables, and *probe_host*
|
168
|
+
to replace the HTTP-based reachability probe. The function returns a sorted
|
115
169
|
list of unique base URLs.
|
116
170
|
"""
|
117
171
|
|
@@ -119,7 +173,9 @@ def discover_servers(
|
|
119
173
|
candidates = sorted(
|
120
174
|
{
|
121
175
|
host
|
122
|
-
for host in _build_default_host_candidates(
|
176
|
+
for host in _build_default_host_candidates(
|
177
|
+
address_sources=address_sources
|
178
|
+
)
|
123
179
|
if _should_include_host(host)
|
124
180
|
}
|
125
181
|
)
|
@@ -132,11 +188,11 @@ def discover_servers(
|
|
132
188
|
if progress_callback is not None:
|
133
189
|
progress_callback(0, total)
|
134
190
|
|
191
|
+
probe_fn: ProbeHost = _probe_host if probe_host is None else probe_host
|
192
|
+
|
135
193
|
with ThreadPoolExecutor(max_workers=32) as executor:
|
136
194
|
scanned = 0
|
137
|
-
for url in executor.map(
|
138
|
-
lambda host: _probe_host(host, port, timeout), candidates
|
139
|
-
):
|
195
|
+
for url in executor.map(lambda host: probe_fn(host, port, timeout), candidates):
|
140
196
|
if url and url not in results:
|
141
197
|
results.append(url)
|
142
198
|
scanned += 1
|