weed-cli 1.0.2__tar.gz → 1.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weed-cli
3
- Version: 1.0.2
3
+ Version: 1.1.3
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
@@ -39,6 +39,26 @@ IDENTITY_PATH = os.path.expanduser('~/.weed_identity.key')
39
39
  IDENTITY_ARMOR_HEADER = '-----BEGIN WEED IDENTITY KEY-----'
40
40
  IDENTITY_ARMOR_FOOTER = '-----END WEED IDENTITY KEY-----'
41
41
 
42
+ TAGLINE = 'we do in 4 what others do in 5'
43
+
44
+
45
+ def weed_version():
46
+ """Installed package version (pyproject.toml's source of truth), with a
47
+ fallback for running straight from source without `pip install -e .`."""
48
+ try:
49
+ from importlib.metadata import version, PackageNotFoundError
50
+ try:
51
+ return version('weed-cli')
52
+ except PackageNotFoundError:
53
+ pass
54
+ except ImportError:
55
+ pass
56
+ return '0.0.0-dev'
57
+
58
+
59
+ def weed_banner():
60
+ return f'weed v{weed_version()} — {TAGLINE}'
61
+
42
62
 
43
63
  def _armor_identity(raw_bytes):
44
64
  """Base64 text with a header/footer — NOT real OpenPGP armor (no
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "weed-cli"
7
- version = "1.0.2"
7
+ version = "1.1.3"
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" }
@@ -42,7 +42,7 @@ def _bg(fn, *args, **kwargs):
42
42
 
43
43
  class WeedShell(cmd.Cmd):
44
44
  intro = (
45
- '\n weed — censorship-resistant video PoC node\n'
45
+ f'\n {node.weed_banner()}\n'
46
46
  ' Type help or ? for commands. Tab completes. Ctrl-D or q to exit.\n'
47
47
  )
48
48
  prompt = 'weed> '
@@ -20,6 +20,7 @@ import mimetypes
20
20
  import os
21
21
  import sys
22
22
  import threading
23
+ import time
23
24
  import uuid
24
25
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
25
26
  from urllib.parse import urlparse, parse_qs
@@ -35,6 +36,7 @@ except ImportError:
35
36
 
36
37
  WEB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web')
37
38
  DEFAULT_RELAY = 'http://127.0.0.1:9101'
39
+ LIBRARY_PATH = os.path.expanduser('~/.weed_library.json')
38
40
 
39
41
 
40
42
  class _JobStdout:
@@ -82,6 +84,53 @@ def _quiet():
82
84
  _hosts = {} # host_id -> dict describing an actively-hosted file
83
85
  _jobs = {} # job_id -> dict describing a download's progress/result
84
86
  _lock = threading.Lock()
87
+
88
+ # what's been downloaded/liked/subscribed, persisted to disk so a page
89
+ # reload -- or a server restart -- doesn't forget any of it. Downloads
90
+ # are keyed by content_hash (one record per piece of content, most
91
+ # recent job wins); likes/subscriptions are just lists of hashes/pubkeys.
92
+ # All access goes through _lock, same as _hosts/_jobs.
93
+ _library = {'downloads': {}, 'likes': [], 'subscriptions': []}
94
+
95
+
96
+ def _load_library():
97
+ global _library
98
+ try:
99
+ with open(LIBRARY_PATH) as f:
100
+ data = json.load(f)
101
+ _library = {
102
+ 'downloads': data.get('downloads') or {},
103
+ 'likes': data.get('likes') or [],
104
+ 'subscriptions': data.get('subscriptions') or [],
105
+ }
106
+ except (FileNotFoundError, json.JSONDecodeError):
107
+ pass
108
+
109
+
110
+ def _save_library():
111
+ """Caller must hold _lock. Written to a tmp file + os.replace so a
112
+ crash mid-write can't leave a half-written, unparseable JSON file
113
+ behind -- this runs on every like/subscribe/download-finish, not just
114
+ at shutdown."""
115
+ tmp = LIBRARY_PATH + '.tmp'
116
+ with open(tmp, 'w') as f:
117
+ json.dump(_library, f)
118
+ os.replace(tmp, LIBRARY_PATH)
119
+
120
+
121
+ def _rehydrate_jobs_from_library():
122
+ """Lets a restarted server's Downloads tab and Discover "already
123
+ downloaded" check keep working against the same job_ids as before --
124
+ _jobs is otherwise purely in-memory and would forget every finished
125
+ download on restart even though the files (and _library) are still
126
+ there."""
127
+ for content_hash, rec in _library['downloads'].items():
128
+ _jobs[rec['job_id']] = {
129
+ 'status': 'done', 'idx': 0, 'n_chunks': None,
130
+ 'content_hash': content_hash, 'path': rec['path'],
131
+ 'title': rec.get('title'), 'size': rec.get('size'), 'bps': rec.get('bps'),
132
+ 'error': None,
133
+ }
85
134
  _lan_url = None # set once in run_web_ui() -- the base URL a phone on the
86
135
  # same LAN can actually reach this server at, or None if
87
136
  # it can't (bound to 127.0.0.1). The client can't compute
@@ -154,17 +203,26 @@ def _run_host_job(host_id, archive_dir, file_name, port, price, relay_urls, adve
154
203
  _hosts[host_id].update(status='error', error=f'{type(e).__name__}: {e}')
155
204
 
156
205
 
157
- def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightning):
206
+ def _run_download_job(job_id, content_hash, relay_urls, out_path, k, use_lightning, title=None):
158
207
  def on_progress(idx, n_chunks):
159
208
  with _lock:
160
209
  _jobs[job_id].update(idx=idx, n_chunks=n_chunks)
161
210
 
162
211
  try:
212
+ t0 = time.time()
163
213
  with _quiet():
164
214
  path = node.download_with_auction(content_hash, relay_urls, out_path=out_path, k=k,
165
215
  use_lightning=use_lightning, on_progress=on_progress)
216
+ elapsed = time.time() - t0
217
+ size = os.path.getsize(path)
218
+ bps = size / elapsed if elapsed > 0 else None
166
219
  with _lock:
167
- _jobs[job_id].update(status='done', path=path)
220
+ _jobs[job_id].update(status='done', path=path, size=size, bps=bps)
221
+ _library['downloads'][content_hash] = {
222
+ 'content_hash': content_hash, 'job_id': job_id, 'path': path,
223
+ 'title': title, 'downloaded_at': time.time(), 'size': size, 'bps': bps,
224
+ }
225
+ _save_library()
168
226
  except SystemExit as e:
169
227
  with _lock:
170
228
  _jobs[job_id].update(status='error', error=str(e))
@@ -202,6 +260,13 @@ class Handler(BaseHTTPRequestHandler):
202
260
  if path == '/api/hosts':
203
261
  with _lock:
204
262
  return self._json({'hosts': list(_hosts.values())})
263
+ if path == '/api/library':
264
+ with _lock:
265
+ return self._json({
266
+ 'downloads': list(_library['downloads'].values()),
267
+ 'likes': list(_library['likes']),
268
+ 'subscriptions': list(_library['subscriptions']),
269
+ })
205
270
  if path.startswith('/api/download/'):
206
271
  with _lock:
207
272
  job = _jobs.get(path[len('/api/download/'):])
@@ -268,13 +333,14 @@ class Handler(BaseHTTPRequestHandler):
268
333
  out_path = body.get('out_path') or f'download_{content_hash[:16]}'
269
334
  k = int(body.get('k') or 3)
270
335
  use_lightning = bool(body.get('lightning'))
336
+ title = body.get('title')
271
337
 
272
338
  job_id = uuid.uuid4().hex[:12]
273
339
  with _lock:
274
340
  _jobs[job_id] = {'status': 'running', 'idx': 0, 'n_chunks': None,
275
- 'content_hash': content_hash, 'path': None, 'error': None}
341
+ 'content_hash': content_hash, 'path': None, 'title': title, 'error': None}
276
342
  threading.Thread(target=_run_download_job,
277
- args=(job_id, content_hash, relay_urls, out_path, k, use_lightning),
343
+ args=(job_id, content_hash, relay_urls, out_path, k, use_lightning, title),
278
344
  daemon=True).start()
279
345
  self._json({'job_id': job_id})
280
346
 
@@ -284,7 +350,12 @@ class Handler(BaseHTTPRequestHandler):
284
350
  return self._json({'error': 'content_hash required'}, status=400)
285
351
  identity = _identity()
286
352
  event = identity.sign_event('like', content_hash=content_hash)
287
- self._json({'result': node.post_event(body.get('relay') or DEFAULT_RELAY, event)})
353
+ result = node.post_event(body.get('relay') or DEFAULT_RELAY, event)
354
+ with _lock:
355
+ if content_hash not in _library['likes']:
356
+ _library['likes'].append(content_hash)
357
+ _save_library()
358
+ self._json({'result': result})
288
359
 
289
360
  def _handle_subscribe(self, body):
290
361
  target_pubkey = body.get('target_pubkey')
@@ -292,7 +363,12 @@ class Handler(BaseHTTPRequestHandler):
292
363
  return self._json({'error': 'target_pubkey required'}, status=400)
293
364
  identity = _identity()
294
365
  event = identity.sign_event('subscribe', target_pubkey=target_pubkey)
295
- self._json({'result': node.post_event(body.get('relay') or DEFAULT_RELAY, event)})
366
+ result = node.post_event(body.get('relay') or DEFAULT_RELAY, event)
367
+ with _lock:
368
+ if target_pubkey not in _library['subscriptions']:
369
+ _library['subscriptions'].append(target_pubkey)
370
+ _save_library()
371
+ self._json({'result': result})
296
372
 
297
373
  def _handle_stream(self, job_id):
298
374
  """Serve an already-downloaded job's file with real HTTP range
@@ -398,6 +474,8 @@ class Handler(BaseHTTPRequestHandler):
398
474
 
399
475
  def run_web_ui(port=8080, bind_host='127.0.0.1', quiet=False):
400
476
  global _lan_url
477
+ _load_library()
478
+ _rehydrate_jobs_from_library()
401
479
  srv = ThreadingHTTPServer((bind_host, port), Handler)
402
480
 
403
481
  reachable_host = _detect_lan_ip() if bind_host == '0.0.0.0' else bind_host
@@ -49,9 +49,11 @@ def run_make(target):
49
49
 
50
50
 
51
51
  def build_parser():
52
+ import node
52
53
  parser = argparse.ArgumentParser(
53
54
  prog='weed',
54
- description='Censorship-resistant video PoC — real mechanisms behind the #all-pdx brainstorm.')
55
+ description=f'{node.weed_banner()} — real mechanisms behind the #all-pdx brainstorm.')
56
+ parser.add_argument('--version', action='version', version=node.weed_banner())
55
57
  sub = parser.add_subparsers(dest='command', required=False)
56
58
 
57
59
  sub.add_parser('shell', help='interactive shell with tab completion (also the default with no command)')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weed-cli
3
- Version: 1.0.2
3
+ Version: 1.1.3
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