weed-cli 1.6.7__tar.gz → 1.6.8__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.
- {weed_cli-1.6.7/weed_cli.egg-info → weed_cli-1.6.8}/PKG-INFO +1 -1
- {weed_cli-1.6.7 → weed_cli-1.6.8}/pyproject.toml +1 -1
- {weed_cli-1.6.7 → weed_cli-1.6.8}/shell.py +19 -1
- {weed_cli-1.6.7 → weed_cli-1.6.8}/web_ui.py +104 -2
- {weed_cli-1.6.7 → weed_cli-1.6.8}/weed.py +13 -1
- {weed_cli-1.6.7 → weed_cli-1.6.8/weed_cli.egg-info}/PKG-INFO +1 -1
- {weed_cli-1.6.7 → weed_cli-1.6.8}/LICENSE +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/README.md +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/dht.py +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/discovery_relay.py +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/lightning_settle.py +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/node.py +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/poc_reputation.py +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/setup.cfg +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/tunnel_relay.py +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-1.6.7 → weed_cli-1.6.8}/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: 1.6.
|
|
3
|
+
Version: 1.6.8
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weed-cli"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.8"
|
|
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" }
|
|
@@ -179,6 +179,23 @@ class WeedShell(cmd.Cmd):
|
|
|
179
179
|
# and only fail later, in the background server thread (onecmd's
|
|
180
180
|
# exception net doesn't reach into background threads)
|
|
181
181
|
all_leaves = {e['sha256']: node.load_leaves(archive_dir, e['sha256']) for e in entries}
|
|
182
|
+
# Same reasoning as web_ui.py's _run_host_job: bind the real,
|
|
183
|
+
# permanent listening socket now, before announcing anything to a
|
|
184
|
+
# relay, and hand this exact socket to run_host_server below
|
|
185
|
+
# instead of it binding a second one later. Without this, a port
|
|
186
|
+
# already in use (another `host` still running from earlier in
|
|
187
|
+
# this same session, or a persisted web-UI host on the same
|
|
188
|
+
# default port) only fails once the background thread started
|
|
189
|
+
# below gets around to its own bind — by then `announced ... on
|
|
190
|
+
# relay: {'ok': True, ...}` has already printed, and the relay
|
|
191
|
+
# has a listing for a host that was never actually reachable, with
|
|
192
|
+
# nothing pointing back at why until a downloader's own possession
|
|
193
|
+
# challenge mysteriously fails against it.
|
|
194
|
+
try:
|
|
195
|
+
bound_sock = node.bind_host_port(port)
|
|
196
|
+
except OSError as e:
|
|
197
|
+
print(f' ✗ cannot bind port {port}: {e} — not announcing, nothing started')
|
|
198
|
+
return
|
|
182
199
|
if relay and not no_announce:
|
|
183
200
|
ott_status = node.ott_commit_status(archive_dir)
|
|
184
201
|
for entry in entries:
|
|
@@ -204,7 +221,8 @@ class WeedShell(cmd.Cmd):
|
|
|
204
221
|
self._host_threads.append(tt)
|
|
205
222
|
t = threading.Thread(target=_bg,
|
|
206
223
|
args=(node.run_host_server, archive_dir, file_name, port),
|
|
207
|
-
kwargs={'quiet': True, 'price': price, 'ln_node': ln_node},
|
|
224
|
+
kwargs={'quiet': True, 'price': price, 'ln_node': ln_node, 'sock': bound_sock},
|
|
225
|
+
daemon=True)
|
|
208
226
|
t.start()
|
|
209
227
|
self._host_threads.append(t)
|
|
210
228
|
price_note = f', {price} sat/download' if price else ', free'
|
|
@@ -125,8 +125,14 @@ _lock = threading.Lock()
|
|
|
125
125
|
# reload -- or a server restart -- doesn't forget any of it. Downloads
|
|
126
126
|
# are keyed by content_hash (one record per piece of content, most
|
|
127
127
|
# recent job wins); likes/subscriptions are just lists of hashes/pubkeys.
|
|
128
|
-
#
|
|
129
|
-
|
|
128
|
+
# playlists are purely local organization -- unlike likes/subscriptions
|
|
129
|
+
# they're never signed or gossiped to a relay, nobody else has any
|
|
130
|
+
# business seeing how you've grouped your own downloads -- a list of
|
|
131
|
+
# {id, name, items: [{content_hash, title, signer_pubkey}, ...]}, id
|
|
132
|
+
# stable across renames so the UI can keep pointing at the same playlist
|
|
133
|
+
# while its name changes underneath it. All access goes through _lock,
|
|
134
|
+
# same as _hosts/_jobs.
|
|
135
|
+
_library = {'downloads': {}, 'likes': [], 'subscriptions': [], 'playlists': []}
|
|
130
136
|
|
|
131
137
|
# what to (re-)host on startup -- _hosts above is pure in-memory runtime
|
|
132
138
|
# state, so a restart (a fresh `make node`, a Docker container recreated
|
|
@@ -245,6 +251,7 @@ def _load_library():
|
|
|
245
251
|
'downloads': data.get('downloads') or {},
|
|
246
252
|
'likes': data.get('likes') or [],
|
|
247
253
|
'subscriptions': data.get('subscriptions') or [],
|
|
254
|
+
'playlists': data.get('playlists') or [],
|
|
248
255
|
}
|
|
249
256
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
250
257
|
pass
|
|
@@ -492,6 +499,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
492
499
|
'downloads': list(_library['downloads'].values()),
|
|
493
500
|
'likes': list(_library['likes']),
|
|
494
501
|
'subscriptions': list(_library['subscriptions']),
|
|
502
|
+
'playlists': list(_library['playlists']),
|
|
495
503
|
})
|
|
496
504
|
if path.startswith('/api/download/'):
|
|
497
505
|
job_id = path[len('/api/download/'):]
|
|
@@ -552,6 +560,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
552
560
|
'/api/host': self._handle_host, '/api/download': self._handle_download,
|
|
553
561
|
'/api/like': self._handle_like, '/api/subscribe': self._handle_subscribe,
|
|
554
562
|
'/api/verify': self._handle_verify,
|
|
563
|
+
'/api/playlists/create': self._handle_playlist_create,
|
|
564
|
+
'/api/playlists/rename': self._handle_playlist_rename,
|
|
565
|
+
'/api/playlists/delete': self._handle_playlist_delete,
|
|
566
|
+
'/api/playlists/add': self._handle_playlist_add,
|
|
567
|
+
'/api/playlists/remove': self._handle_playlist_remove,
|
|
568
|
+
'/api/playlists/move': self._handle_playlist_move,
|
|
555
569
|
}
|
|
556
570
|
handler = handlers.get(path)
|
|
557
571
|
if not handler:
|
|
@@ -630,6 +644,94 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
630
644
|
_save_library()
|
|
631
645
|
self._json({'result': result})
|
|
632
646
|
|
|
647
|
+
def _handle_playlist_create(self, body):
|
|
648
|
+
name = (body.get('name') or '').strip()
|
|
649
|
+
if not name:
|
|
650
|
+
return self._json({'error': 'name required'}, status=400)
|
|
651
|
+
playlist = {'id': uuid.uuid4().hex[:12], 'name': name, 'items': []}
|
|
652
|
+
with _lock:
|
|
653
|
+
_library['playlists'].append(playlist)
|
|
654
|
+
_save_library()
|
|
655
|
+
self._json({'playlist': playlist})
|
|
656
|
+
|
|
657
|
+
def _handle_playlist_rename(self, body):
|
|
658
|
+
playlist_id = body.get('playlist_id')
|
|
659
|
+
name = (body.get('name') or '').strip()
|
|
660
|
+
if not playlist_id or not name:
|
|
661
|
+
return self._json({'error': 'playlist_id and name required'}, status=400)
|
|
662
|
+
with _lock:
|
|
663
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
664
|
+
if not playlist:
|
|
665
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
666
|
+
playlist['name'] = name
|
|
667
|
+
_save_library()
|
|
668
|
+
self._json({'playlist': playlist})
|
|
669
|
+
|
|
670
|
+
def _handle_playlist_delete(self, body):
|
|
671
|
+
playlist_id = body.get('playlist_id')
|
|
672
|
+
if not playlist_id:
|
|
673
|
+
return self._json({'error': 'playlist_id required'}, status=400)
|
|
674
|
+
with _lock:
|
|
675
|
+
_library['playlists'] = [p for p in _library['playlists'] if p['id'] != playlist_id]
|
|
676
|
+
_save_library()
|
|
677
|
+
self._json({'ok': True})
|
|
678
|
+
|
|
679
|
+
def _handle_playlist_add(self, body):
|
|
680
|
+
playlist_id = body.get('playlist_id')
|
|
681
|
+
content_hash = body.get('content_hash')
|
|
682
|
+
if not playlist_id or not content_hash:
|
|
683
|
+
return self._json({'error': 'playlist_id and content_hash required'}, status=400)
|
|
684
|
+
with _lock:
|
|
685
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
686
|
+
if not playlist:
|
|
687
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
688
|
+
# re-adding something already in the playlist just moves it to
|
|
689
|
+
# this spot instead of piling up a duplicate entry -- same
|
|
690
|
+
# dedup-by-content_hash instinct _library['downloads'] already
|
|
691
|
+
# has, just for a list instead of a dict
|
|
692
|
+
playlist['items'] = [it for it in playlist['items'] if it['content_hash'] != content_hash]
|
|
693
|
+
playlist['items'].append({
|
|
694
|
+
'content_hash': content_hash,
|
|
695
|
+
'title': body.get('title') or None,
|
|
696
|
+
'signer_pubkey': body.get('signer_pubkey') or None,
|
|
697
|
+
})
|
|
698
|
+
_save_library()
|
|
699
|
+
self._json({'playlist': playlist})
|
|
700
|
+
|
|
701
|
+
def _handle_playlist_remove(self, body):
|
|
702
|
+
playlist_id = body.get('playlist_id')
|
|
703
|
+
content_hash = body.get('content_hash')
|
|
704
|
+
if not playlist_id or not content_hash:
|
|
705
|
+
return self._json({'error': 'playlist_id and content_hash required'}, status=400)
|
|
706
|
+
with _lock:
|
|
707
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
708
|
+
if not playlist:
|
|
709
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
710
|
+
playlist['items'] = [it for it in playlist['items'] if it['content_hash'] != content_hash]
|
|
711
|
+
_save_library()
|
|
712
|
+
self._json({'playlist': playlist})
|
|
713
|
+
|
|
714
|
+
def _handle_playlist_move(self, body):
|
|
715
|
+
playlist_id = body.get('playlist_id')
|
|
716
|
+
content_hash = body.get('content_hash')
|
|
717
|
+
direction = body.get('direction')
|
|
718
|
+
if not playlist_id or not content_hash or direction not in ('up', 'down'):
|
|
719
|
+
return self._json({'error': "playlist_id, content_hash, and direction ('up'|'down') required"},
|
|
720
|
+
status=400)
|
|
721
|
+
with _lock:
|
|
722
|
+
playlist = next((p for p in _library['playlists'] if p['id'] == playlist_id), None)
|
|
723
|
+
if not playlist:
|
|
724
|
+
return self._json({'error': 'no such playlist'}, status=404)
|
|
725
|
+
items = playlist['items']
|
|
726
|
+
idx = next((i for i, it in enumerate(items) if it['content_hash'] == content_hash), None)
|
|
727
|
+
if idx is None:
|
|
728
|
+
return self._json({'error': 'content_hash not in this playlist'}, status=404)
|
|
729
|
+
swap_with = idx - 1 if direction == 'up' else idx + 1
|
|
730
|
+
if 0 <= swap_with < len(items):
|
|
731
|
+
items[idx], items[swap_with] = items[swap_with], items[idx]
|
|
732
|
+
_save_library()
|
|
733
|
+
self._json({'playlist': playlist})
|
|
734
|
+
|
|
633
735
|
def _handle_verify(self, body):
|
|
634
736
|
"""Re-checks an already-downloaded file against its own
|
|
635
737
|
Merkle-committed chunk hashes without re-fetching it -- see
|
|
@@ -188,6 +188,18 @@ def cmd_host(args):
|
|
|
188
188
|
# matching chunk data would otherwise get announced to the relay and
|
|
189
189
|
# only fail later, in the background server thread
|
|
190
190
|
all_leaves = {e['sha256']: node.load_leaves(args.archive_dir, e['sha256']) for e in entries}
|
|
191
|
+
# Bind the real listening socket now, before announcing anything --
|
|
192
|
+
# same reasoning as web_ui.py's _run_host_job and shell.py's do_host:
|
|
193
|
+
# a port already in use only used to surface once run_host_server got
|
|
194
|
+
# around to its own (later) bind, by which point every relay below
|
|
195
|
+
# had already been told this host is reachable at host_addr. Binding
|
|
196
|
+
# first and reusing the same socket in run_host_server (see sock=
|
|
197
|
+
# below) means a taken port fails loudly here instead, before a
|
|
198
|
+
# single relay ever hears about it.
|
|
199
|
+
try:
|
|
200
|
+
bound_sock = node.bind_host_port(args.port)
|
|
201
|
+
except OSError as e:
|
|
202
|
+
sys.exit(f'cannot bind port {args.port}: {e} — not announcing, nothing started')
|
|
191
203
|
# one archive_dir, one merkle root, one commit -- computed once outside
|
|
192
204
|
# the per-relay/per-entry loops below, not per file
|
|
193
205
|
ott_status = node.ott_commit_status(args.archive_dir)
|
|
@@ -213,7 +225,7 @@ def cmd_host(args):
|
|
|
213
225
|
kwargs={'use_tls': use_tls, 'ln_node': args.lightning_node},
|
|
214
226
|
daemon=True).start()
|
|
215
227
|
node.run_host_server(args.archive_dir, args.file, args.port, price=args.price,
|
|
216
|
-
ln_node=args.lightning_node)
|
|
228
|
+
ln_node=args.lightning_node, sock=bound_sock)
|
|
217
229
|
|
|
218
230
|
|
|
219
231
|
def cmd_discover(args):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.8
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|