aboutsummaryrefslogtreecommitdiff
path: root/app/backends/common.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-01 14:32:05 -0400
committerhistoria <historiavg@proton.me>2026-09-01 14:32:05 -0400
commit6cfcd564c0684c52618235e6366f4a81c02b9a5b (patch)
tree55321760a8103bc6b5d79489fac4135a60e6e3ba /app/backends/common.py
parentdc6e7cd43029da62dabe2513fb5aa8a34df1bd6d (diff)
downloadtts-audiobook-generator-6cfcd564c0684c52618235e6366f4a81c02b9a5b.tar.gz
slop refactor/dedup
Diffstat (limited to 'app/backends/common.py')
-rw-r--r--app/backends/common.py67
1 files changed, 53 insertions, 14 deletions
diff --git a/app/backends/common.py b/app/backends/common.py
index d25a03e..24746b6 100644
--- a/app/backends/common.py
+++ b/app/backends/common.py
@@ -193,12 +193,32 @@ def wav_dir_preview(directory: Path) -> Tuple[str, str]:
return ("no .wav files", "info")
+def port_of(url: str, fallback: int) -> int:
+ """URL's explicit port, else FALLBACK (invalid URLs fall back too)."""
+ try:
+ return urllib.parse.urlsplit(url).port or fallback
+ except ValueError:
+ return fallback
+
+
def url_with_port(url: str, port: int) -> str:
- """Return URL with its port replaced/inserted as PORT."""
+ """Return URL with its port replaced/inserted as PORT.
+
+ Preserves the userinfo ("user:pass@host") and brackets IPv6 hosts
+ ("[::1]"), which a plain f"{host}:{port}" rebuild would mangle.
+ """
parts = urllib.parse.urlsplit(url)
host = parts.hostname or "127.0.0.1"
+ if ":" in host and not host.startswith("["):
+ host = f"[{host}]"
+ netloc = f"{host}:{port}"
+ if parts.username:
+ cred = parts.username
+ if parts.password:
+ cred = f"{cred}:{parts.password}"
+ netloc = f"{cred}@{netloc}"
return urllib.parse.urlunsplit(
- (parts.scheme or "http", f"{host}:{port}", parts.path, "", ""))
+ (parts.scheme or "http", netloc, parts.path, "", ""))
def normalize_remote_url(value: str) -> str:
@@ -408,14 +428,27 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
def _reader() -> None:
nonlocal last_output
try:
- for raw in iter(proc.stdout.readline, b""):
- if not raw:
+ pending = ""
+ while True:
+ # read1 (not read/readline) returns whatever a single pipe
+ # read yields, without waiting to fill the buffer, and \r
+ # is treated as a line break: carriage-return progress bars
+ # (git clone --progress, tqdm/HuggingFace downloads) then
+ # surface incrementally and keep the stall watchdog fed —
+ # a readline-based reader blocked until the next \n would
+ # let a healthy download starve to the timeout.
+ chunk = proc.stdout.read1(4096)
+ if not chunk:
break
last_output = time.monotonic()
- text = raw.decode("utf-8", errors="replace")
- for line in text.splitlines():
+ pending += chunk.decode("utf-8", errors="replace")
+ parts = re.split(r"[\r\n]+", pending)
+ pending = parts.pop()
+ for line in parts:
if line:
emit(line)
+ if pending.strip():
+ emit(pending)
except (OSError, ValueError):
pass
@@ -448,11 +481,10 @@ def run_console_subprocess(argv: List[str], cwd: Optional[Path] = None,
_terminate_process_group(proc)
break
time.sleep(0.1)
- try:
- reader.join(timeout=5)
- finally:
- if reader.is_alive():
- reader.join(timeout=0)
+ # The reader is a daemon: a join timeout here only means the child
+ # closed its stdout but the thread is still draining — there is
+ # nothing useful left to wait for.
+ reader.join(timeout=5)
if cancelled:
return 130
if stalled:
@@ -476,10 +508,17 @@ def _terminate_process_group(proc) -> None:
"""
import signal
if sys.platform == "win32":
+ # TerminateProcess hits a single pid; children the server spawned
+ # would survive as orphans. taskkill /T walks and kills the whole
+ # process tree, then the poll loop reaps the direct child.
try:
- proc.terminate()
- except OSError:
- pass
+ subprocess.run(["taskkill", "/T", "/F", "/PID", str(proc.pid)],
+ capture_output=True, timeout=10)
+ except (OSError, subprocess.SubprocessError):
+ try:
+ proc.terminate()
+ except OSError:
+ pass
deadline = time.time() + 10
while time.time() < deadline:
if proc.poll() is not None: