mps: torrent web seed + torrent_file_url for CDN-bootstrapped swarm

- embed CDN URL as BEP 19 web seed in .torrent + magnet link
- store torrent_file_url (CDN path to .torrent) on Product
- migration adds torrent_file_url column alongside magnet_link
- content page shows Magnet + .torrent download buttons
- product edit shows Copy Magnet + Open + .torrent download
- cdn_endpoint flows from request.shop_cdn_endpoint through async thread
This commit is contained in:
russell@unturf.com 2026-04-05 21:31:09 -04:00
parent 7d78dff6ac
commit e06cf1c230
7 changed files with 47 additions and 16 deletions

View file

@ -51,7 +51,7 @@ def _make_client(creds):
)
def _build_torrent(file_path, name, trackers):
def _build_torrent(file_path, name, trackers, webseeds=None):
"""Create a torf.Torrent from a local file and return (torrent, magnet_uri)."""
try:
import torf
@ -64,6 +64,7 @@ def _build_torrent(file_path, name, trackers):
path=file_path,
name=name,
trackers=[[tr] for tr in trackers],
webseeds=webseeds or [],
private=False,
comment="permacomputer.com — CWE-407 patched OS image",
source="permacomputer",
@ -74,17 +75,24 @@ def _build_torrent(file_path, name, trackers):
def generate_torrent(s3_client, bucket, s3_key, s3_path, product_id,
session_factory, trackers=None):
"""Download file from S3, generate .torrent, upload, save magnet link.
session_factory, trackers=None, cdn_endpoint=None):
"""Download file from S3, generate .torrent, upload, save magnet + torrent URL.
Designed to run in a background thread never call directly from a request.
cdn_endpoint: base CDN URL (e.g. "https://cdn.example.com") embedded as web seed
so clients can download via HTTP and then seed to peers (BEP 19).
"""
if trackers is None:
trackers = DEFAULT_TRACKERS
name = os.path.basename(s3_key)
log.info("torrent: generating for product=%s key=%s", product_id, s3_key)
# Web seed URL: CDN serves the product file directly (BEP 19)
webseed_url = f"{cdn_endpoint}/{s3_key}" if cdn_endpoint else None
webseeds = [webseed_url] if webseed_url else []
log.info("torrent: generating for product=%s key=%s webseed=%s",
product_id, s3_key, webseed_url)
with tempfile.TemporaryDirectory() as tmpdir:
local_path = os.path.join(tmpdir, name)
@ -96,7 +104,7 @@ def generate_torrent(s3_client, bucket, s3_key, s3_path, product_id,
# 2. Build .torrent + extract magnet link
log.info("torrent: building torrent for %s", local_path)
torrent_obj, magnet_uri = _build_torrent(local_path, name, trackers)
torrent_obj, magnet_uri = _build_torrent(local_path, name, trackers, webseeds=webseeds)
# 3. Write .torrent file
torrent_obj.write(torrent_path)
@ -117,22 +125,28 @@ def generate_torrent(s3_client, bucket, s3_key, s3_path, product_id,
)
log.info("torrent: uploaded .torrent to s3://%s/%s", bucket, torrent_s3_key)
# 5. Save magnet link to DB
# CDN URL of the .torrent file itself
torrent_file_url = (
f"{cdn_endpoint}/{torrent_s3_key}" if cdn_endpoint else None
)
# 5. Save magnet link + torrent file URL to DB
try:
with session_factory() as session:
from ..models.product import Product
product = session.get(Product, product_id)
if product is not None:
product.torrent_magnet_link = magnet_uri
product.torrent_file_url = torrent_file_url
session.add(product)
session.commit()
log.info("torrent: saved magnet link for product=%s", product_id)
log.info("torrent: saved magnet + torrent_file_url for product=%s", product_id)
except Exception:
log.exception("torrent: failed to save magnet link for product=%s", product_id)
log.exception("torrent: failed to save torrent data for product=%s", product_id)
def generate_torrent_async(s3_client, bucket, s3_key, s3_path, product_id,
session_factory, trackers=None):
session_factory, trackers=None, cdn_endpoint=None):
"""Fire-and-forget: generate torrent in a daemon thread."""
creds = _capture_s3_creds(s3_client)
@ -141,7 +155,7 @@ def generate_torrent_async(s3_client, bucket, s3_key, s3_path, product_id,
fresh_client = _make_client(creds)
generate_torrent(
fresh_client, bucket, s3_key, s3_path, product_id,
session_factory, trackers=trackers,
session_factory, trackers=trackers, cdn_endpoint=cdn_endpoint,
)
except Exception:
log.exception(

View file

@ -112,6 +112,9 @@ class Product(RBase, Base):
# Optional magnet link for BitTorrent distribution (set when shop.torrent_enabled)
torrent_magnet_link = Column(UnicodeText, nullable=True)
# CDN URL of the .torrent file (set alongside torrent_magnet_link)
torrent_file_url = Column(UnicodeText, nullable=True)
# a denormalized json field to relate products to bundles, and vice versa.
json_bundle_metadata = Column(
UnicodeText, default=DEFAULT_BUNDLE_METADATA, nullable=False

View file

@ -26,8 +26,13 @@ def upgrade():
"mps_product",
sa.Column("torrent_magnet_link", sa.UnicodeText(), nullable=True),
)
op.add_column(
"mps_product",
sa.Column("torrent_file_url", sa.UnicodeText(), nullable=True),
)
def downgrade():
op.drop_column("mps_shop", "torrent_enabled")
op.drop_column("mps_product", "torrent_magnet_link")
op.drop_column("mps_product", "torrent_file_url")

View file

@ -157,7 +157,10 @@
<a href="{{ signed_get_object_url }}" class="product-download-button mps-button" target="_blank" download>&#11123 Download</a>
{% endif %}
{% if torrent_magnet_link %}
<a href="{{ torrent_magnet_link }}" class="mps-button" style="margin-left:.5rem">&#9697; Torrent</a>
<a href="{{ torrent_magnet_link }}" class="mps-button" style="margin-left:.5rem">&#9697; Magnet</a>
{% endif %}
{% if torrent_file_url %}
<a href="{{ torrent_file_url }}" class="mps-button" style="margin-left:.5rem" download>&#8615; .torrent</a>
{% endif %}
</div>
{% endif %}

View file

@ -289,24 +289,27 @@ Your cover (<code>thumbnail1</code>) will show up on search pages.
<br />
{% if torrent_enabled %}
<label>Magnet Link
<label>Torrent Distribution
<span style="font-weight:normal;color:#888">
— generated automatically when you upload the product file
</span>
</label>
{% if torrent_magnet_link %}
<div style="display:flex;gap:.5rem;align-items:center">
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
<input type="text" id="torrent_magnet_link_display"
value="{{ torrent_magnet_link }}" readonly
class="mps-text-input" style="flex:1;color:#888" />
class="mps-text-input" style="flex:1;min-width:0;color:#888" />
<button type="button"
onclick="navigator.clipboard.writeText(document.getElementById('torrent_magnet_link_display').value)"
class="mps-button" style="white-space:nowrap">Copy</button>
class="mps-button" style="white-space:nowrap">Copy Magnet</button>
<a href="{{ torrent_magnet_link }}" class="mps-button" style="white-space:nowrap">&#9697; Open</a>
{% if torrent_file_url %}
<a href="{{ torrent_file_url }}" class="mps-button" style="white-space:nowrap" download>&#8615; .torrent</a>
{% endif %}
</div>
{% else %}
<p style="color:#888;margin:.25rem 0">
Upload the product file to generate the magnet link automatically.
Upload the product file to generate the magnet link and .torrent automatically.
</p>
{% endif %}

View file

@ -111,4 +111,5 @@ def content(request):
"vocals_url": vocals_url,
"karaoke_eligible": karaoke_eligible,
"torrent_magnet_link": product.torrent_magnet_link if product.shop.torrent_enabled else None,
"torrent_file_url": product.torrent_file_url if product.shop.torrent_enabled else None,
}

View file

@ -391,6 +391,7 @@ def product_edit(request):
product.s3_path,
product.id,
request.registry["dbsession_factory"],
cdn_endpoint=request.shop_cdn_endpoint,
)
# Generate vocal isolation tracks if this is audio/video
@ -527,6 +528,7 @@ def product_edit(request):
"price_history": price_history,
"torrent_enabled": product.shop.torrent_enabled,
"torrent_magnet_link": product.torrent_magnet_link or "",
"torrent_file_url": product.torrent_file_url or "",
}