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:
parent
7d78dff6ac
commit
e06cf1c230
7 changed files with 47 additions and 16 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -157,7 +157,10 @@
|
|||
<a href="{{ signed_get_object_url }}" class="product-download-button mps-button" target="_blank" download>⭳ Download</a>
|
||||
{% endif %}
|
||||
{% if torrent_magnet_link %}
|
||||
<a href="{{ torrent_magnet_link }}" class="mps-button" style="margin-left:.5rem">◡ Torrent</a>
|
||||
<a href="{{ torrent_magnet_link }}" class="mps-button" style="margin-left:.5rem">◡ Magnet</a>
|
||||
{% endif %}
|
||||
{% if torrent_file_url %}
|
||||
<a href="{{ torrent_file_url }}" class="mps-button" style="margin-left:.5rem" download>↧ .torrent</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -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">◡ Open</a>
|
||||
{% if torrent_file_url %}
|
||||
<a href="{{ torrent_file_url }}" class="mps-button" style="white-space:nowrap" download>↧ .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 %}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue