mps: torrent distribution — shop toggle + per-product magnet link

Shop settings: new 'Torrent Distribution' section with single checkbox
(torrent_enabled, default False). When on, product edit shows a magnet
link field; content page shows a Torrent button alongside Download.

- models/shop.py: torrent_enabled (Boolean, default False)
- models/product.py: torrent_magnet_link (UnicodeText, nullable)
- views/shop.py: torrent-settings form section handler
- views/product.py: save/validate magnet link on product edit
- views/content.py: pass torrent_magnet_link to content template
- templates/shop_settings.j2: Torrent Distribution section
- templates/product_edit.j2: magnet link field (shown when torrent enabled)
- templates/content.j2: Torrent button beside Download
- migration: a1b2c3d4e5f6 adds both columns
- tests: 6 functional tests covering enable/disable, magnet save,
  invalid scheme rejection, and no-leak when disabled
This commit is contained in:
russell@unturf.com 2026-04-05 19:57:44 -04:00
parent 41a8525953
commit d5f55f8131
10 changed files with 208 additions and 0 deletions

View file

@ -109,6 +109,9 @@ class Product(RBase, Base):
total_file_bytes = Column(BigInteger, nullable=False, default=0)
# Optional magnet link for BitTorrent distribution (set when shop.torrent_enabled)
torrent_magnet_link = 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

@ -183,6 +183,9 @@ class Shop(RBase, Base):
primary_s3_cdn_endpoint = Column(Unicode(256), nullable=True)
primary_s3_enabled = Column(Boolean, default=False)
# Torrent / magnet link distribution (off by default)
torrent_enabled = Column(Boolean, default=False)
# Precomputed discovery ring: circular ordering of all public products
json_discovery_ring = Column(UnicodeText, nullable=True)

View file

