diff --git a/app/core/settings.py b/app/core/settings.py index a73cf84a..93cdf121 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -34,12 +34,14 @@ def _default_allow_network() -> bool: - # Off by default everywhere — the user explicitly opts other devices in. - # STEMDECK_ALLOW_NETWORK=1 can pre-enable it (e.g. headless/Docker deploys). + # STEMDECK_ALLOW_NETWORK takes precedence when set explicitly. + # Otherwise: desktop keeps network off (user opts in via UI toggle); + # server/Docker deployments open it by default since network access is + # the entire point of a headless deployment. env = os.environ.get("STEMDECK_ALLOW_NETWORK") if env is not None: return env.strip() == "1" - return False + return os.environ.get("STEMDECK_DESKTOP") != "1" def _load() -> dict: diff --git a/app/pipeline/download.py b/app/pipeline/download.py index c69d6095..6562b289 100644 --- a/app/pipeline/download.py +++ b/app/pipeline/download.py @@ -115,6 +115,7 @@ def normalize_youtube_url(url: str) -> str: playlists embed the seed in the list ID; YouTube refuses to view the playlist directly with "This playlist type is unviewable.") * `youtu.be/` -> `watch?v=` + * `youtube.com/shorts/` -> `watch?v=` Everything else (PL/OL/algorithmic playlists with no derivable seed) is left alone -- yt-dlp will surface its own error. """ @@ -146,6 +147,11 @@ def normalize_youtube_url(url: str) -> str: if _VIDEO_ID_RE.match(vid): return f"https://www.youtube.com/watch?v={vid}" + if host == "youtube.com" and parsed.path.startswith("/shorts/"): + vid = parsed.path[len("/shorts/") :].lstrip("/").split("/")[0] + if _VIDEO_ID_RE.match(vid): + return f"https://www.youtube.com/watch?v={vid}" + return url diff --git a/build/docker-compose.yml b/build/docker-compose.yml index 6b9ec74f..2201d457 100644 --- a/build/docker-compose.yml +++ b/build/docker-compose.yml @@ -13,8 +13,7 @@ services: image: stemdeck container_name: stemdeck ports: - # Bind to loopback only -- StemDeck has no auth and is local-only. - - "127.0.0.1:8000:8000" + - "8000:8000" volumes: # Stems and downloaded audio land in /jobs on the # host so you can grab them without going through the container. diff --git a/static/css/daw.css b/static/css/daw.css index 7301191b..cfe24e6f 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -811,6 +811,7 @@ input, textarea { font-family: inherit; } .settings-net-list { display: flex; flex-direction: column; gap: 5px; align-items: flex-start; } .settings-net-list code { color: var(--accent); background: rgba(244,183,64,0.1); padding: 3px 9px; border-radius: 5px; font-size: 11.5px; } .settings-net-empty { color: var(--muted); } +.settings-server-note { font-size: 10.5px; color: var(--muted); margin: 0 0 12px; line-height: 1.5; } .settings-subhead { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); font-weight: 600; margin: 4px 0 7px; } .library-editor-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 11px; } diff --git a/static/js/catalog.js b/static/js/catalog.js index 485cca90..c1ad1a44 100644 --- a/static/js/catalog.js +++ b/static/js/catalog.js @@ -2046,8 +2046,18 @@ function openLibraryEditor() { document.body.appendChild(overlay); libraryEditor = overlay; refreshLibrarySyncSummary(); + const isDesktop = Boolean(window.__TAURI__?.core?.invoke); wireGeneralSettings(overlay); wireNetworkSetting(overlay); + if (!isDesktop) { + overlay.querySelector(".net-access-input")?.setAttribute("disabled", ""); + overlay.querySelector(".set-port")?.setAttribute("readonly", ""); + overlay.querySelector(".set-port")?.setAttribute("disabled", ""); + const note = document.createElement("p"); + note.className = "settings-server-note"; + note.textContent = "These settings are read-only in server mode. To change them, update your server configuration (e.g. docker-compose.yml) and restart."; + overlay.querySelector("[data-pane='advanced']")?.prepend(note); + } } // Poll a job until it reaches a terminal state, so auto-restores run one at a diff --git a/tests/test_network_gate.py b/tests/test_network_gate.py index 927b218b..ed6c90e0 100644 --- a/tests/test_network_gate.py +++ b/tests/test_network_gate.py @@ -36,12 +36,20 @@ def test_host_request_recognizes_own_lan_ip(monkeypatch): assert _is_host_request("192.168.1.99") is False # a different device -def test_default_is_off(monkeypatch): - # Off by default everywhere — the user must opt in. +def test_default_is_off_in_desktop_mode(monkeypatch): + # Desktop keeps network off; the user opts in via the UI toggle. monkeypatch.delenv("STEMDECK_ALLOW_NETWORK", raising=False) + monkeypatch.setenv("STEMDECK_DESKTOP", "1") assert settings_mod._default_allow_network() is False +def test_default_is_on_in_server_mode(monkeypatch): + # Server/Docker deployments open the gate by default. + monkeypatch.delenv("STEMDECK_ALLOW_NETWORK", raising=False) + monkeypatch.delenv("STEMDECK_DESKTOP", raising=False) + assert settings_mod._default_allow_network() is True + + def test_env_var_pre_enables(monkeypatch): monkeypatch.setenv("STEMDECK_ALLOW_NETWORK", "1") assert settings_mod._default_allow_network() is True diff --git a/tests/test_url_validation.py b/tests/test_url_validation.py index 6538d806..b1ee9c39 100644 --- a/tests/test_url_validation.py +++ b/tests/test_url_validation.py @@ -28,6 +28,14 @@ " https://www.youtube.com/watch?v=dQw4w9WgXcQ ", "https://www.youtube.com/watch?v=dQw4w9WgXcQ", ), + ( + "https://www.youtube.com/shorts/dQw4w9WgXcQ", + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + ), + ( + "https://m.youtube.com/shorts/dQw4w9WgXcQ", + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + ), ], ) def test_accepts_youtube_urls(url: str, expected: str) -> None: