weed-cli 2.4.4__tar.gz → 2.5.1__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.
Files changed (29) hide show
  1. {weed_cli-2.4.4/weed_cli.egg-info → weed_cli-2.5.1}/PKG-INFO +70 -18
  2. {weed_cli-2.4.4 → weed_cli-2.5.1}/README.md +69 -17
  3. {weed_cli-2.4.4 → weed_cli-2.5.1}/pyproject.toml +1 -1
  4. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_web_ui_api.py +47 -0
  5. {weed_cli-2.4.4 → weed_cli-2.5.1}/web_ui.py +72 -4
  6. {weed_cli-2.4.4 → weed_cli-2.5.1/weed_cli.egg-info}/PKG-INFO +70 -18
  7. {weed_cli-2.4.4 → weed_cli-2.5.1}/LICENSE +0 -0
  8. {weed_cli-2.4.4 → weed_cli-2.5.1}/dht.py +0 -0
  9. {weed_cli-2.4.4 → weed_cli-2.5.1}/discovery_relay.py +0 -0
  10. {weed_cli-2.4.4 → weed_cli-2.5.1}/lightning_settle.py +0 -0
  11. {weed_cli-2.4.4 → weed_cli-2.5.1}/node.py +0 -0
  12. {weed_cli-2.4.4 → weed_cli-2.5.1}/poc_reputation.py +0 -0
  13. {weed_cli-2.4.4 → weed_cli-2.5.1}/setup.cfg +0 -0
  14. {weed_cli-2.4.4 → weed_cli-2.5.1}/shell.py +0 -0
  15. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_dht.py +0 -0
  16. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_discovery_relay.py +0 -0
  17. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_host_live_reload.py +0 -0
  18. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_node_manifest.py +0 -0
  19. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_orbit_ws.py +0 -0
  20. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_timing_challenge.py +0 -0
  21. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/test_tunnel_failover.py +0 -0
  22. {weed_cli-2.4.4 → weed_cli-2.5.1}/tests/testutil.py +0 -0
  23. {weed_cli-2.4.4 → weed_cli-2.5.1}/tunnel_relay.py +0 -0
  24. {weed_cli-2.4.4 → weed_cli-2.5.1}/weed.py +0 -0
  25. {weed_cli-2.4.4 → weed_cli-2.5.1}/weed_cli.egg-info/SOURCES.txt +0 -0
  26. {weed_cli-2.4.4 → weed_cli-2.5.1}/weed_cli.egg-info/dependency_links.txt +0 -0
  27. {weed_cli-2.4.4 → weed_cli-2.5.1}/weed_cli.egg-info/entry_points.txt +0 -0
  28. {weed_cli-2.4.4 → weed_cli-2.5.1}/weed_cli.egg-info/requires.txt +0 -0
  29. {weed_cli-2.4.4 → weed_cli-2.5.1}/weed_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weed-cli
3
- Version: 2.4.4
3
+ Version: 2.5.1
4
4
  Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
5
5
  License: MIT
6
6
  Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
@@ -55,6 +55,8 @@ output (not simulated) — see [Status](#status) for what's verified.
55
55
  - [CLI](#cli)
56
56
  - [Interactive shell](#interactive-shell)
57
57
  - [Web UI](#web-ui)
58
+ - [Orbit Visualizer](#orbit-visualizer)
59
+ - [Orbit Visualizer plugins](#orbit-visualizer-plugins)
58
60
  - [Docker](#docker)
59
61
  - [Core mechanisms](#core-mechanisms)
60
62
  - [The integrated node](#the-integrated-node)
@@ -185,9 +187,19 @@ browser opened with that token gets the **party view** and nothing
185
187
  else — the live Orbit stream, what's playing, a vote on which
186
188
  downloaded track plays next (one vote per person per track, toggleable),
187
189
  and whatever links you set. The admin's **Party** tab holds the guest
188
- link and its QR code, the title and links, the live tally with a ▶ per
189
- track, and an "auto-play the top vote when a track ends" switch.
190
+ link and its QR code, the title and links, the live tally with a ▶ and
191
+ a ♫+ (add to playlist) per track, and an "auto-play the top vote when a
192
+ track ends" switch. The guest page itself is bare: the live picture and
193
+ "now playing" pinned to the top, the vote list scrolling beneath —
194
+ alphabetical, with an iPod-style A–Z strip down the right edge to jump
195
+ by letter, and a "leading:" line for the current front-runners.
190
196
  Guests can't host, download, like, subscribe, or read the library.
197
+ Switch on **guest chat** in the Party tab and everyone gets a message
198
+ box: the last few messages are drawn over the picture — into the
199
+ visualizer's canvas, so the stream carries them to VLC, a Roku and the
200
+ guest page's picture alike (the visualizer's 💬 toggles that), and as
201
+ an overlay on the plain player window. Messages live in memory only
202
+ (the last 200), one per second per person, 200 characters each.
191
203
 
192
204
  `--stream-plain-port` adds a plain-HTTP listener that serves *only* the
193
205
  stream endpoints (the Orbit MJPEG stream and downloaded files), for
@@ -201,17 +213,44 @@ Includes real HTTP range support (`/api/stream/<job_id>`) so a
201
213
  `<video>` tag can seek a completed download instead of downloading it
202
214
  blind.
203
215
 
204
- The visualizer's 🎹 panel binds an AKAI MPK mini (or any Web MIDI
205
- controller) to modes, transitions and sliders; **export** saves the
206
- keymap as a `.json` file and **import** loads one, so a layout travels
207
- between browsers and machines.
208
-
209
- The player's button is **video swap**: pick another download whose
210
- picture stands in for the current track's for an mp3 or a
211
- static-image video, so the visualizer (and the player window) have real
212
- footage. The audio stays with the track; the borrowed video loops on its
213
- own, follows play/pause, and the choice is remembered per track in the
214
- browser.
216
+ ### Orbit Visualizer
217
+
218
+ The player's 🌀 button opens the **Orbit Visualizer**: a canvas driven by
219
+ the track's audio (Web Audio analyser) and a low-res sample of its
220
+ picture. Eleven built-in modes — Tunnel, Bars, Mirror, Scope, Spiral,
221
+ Pixels, ASCII (six character sets, natural or neon), Plasma, Kaleido,
222
+ Particles, Freefall plus the seventeen in `web/orbit_extras.js` (next
223
+ section). Click the lit mode again for the plain video. Speed, React and
224
+ Zoom retune every mode; every setting persists in the browser.
225
+
226
+ - **Fade**: the transition between modes and between tracks — Burn,
227
+ Warp, Glitch, Pixelate, Crossfade, Wipe, the plugin ones, or Random
228
+ (the ⚄ button picks which ones Random may draw from), with a length
229
+ slider. A transition can declare its own length multiple (Win95's
230
+ crash runs at 3×).
231
+ - **Navigation**, Blender-style: scroll zooms, left-drag pans,
232
+ middle-drag rotates, shift+middle pans, ctrl+middle zooms,
233
+ double-click resets. `f` fullscreen, ←/→ cycle modes (fullscreen),
234
+ shift+1-9,0 jump to a mode, ↑/↓ ASCII brightness, `[`/`]` resolution.
235
+ - **Network stream** (📡): the canvas goes out as MJPEG on
236
+ `/api/orbit-view` for VLC (`vlc --demux=mjpeg --network-caching=300
237
+ <url>`; 🔗 copies the URL, token included), a Roku IP-camera viewer,
238
+ or the guest party page. Resolution and JPEG quality are selectable;
239
+ the ⏱ slider delays the local audio by up to 10 s to line up with a
240
+ laggy viewer. The stream keeps running while the visualizer is closed,
241
+ minimised to PIP, or in a background tab. Picture only — audio stays
242
+ wherever the browser is playing it.
243
+ - **MIDI** (🎹): an AKAI MPK mini or any Web MIDI controller
244
+ (Chrome/Edge/Firefox). Pads pick modes and fire actions, knobs turn
245
+ the sliders or sweep through modes/transitions/character sets;
246
+ endless encoders are detected on their own. Every row, plugin modes
247
+ and transitions included, has a Learn button; **export** saves the
248
+ keymap as a `.json` file and **import** loads one.
249
+ - **Video swap** (⇄ on the player): another download's picture stands
250
+ in for the current track's — for an mp3 or a static-image video, so
251
+ the visuals have real footage. Audio stays with the track; the
252
+ borrowed video loops on its own, follows play/pause, and the pairing
253
+ is remembered per track.
215
254
 
216
255
  ### Orbit Visualizer plugins
217
256
 
@@ -225,8 +264,12 @@ mode selector; a registered transition gets a Fade option and a turn in
225
264
  "Random". A plugin that throws is logged once and disabled without
226
265
  taking the draw loop down. `web/orbit_extras.js` (loaded by default) is
227
266
  built entirely on that API and adds Halftone, Lava, Terrain, Rain,
228
- Lissajous, Ripples, Cube, VHS, Win95 and Joy Division, plus the Melt, Dissolve, Iris,
229
- Shatter, Wave, Spin, Zoom blur, RGB split, VHS and Win95 transitions; `web/orbit_plugin_example.js`
267
+ Lissajous, Ripples, Cube, VHS, Win95, Joy Division, Spectrogram, Stained
268
+ glass, Fireworks, Screensaver, Slit-scan, Skyline and Globe, plus the
269
+ Melt, Dissolve, Iris, Shatter, Wave, Spin, Zoom blur, RGB split, VHS,
270
+ Win95, Blinds, Flip tiles, CRT off, Droplet, Blur, Slide and Flash
271
+ transitions (Globe's coastlines are Natural Earth's public-domain 110m
272
+ land set, simplified into `web/land.json`); `web/orbit_plugin_example.js`
230
273
  is a minimal one to copy.
231
274
 
232
275
  ### Docker
@@ -444,8 +487,13 @@ just designed:
444
487
  - Transitive trust through the subscribe graph
445
488
  - NAT traversal via relay-mediated tunneling, with TLS and heartbeat
446
489
  - Kademlia DHT discovery, survives the announcing node going offline
447
- - Local web UI with live progress, QR onboarding, and HTTP range streaming
448
- - Containerized node (`Dockerfile.node`, `docker-compose.node.yml`)
490
+ - Local web UI with live progress, QR onboarding, HTTP range streaming,
491
+ optional two-tier token auth, and the guest party page with voting
492
+ - Orbit Visualizer: 28 modes and 23 transitions behind a plugin API,
493
+ MJPEG network stream (VLC/Roku), Web MIDI control with keymap files,
494
+ video swap — 44 Playwright tests against real Chromium (`tests/e2e/`)
495
+ - Containerized node (`Dockerfile.node`, `docker-compose.node.yml`),
496
+ with `web/` bind-mounted so frontend edits need no rebuild
449
497
 
450
498
  ## Known limitations
451
499
 
@@ -484,3 +532,7 @@ mechanisms hold up:
484
532
  and votes are anonymous per browser cookie — enough to put a
485
533
  LAN-bound UI behind something and hand guests a safe subset, not a
486
534
  reason to face it at the internet.
535
+ - The Orbit stream is picture only, as MJPEG: fine for VLC, an
536
+ IP-camera app or the party page, but no audio travels with it and
537
+ there's no HLS/MP4 endpoint, so a stock Roku or smart-TV player
538
+ can't take it directly.
@@ -23,6 +23,8 @@ output (not simulated) — see [Status](#status) for what's verified.
23
23
  - [CLI](#cli)
24
24
  - [Interactive shell](#interactive-shell)
25
25
  - [Web UI](#web-ui)
26
+ - [Orbit Visualizer](#orbit-visualizer)
27
+ - [Orbit Visualizer plugins](#orbit-visualizer-plugins)
26
28
  - [Docker](#docker)
27
29
  - [Core mechanisms](#core-mechanisms)
28
30
  - [The integrated node](#the-integrated-node)
@@ -153,9 +155,19 @@ browser opened with that token gets the **party view** and nothing
153
155
  else — the live Orbit stream, what's playing, a vote on which
154
156
  downloaded track plays next (one vote per person per track, toggleable),
155
157
  and whatever links you set. The admin's **Party** tab holds the guest
156
- link and its QR code, the title and links, the live tally with a ▶ per
157
- track, and an "auto-play the top vote when a track ends" switch.
158
+ link and its QR code, the title and links, the live tally with a ▶ and
159
+ a ♫+ (add to playlist) per track, and an "auto-play the top vote when a
160
+ track ends" switch. The guest page itself is bare: the live picture and
161
+ "now playing" pinned to the top, the vote list scrolling beneath —
162
+ alphabetical, with an iPod-style A–Z strip down the right edge to jump
163
+ by letter, and a "leading:" line for the current front-runners.
158
164
  Guests can't host, download, like, subscribe, or read the library.
165
+ Switch on **guest chat** in the Party tab and everyone gets a message
166
+ box: the last few messages are drawn over the picture — into the
167
+ visualizer's canvas, so the stream carries them to VLC, a Roku and the
168
+ guest page's picture alike (the visualizer's 💬 toggles that), and as
169
+ an overlay on the plain player window. Messages live in memory only
170
+ (the last 200), one per second per person, 200 characters each.
159
171
 
160
172
  `--stream-plain-port` adds a plain-HTTP listener that serves *only* the
161
173
  stream endpoints (the Orbit MJPEG stream and downloaded files), for
@@ -169,17 +181,44 @@ Includes real HTTP range support (`/api/stream/<job_id>`) so a
169
181
  `<video>` tag can seek a completed download instead of downloading it
170
182
  blind.
171
183
 
172
- The visualizer's 🎹 panel binds an AKAI MPK mini (or any Web MIDI
173
- controller) to modes, transitions and sliders; **export** saves the
174
- keymap as a `.json` file and **import** loads one, so a layout travels
175
- between browsers and machines.
176
-
177
- The player's button is **video swap**: pick another download whose
178
- picture stands in for the current track's for an mp3 or a
179
- static-image video, so the visualizer (and the player window) have real
180
- footage. The audio stays with the track; the borrowed video loops on its
181
- own, follows play/pause, and the choice is remembered per track in the
182
- browser.
184
+ ### Orbit Visualizer
185
+
186
+ The player's 🌀 button opens the **Orbit Visualizer**: a canvas driven by
187
+ the track's audio (Web Audio analyser) and a low-res sample of its
188
+ picture. Eleven built-in modes — Tunnel, Bars, Mirror, Scope, Spiral,
189
+ Pixels, ASCII (six character sets, natural or neon), Plasma, Kaleido,
190
+ Particles, Freefall plus the seventeen in `web/orbit_extras.js` (next
191
+ section). Click the lit mode again for the plain video. Speed, React and
192
+ Zoom retune every mode; every setting persists in the browser.
193
+
194
+ - **Fade**: the transition between modes and between tracks — Burn,
195
+ Warp, Glitch, Pixelate, Crossfade, Wipe, the plugin ones, or Random
196
+ (the ⚄ button picks which ones Random may draw from), with a length
197
+ slider. A transition can declare its own length multiple (Win95's
198
+ crash runs at 3×).
199
+ - **Navigation**, Blender-style: scroll zooms, left-drag pans,
200
+ middle-drag rotates, shift+middle pans, ctrl+middle zooms,
201
+ double-click resets. `f` fullscreen, ←/→ cycle modes (fullscreen),
202
+ shift+1-9,0 jump to a mode, ↑/↓ ASCII brightness, `[`/`]` resolution.
203
+ - **Network stream** (📡): the canvas goes out as MJPEG on
204
+ `/api/orbit-view` for VLC (`vlc --demux=mjpeg --network-caching=300
205
+ <url>`; 🔗 copies the URL, token included), a Roku IP-camera viewer,
206
+ or the guest party page. Resolution and JPEG quality are selectable;
207
+ the ⏱ slider delays the local audio by up to 10 s to line up with a
208
+ laggy viewer. The stream keeps running while the visualizer is closed,
209
+ minimised to PIP, or in a background tab. Picture only — audio stays
210
+ wherever the browser is playing it.
211
+ - **MIDI** (🎹): an AKAI MPK mini or any Web MIDI controller
212
+ (Chrome/Edge/Firefox). Pads pick modes and fire actions, knobs turn
213
+ the sliders or sweep through modes/transitions/character sets;
214
+ endless encoders are detected on their own. Every row, plugin modes
215
+ and transitions included, has a Learn button; **export** saves the
216
+ keymap as a `.json` file and **import** loads one.
217
+ - **Video swap** (⇄ on the player): another download's picture stands
218
+ in for the current track's — for an mp3 or a static-image video, so
219
+ the visuals have real footage. Audio stays with the track; the
220
+ borrowed video loops on its own, follows play/pause, and the pairing
221
+ is remembered per track.
183
222
 
184
223
  ### Orbit Visualizer plugins
185
224
 
@@ -193,8 +232,12 @@ mode selector; a registered transition gets a Fade option and a turn in
193
232
  "Random". A plugin that throws is logged once and disabled without
194
233
  taking the draw loop down. `web/orbit_extras.js` (loaded by default) is
195
234
  built entirely on that API and adds Halftone, Lava, Terrain, Rain,
196
- Lissajous, Ripples, Cube, VHS, Win95 and Joy Division, plus the Melt, Dissolve, Iris,
197
- Shatter, Wave, Spin, Zoom blur, RGB split, VHS and Win95 transitions; `web/orbit_plugin_example.js`
235
+ Lissajous, Ripples, Cube, VHS, Win95, Joy Division, Spectrogram, Stained
236
+ glass, Fireworks, Screensaver, Slit-scan, Skyline and Globe, plus the
237
+ Melt, Dissolve, Iris, Shatter, Wave, Spin, Zoom blur, RGB split, VHS,
238
+ Win95, Blinds, Flip tiles, CRT off, Droplet, Blur, Slide and Flash
239
+ transitions (Globe's coastlines are Natural Earth's public-domain 110m
240
+ land set, simplified into `web/land.json`); `web/orbit_plugin_example.js`
198
241
  is a minimal one to copy.
199
242
 
200
243
  ### Docker
@@ -412,8 +455,13 @@ just designed:
412
455
  - Transitive trust through the subscribe graph
413
456
  - NAT traversal via relay-mediated tunneling, with TLS and heartbeat
414
457
  - Kademlia DHT discovery, survives the announcing node going offline
415
- - Local web UI with live progress, QR onboarding, and HTTP range streaming
416
- - Containerized node (`Dockerfile.node`, `docker-compose.node.yml`)
458
+ - Local web UI with live progress, QR onboarding, HTTP range streaming,
459
+ optional two-tier token auth, and the guest party page with voting
460
+ - Orbit Visualizer: 28 modes and 23 transitions behind a plugin API,
461
+ MJPEG network stream (VLC/Roku), Web MIDI control with keymap files,
462
+ video swap — 44 Playwright tests against real Chromium (`tests/e2e/`)
463
+ - Containerized node (`Dockerfile.node`, `docker-compose.node.yml`),
464
+ with `web/` bind-mounted so frontend edits need no rebuild
417
465
 
418
466
  ## Known limitations
419
467
 
@@ -452,3 +500,7 @@ mechanisms hold up:
452
500
  and votes are anonymous per browser cookie — enough to put a
453
501
  LAN-bound UI behind something and hand guests a safe subset, not a
454
502
  reason to face it at the internet.
503
+ - The Orbit stream is picture only, as MJPEG: fine for VLC, an
504
+ IP-camera app or the party page, but no audio travels with it and
505
+ there's no HLS/MP4 endpoint, so a stock Roku or smart-TV player
506
+ can't take it directly.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weed-cli"
7
- version = "2.4.4"
7
+ version = "2.5.1"
8
8
  description = "Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -660,3 +660,50 @@ def test_request_with_no_origin_header_is_allowed(web_server):
660
660
  through (see web_ui.py's _check_origin docstring)."""
661
661
  status, resp = http_post_json(f'{web_server}/api/like', {'content_hash': 'd' * 64})
662
662
  assert status == 200
663
+
664
+
665
+ def test_party_chat_is_gated_rate_limited_and_polled_by_id(web_server, monkeypatch):
666
+ """The party chat: off until the admin turns it on, one message a
667
+ second per person, 200 characters, guests and admin both post, the
668
+ ring is polled by id, and only the admin can wipe it."""
669
+ import web_ui
670
+ monkeypatch.setattr(web_ui, 'AUTH_TOKEN', 'admin-secret')
671
+ monkeypatch.setattr(web_ui, 'STREAM_TOKEN', 'guest-secret')
672
+ monkeypatch.setattr(web_ui, 'CHAT_MIN_INTERVAL', 0.3)
673
+ with web_ui._chat_lock:
674
+ web_ui._chat_messages.clear(); web_ui._chat_last_post.clear()
675
+ guest = {'Authorization': 'Bearer guest-secret'}
676
+ admin = {'Authorization': 'Bearer admin-secret'}
677
+
678
+ # off by default: readable, but nobody can post
679
+ status, headers, body = _raw_get(f'{web_server}/api/chat', guest)
680
+ assert status == 200 and json.loads(body) == {'enabled': False, 'messages': [], 'you': json.loads(body)['you']}
681
+ assert json.loads(body)['you'].startswith('guest-')
682
+ assert _raw_post(f'{web_server}/api/chat', {'text': 'hi'}, guest)[0] == 403
683
+ assert _raw_post(f'{web_server}/api/chat', {'text': 'hi'}, admin)[0] == 403
684
+ assert _raw_post(f'{web_server}/api/chat', {'text': 'hi'})[0] == 401 # no token at all
685
+
686
+ status, _, r = _raw_post(f'{web_server}/api/party/config', {'chat': True}, admin)
687
+ assert status == 200 and r['party']['chat'] is True
688
+ assert json.loads(_raw_get(f'{web_server}/api/party', guest)[2])['chat'] is True
689
+
690
+ voter = {**guest, 'Cookie': headers['set-cookie'].split(';')[0]}
691
+ status, _, r = _raw_post(f'{web_server}/api/chat', {'text': ' turn it up ', 'name': 'dave'}, voter)
692
+ assert status == 200 and r['message']['name'] == 'dave' and r['message']['text'] == 'turn it up' and r['message']['role'] == 'guest'
693
+ first_id = r['message']['id']
694
+ assert _raw_post(f'{web_server}/api/chat', {'text': 'again'}, voter)[0] == 429 # too fast
695
+ assert _raw_post(f'{web_server}/api/chat', {'text': ' '}, admin)[0] == 400
696
+ assert _raw_post(f'{web_server}/api/chat', {'text': 'x' * 201}, admin)[0] == 400
697
+ status, _, r = _raw_post(f'{web_server}/api/chat', {'text': 'no'}, admin)
698
+ assert status == 200 and r['message']['name'] == 'host' and r['message']['role'] == 'admin'
699
+
700
+ _, _, body = _raw_get(f'{web_server}/api/chat', voter)
701
+ msgs = json.loads(body)['messages']
702
+ assert [m['text'] for m in msgs] == ['turn it up', 'no']
703
+ _, _, body = _raw_get(f'{web_server}/api/chat?since={first_id}', voter)
704
+ assert [m['text'] for m in json.loads(body)['messages']] == ['no']
705
+
706
+ assert _raw_post(f'{web_server}/api/chat/clear', {}, voter)[0] == 401
707
+ status, _, r = _raw_post(f'{web_server}/api/chat/clear', {}, admin)
708
+ assert status == 200 and r['cleared'] == 2
709
+ assert json.loads(_raw_get(f'{web_server}/api/chat', voter)[2])['messages'] == []
@@ -77,13 +77,24 @@ AUTH_COOKIE_MAX_AGE = 30 * 24 * 3600
77
77
  # track rather than one per click; nothing else is keyed on it
78
78
  GUEST_COOKIE = 'weed_guest'
79
79
  _party_votes = {} # content_hash -> set of guest ids (in memory; a restart clears the tally)
80
+ # the party chat: a short in-memory ring of {id, name, text, ts, role}.
81
+ # Guests and the admin post; the visualizer draws the last few over the
82
+ # picture (so the stream carries them) and the guest page lists them.
83
+ # Off unless the admin turns it on in the Party tab (party['chat']).
84
+ CHAT_KEEP = 200
85
+ CHAT_MAX_TEXT = 200
86
+ CHAT_MIN_INTERVAL = 1.0 # seconds between posts, per person
87
+ _chat_messages = []
88
+ _chat_seq = 0
89
+ _chat_last_post = {} # guest id (or 'admin') -> last post time
90
+ _chat_lock = threading.Lock()
80
91
 
81
92
 
82
93
  def _party_settings():
83
94
  """Caller must hold _lock. The library's 'party' dict, created on
84
95
  demand -- a library written (or a test fixture built) before this
85
96
  key existed simply doesn't have it."""
86
- return _library.setdefault('party', {'title': '', 'links': [], 'autoplay': False})
97
+ return _library.setdefault('party', {'title': '', 'links': [], 'autoplay': False, 'chat': False})
87
98
  _orbit_ws_since = 0.0 # when the current stream started -- the party page keys its <img> on it
88
99
  # Optional second listener, plain HTTP, serving *only* the stream
89
100
  # endpoints (/api/orbit-view, /api/orbit-stream, /api/stream/<job>).
@@ -863,7 +874,7 @@ class Handler(BaseHTTPRequestHandler):
863
874
 
864
875
  # GET endpoints a guest (stream token) may use; everything else under
865
876
  # /api/ is admin-only. The stream endpoints also take ?token=.
866
- _GUEST_GET = ('/api/orbit-view', '/api/orbit-stream', '/api/config', '/api/party')
877
+ _GUEST_GET = ('/api/orbit-view', '/api/orbit-stream', '/api/config', '/api/party', '/api/chat')
867
878
 
868
879
  def do_GET(self):
869
880
  parsed = urlparse(self.path)
@@ -910,6 +921,8 @@ class Handler(BaseHTTPRequestHandler):
910
921
  return self._json(cfg)
911
922
  if path == '/api/party':
912
923
  return self._handle_party()
924
+ if path == '/api/chat':
925
+ return self._handle_chat_get(qs)
913
926
  if path == '/api/discover':
914
927
  results = node.group_discover_by_content(node.discover(qs.get('relay') or [DEFAULT_RELAY]))
915
928
  return self._json({'results': results})
@@ -998,7 +1011,7 @@ class Handler(BaseHTTPRequestHandler):
998
1011
  # /api/login is how a browser *gets* the cookie (the unlock
999
1012
  # prompt); voting is the one thing a guest may POST; everything
1000
1013
  # else needs the admin token already
1001
- if path == '/api/party/vote':
1014
+ if path in ('/api/party/vote', '/api/chat'):
1002
1015
  if not self._guest_ok():
1003
1016
  return self._deny()
1004
1017
  elif path != '/api/login' and not self._authorized():
@@ -1036,6 +1049,7 @@ class Handler(BaseHTTPRequestHandler):
1036
1049
  handlers = {
1037
1050
  '/api/party/vote': self._handle_party_vote, '/api/party/config': self._handle_party_config,
1038
1051
  '/api/party/played': self._handle_party_played,
1052
+ '/api/chat': self._handle_chat_post, '/api/chat/clear': self._handle_chat_clear,
1039
1053
  '/api/host': self._handle_host, '/api/host/forget': self._handle_forget_host,
1040
1054
  '/api/download': self._handle_download,
1041
1055
  '/api/like': self._handle_like, '/api/subscribe': self._handle_subscribe,
@@ -1339,7 +1353,7 @@ class Handler(BaseHTTPRequestHandler):
1339
1353
  plain = self._stream_plain_base()
1340
1354
  self._json({
1341
1355
  'role': self._role(), 'title': party.get('title') or '', 'links': party.get('links') or [],
1342
- 'autoplay': bool(party.get('autoplay')),
1356
+ 'autoplay': bool(party.get('autoplay')), 'chat': bool(party.get('chat')),
1343
1357
  'now_playing': now_playing,
1344
1358
  'stream': {
1345
1359
  'active': active, 'since': _orbit_ws_since,
@@ -1386,9 +1400,63 @@ class Handler(BaseHTTPRequestHandler):
1386
1400
  party['links'] = links[:20]
1387
1401
  if 'autoplay' in body:
1388
1402
  party['autoplay'] = bool(body.get('autoplay'))
1403
+ if 'chat' in body:
1404
+ party['chat'] = bool(body.get('chat'))
1389
1405
  _save_library()
1390
1406
  self._json({'ok': True, 'party': dict(party)})
1391
1407
 
1408
+ # ── party chat ──────────────────────────────────────────────────
1409
+ def _handle_chat_get(self, qs):
1410
+ """GET /api/chat?since=<id>: whether chat is on, plus every message
1411
+ newer than <since> (all of the ring when omitted)."""
1412
+ gid, extra = self._ensure_guest_id()
1413
+ try:
1414
+ since = int((qs.get('since') or ['0'])[0])
1415
+ except ValueError:
1416
+ since = 0
1417
+ with _lock:
1418
+ enabled = bool(_party_settings().get('chat'))
1419
+ with _chat_lock:
1420
+ msgs = [m for m in _chat_messages if m['id'] > since]
1421
+ self._json({'enabled': enabled, 'messages': msgs, 'you': 'host' if self._role() == 'admin' else 'guest-' + gid[:4]},
1422
+ headers=extra)
1423
+
1424
+ def _handle_chat_post(self, body):
1425
+ """POST /api/chat {text, name?}: one message. Refused while chat is
1426
+ off, over-long, empty, or faster than one a second per person."""
1427
+ global _chat_seq
1428
+ with _lock:
1429
+ enabled = bool(_party_settings().get('chat'))
1430
+ if not enabled:
1431
+ return self._json({'error': 'chat is off'}, status=403)
1432
+ gid, extra = self._ensure_guest_id()
1433
+ role = self._role()
1434
+ who = 'admin' if role == 'admin' else gid
1435
+ text = ' '.join(str(body.get('text') or '').split())
1436
+ if not text:
1437
+ return self._json({'error': 'empty message'}, status=400)
1438
+ if len(text) > CHAT_MAX_TEXT:
1439
+ return self._json({'error': f'message too long (max {CHAT_MAX_TEXT} characters)'}, status=400)
1440
+ name = ' '.join(str(body.get('name') or '').split())[:24] or ('host' if role == 'admin' else 'guest-' + gid[:4])
1441
+ now = time.time()
1442
+ with _chat_lock:
1443
+ last = _chat_last_post.get(who, 0)
1444
+ if now - last < CHAT_MIN_INTERVAL:
1445
+ return self._json({'error': 'slow down'}, status=429)
1446
+ _chat_last_post[who] = now
1447
+ _chat_seq += 1
1448
+ msg = {'id': _chat_seq, 'name': name, 'text': text, 'ts': now, 'role': role or 'admin'}
1449
+ _chat_messages.append(msg)
1450
+ del _chat_messages[:-CHAT_KEEP]
1451
+ self._json({'ok': True, 'message': msg}, headers=extra)
1452
+
1453
+ def _handle_chat_clear(self, body):
1454
+ """Admin: wipe the ring."""
1455
+ with _chat_lock:
1456
+ n = len(_chat_messages)
1457
+ _chat_messages.clear()
1458
+ self._json({'ok': True, 'cleared': n})
1459
+
1392
1460
  def _handle_party_played(self, body):
1393
1461
  """Admin: a voted track got played -- clear its tally so the next
1394
1462
  vote starts fresh."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weed-cli
3
- Version: 2.4.4
3
+ Version: 2.5.1
4
4
  Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
5
5
  License: MIT
6
6
  Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
@@ -55,6 +55,8 @@ output (not simulated) — see [Status](#status) for what's verified.
55
55
  - [CLI](#cli)
56
56
  - [Interactive shell](#interactive-shell)
57
57
  - [Web UI](#web-ui)
58
+ - [Orbit Visualizer](#orbit-visualizer)
59
+ - [Orbit Visualizer plugins](#orbit-visualizer-plugins)
58
60
  - [Docker](#docker)
59
61
  - [Core mechanisms](#core-mechanisms)
60
62
  - [The integrated node](#the-integrated-node)
@@ -185,9 +187,19 @@ browser opened with that token gets the **party view** and nothing
185
187
  else — the live Orbit stream, what's playing, a vote on which
186
188
  downloaded track plays next (one vote per person per track, toggleable),
187
189
  and whatever links you set. The admin's **Party** tab holds the guest
188
- link and its QR code, the title and links, the live tally with a ▶ per
189
- track, and an "auto-play the top vote when a track ends" switch.
190
+ link and its QR code, the title and links, the live tally with a ▶ and
191
+ a ♫+ (add to playlist) per track, and an "auto-play the top vote when a
192
+ track ends" switch. The guest page itself is bare: the live picture and
193
+ "now playing" pinned to the top, the vote list scrolling beneath —
194
+ alphabetical, with an iPod-style A–Z strip down the right edge to jump
195
+ by letter, and a "leading:" line for the current front-runners.
190
196
  Guests can't host, download, like, subscribe, or read the library.
197
+ Switch on **guest chat** in the Party tab and everyone gets a message
198
+ box: the last few messages are drawn over the picture — into the
199
+ visualizer's canvas, so the stream carries them to VLC, a Roku and the
200
+ guest page's picture alike (the visualizer's 💬 toggles that), and as
201
+ an overlay on the plain player window. Messages live in memory only
202
+ (the last 200), one per second per person, 200 characters each.
191
203
 
192
204
  `--stream-plain-port` adds a plain-HTTP listener that serves *only* the
193
205
  stream endpoints (the Orbit MJPEG stream and downloaded files), for
@@ -201,17 +213,44 @@ Includes real HTTP range support (`/api/stream/<job_id>`) so a
201
213
  `<video>` tag can seek a completed download instead of downloading it
202
214
  blind.
203
215
 
204
- The visualizer's 🎹 panel binds an AKAI MPK mini (or any Web MIDI
205
- controller) to modes, transitions and sliders; **export** saves the
206
- keymap as a `.json` file and **import** loads one, so a layout travels
207
- between browsers and machines.
208
-
209
- The player's button is **video swap**: pick another download whose
210
- picture stands in for the current track's for an mp3 or a
211
- static-image video, so the visualizer (and the player window) have real
212
- footage. The audio stays with the track; the borrowed video loops on its
213
- own, follows play/pause, and the choice is remembered per track in the
214
- browser.
216
+ ### Orbit Visualizer
217
+
218
+ The player's 🌀 button opens the **Orbit Visualizer**: a canvas driven by
219
+ the track's audio (Web Audio analyser) and a low-res sample of its
220
+ picture. Eleven built-in modes — Tunnel, Bars, Mirror, Scope, Spiral,
221
+ Pixels, ASCII (six character sets, natural or neon), Plasma, Kaleido,
222
+ Particles, Freefall plus the seventeen in `web/orbit_extras.js` (next
223
+ section). Click the lit mode again for the plain video. Speed, React and
224
+ Zoom retune every mode; every setting persists in the browser.
225
+
226
+ - **Fade**: the transition between modes and between tracks — Burn,
227
+ Warp, Glitch, Pixelate, Crossfade, Wipe, the plugin ones, or Random
228
+ (the ⚄ button picks which ones Random may draw from), with a length
229
+ slider. A transition can declare its own length multiple (Win95's
230
+ crash runs at 3×).
231
+ - **Navigation**, Blender-style: scroll zooms, left-drag pans,
232
+ middle-drag rotates, shift+middle pans, ctrl+middle zooms,
233
+ double-click resets. `f` fullscreen, ←/→ cycle modes (fullscreen),
234
+ shift+1-9,0 jump to a mode, ↑/↓ ASCII brightness, `[`/`]` resolution.
235
+ - **Network stream** (📡): the canvas goes out as MJPEG on
236
+ `/api/orbit-view` for VLC (`vlc --demux=mjpeg --network-caching=300
237
+ <url>`; 🔗 copies the URL, token included), a Roku IP-camera viewer,
238
+ or the guest party page. Resolution and JPEG quality are selectable;
239
+ the ⏱ slider delays the local audio by up to 10 s to line up with a
240
+ laggy viewer. The stream keeps running while the visualizer is closed,
241
+ minimised to PIP, or in a background tab. Picture only — audio stays
242
+ wherever the browser is playing it.
243
+ - **MIDI** (🎹): an AKAI MPK mini or any Web MIDI controller
244
+ (Chrome/Edge/Firefox). Pads pick modes and fire actions, knobs turn
245
+ the sliders or sweep through modes/transitions/character sets;
246
+ endless encoders are detected on their own. Every row, plugin modes
247
+ and transitions included, has a Learn button; **export** saves the
248
+ keymap as a `.json` file and **import** loads one.
249
+ - **Video swap** (⇄ on the player): another download's picture stands
250
+ in for the current track's — for an mp3 or a static-image video, so
251
+ the visuals have real footage. Audio stays with the track; the
252
+ borrowed video loops on its own, follows play/pause, and the pairing
253
+ is remembered per track.
215
254
 
216
255
  ### Orbit Visualizer plugins
217
256
 
@@ -225,8 +264,12 @@ mode selector; a registered transition gets a Fade option and a turn in
225
264
  "Random". A plugin that throws is logged once and disabled without
226
265
  taking the draw loop down. `web/orbit_extras.js` (loaded by default) is
227
266
  built entirely on that API and adds Halftone, Lava, Terrain, Rain,
228
- Lissajous, Ripples, Cube, VHS, Win95 and Joy Division, plus the Melt, Dissolve, Iris,
229
- Shatter, Wave, Spin, Zoom blur, RGB split, VHS and Win95 transitions; `web/orbit_plugin_example.js`
267
+ Lissajous, Ripples, Cube, VHS, Win95, Joy Division, Spectrogram, Stained
268
+ glass, Fireworks, Screensaver, Slit-scan, Skyline and Globe, plus the
269
+ Melt, Dissolve, Iris, Shatter, Wave, Spin, Zoom blur, RGB split, VHS,
270
+ Win95, Blinds, Flip tiles, CRT off, Droplet, Blur, Slide and Flash
271
+ transitions (Globe's coastlines are Natural Earth's public-domain 110m
272
+ land set, simplified into `web/land.json`); `web/orbit_plugin_example.js`
230
273
  is a minimal one to copy.
231
274
 
232
275
  ### Docker
@@ -444,8 +487,13 @@ just designed:
444
487
  - Transitive trust through the subscribe graph
445
488
  - NAT traversal via relay-mediated tunneling, with TLS and heartbeat
446
489
  - Kademlia DHT discovery, survives the announcing node going offline
447
- - Local web UI with live progress, QR onboarding, and HTTP range streaming
448
- - Containerized node (`Dockerfile.node`, `docker-compose.node.yml`)
490
+ - Local web UI with live progress, QR onboarding, HTTP range streaming,
491
+ optional two-tier token auth, and the guest party page with voting
492
+ - Orbit Visualizer: 28 modes and 23 transitions behind a plugin API,
493
+ MJPEG network stream (VLC/Roku), Web MIDI control with keymap files,
494
+ video swap — 44 Playwright tests against real Chromium (`tests/e2e/`)
495
+ - Containerized node (`Dockerfile.node`, `docker-compose.node.yml`),
496
+ with `web/` bind-mounted so frontend edits need no rebuild
449
497
 
450
498
  ## Known limitations
451
499
 
@@ -484,3 +532,7 @@ mechanisms hold up:
484
532
  and votes are anonymous per browser cookie — enough to put a
485
533
  LAN-bound UI behind something and hand guests a safe subset, not a
486
534
  reason to face it at the internet.
535
+ - The Orbit stream is picture only, as MJPEG: fine for VLC, an
536
+ IP-camera app or the party page, but no audio travels with it and
537
+ there's no HLS/MP4 endpoint, so a stock Roku or smart-TV player
538
+ can't take it directly.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes