nstream 1.43.0__tar.gz
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.
- nstream-1.43.0/.claude-plugin/plugin.json +10 -0
- nstream-1.43.0/.github/workflows/check.yml +24 -0
- nstream-1.43.0/.github/workflows/pypi.yml +28 -0
- nstream-1.43.0/.gitignore +18 -0
- nstream-1.43.0/AGENTS.md +97 -0
- nstream-1.43.0/CHANGELOG.md +251 -0
- nstream-1.43.0/CLAUDE.md +97 -0
- nstream-1.43.0/CONTRIBUTING.md +95 -0
- nstream-1.43.0/LICENSE +21 -0
- nstream-1.43.0/PKG-INFO +183 -0
- nstream-1.43.0/README.md +163 -0
- nstream-1.43.0/SECURITY.md +21 -0
- nstream-1.43.0/config.example.json +51 -0
- nstream-1.43.0/docs/README.md +35 -0
- nstream-1.43.0/docs/adr/0000-template.md +32 -0
- nstream-1.43.0/docs/adr/0001-native-debrid-resolver-adapter-layer.md +96 -0
- nstream-1.43.0/docs/adr/0002-realdebrid-native-integration.md +52 -0
- nstream-1.43.0/docs/adr/0003-torbox-native-integration.md +66 -0
- nstream-1.43.0/docs/adr/0004-premiumize-native-integration.md +65 -0
- nstream-1.43.0/docs/adr/0005-cast-delivery-tier2-remux.md +90 -0
- nstream-1.43.0/docs/adr/0006-cast-mirror-realtime-1080p.md +93 -0
- nstream-1.43.0/docs/adr/0007-cast-metadata-via-castbridge.md +131 -0
- nstream-1.43.0/docs/adr/0008-castbridge-socket-activation.md +50 -0
- nstream-1.43.0/docs/adr/0009-movies-series-separation.md +83 -0
- nstream-1.43.0/docs/adr/0010-background-cast-discovery.md +68 -0
- nstream-1.43.0/docs/adr/0011-unify-cast-delivery-state-machine.md +76 -0
- nstream-1.43.0/docs/adr/0012-cast-subtitles-webvtt-text-track.md +86 -0
- nstream-1.43.0/docs/adr/0013-custom-cast-receiver.md +110 -0
- nstream-1.43.0/docs/adr/0014-verified-cache-preranking.md +91 -0
- nstream-1.43.0/docs/adr/0015-mirror-over-4k-remux.md +85 -0
- nstream-1.43.0/docs/adr/0016-receiver-track-observability.md +96 -0
- nstream-1.43.0/docs/adr/0017-cast-video-codec-vetting.md +55 -0
- nstream-1.43.0/docs/adr/0018-subtitle-hash-match-and-retime.md +62 -0
- nstream-1.43.0/docs/adr/0019-audio-anchored-subtitle-sync.md +70 -0
- nstream-1.43.0/docs/adr/0020-native-subtitle-alignment.md +80 -0
- nstream-1.43.0/docs/adr/0020-phase0/RESULTS.md +37 -0
- nstream-1.43.0/docs/adr/0020-phase0/curve-0.tsv +962 -0
- nstream-1.43.0/docs/adr/0020-phase0/curve-5.tsv +962 -0
- nstream-1.43.0/docs/adr/0021-per-invocation-constraint-parity.md +52 -0
- nstream-1.43.0/docs/adr/0022-cast-container-vetting.md +78 -0
- nstream-1.43.0/docs/adr/0023-mirror-fidelity-and-explicit-force.md +44 -0
- nstream-1.43.0/docs/adr/0024-multi-source-stream-discovery.md +68 -0
- nstream-1.43.0/docs/adr/0025-dead-source-classification.md +137 -0
- nstream-1.43.0/docs/adr/0026-structured-stream-metadata-precedence.md +89 -0
- nstream-1.43.0/docs/adr/0027-per-addon-circuit-breaker.md +106 -0
- nstream-1.43.0/docs/adr/0028-content-duration-vetting.md +116 -0
- nstream-1.43.0/docs/adr/0029-one-continuation-policy.md +123 -0
- nstream-1.43.0/docs/adr/0030-explicit-year-hard-constraint.md +126 -0
- nstream-1.43.0/docs/adr/0031-delivery-backend-reports-started.md +234 -0
- nstream-1.43.0/docs/adr/0032-p2p-privacy-gate-at-the-swarm-join.md +120 -0
- nstream-1.43.0/docs/adr/0033-exhaustion-raises-none-means-esc.md +62 -0
- nstream-1.43.0/docs/adr/0034-bounded-effects-and-playback-evidence.md +51 -0
- nstream-1.43.0/docs/adr/0035-direct-cast-before-full-remux.md +62 -0
- nstream-1.43.0/docs/adr/0036-remux-feasibility-before-prepare.md +79 -0
- nstream-1.43.0/docs/adr/0037-explicit-interactivity-and-injected-prompts.md +81 -0
- nstream-1.43.0/docs/adr/0038-dead-source-key-by-what-failed.md +50 -0
- nstream-1.43.0/docs/adr/0039-disk-free-tier2-via-live-hls-ts.md +131 -0
- nstream-1.43.0/docs/adr/0039-phase0/receiver-matrix-2026-10-01.md +59 -0
- nstream-1.43.0/docs/adr/0040-subtitles-off-the-start-path.md +93 -0
- nstream-1.43.0/docs/adr/0041-live-tier-before-another-dub.md +43 -0
- nstream-1.43.0/docs/adr/0042-embedded-subtitles-on-the-live-path.md +101 -0
- nstream-1.43.0/docs/adr/0043-live-subtitle-timing-without-reload.md +88 -0
- nstream-1.43.0/docs/adr/0044-live-started-is-observed-state.md +108 -0
- nstream-1.43.0/docs/adr/README.md +76 -0
- nstream-1.43.0/docs/architecture.md +184 -0
- nstream-1.43.0/docs/archive/audit-2026-06-09.md +479 -0
- nstream-1.43.0/docs/headless.md +155 -0
- nstream-1.43.0/docs/roadmap.md +40 -0
- nstream-1.43.0/docs/selection.md +272 -0
- nstream-1.43.0/docs/user/cast.md +131 -0
- nstream-1.43.0/docs/user/config.md +165 -0
- nstream-1.43.0/docs/user/troubleshooting.md +80 -0
- nstream-1.43.0/docs/user/tui.md +89 -0
- nstream-1.43.0/docs/verification.md +60 -0
- nstream-1.43.0/install.sh +48 -0
- nstream-1.43.0/nstream-fuzzel +9 -0
- nstream-1.43.0/nstream.desktop +9 -0
- nstream-1.43.0/packaging/.SRCINFO +26 -0
- nstream-1.43.0/packaging/PKGBUILD +52 -0
- nstream-1.43.0/packaging/build-local.sh +49 -0
- nstream-1.43.0/packaging/nstream.install +16 -0
- nstream-1.43.0/pyproject.toml +60 -0
- nstream-1.43.0/scripts/benchmark.py +68 -0
- nstream-1.43.0/scripts/check-package.py +57 -0
- nstream-1.43.0/scripts/check-playback.py +68 -0
- nstream-1.43.0/scripts/check.sh +9 -0
- nstream-1.43.0/scripts/refresh-stream-addons.py +110 -0
- nstream-1.43.0/skills/nstream/SKILL.md +113 -0
- nstream-1.43.0/src/nstream/__init__.py +3 -0
- nstream-1.43.0/src/nstream/__main__.py +8 -0
- nstream-1.43.0/src/nstream/_bench.py +260 -0
- nstream-1.43.0/src/nstream/_subalign_remote.py +200 -0
- nstream-1.43.0/src/nstream/addons.py +338 -0
- nstream-1.43.0/src/nstream/api.py +841 -0
- nstream-1.43.0/src/nstream/application.py +91 -0
- nstream-1.43.0/src/nstream/availability.py +178 -0
- nstream-1.43.0/src/nstream/bridge.py +533 -0
- nstream-1.43.0/src/nstream/cast_control.py +194 -0
- nstream-1.43.0/src/nstream/cast_delivery.py +181 -0
- nstream-1.43.0/src/nstream/cast_flow.py +813 -0
- nstream-1.43.0/src/nstream/cast_vet.py +468 -0
- nstream-1.43.0/src/nstream/caster.py +694 -0
- nstream-1.43.0/src/nstream/cli.py +1231 -0
- nstream-1.43.0/src/nstream/cli_args.py +257 -0
- nstream-1.43.0/src/nstream/config.py +440 -0
- nstream-1.43.0/src/nstream/debrid.py +389 -0
- nstream-1.43.0/src/nstream/discovery.py +177 -0
- nstream-1.43.0/src/nstream/doctor.py +67 -0
- nstream-1.43.0/src/nstream/engine.py +461 -0
- nstream-1.43.0/src/nstream/explain.py +301 -0
- nstream-1.43.0/src/nstream/failures.py +110 -0
- nstream-1.43.0/src/nstream/headless.py +572 -0
- nstream-1.43.0/src/nstream/headless_play.py +457 -0
- nstream-1.43.0/src/nstream/labels.py +142 -0
- nstream-1.43.0/src/nstream/languages.py +132 -0
- nstream-1.43.0/src/nstream/live.py +609 -0
- nstream-1.43.0/src/nstream/log.py +171 -0
- nstream-1.43.0/src/nstream/menus.py +100 -0
- nstream-1.43.0/src/nstream/mirror.py +437 -0
- nstream-1.43.0/src/nstream/net.py +281 -0
- nstream-1.43.0/src/nstream/notices.py +59 -0
- nstream-1.43.0/src/nstream/nstream.lua +209 -0
- nstream-1.43.0/src/nstream/oshash.py +77 -0
- nstream-1.43.0/src/nstream/picker.py +280 -0
- nstream-1.43.0/src/nstream/playback.py +37 -0
- nstream-1.43.0/src/nstream/player.py +395 -0
- nstream-1.43.0/src/nstream/preview.py +259 -0
- nstream-1.43.0/src/nstream/providers.py +21 -0
- nstream-1.43.0/src/nstream/quality.py +1011 -0
- nstream-1.43.0/src/nstream/remux.py +1540 -0
- nstream-1.43.0/src/nstream/series.py +254 -0
- nstream-1.43.0/src/nstream/serve.py +792 -0
- nstream-1.43.0/src/nstream/settings.py +742 -0
- nstream-1.43.0/src/nstream/sources.py +150 -0
- nstream-1.43.0/src/nstream/srt.py +195 -0
- nstream-1.43.0/src/nstream/state/__init__.py +110 -0
- nstream-1.43.0/src/nstream/state/breaker.py +212 -0
- nstream-1.43.0/src/nstream/state/cast_session.py +137 -0
- nstream-1.43.0/src/nstream/state/dead.py +71 -0
- nstream-1.43.0/src/nstream/state/history.py +310 -0
- nstream-1.43.0/src/nstream/state/throughput.py +63 -0
- nstream-1.43.0/src/nstream/stream_select.py +988 -0
- nstream-1.43.0/src/nstream/subalign.py +563 -0
- nstream-1.43.0/src/nstream/subs.py +394 -0
- nstream-1.43.0/src/nstream/tracks.py +162 -0
- nstream-1.43.0/src/nstream/types.py +93 -0
- nstream-1.43.0/src/nstream/ui.py +463 -0
- nstream-1.43.0/src/nstream/urlproxy.py +185 -0
- nstream-1.43.0/src/nstream/util.py +406 -0
- nstream-1.43.0/tests/conftest.py +106 -0
- nstream-1.43.0/tests/data/backend.py +60 -0
- nstream-1.43.0/tests/data/coherence_local.json +47335 -0
- nstream-1.43.0/tests/data/coherence_sparse.json +42295 -0
- nstream-1.43.0/tests/data/moon_local.json +13713 -0
- nstream-1.43.0/tests/test_addons.py +209 -0
- nstream-1.43.0/tests/test_api.py +928 -0
- nstream-1.43.0/tests/test_application.py +69 -0
- nstream-1.43.0/tests/test_architecture.py +98 -0
- nstream-1.43.0/tests/test_availability.py +149 -0
- nstream-1.43.0/tests/test_breaker.py +120 -0
- nstream-1.43.0/tests/test_bridge.py +558 -0
- nstream-1.43.0/tests/test_cast_control.py +156 -0
- nstream-1.43.0/tests/test_cast_delivery.py +185 -0
- nstream-1.43.0/tests/test_cast_flow.py +1296 -0
- nstream-1.43.0/tests/test_cast_vet.py +796 -0
- nstream-1.43.0/tests/test_caster.py +768 -0
- nstream-1.43.0/tests/test_cli.py +1545 -0
- nstream-1.43.0/tests/test_cli_args.py +43 -0
- nstream-1.43.0/tests/test_config.py +349 -0
- nstream-1.43.0/tests/test_config_docs.py +71 -0
- nstream-1.43.0/tests/test_debrid.py +370 -0
- nstream-1.43.0/tests/test_discovery.py +218 -0
- nstream-1.43.0/tests/test_doctor.py +35 -0
- nstream-1.43.0/tests/test_e2e.py +198 -0
- nstream-1.43.0/tests/test_engine.py +435 -0
- nstream-1.43.0/tests/test_explain.py +139 -0
- nstream-1.43.0/tests/test_failures.py +52 -0
- nstream-1.43.0/tests/test_headless.py +1745 -0
- nstream-1.43.0/tests/test_labels.py +95 -0
- nstream-1.43.0/tests/test_languages.py +77 -0
- nstream-1.43.0/tests/test_live.py +560 -0
- nstream-1.43.0/tests/test_log.py +112 -0
- nstream-1.43.0/tests/test_mirror.py +456 -0
- nstream-1.43.0/tests/test_net.py +386 -0
- nstream-1.43.0/tests/test_notices.py +32 -0
- nstream-1.43.0/tests/test_oshash.py +72 -0
- nstream-1.43.0/tests/test_packaging.py +14 -0
- nstream-1.43.0/tests/test_picker.py +231 -0
- nstream-1.43.0/tests/test_playback.py +48 -0
- nstream-1.43.0/tests/test_player.py +311 -0
- nstream-1.43.0/tests/test_preview.py +289 -0
- nstream-1.43.0/tests/test_quality.py +1179 -0
- nstream-1.43.0/tests/test_remux.py +1417 -0
- nstream-1.43.0/tests/test_series.py +430 -0
- nstream-1.43.0/tests/test_serve.py +386 -0
- nstream-1.43.0/tests/test_settings.py +477 -0
- nstream-1.43.0/tests/test_sources.py +58 -0
- nstream-1.43.0/tests/test_srt.py +168 -0
- nstream-1.43.0/tests/test_state.py +537 -0
- nstream-1.43.0/tests/test_stream_select.py +1487 -0
- nstream-1.43.0/tests/test_subalign.py +130 -0
- nstream-1.43.0/tests/test_subalign_fixtures.py +80 -0
- nstream-1.43.0/tests/test_subalign_remote.py +124 -0
- nstream-1.43.0/tests/test_subs.py +502 -0
- nstream-1.43.0/tests/test_throughput.py +19 -0
- nstream-1.43.0/tests/test_tracks.py +173 -0
- nstream-1.43.0/tests/test_types.py +12 -0
- nstream-1.43.0/tests/test_ui.py +316 -0
- nstream-1.43.0/tests/test_urlproxy.py +116 -0
- nstream-1.43.0/tests/test_util.py +242 -0
- nstream-1.43.0/uv.lock +134 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nstream",
|
|
3
|
+
"description": "Headless streaming control for nstream — search a movie or TV series and play it locally (mpv) or cast it to a Chromecast, non-interactively, with machine-readable JSON output.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Gianluca Mazza"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/gianlucamazza/nstream",
|
|
9
|
+
"repository": "https://github.com/gianlucamazza/nstream"
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Verify
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
pull_request:
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
concurrency:
|
|
8
|
+
group: verify-${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
jobs:
|
|
11
|
+
check:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
timeout-minutes: 15
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python: ['3.13', '3.14']
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python }}
|
|
23
|
+
enable-cache: true
|
|
24
|
+
- run: bash scripts/check.sh
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v*"
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
concurrency:
|
|
9
|
+
group: pypi-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
jobs:
|
|
12
|
+
pypi:
|
|
13
|
+
if: github.repository == 'gianlucamazza/nstream'
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
timeout-minutes: 15
|
|
16
|
+
environment:
|
|
17
|
+
name: pypi
|
|
18
|
+
url: https://pypi.org/p/nstream
|
|
19
|
+
permissions:
|
|
20
|
+
id-token: write
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: astral-sh/setup-uv@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.13"
|
|
26
|
+
enable-cache: true
|
|
27
|
+
- run: uv build
|
|
28
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Never commit the real config: it contains the Real-Debrid API token
|
|
2
|
+
config.json
|
|
3
|
+
*.local.json
|
|
4
|
+
|
|
5
|
+
# Python / build
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.pyc
|
|
8
|
+
.venv/
|
|
9
|
+
dist/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
|
|
12
|
+
# Arch packaging build artifacts
|
|
13
|
+
packaging/pkg/
|
|
14
|
+
packaging/src/
|
|
15
|
+
packaging/nstream/
|
|
16
|
+
packaging/*.pkg.tar.zst
|
|
17
|
+
packaging/*.tar.gz
|
|
18
|
+
packaging/*.sig
|
nstream-1.43.0/AGENTS.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
nstream is a native, terminal-first Stremio alternative: no Electron, no embedded browser,
|
|
8
|
+
no Node server. Pure Python 3.13+ stdlib (zero runtime dependencies) orchestrating external
|
|
9
|
+
CLIs (`fzf`, `mpv`, `ffprobe`, `catt`, `chafa`, optional TorrServer / castbridge) over Stremio
|
|
10
|
+
addon HTTP APIs. Entry point is `nstream.cli:_entry` (`src/nstream/cli.py`).
|
|
11
|
+
|
|
12
|
+
Pipeline: search/browse (Cinemeta + catalogs) → pick title/episode (fzf or `--json`) → optional
|
|
13
|
+
subtitles (OpenSubtitles v3 + alignment) → fetch streams (Torrentio and/or `cfg.addons`) →
|
|
14
|
+
hardware- or cast-aware rank/filter → play (mpv) or cast (castbridge/catt/remux/mirror) →
|
|
15
|
+
track progress (resume / continue-watching).
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
Tooling is `uv`-based (no venv activation needed).
|
|
20
|
+
|
|
21
|
+
| Task | Command |
|
|
22
|
+
| ---- | ------- |
|
|
23
|
+
| Run tests | `uv run python -m pytest` |
|
|
24
|
+
| Single test | `uv run python -m pytest tests/test_labels.py::test_display_title_movie` |
|
|
25
|
+
| Lint | `uvx ruff check . && uvx ruff format --check .` |
|
|
26
|
+
| Type check | `uvx ty check` |
|
|
27
|
+
| Dev run | `uv run nstream "the matrix"` |
|
|
28
|
+
| Install (dev) | `./install.sh` (uses `uv tool install`) |
|
|
29
|
+
|
|
30
|
+
Use `python -m pytest` (not bare `uv run pytest`): if a system-wide `nstream` is installed,
|
|
31
|
+
bare `pytest` resolves to the system interpreter and imports the **installed** package instead
|
|
32
|
+
of `src/`. `python -m` runs the project venv with the editable `src/` checkout.
|
|
33
|
+
|
|
34
|
+
Ruff: line-length 100, rules `E,F,I,UP,B,SIM`. Pytest: `testpaths=["tests"]`, `-q`.
|
|
35
|
+
Each `src/nstream/<mod>.py` has a matching `tests/test_<mod>.py` (or package coverage).
|
|
36
|
+
|
|
37
|
+
## Documentation map (do not re-duplicate)
|
|
38
|
+
|
|
39
|
+
| Topic | Source of truth |
|
|
40
|
+
| ----- | --------------- |
|
|
41
|
+
| Module map / domains | [`docs/architecture.md`](docs/architecture.md) |
|
|
42
|
+
| Stream ranking / cast vet | [`docs/selection.md`](docs/selection.md) |
|
|
43
|
+
| Headless `--json` contract | [`docs/headless.md`](docs/headless.md) |
|
|
44
|
+
| Config keys | [`docs/user/config.md`](docs/user/config.md) |
|
|
45
|
+
| Decisions (why) | [`docs/adr/`](docs/adr/README.md) |
|
|
46
|
+
| Hub / audience routing | [`docs/README.md`](docs/README.md) |
|
|
47
|
+
| Agent playbook (intent→cmd) | [`skills/nstream/SKILL.md`](skills/nstream/SKILL.md) |
|
|
48
|
+
| Contributor process | [`CONTRIBUTING.md`](CONTRIBUTING.md) |
|
|
49
|
+
|
|
50
|
+
Update architecture when adding/moving modules — **not** the README module list (there is none).
|
|
51
|
+
|
|
52
|
+
### Import-graph discipline (non-negotiable)
|
|
53
|
+
|
|
54
|
+
- Top: `util` / `ui` / `languages` / `types` / `sources` / `net` / `log` (see architecture)
|
|
55
|
+
- `cli` orchestrates at the bottom
|
|
56
|
+
- Everything below `cli` **never** imports `cli` (including `headless`, `cast_flow`,
|
|
57
|
+
`stream_select`, `cast_vet`, `player`, `caster`, …)
|
|
58
|
+
- Cast policy lives in `cast_vet`; cast decision tree in `cast_flow`; backends in
|
|
59
|
+
`caster` / `remux` / `mirror`; TUI cast lifecycle in `cast_control`. Selection facade:
|
|
60
|
+
`stream_select.prepare_stream`. Availability probes/denylist: `availability`. Domain
|
|
61
|
+
TypedDicts: `types` (not `config`). Headless play body: `headless_play`; argparse:
|
|
62
|
+
`cli_args`. State package: `state/` (history, cast_session, dead, breaker)
|
|
63
|
+
|
|
64
|
+
### Hard constraints when editing
|
|
65
|
+
|
|
66
|
+
- **Debrid provider-agnostic:** never special-case RealDebrid; tokens/markers/redaction are generic
|
|
67
|
+
- **Never log stream URLs or debrid tokens in clear** — use `what=` labels; `log.RedactFormatter`
|
|
68
|
+
- **Cast audio language** is decided at selection (`cast_vet.vet_cast_audio`), not on the DMR
|
|
69
|
+
- **Dead sources (ADR 0025):** only `gone` probes are persisted; incomplete reads are not proof
|
|
70
|
+
- **P2P privacy gate (ADR 0032):** at every swarm join, not one call site
|
|
71
|
+
- **Disk I/O best-effort** try/except — never block playback
|
|
72
|
+
- fzf sentinels are module-level `object()` values (`_PLAY`, `_ALL`, …) so `None` means ESC
|
|
73
|
+
- Comments/docs in English; concise, no over-engineering
|
|
74
|
+
- **`ui.Caps`** = terminal caps; **`quality.HwCaps`** = GPU/vainfo — different types
|
|
75
|
+
- New architectural trade-off → ADR ([template](docs/adr/0000-template.md)); cite symbols not lines
|
|
76
|
+
|
|
77
|
+
### Stream sources (ADR 0024)
|
|
78
|
+
|
|
79
|
+
Torrentio (`torrentio_enabled`) + `cfg.addons` manifests. `api.streams` stamps `addon`, fuses
|
|
80
|
+
debrid url with torrent infoHash by filename, collapses duplicates (cached > url > infoHash).
|
|
81
|
+
|
|
82
|
+
### Secrets & paths
|
|
83
|
+
|
|
84
|
+
- Config: `$XDG_CONFIG_HOME/nstream/config.json` (chmod 600; template `config.example.json`)
|
|
85
|
+
- History: `$XDG_STATE_HOME/nstream/history.json`
|
|
86
|
+
- Library: `$XDG_STATE_HOME/nstream/library.json` (recent + watchlist metadata only)
|
|
87
|
+
- Dead sources: `$XDG_STATE_HOME/nstream/dead-sources.json` (`--forget-dead`)
|
|
88
|
+
- Addon breakers: `$XDG_STATE_HOME/nstream/addon-breakers.json` (`--forget-breakers`, ADR 0027)
|
|
89
|
+
- Caches: `$XDG_CACHE_HOME/nstream/` (`manifests.json`, `hwcaps.json`, `devices.json`, `meta/`, `posters/`, `torrents/`)
|
|
90
|
+
- Log: `$XDG_STATE_HOME/nstream/nstream.log` (rotating); `--debug` / `NSTREAM_DEBUG=1` → stderr DEBUG
|
|
91
|
+
- Runtime deps: `mpv`, `fzf` (required); `ffmpeg`/`ffprobe`, `catt`, TorrServer, `vainfo`, `chafa`, `foot`, castbridge/mirror (optional)
|
|
92
|
+
|
|
93
|
+
## Packaging hygiene
|
|
94
|
+
|
|
95
|
+
`packaging/` tracked files: `PKGBUILD`, `build-local.sh`, `nstream.install`, `.SRCINFO` only.
|
|
96
|
+
Build artifacts (`pkg/`, `src/`, `*.pkg.tar.zst`, `*.sig`) are gitignored; `build-local.sh`
|
|
97
|
+
cleans them. Do not commit wheels or makepkg trees.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to nstream are documented here. Version source:
|
|
4
|
+
`src/nstream/__init__.py` (and `packaging/PKGBUILD` `pkgver`).
|
|
5
|
+
|
|
6
|
+
Format loosely follows [Keep a Changelog](https://keepachangelog.com/).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## [1.43.0] — 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `SECURITY.md`: report debrid-token leaks and LAN/remux-server issues via GitHub
|
|
15
|
+
private advisories.
|
|
16
|
+
- GitHub Actions Trusted Publisher workflow publishes to PyPI on `v*` tags (OIDC, no
|
|
17
|
+
API token in secrets).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Field notes and tests use RFC 5737 TEST-NET-1 (`192.0.2.10`) and a dummy Cast
|
|
22
|
+
application id (`CA5T0001`) in place of a home LAN address and a personal receiver id.
|
|
23
|
+
- Arch `PKGBUILD` source is the GitHub tag tarball over HTTPS (directory
|
|
24
|
+
`nstream-$pkgver`). `sha256sums` stays `SKIP` until the public `v1.43.0` tarball exists.
|
|
25
|
+
|
|
26
|
+
## [1.42.0] — 2026-10-02
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **ADR 0044 Accepted:** live `started` is an observed player state
|
|
31
|
+
(PLAYING/PAUSED/BUFFERING). Dual-layer Dolby Vision and a first HLS segment the DMR
|
|
32
|
+
cannot decode skip live; the complete-file remux is the fallback.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Live fire-and-return no longer reports `ok: true` on a mere LOAD ack, so a DMR that
|
|
37
|
+
fetches the playlist and then ERRORs falls back to the complete-file remux.
|
|
38
|
+
- Dual-layer Dolby Vision (enhancement layer) and a first HLS segment without decodable
|
|
39
|
+
audio skip the live path. Profile 8 single-layer stays live. Field 2026-10-02:
|
|
40
|
+
*After Hours* live head refused → `delivery: file` → PLAYING.
|
|
41
|
+
|
|
42
|
+
## [1.41.0] — 2026-10-02
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **ADR 0042 Accepted:** a live cast that wants subtitles uses the release's own full text
|
|
47
|
+
track (never a `forced` one) as an HLS WebVTT rendition, activated by castbridge ≥ 0.4.1
|
|
48
|
+
(`textLanguage`). The complete-file path extracts the same track in its pass.
|
|
49
|
+
`subtitles_match: "embedded"`.
|
|
50
|
+
- `--sub-shift ±S` (and TUI cast menu ±0.5 s) moves a live cast's subtitles.
|
|
51
|
+
- After-start subtitle alignment on live casts: the producer measures speech activity, and
|
|
52
|
+
after 10 min the cues are aligned with the same gates as the complete-file path.
|
|
53
|
+
`--status` reports `subtitles_offset`.
|
|
54
|
+
- Live seeks anywhere (producer restart at the target), the TV clock in film time
|
|
55
|
+
(`EXT-X-GAP` filler), bandwidth-aware ranking and a `live_slow` warning.
|
|
56
|
+
- Binge: the next episode's live producer is prepared near the end of the current one
|
|
57
|
+
and adopted at the change.
|
|
58
|
+
- Interactive (TUI) live casts share the live state: seeks, sub-shift and film-time status.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **ADR 0043 Accepted:** a downloaded subtitle on a live cast is a rendition too (the
|
|
63
|
+
producer's second input), so `--sub-shift` and the after-start alignment only rewrite
|
|
64
|
+
the cues the TV fetches next: the media is never reloaded.
|
|
65
|
+
- With castbridge ≥ 0.4.2 a live seek's LOAD rides the running receiver session (no app
|
|
66
|
+
relaunch, no INTERRUPTED playback).
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- `--sub-shift` on a live cast no longer leaves the TV IDLE or stuck in BUFFERING (it
|
|
71
|
+
re-LOADed the media).
|
|
72
|
+
- A live seek that restarts the producer no longer sends the TV IDLE (ERROR) before the new
|
|
73
|
+
playlist loads: the old generation is kept until the receiver moves on.
|
|
74
|
+
- `tests/test_e2e.py` no longer spawns the real castbridge (216 orphaned daemons found).
|
|
75
|
+
- `--follow` events report film time and the probed duration on live casts.
|
|
76
|
+
|
|
77
|
+
## [1.40.0] — 2026-10-01
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **ADR 0039 Accepted:** a cast whose audio needs conversion (Dolby/DTS, or an `.mkv`
|
|
82
|
+
rewrap) streams a live HLS-TS playlist and starts in seconds instead of after the
|
|
83
|
+
whole-file remux. Stereo AAC; disk bounded to a ~40-min window; the complete file is the
|
|
84
|
+
fallback. `cast_live` config key; JSON `delivery` (`live`/`file`/`direct`/`mirror`/`local`).
|
|
85
|
+
- Live seeks: far jumps re-LOAD the playlist at the target (the receiver clamps them);
|
|
86
|
+
a resume starts the producer at the resume point (27 s instead of 223 s for 50 min).
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
|
|
90
|
+
- **ADR 0041 Accepted:** with the live tier, a soft `audio_langs` preference starts the
|
|
91
|
+
preferred dub instead of yielding to another one (ADR 0035 stays the fallback when live
|
|
92
|
+
cannot run; its notice now names `--audio-lang <lang>`).
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- A live cast's progress reaches history (the playlist reports no duration).
|
|
97
|
+
- A live LOAD always uses the Default Media Receiver: the custom receiver refuses HLS.
|
|
98
|
+
- A recast of the title the TV is playing (another dub, a retry) resumes where the TV is
|
|
99
|
+
at LOAD time, not where it was when the command began.
|
|
100
|
+
- `urlproxy` resumes dropped upstream reads and never prints tracebacks (#5).
|
|
101
|
+
- Releases with burned-in CJK subtitles are demoted (CJK-script name on any addon, HC/CHS
|
|
102
|
+
tags) and never picked for an instant direct cast (#6).
|
|
103
|
+
- A search whose catalog (Cinemeta) never answered is a `network` error to retry, not
|
|
104
|
+
`no_result` (#4).
|
|
105
|
+
|
|
106
|
+
## [1.39.0] — 2026-10-01
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- **ADR 0036 Accepted:** remux feasibility (disk, size budget) is decided before the
|
|
111
|
+
prepare, never after a multi-GB download; an infeasible conversion fails as
|
|
112
|
+
`remux_infeasible` instead of a mute direct cast. Search, cast ranking and disc-sized
|
|
113
|
+
releases fixed from the 2026-10-01 incident.
|
|
114
|
+
- **ADR 0038 Accepted:** a dead source is keyed by what failed, never by a shared
|
|
115
|
+
display name.
|
|
116
|
+
- Debrid tokens no longer appear in child-process argv: ffmpeg/ffprobe read through a
|
|
117
|
+
loopback proxy (`urlproxy`), mpv through a private playlist file.
|
|
118
|
+
- Subtitles: local audio alignment works (it had failed every time since July: mono
|
|
119
|
+
16 kHz analysis, runtime-based timeout). One cue model (`srt`): CP1252/UTF-16 decoding,
|
|
120
|
+
valid WebVTT (3-digit ms, ASS/`<font>` tags removed, `<`/`&` escaped), cues before 0
|
|
121
|
+
dropped instead of piled at 00:00. A failed download tries the next candidate.
|
|
122
|
+
Downloads are size-capped (gzip bombs) and cached by URL. catt receives the cleaned
|
|
123
|
+
WebVTT. `subs_delivered` follows the receiver's confirmed active tracks; the caption
|
|
124
|
+
track carries a BCP-47 language and a display name.
|
|
125
|
+
- Reselects (dub, MP4 twin, castable video, in-cast switch, forced `--audio-lang`) rank
|
|
126
|
+
with the searched title like the initial pick. Accented titles ("Léon") no longer
|
|
127
|
+
demote their own releases.
|
|
128
|
+
- Lifecycle: no signal to a reused pid; detached servers and ffmpeg are reclaimed; the
|
|
129
|
+
remux prepare lock is published only once held; the previous cast session is kept
|
|
130
|
+
until a new cast really starts; headless Ctrl-C on a direct cast aborts instead of
|
|
131
|
+
re-casting through catt.
|
|
132
|
+
- `--doctor` detects castbridge; series resume follows the next episode with autoplay
|
|
133
|
+
off; headless resolves the TV before any stream work; a zero receiver volume is
|
|
134
|
+
re-read before warning; short language codes ("Chi", "Por") count only in capitals;
|
|
135
|
+
untagged MP4/WebM releases are considered for an instant direct cast; never-started
|
|
136
|
+
addon tasks no longer trip breakers; poster downloads are capped; the P2P privacy
|
|
137
|
+
notice shows once per process.
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
|
|
141
|
+
- **ADR 0037 Accepted:** interactivity is an explicit mode; menus and confirmations are
|
|
142
|
+
injected by the frontend, and the domain never opens fzf. One failure table
|
|
143
|
+
(`failures.describe`) for the TUI and `--json`; domain notices reach `--json` as
|
|
144
|
+
`notices` with stable codes (`remux_infeasible`, `subs_unverified`, `volume_zero`, …).
|
|
145
|
+
- Performance: cold search 25.6 s → 0.1–1.6 s (bounded half-open probes, search quorum,
|
|
146
|
+
stale-while-revalidate manifests); bounded ffprobe; subtitles are fetched while the
|
|
147
|
+
Tier-2 remux runs.
|
|
148
|
+
- Configurable custom receiver app id (ADR 0013).
|
|
149
|
+
- The bench-only sparse alignment moved to `_subalign_remote` (not shipped); debrid
|
|
150
|
+
provider keys live in the `providers` leaf; the bottom import tier is test-enforced.
|
|
151
|
+
|
|
152
|
+
### Proposed
|
|
153
|
+
|
|
154
|
+
- ADR 0039 (Tier-2 as live HLS-TS; receiver gate 3 passed: 62 min, 0 stalls) and
|
|
155
|
+
ADR 0040 (subtitles off the cast start path).
|
|
156
|
+
|
|
157
|
+
## [1.38.0] — 2026-09-25
|
|
158
|
+
|
|
159
|
+
### Added
|
|
160
|
+
|
|
161
|
+
- Shared local playback service, explicit media-start evidence, and actionable player
|
|
162
|
+
errors. Movies no longer activate the next-episode overlay or quit half a second
|
|
163
|
+
before EOF.
|
|
164
|
+
- `--doctor` / `--json --doctor`: read-only local diagnostics. Optional binaries are
|
|
165
|
+
reported apart from required ones.
|
|
166
|
+
- Reproducible verification: `scripts/check.sh` (Ruff, ty, pytest, wheel contents,
|
|
167
|
+
disposable install) on Python 3.13 and 3.14, architecture contracts, subprocess
|
|
168
|
+
playback acceptance, benchmarks, and an mpv smoke that does not touch the desktop
|
|
169
|
+
or the TV.
|
|
170
|
+
- **ADR 0034 Accepted:** bounded addon gathering, deadline-aware retries, bounded JSON
|
|
171
|
+
decoding, validated stream fields, collector-owned breaker updates, serialized
|
|
172
|
+
best-effort state writes, atomic runtime snapshots, and a private recovery copy
|
|
173
|
+
before a malformed JSON file is replaced. Existing state files stay compatible.
|
|
174
|
+
|
|
175
|
+
### Changed
|
|
176
|
+
|
|
177
|
+
- **ADR 0035 Accepted:** a soft `audio_langs` preference no longer forces a full-file
|
|
178
|
+
remux when a verified direct cast (MP4/WebM, decodable first track) exists at the
|
|
179
|
+
same quality. Headless starts that direct cast, keeps primary-language safety
|
|
180
|
+
subtitles, and explains the choice in `notice`. A TUI asks once. `--audio-lang`
|
|
181
|
+
still waits for the preferred dub. Remux progress is printed even when stderr is
|
|
182
|
+
not a terminal.
|
|
183
|
+
- Playback no longer changes the host firewall. Capability URLs are redacted in logs
|
|
184
|
+
and machine-readable output.
|
|
185
|
+
- The development-only subtitle benchmark is excluded from the wheel with `exclude`,
|
|
186
|
+
not `force-exclude`.
|
|
187
|
+
|
|
188
|
+
## [1.37.1] — 2026-08-10
|
|
189
|
+
|
|
190
|
+
### Fixed
|
|
191
|
+
|
|
192
|
+
- **ADR 0033 Accepted:** a title with nothing playable no longer returns to the menu in
|
|
193
|
+
silence. `stream_select.prepare_stream` reserves `None` for the user backing out (ESC) and
|
|
194
|
+
raises `NoPlayableStream(reason)` on exhaustion; the reason reaches the fzf header (TUI) and
|
|
195
|
+
the `no_playable_stream` message (headless). Typical case: every source is a pure torrent and
|
|
196
|
+
the P2P privacy gate (ADR 0032) is closed, or the debrid left no direct link.
|
|
197
|
+
|
|
198
|
+
### Added
|
|
199
|
+
|
|
200
|
+
- `--explain`: `SORGENTI: n/m con link diretto · k torrent` line, plus `counts.direct_links`,
|
|
201
|
+
`counts.torrents` and `unresolvable_reason` in the JSON output — the same diagnosis before
|
|
202
|
+
attempting playback.
|
|
203
|
+
- `engine.p2p_block_reason`: the privacy gate's predicate without its side effects, so
|
|
204
|
+
explainers can state the refusal without triggering it.
|
|
205
|
+
|
|
206
|
+
## [1.37.0] — 2026-08-09
|
|
207
|
+
|
|
208
|
+
### Added
|
|
209
|
+
|
|
210
|
+
- **ADR 0027 Accepted:** per-addon circuit breaker (`state/breaker.py`) on gather paths;
|
|
211
|
+
Open sources skipped without network; `--forget-breakers`; listed in `--explain`.
|
|
212
|
+
- TUI cast session menu: pause / resume / relative seek / refresh / volume / stop; cast row
|
|
213
|
+
also on Film/Serie sections (`cast_control`).
|
|
214
|
+
- Settings: cast/P2P/subs knobs, hybrid `playback_backend: auto`, `default_quality`,
|
|
215
|
+
diagnostics for required bins (`mpv`, `fzf`) and optional cast/P2P deps.
|
|
216
|
+
- Config load tightens world-readable `config.json` to 0600 (debrid token hygiene).
|
|
217
|
+
- Multi-addon gather progress on TTY (completion order, not submit order).
|
|
218
|
+
|
|
219
|
+
### Fixed
|
|
220
|
+
|
|
221
|
+
- Alt-C reuses full cast decision tree (`run_cast`); quality miss surfaces available res;
|
|
222
|
+
`default_quality` on headless + TUI paths.
|
|
223
|
+
- Headless-only flags without `--json` are usage errors (no silent TUI no-op).
|
|
224
|
+
|
|
225
|
+
### Documentation
|
|
226
|
+
|
|
227
|
+
- Full documentation restructure: audience hub (`docs/README.md`), user guides
|
|
228
|
+
(`docs/user/`), official headless contract (`docs/headless.md`), config reference tables,
|
|
229
|
+
cast/TUI/troubleshooting pages, CONTRIBUTING, roadmap/non-goals, CHANGELOG.
|
|
230
|
+
- Aligned `config.example.json` with `Config` defaults (`playback_backend: local`, all keys
|
|
231
|
+
including `cast_mirror_over_remux_gb` / `sub_align*`).
|
|
232
|
+
- ADR 0026/0027 normalized to English MADR template; 0027 status **Accepted**.
|
|
233
|
+
- Config/example drift test (`tests/test_config_docs.py`).
|
|
234
|
+
|
|
235
|
+
## [1.36.1] — 2026-08-08
|
|
236
|
+
|
|
237
|
+
- P2P privacy gate enforced on every swarm-join path (ADR 0032).
|
|
238
|
+
|
|
239
|
+
## [1.36.0] and earlier — summary of major features
|
|
240
|
+
|
|
241
|
+
Historical releases were not logged in this file. Major capabilities present by 1.36.x:
|
|
242
|
+
|
|
243
|
+
- Terminal-first Stremio-like client (Cinemeta, fzf, mpv), zero runtime Python deps.
|
|
244
|
+
- Multi-source streams (Torrentio + user manifests; ADR 0024).
|
|
245
|
+
- Playback backends: local TorrServer P2P, debrid, auto hybrid, native TorBox/Premiumize.
|
|
246
|
+
- Hardware-aware ranking, cast-aware ranking, dead-source denylist, duration vetting.
|
|
247
|
+
- Cast stack: castbridge + catt fallback, Tier-2 remux, realtime mirror, discovery cache.
|
|
248
|
+
- Headless `--json` API for agents/scripts; series binge overlay; resume / continue-watching.
|
|
249
|
+
- Native subtitle alignment (ADR 0020); structured stream metadata precedence (ADR 0026).
|
|
250
|
+
|
|
251
|
+
See [docs/adr/](docs/adr/README.md) for the decision timeline.
|
nstream-1.43.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
nstream is a native, terminal-first Stremio alternative: no Electron, no embedded browser,
|
|
8
|
+
no Node server. Pure Python 3.13+ stdlib (zero runtime dependencies) orchestrating external
|
|
9
|
+
CLIs (`fzf`, `mpv`, `ffprobe`, `catt`, `chafa`, optional TorrServer / castbridge) over Stremio
|
|
10
|
+
addon HTTP APIs. Entry point is `nstream.cli:_entry` (`src/nstream/cli.py`).
|
|
11
|
+
|
|
12
|
+
Pipeline: search/browse (Cinemeta + catalogs) → pick title/episode (fzf or `--json`) → optional
|
|
13
|
+
subtitles (OpenSubtitles v3 + alignment) → fetch streams (Torrentio and/or `cfg.addons`) →
|
|
14
|
+
hardware- or cast-aware rank/filter → play (mpv) or cast (castbridge/catt/remux/mirror) →
|
|
15
|
+
track progress (resume / continue-watching).
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
Tooling is `uv`-based (no venv activation needed).
|
|
20
|
+
|
|
21
|
+
| Task | Command |
|
|
22
|
+
| ---- | ------- |
|
|
23
|
+
| Run tests | `uv run python -m pytest` |
|
|
24
|
+
| Single test | `uv run python -m pytest tests/test_labels.py::test_display_title_movie` |
|
|
25
|
+
| Lint | `uvx ruff check . && uvx ruff format --check .` |
|
|
26
|
+
| Type check | `uvx ty check` |
|
|
27
|
+
| Dev run | `uv run nstream "the matrix"` |
|
|
28
|
+
| Install (dev) | `./install.sh` (uses `uv tool install`) |
|
|
29
|
+
|
|
30
|
+
Use `python -m pytest` (not bare `uv run pytest`): if a system-wide `nstream` is installed,
|
|
31
|
+
bare `pytest` resolves to the system interpreter and imports the **installed** package instead
|
|
32
|
+
of `src/`. `python -m` runs the project venv with the editable `src/` checkout.
|
|
33
|
+
|
|
34
|
+
Ruff: line-length 100, rules `E,F,I,UP,B,SIM`. Pytest: `testpaths=["tests"]`, `-q`.
|
|
35
|
+
Each `src/nstream/<mod>.py` has a matching `tests/test_<mod>.py` (or package coverage).
|
|
36
|
+
|
|
37
|
+
## Documentation map (do not re-duplicate)
|
|
38
|
+
|
|
39
|
+
| Topic | Source of truth |
|
|
40
|
+
| ----- | --------------- |
|
|
41
|
+
| Module map / domains | [`docs/architecture.md`](docs/architecture.md) |
|
|
42
|
+
| Stream ranking / cast vet | [`docs/selection.md`](docs/selection.md) |
|
|
43
|
+
| Headless `--json` contract | [`docs/headless.md`](docs/headless.md) |
|
|
44
|
+
| Config keys | [`docs/user/config.md`](docs/user/config.md) |
|
|
45
|
+
| Decisions (why) | [`docs/adr/`](docs/adr/README.md) |
|
|
46
|
+
| Hub / audience routing | [`docs/README.md`](docs/README.md) |
|
|
47
|
+
| Agent playbook (intent→cmd) | [`skills/nstream/SKILL.md`](skills/nstream/SKILL.md) |
|
|
48
|
+
| Contributor process | [`CONTRIBUTING.md`](CONTRIBUTING.md) |
|
|
49
|
+
|
|
50
|
+
Update architecture when adding/moving modules — **not** the README module list (there is none).
|
|
51
|
+
|
|
52
|
+
### Import-graph discipline (non-negotiable)
|
|
53
|
+
|
|
54
|
+
- Top: `util` / `ui` / `languages` / `types` / `sources` / `net` / `log` (see architecture)
|
|
55
|
+
- `cli` orchestrates at the bottom
|
|
56
|
+
- Everything below `cli` **never** imports `cli` (including `headless`, `cast_flow`,
|
|
57
|
+
`stream_select`, `cast_vet`, `player`, `caster`, …)
|
|
58
|
+
- Cast policy lives in `cast_vet`; cast decision tree in `cast_flow`; backends in
|
|
59
|
+
`caster` / `remux` / `mirror`; TUI cast lifecycle in `cast_control`. Selection facade:
|
|
60
|
+
`stream_select.prepare_stream`. Availability probes/denylist: `availability`. Domain
|
|
61
|
+
TypedDicts: `types` (not `config`). Headless play body: `headless_play`; argparse:
|
|
62
|
+
`cli_args`. State package: `state/` (history, cast_session, dead, breaker)
|
|
63
|
+
|
|
64
|
+
### Hard constraints when editing
|
|
65
|
+
|
|
66
|
+
- **Debrid provider-agnostic:** never special-case RealDebrid; tokens/markers/redaction are generic
|
|
67
|
+
- **Never log stream URLs or debrid tokens in clear** — use `what=` labels; `log.RedactFormatter`
|
|
68
|
+
- **Cast audio language** is decided at selection (`cast_vet.vet_cast_audio`), not on the DMR
|
|
69
|
+
- **Dead sources (ADR 0025):** only `gone` probes are persisted; incomplete reads are not proof
|
|
70
|
+
- **P2P privacy gate (ADR 0032):** at every swarm join, not one call site
|
|
71
|
+
- **Disk I/O best-effort** try/except — never block playback
|
|
72
|
+
- fzf sentinels are module-level `object()` values (`_PLAY`, `_ALL`, …) so `None` means ESC
|
|
73
|
+
- Comments/docs in English; concise, no over-engineering
|
|
74
|
+
- **`ui.Caps`** = terminal caps; **`quality.HwCaps`** = GPU/vainfo — different types
|
|
75
|
+
- New architectural trade-off → ADR ([template](docs/adr/0000-template.md)); cite symbols not lines
|
|
76
|
+
|
|
77
|
+
### Stream sources (ADR 0024)
|
|
78
|
+
|
|
79
|
+
Torrentio (`torrentio_enabled`) + `cfg.addons` manifests. `api.streams` stamps `addon`, fuses
|
|
80
|
+
debrid url with torrent infoHash by filename, collapses duplicates (cached > url > infoHash).
|
|
81
|
+
|
|
82
|
+
### Secrets & paths
|
|
83
|
+
|
|
84
|
+
- Config: `$XDG_CONFIG_HOME/nstream/config.json` (chmod 600; template `config.example.json`)
|
|
85
|
+
- History: `$XDG_STATE_HOME/nstream/history.json`
|
|
86
|
+
- Library: `$XDG_STATE_HOME/nstream/library.json` (recent + watchlist metadata only)
|
|
87
|
+
- Dead sources: `$XDG_STATE_HOME/nstream/dead-sources.json` (`--forget-dead`)
|
|
88
|
+
- Addon breakers: `$XDG_STATE_HOME/nstream/addon-breakers.json` (`--forget-breakers`, ADR 0027)
|
|
89
|
+
- Caches: `$XDG_CACHE_HOME/nstream/` (`manifests.json`, `hwcaps.json`, `devices.json`, `meta/`, `posters/`, `torrents/`)
|
|
90
|
+
- Log: `$XDG_STATE_HOME/nstream/nstream.log` (rotating); `--debug` / `NSTREAM_DEBUG=1` → stderr DEBUG
|
|
91
|
+
- Runtime deps: `mpv`, `fzf` (required); `ffmpeg`/`ffprobe`, `catt`, TorrServer, `vainfo`, `chafa`, `foot`, castbridge/mirror (optional)
|
|
92
|
+
|
|
93
|
+
## Packaging hygiene
|
|
94
|
+
|
|
95
|
+
`packaging/` tracked files: `PKGBUILD`, `build-local.sh`, `nstream.install`, `.SRCINFO` only.
|
|
96
|
+
Build artifacts (`pkg/`, `src/`, `*.pkg.tar.zst`, `*.sig`) are gitignored; `build-local.sh`
|
|
97
|
+
cleans them. Do not commit wheels or makepkg trees.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Contributing to nstream
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
./install.sh # uv tool install + optional config bootstrap
|
|
7
|
+
uv run python -m pytest # tests (always python -m — see below)
|
|
8
|
+
uvx ruff check . && uvx ruff format --check .
|
|
9
|
+
uvx ty check
|
|
10
|
+
uv run nstream "the matrix" # run from source
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Tooling is **uv**-based. Runtime stays **stdlib-only**; `ruff` / `ty` / `pytest` are
|
|
14
|
+
dev-group tools.
|
|
15
|
+
|
|
16
|
+
### Why `python -m pytest`
|
|
17
|
+
|
|
18
|
+
If a system-wide `nstream` is installed, bare `pytest` can resolve to the system interpreter
|
|
19
|
+
and import the **installed** package instead of `src/`. `uv run python -m pytest` uses the
|
|
20
|
+
project venv with the editable checkout.
|
|
21
|
+
|
|
22
|
+
## Project rules
|
|
23
|
+
|
|
24
|
+
Read [CLAUDE.md](CLAUDE.md) for agent-oriented hard constraints. Summary for humans:
|
|
25
|
+
|
|
26
|
+
- **Import graph:** `cli` at the bottom — nothing imports `cli`. Leaves: `util`, `ui`,
|
|
27
|
+
`languages`, `types`, `sources`, `net`, `log` (and related pure helpers). See
|
|
28
|
+
[docs/architecture.md](docs/architecture.md).
|
|
29
|
+
- **Debrid provider-agnostic:** no RealDebrid special-cases; tokens/markers/redaction are
|
|
30
|
+
generic.
|
|
31
|
+
- **Never log stream URLs or debrid tokens** — use `what=` labels; `log.RedactFormatter`.
|
|
32
|
+
- **Disk I/O best-effort** — never block playback on state writes.
|
|
33
|
+
- **Dead sources (ADR 0025):** only `gone` probes are persisted.
|
|
34
|
+
- **Cast audio language** decided in `cast_vet.vet_cast_audio`, not on the DMR.
|
|
35
|
+
- Comments and technical docs: **English**. TUI strings may stay Italian.
|
|
36
|
+
- Concise code; no over-engineering.
|
|
37
|
+
|
|
38
|
+
## Adding or moving a module
|
|
39
|
+
|
|
40
|
+
1. Place it where the **policy** lives (not the first caller).
|
|
41
|
+
2. Preserve import discipline.
|
|
42
|
+
3. Add `tests/test_<mod>.py` (or package tests under `tests/`).
|
|
43
|
+
4. Update **[docs/architecture.md](docs/architecture.md)** only (not README module tables).
|
|
44
|
+
5. Architectural trade-off → new ADR (below).
|
|
45
|
+
|
|
46
|
+
## Tests
|
|
47
|
+
|
|
48
|
+
The complete gate is `bash scripts/check.sh` (also run in CI on Python 3.13/3.14).
|
|
49
|
+
See [verification and recovery](docs/verification.md) for package, benchmark, and real
|
|
50
|
+
mpv checks and the separate audiovisual receiver acceptance procedure.
|
|
51
|
+
|
|
52
|
+
| Kind | Location | Notes |
|
|
53
|
+
| ---- | -------- | ----- |
|
|
54
|
+
| Unit | `tests/test_*.py` | Prefer unit before integration |
|
|
55
|
+
| Fixtures | `tests/data/`, `tests/conftest.py` | Offline stream/meta samples |
|
|
56
|
+
| Config/doc drift | `tests/test_config_docs.py` | `Config` ↔ `config.example.json` |
|
|
57
|
+
|
|
58
|
+
Mock network at `net` / `api` boundaries; do not hit live debrid in unit tests. Cast and
|
|
59
|
+
TorrServer paths should use fakes or skip when binaries are absent.
|
|
60
|
+
|
|
61
|
+
Ruff: line-length 100, rules `E,F,I,UP,B,SIM`. Pytest: `testpaths=["tests"]`, quiet by default.
|
|
62
|
+
|
|
63
|
+
## Architecture Decision Records
|
|
64
|
+
|
|
65
|
+
Format: MADR-light — see [docs/adr/0000-template.md](docs/adr/0000-template.md).
|
|
66
|
+
|
|
67
|
+
1. Copy the template → next number `NNNN-short-title.md`.
|
|
68
|
+
2. Status starts as **Proposed**; mark **Accepted** when landed in code.
|
|
69
|
+
3. **Append-only:** do not rewrite an Accepted ADR’s decision. Supersede with a new ADR and
|
|
70
|
+
set the old one to `Superseded by NNNN`.
|
|
71
|
+
4. Add a row to [docs/adr/README.md](docs/adr/README.md).
|
|
72
|
+
5. Cite **symbols** (`stream_select.prepare_stream`), not fragile line numbers.
|
|
73
|
+
6. No personal vault wikilinks (`[[…]]`) — they do not resolve in the public repo.
|
|
74
|
+
7. Empirical data may live in `docs/adr/NNNN-phase*/` (see ADR 0020).
|
|
75
|
+
|
|
76
|
+
When to write an ADR: a constraint future code must respect, a rejected alternative that
|
|
77
|
+
would otherwise be re-proposed, or a cross-module policy (cast finish predicate, privacy
|
|
78
|
+
gate, denylist rules, …).
|
|
79
|
+
|
|
80
|
+
## Packaging
|
|
81
|
+
|
|
82
|
+
Tracked under `packaging/`: `PKGBUILD`, `build-local.sh`, `nstream.install`, `.SRCINFO` only.
|
|
83
|
+
Do not commit build trees, wheels, or `*.pkg.tar.zst`. Keep `pkgver` in sync with
|
|
84
|
+
`src/nstream/__init__.py`. See [docs/roadmap.md](docs/roadmap.md) release process.
|
|
85
|
+
|
|
86
|
+
## Maintainer scripts
|
|
87
|
+
|
|
88
|
+
| Script | Role |
|
|
89
|
+
| ------ | ---- |
|
|
90
|
+
| `scripts/refresh-stream-addons.py` | Rebuild RD-backed stream addon URLs in the local config from `torrentio_base` (never prints the token). Not part of the runtime package. |
|
|
91
|
+
|
|
92
|
+
## Documentation map
|
|
93
|
+
|
|
94
|
+
[docs/README.md](docs/README.md) — audience routing and single sources of truth. Do not
|
|
95
|
+
duplicate module maps or full ranking tables in the root README.
|
nstream-1.43.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gianluca Mazza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|