summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-03 16:20:23 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-03 16:20:28 +0200
commit753653b4df62723b82d32799825135822886eac7 (patch)
tree14829922bf343e6353093eaceaa84986fe5f84df /packages/meshbay-node/src/meshbay_node
parent675beed6ff688733a9598f9d82d41578f48316be (diff)
downloadmeshbay-753653b4df62723b82d32799825135822886eac7.tar.gz
refactor(node): platform abstraction for Windows portability (W1-W2-W5-W6-W7)
Platform directories, signal handling, chmod guards, ffmpeg discovery, and platform-conditional CLI messages — all testable on Linux. See docs/WINDOWS-PORT.md §5 for the plan these implement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/config.py12
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py72
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py3
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich.py3
-rw-r--r--packages/meshbay-node/src/meshbay_node/keystore.py22
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_probe.py3
-rw-r--r--packages/meshbay-node/src/meshbay_node/platform.py70
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py3
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_server.py8
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/tls_cert.py10
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py8
11 files changed, 157 insertions, 57 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py
index 78ee873..cfbec50 100644
--- a/packages/meshbay-node/src/meshbay_node/config.py
+++ b/packages/meshbay-node/src/meshbay_node/config.py
@@ -11,6 +11,8 @@ import os
from dataclasses import dataclass, field
from pathlib import Path
+from meshbay_node.platform import config_dir, data_dir
+
try:
import tomllib # Python 3.11+
except ImportError:
@@ -25,7 +27,7 @@ DEFAULT_STUN_SERVERS: list[str] = [
"stun:stun.services.mozilla.com:3478",
]
-DEFAULT_CONFIG_PATH = Path.home() / ".config" / "meshbay" / "node.toml"
+DEFAULT_CONFIG_PATH = config_dir() / "node.toml"
EXAMPLE_CONFIG = """\
# MeshBay Node configuration — multi-group example
@@ -169,6 +171,8 @@ class NodeConfig:
# machine with a Tailscale wt0 interface.
ice_interfaces: list[str] = field(default_factory=list) # include-list overrides auto
stun_servers: list[str] = field(default_factory=list) # empty = DEFAULT_STUN_SERVERS
+ ffmpeg_path: str = "ffmpeg"
+ ffprobe_path: str = "ffprobe"
@dataclass
@@ -234,7 +238,7 @@ class Config:
node: NodeConfig = field(default_factory=NodeConfig)
groups: list[GroupConfig] = field(default_factory=list)
keystore: KeystoreConfig = field(default_factory=KeystoreConfig)
- data_dir: Path = field(default_factory=lambda: Path.home() / ".local" / "share" / "meshbay")
+ data_dir: Path = field(default_factory=data_dir)
# Back-compat: single-group access
@property
@@ -333,6 +337,10 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config:
stun = nd.get("stun_servers")
if isinstance(stun, list):
cfg.node.stun_servers = [str(s) for s in stun]
+ if "ffmpeg_path" in nd:
+ cfg.node.ffmpeg_path = str(nd["ffmpeg_path"])
+ if "ffprobe_path" in nd:
+ cfg.node.ffprobe_path = str(nd["ffprobe_path"])
# Multi-group: [[groups]] array
if "groups" in raw:
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 7341423..47241cd 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -52,6 +52,7 @@ from meshbay_node.media_cache import MediaCache
from meshbay_node.tmdb import TmdbClient
from meshbay_node.musicbrainz import MusicBrainzClient
from meshbay_node.keystore import create_keystore, load_keystore, load_or_create_keystore
+from meshbay_node.platform import chmod_private, config_dir, data_dir, state_dir
from meshbay_node.roster import Roster
from meshbay_node.transport import (
Denylist,
@@ -219,7 +220,7 @@ class NodeDaemon:
self._config.data_dir.mkdir(parents=True, exist_ok=True)
self._ui_token_file = self._config.data_dir / "ui-token"
self._ui_token_file.write_text(ui_token)
- self._ui_token_file.chmod(0o600)
+ chmod_private(self._ui_token_file)
from meshbay_node.ui import create_ui_app
ui_app = create_ui_app(self._state)
ui_cfg = uvicorn.Config(
@@ -677,8 +678,12 @@ class NodeDaemon:
# 12. Wait for shutdown
stop_event = asyncio.Event()
loop = asyncio.get_event_loop()
- for sig in (signal.SIGINT, signal.SIGTERM):
- loop.add_signal_handler(sig, stop_event.set)
+ if sys.platform == "win32":
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ signal.signal(sig, lambda *_: stop_event.set())
+ else:
+ for sig in (signal.SIGINT, signal.SIGTERM):
+ loop.add_signal_handler(sig, stop_event.set)
# Milestone 14.8: re-read node.toml without dropping connections.
try:
loop.add_signal_handler(
@@ -1734,7 +1739,7 @@ def main() -> None:
"",
]
cfg_path.write_text("\n".join(toml_lines) + "\n")
- os.chmod(cfg_path, 0o600)
+ chmod_private(cfg_path)
print(f"Config written to {cfg_path}")
unlock_file = config_dir / "unlock.key"
@@ -1742,7 +1747,7 @@ def main() -> None:
import secrets
key = secrets.token_urlsafe(32)
unlock_file.write_text(key + "\n")
- os.chmod(unlock_file, 0o600)
+ chmod_private(unlock_file)
print(f"Unlock key created: {unlock_file}")
cfg = load_config(cfg_path)
@@ -1759,7 +1764,10 @@ def main() -> None:
print()
print("Next steps:")
print(f" 1. Link this node key on {hub_url} → Settings → Link Node")
- print(" 2. systemctl --user enable --now meshbay-node")
+ if sys.platform == "win32":
+ print(" 2. meshbay-node (start the daemon)")
+ else:
+ print(" 2. systemctl --user enable --now meshbay-node")
print(" 3. meshbay-node group add <name> --dir /path/to/files")
print(" 4. meshbay-node gek init")
print(" 5. meshbay-node operator pair")
@@ -1768,12 +1776,12 @@ def main() -> None:
if args.command == "reset":
import shutil
- config_dir = Path.home() / ".config" / "meshbay"
- data_dir = Path.home() / ".local" / "share" / "meshbay"
- state_dir = Path.home() / ".local" / "state" / "meshbay"
+ config_dir_ = config_dir()
+ data_dir_ = data_dir()
+ state_dir_ = state_dir()
items = []
- for d in (config_dir, data_dir):
+ for d in (config_dir_, data_dir_):
if d.exists():
for child in sorted(d.iterdir()):
items.append(child)
@@ -1800,10 +1808,10 @@ def main() -> None:
import urllib.request
import urllib.error
- token_file = data_dir / "ui-token"
+ token_file = data_dir_ / "ui-token"
if token_file.exists():
try:
- cfg = Config(config_dir / "node.toml")
+ cfg = Config(config_dir_ / "node.toml")
tok = token_file.read_text().strip()
url = (f"http://127.0.0.1:{cfg.node.ui_port}"
f"/api/unlink?t={tok}")
@@ -1814,16 +1822,17 @@ def main() -> None:
except Exception:
print("Could not unlink from hub (daemon not reachable).")
- _sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"],
- capture_output=True)
+ if sys.platform != "win32":
+ _sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"],
+ capture_output=True)
- for d in (config_dir, data_dir):
+ for d in (config_dir_, data_dir_):
if d.exists():
shutil.rmtree(d)
print(f"Removed {d}")
- if state_dir.is_dir():
- shutil.rmtree(state_dir)
- print(f"Removed {state_dir}")
+ if state_dir_.is_dir():
+ shutil.rmtree(state_dir_)
+ print(f"Removed {state_dir_}")
print("Node state erased. Run 'meshbay-node init' to start over.")
return
@@ -2056,16 +2065,9 @@ def main() -> None:
return
if args.command == "reload":
- # Milestone 14.8. The daemon re-reads node.toml; groups that appeared or
- # whose roots changed are picked up without dropping live connections.
- #
- # Delegated to systemd rather than hunting a PID with pgrep and signalling
- # it directly: an unanchored (or merely unlucky) pattern match there has
- # already SIGHUPed a developer's own running node by accident — see the
- # comment this replaced, and test_cli_dispatch.py's stub_daemon fixture,
- # which had to stub os.kill for exactly that reason. The unit already
- # declares `ExecReload=/bin/kill -HUP $MAINPID`, so systemd sends the
- # signal to the one process it actually started.
+ if sys.platform == "win32":
+ print("reload is not supported on Windows — restart the daemon instead.")
+ sys.exit(1)
_systemctl_user(
"reload", "meshbay-node",
not_running_hint="Node is not running as a systemd unit — start it "
@@ -2075,9 +2077,10 @@ def main() -> None:
return
if args.command == "restart-daemon":
- # Same reasoning as reload: no PID hunting, no manual respawn — systemd
- # already knows how to stop and start this unit, and does not need this
- # process to guess where its log file is.
+ if sys.platform == "win32":
+ print("restart-daemon is not supported on Windows — stop and start "
+ "the daemon manually.")
+ sys.exit(1)
_systemctl_user(
"restart", "meshbay-node",
not_running_hint="meshbay-node is not installed as a systemd unit — "
@@ -2356,6 +2359,13 @@ def main() -> None:
print("Error: hub.username not set in config. Run: meshbay-node init")
sys.exit(1)
+ from meshbay_node.platform import check_media_tools
+ try:
+ check_media_tools(cfg.node.ffmpeg_path, cfg.node.ffprobe_path)
+ except RuntimeError as e:
+ print(f"Error: {e}")
+ sys.exit(1)
+
daemon = NodeDaemon(cfg, Path(args.config or DEFAULT_CONFIG_PATH))
asyncio.run(daemon.run())
diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py
index 1ed1662..1e975fa 100644
--- a/packages/meshbay-node/src/meshbay_node/hub_client.py
+++ b/packages/meshbay-node/src/meshbay_node/hub_client.py
@@ -26,6 +26,7 @@ import httpx
import jwt
from meshbay_node.keystore import NodeKeys
+from meshbay_node.platform import config_dir
log = logging.getLogger(__name__)
@@ -61,7 +62,7 @@ class HubSession:
class HubConfig:
hub_url: str
username: str
- cache_dir: Path = field(default_factory=lambda: Path.home() / ".config" / "meshbay")
+ cache_dir: Path = field(default_factory=config_dir)
@property
def hub_pk_cache_path(self) -> Path:
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
index 8b2eca5..19ab9ce 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
@@ -161,8 +161,9 @@ def _synthetic_episode_number(file_path: Path, show_root: Path, season: int) ->
async def _make_thumbnail(file_path: Path, duration: float | None) -> bytes | None:
"""One ffmpeg frame grab at ~10% of duration (or 5s if unknown), scaled down."""
seek = max(0.0, (duration or 50.0) * 0.1)
+ from meshbay_node.platform import ffmpeg_cmd
proc = await asyncio.create_subprocess_exec(
- "ffmpeg", "-v", "error", "-ss", str(seek), "-i", str(file_path),
+ ffmpeg_cmd(), "-v", "error", "-ss", str(seek), "-i", str(file_path),
"-frames:v", "1", "-vf", f"scale={THUMB_WIDTH}:-1",
"-f", "image2", "-c:v", "mjpeg", "pipe:1",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
diff --git a/packages/meshbay-node/src/meshbay_node/keystore.py b/packages/meshbay-node/src/meshbay_node/keystore.py
index 59fc719..dff443d 100644
--- a/packages/meshbay-node/src/meshbay_node/keystore.py
+++ b/packages/meshbay-node/src/meshbay_node/keystore.py
@@ -31,6 +31,7 @@ import getpass
import json
import logging
import os
+import sys
from dataclasses import dataclass
from pathlib import Path
@@ -38,6 +39,7 @@ import msgpack
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+from meshbay_node.platform import chmod_private, config_dir
from meshbay_common.crypto import (
ARGON2_ITERATIONS,
ARGON2_LANES,
@@ -57,8 +59,8 @@ from meshbay_common.crypto import (
log = logging.getLogger(__name__)
KEYSTORE_VERSION = 1
-DEFAULT_KEYSTORE_PATH = Path.home() / ".config" / "meshbay" / "keystore.enc"
-DEFAULT_UNLOCK_FILE = Path.home() / ".config" / "meshbay" / "unlock.key"
+DEFAULT_KEYSTORE_PATH = config_dir() / "keystore.enc"
+DEFAULT_UNLOCK_FILE = config_dir() / "unlock.key"
@dataclass
@@ -94,12 +96,14 @@ def _resolve_password(unlock_file: Path | None = None) -> str:
# 2. Unlock key file
key_file = unlock_file or DEFAULT_UNLOCK_FILE
if key_file.exists():
- mode = oct(key_file.stat().st_mode)[-3:]
- if mode != "600":
- log.warning(
- "unlock.key permissions are %s (expected 600) — fix with: chmod 600 %s",
- mode, key_file,
- )
+ if sys.platform != "win32":
+ mode = oct(key_file.stat().st_mode)[-3:]
+ if mode != "600":
+ log.warning(
+ "unlock.key permissions are %s (expected 600) — fix with: "
+ "chmod 600 %s",
+ mode, key_file,
+ )
log.debug("Keystore password from %s", key_file)
return key_file.read_text().strip()
@@ -233,7 +237,7 @@ def _write_keystore(path: Path, keys: NodeKeys, password: str) -> None:
"ciphertext_b64": base64.b64encode(ct).decode(),
}
path.write_text(json.dumps(envelope, indent=2))
- path.chmod(0o600)
+ chmod_private(path)
def load_or_create_keystore(
diff --git a/packages/meshbay-node/src/meshbay_node/media_probe.py b/packages/meshbay-node/src/meshbay_node/media_probe.py
index 6dd3baa..e0c59e3 100644
--- a/packages/meshbay-node/src/meshbay_node/media_probe.py
+++ b/packages/meshbay-node/src/meshbay_node/media_probe.py
@@ -47,8 +47,9 @@ async def probe_video(
field, no second process spawn) — resolution is deliberately never
guessed from the filename (docs/mediacenter.md §3.5).
"""
+ from meshbay_node.platform import ffprobe_cmd
proc = await asyncio.create_subprocess_exec(
- "ffprobe", "-v", "error",
+ ffprobe_cmd(), "-v", "error",
"-show_entries", "stream=codec_name,profile,level,codec_type,width,height",
"-show_entries", "format=duration",
"-of", "json", path,
diff --git a/packages/meshbay-node/src/meshbay_node/platform.py b/packages/meshbay-node/src/meshbay_node/platform.py
new file mode 100644
index 0000000..bc48169
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/platform.py
@@ -0,0 +1,70 @@
+"""Platform-specific paths and tool resolution for meshbay-node."""
+
+import os
+import shutil
+import sys
+from pathlib import Path
+
+# ── Directories ──────────────────────────────────────────────────────────────
+
+
+def config_dir() -> Path:
+ if sys.platform == "win32":
+ return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "meshbay"
+ return Path.home() / ".config" / "meshbay"
+
+
+def data_dir() -> Path:
+ if sys.platform == "win32":
+ return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "meshbay" / "data"
+ return Path.home() / ".local" / "share" / "meshbay"
+
+
+def state_dir() -> Path:
+ if sys.platform == "win32":
+ return Path(os.environ.get("LOCALAPPDATA") or Path.home()) / "meshbay" / "state"
+ return Path.home() / ".local" / "state" / "meshbay"
+
+
+# ── File permissions ─────────────────────────────────────────────────────────
+
+
+def chmod_private(path: Path, *, mode: int = 0o600) -> None:
+ """Set restrictive permissions on a file. No-op on Windows (NTFS ignores mode bits)."""
+ if sys.platform != "win32":
+ path.chmod(mode)
+
+
+# ── Media tools ──────────────────────────────────────────────────────────────
+
+_ffmpeg_path: str = "ffmpeg"
+_ffprobe_path: str = "ffprobe"
+
+
+def check_media_tools(
+ ffmpeg: str = "ffmpeg", ffprobe: str = "ffprobe",
+) -> None:
+ """Resolve ffmpeg/ffprobe at daemon startup. Raises RuntimeError if not found."""
+ global _ffmpeg_path, _ffprobe_path
+ resolved = shutil.which(ffmpeg)
+ if not resolved:
+ raise RuntimeError(
+ f"{ffmpeg!r} not found in PATH. "
+ "Install ffmpeg or set [node] ffmpeg_path in node.toml."
+ )
+ _ffmpeg_path = resolved
+ resolved = shutil.which(ffprobe)
+ if not resolved:
+ raise RuntimeError(
+ f"{ffprobe!r} not found in PATH. "
+ "Install ffmpeg or set [node] ffprobe_path in node.toml."
+ )
+ _ffprobe_path = resolved
+
+
+def ffmpeg_cmd() -> str:
+ return _ffmpeg_path
+
+
+def ffprobe_cmd() -> str:
+ return _ffprobe_path
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index 5af2b55..a50dd3e 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -914,6 +914,7 @@ def write_code_file(data_dir: Path, code: str, expires_at: str,
"""
path = data_dir / name
path.parent.mkdir(parents=True, exist_ok=True)
+ from meshbay_node.platform import chmod_private
path.write_text(f"{code}\nexpires {expires_at}\n")
- os.chmod(path, 0o600)
+ chmod_private(path)
return path
diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
index 6dde3ff..95fccf1 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
@@ -53,6 +53,7 @@ from meshbay_common.groupbox import PURPOSE_ACK, seal
from meshbay_common.protocol import MNP, file_chunk_wire
from meshbay_node.indexer import GroupIndex
from meshbay_node.transport.wire import index_sync_message
+from meshbay_node import platform
log = logging.getLogger(__name__)
@@ -545,7 +546,7 @@ def _extract_segment(file_path: Path, start_time: float, duration: float) -> byt
"""Extract one HLS segment via ffmpeg. Returns MPEG-TS bytes or None on failure."""
try:
result = subprocess.run(
- ["ffmpeg", "-hide_banner", "-loglevel", "error",
+ [platform.ffmpeg_cmd(), "-hide_banner", "-loglevel", "error",
"-ss", str(start_time),
"-i", str(file_path),
"-t", str(duration),
@@ -596,8 +597,9 @@ class QuicChunkServer:
# Peer sets are per group now — see _MNPServerProtocol._peer_registry().
self._host = host
self._port = port
- self._cert_path = cert_path or Path.home() / ".config/meshbay/node_tls.crt"
- self._key_path = key_path or Path.home() / ".config/meshbay/node_tls.key"
+ from meshbay_node.platform import config_dir
+ self._cert_path = cert_path or config_dir() / "node_tls.crt"
+ self._key_path = key_path or config_dir() / "node_tls.key"
self._server = None
self._task = None
self._session_tickets: dict[bytes, Any] = {}
diff --git a/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py b/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py
index 374fd08..8d680ea 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py
@@ -21,10 +21,12 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
+from meshbay_node.platform import chmod_private, config_dir
+
log = logging.getLogger(__name__)
-DEFAULT_CERT = Path.home() / ".config" / "meshbay" / "node_tls.crt"
-DEFAULT_KEY = Path.home() / ".config" / "meshbay" / "node_tls.key"
+DEFAULT_CERT = config_dir() / "node_tls.crt"
+DEFAULT_KEY = config_dir() / "node_tls.key"
def generate_self_signed_cert(
@@ -64,8 +66,8 @@ def generate_self_signed_cert(
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
))
- cert_path.chmod(0o644)
- key_path.chmod(0o600)
+ chmod_private(cert_path, mode=0o644)
+ chmod_private(key_path)
log.info("TLS cert generated: %s", cert_path)
return cert_path, key_path
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 0154319..e333ae8 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -100,7 +100,7 @@ from meshbay_common.protocol import MNP, chunk_ciphertext, file_chunk_wire
from meshbay_node.transport.wire import index_sync_message
from meshbay_node.indexer import GroupIndex
from meshbay_node.indexer.indexer import DirectoryIndexer
-from meshbay_node import linkpreview, ops
+from meshbay_node import linkpreview, ops, platform
# Re-imported under its original name: every call site and existing test in
# this module still refers to it as `_probe_video`. The implementation lives
# in media_probe.py so the indexer package (imported just above) can call it
@@ -3441,7 +3441,7 @@ class WebRTCPeerSession:
try:
async with sem:
proc = await asyncio.create_subprocess_exec(
- "ffmpeg", "-hide_banner", "-loglevel", "error",
+ platform.ffmpeg_cmd(), "-hide_banner", "-loglevel", "error",
"-ss", str(segment_index * segment_duration),
"-i", str(file_path),
"-t", str(segment_duration),
@@ -4360,7 +4360,7 @@ class WebRTCPeerSession:
# forced out of its MediaSource with no further explanation.
codec_args += ["-c:a", "aac", "-ac", "2", "-b:a", "192k"]
proc = await asyncio.create_subprocess_exec(
- "ffmpeg", "-hide_banner", "-loglevel", "error",
+ platform.ffmpeg_cmd(), "-hide_banner", "-loglevel", "error",
*seek_args,
"-i", str(file_path),
*map_args,
@@ -4556,7 +4556,7 @@ async def _transcode_audio_to_aac(file_path: Path) -> bytes:
tmp_path = Path(tmp_name)
try:
proc = await asyncio.create_subprocess_exec(
- "ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
+ platform.ffmpeg_cmd(), "-hide_banner", "-loglevel", "error", "-y",
"-i", str(file_path),
"-vn", "-c:a", "aac", "-ac", "2", "-b:a", "192k",
"-f", "ipod", str(tmp_path),