@ -0,0 +1,33 @@
"""torrent magnet links
Revision ID: a1b2c3d4e5f6
Revises: fd9f7e2f2b78
Create Date: 2026-04-05 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a1b2c3d4e5f6"
down_revision = "fd9f7e2f2b78"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"mps_shop",
sa.Column("torrent_enabled", sa.Boolean(), nullable=True, server_default=sa.false()),
)
op.add_column(
"mps_product",
sa.Column("torrent_magnet_link", sa.UnicodeText(), nullable=True),
)
def downgrade():
op.drop_column("mps_shop", "torrent_enabled")
op.drop_column("mps_product", "torrent_magnet_link")

View file

@ -156,6 +156,9 @@
{% if signed_get_object_url is not none %}
<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>
{% endif %}
</div>
{% endif %}
<div id="watch-queue" class="js-only" style="display:none">

View file

@ -288,6 +288,17 @@ Your cover (<code>thumbnail1</code>) will show up on search pages.
<br />
<br />
{% if torrent_enabled %}
<label for="torrent_magnet_link">Magnet Link <span style="font-weight:normal;color:#888">(optional — torrent download for buyers)</span></label>
<input type="text" name="torrent_magnet_link" id="torrent_magnet_link"
value="{{ torrent_magnet_link }}"
placeholder="magnet:?xt=urn:btih:..."
class="mps-text-input" />
<br />
<br />
{% endif %}
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
</form>
</section>

View file

@ -1284,6 +1284,36 @@ Existing sales honored for download buy purchasers.
</section>
<br />
<br />
<section class="one-column">
<section class="shop-settings well">
<h3>Torrent Distribution</h3>
<p>When enabled, you can add a magnet link to each product. Buyers see a torrent download option alongside the standard download link. Off by default.</p>
<form method="post" action="/s/{{ request.shop.id }}/settings" onsubmit="submit.disabled = true; return true;">
<input type="hidden" name="form_section" value="torrent-settings" />
<label>
<input type="checkbox" name="torrent_enabled_checkbox"
{% if torrent_enabled %}checked{% endif %} />
Enable torrent distribution
</label>
<br /><br />
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
</form>
</section>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Crypto wallets toggle (still uses checkbox pattern)

View file

@ -4861,3 +4861,100 @@ class TestBucketSettings(_AuthenticatedBase):
self.assertIn("Storage bucket settings updated", flash)
self.dbsession.refresh(shop)
self.assertFalse(shop.primary_s3_enabled)
class TestTorrentSettings(_AuthenticatedBase):
"""Functional tests for torrent distribution settings."""
def test_enable_torrent(self):
shop = self._create_shop_helper()
self.assertFalse(shop.torrent_enabled)
res = self.testapp.post(
f"/s/{shop.id}/settings",
{
"form_section": "torrent-settings",
"torrent_enabled_checkbox": "on",
"submit": "Save Settings",
},
status=302,
)
res = res.follow()
flash = self._get_flash_messages(res)
self.assertIn("Torrent distribution enabled", flash)
self.dbsession.refresh(shop)
self.assertTrue(shop.torrent_enabled)
def test_disable_torrent(self):
shop = self._create_shop_helper()
shop.torrent_enabled = True
self.dbsession.flush()
res = self.testapp.post(
f"/s/{shop.id}/settings",
{
"form_section": "torrent-settings",
"submit": "Save Settings",
},
status=302,
)
res = res.follow()
flash = self._get_flash_messages(res)
self.assertIn("Torrent distribution disabled", flash)
self.dbsession.refresh(shop)
self.assertFalse(shop.torrent_enabled)
def test_torrent_off_by_default(self):
shop = self._create_shop_helper()
self.assertFalse(shop.torrent_enabled)
def test_magnet_link_saved_on_product(self):
shop = self._create_shop_helper()
shop.torrent_enabled = True
self.dbsession.flush()
product = self._create_product_helper(shop)
magnet = "magnet:?xt=urn:btih:abc123&dn=test"
res = self.testapp.post(
f"/p/{product.id}/edit",
{
"title": product.title,
"description": product.description or "",
"price": "0.00",
"visibility": str(product.visibility),
"torrent_magnet_link": magnet,
"submit": "Save Settings",
},
status=302,
)
self.dbsession.refresh(product)
self.assertEqual(product.torrent_magnet_link, magnet)
def test_magnet_link_rejected_if_invalid_scheme(self):
shop = self._create_shop_helper()
shop.torrent_enabled = True
self.dbsession.flush()
product = self._create_product_helper(shop)
res = self.testapp.post(
f"/p/{product.id}/edit",
{
"title": product.title,
"description": product.description or "",
"price": "0.00",
"visibility": str(product.visibility),
"torrent_magnet_link": "https://not-a-magnet-link",
"submit": "Save Settings",
},
status=302,
)
res = res.follow()
flash = self._get_flash_messages(res)
self.assertIn("Magnet link must start with magnet:", flash)
self.dbsession.refresh(product)
self.assertIsNone(product.torrent_magnet_link)
def test_magnet_link_not_shown_when_torrent_disabled(self):
shop = self._create_shop_helper()
self.assertFalse(shop.torrent_enabled)
product = self._create_product_helper(shop)
product.torrent_magnet_link = "magnet:?xt=urn:btih:abc123"
self.dbsession.flush()
res = self.testapp.get(f"/c/{product.id}/{product.slug}", status=200)
self.assertNotIn("Torrent", res.text)

View file

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

View file

@ -297,6 +297,18 @@ def product_edit(request):
request.dbsession.add(product.set_price(price))
request.session.flash(("You updated the product's price.", "success"))
if product.shop.torrent_enabled:
torrent_magnet_link = request.params.get("torrent_magnet_link", "").strip()
if torrent_magnet_link and not torrent_magnet_link.startswith("magnet:"):
request.session.flash(("Magnet link must start with magnet:", "error"))
elif torrent_magnet_link != (product.torrent_magnet_link or ""):
product_modified = True
product.torrent_magnet_link = torrent_magnet_link or None
if torrent_magnet_link:
request.session.flash(("Magnet link saved.", "success"))
else:
request.session.flash(("Magnet link removed.", "success"))
if s3_webhook_key and s3_webhook_bucket and s3_webhook_etag:
# Check if the file exists and has a non-zero size
try:
@ -511,6 +523,8 @@ def product_edit(request):
"locations": locations,
"inventories": inventories if product.is_physical else {},
"price_history": price_history,
"torrent_enabled": product.shop.torrent_enabled,
"torrent_magnet_link": product.torrent_magnet_link or "",
}

View file

@ -1176,6 +1176,18 @@ def shop_settings(request):
else:
request.session.flash(("Storage bucket settings updated.", "success"))
if form_section == "torrent-settings":
torrent_enabled = checkbox_to_bool(
request.params.get("torrent_enabled_checkbox", "off")
)
changed = False
if shop.torrent_enabled != torrent_enabled:
shop.torrent_enabled = torrent_enabled
changed = True
if changed:
status = "enabled" if torrent_enabled else "disabled"
request.session.flash((f"Torrent distribution {status}.", "success"))
# If we processed any form submission, respond accordingly
if form_section:
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
@ -1380,6 +1392,7 @@ def shop_settings(request):
"primary_s3_secret_key": shop.primary_s3_secret_key or "",
"primary_s3_cdn_endpoint": shop.primary_s3_cdn_endpoint or "",
"primary_s3_enabled": shop.primary_s3_enabled,
"torrent_enabled": shop.torrent_enabled,
"signed_posts": signed_posts,
"get_endpoints": get_endpoints,
}