diff --git a/make_post_sell/models/product.py b/make_post_sell/models/product.py index f223ffe..99975b6 100644 --- a/make_post_sell/models/product.py +++ b/make_post_sell/models/product.py @@ -109,12 +109,18 @@ class Product(RBase, Base): total_file_bytes = Column(BigInteger, nullable=False, default=0) - # Optional magnet link for BitTorrent distribution (set when shop.torrent_enabled) + # Optional magnet link for BitTorrent distribution. + # Only generated when torrent_opt_in=True AND visibility=1 (public). 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) + # Explicit per-product opt-in to BitTorrent seeding. + # False by default — owner must consciously enable per product. + # Once seeded, files persist in the swarm even if this is later disabled. + torrent_opt_in = Column(Boolean, nullable=False, server_default="0", default=False) + # a denormalized json field to relate products to bundles, and vice versa. json_bundle_metadata = Column( UnicodeText, default=DEFAULT_BUNDLE_METADATA, nullable=False diff --git a/make_post_sell/scripts/alembic/versions/0ee5654cfe7d_add_torrent_opt_in_column_to_mps_product.py b/make_post_sell/scripts/alembic/versions/0ee5654cfe7d_add_torrent_opt_in_column_to_mps_product.py new file mode 100644 index 0000000..d8641a4 --- /dev/null +++ b/make_post_sell/scripts/alembic/versions/0ee5654cfe7d_add_torrent_opt_in_column_to_mps_product.py @@ -0,0 +1,37 @@ +"""add torrent_opt_in column to mps_product + +Revision ID: 0ee5654cfe7d +Revises: 4f4d7c147437 +Create Date: 2026-04-06 18:03:53.663932 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0ee5654cfe7d' +down_revision = '4f4d7c147437' +branch_labels = None +depends_on = None + + +def _column_exists(table, column): + from alembic import op as _op + from sqlalchemy import inspect + bind = _op.get_bind() + insp = inspect(bind) + return column in [c["name"] for c in insp.get_columns(table)] + + +def upgrade(): + if not _column_exists('mps_product', 'torrent_opt_in'): + op.add_column( + 'mps_product', + sa.Column('torrent_opt_in', sa.Boolean(), server_default='0', nullable=False), + ) + + +def downgrade(): + if _column_exists('mps_product', 'torrent_opt_in'): + op.drop_column('mps_product', 'torrent_opt_in') diff --git a/make_post_sell/templates/product_edit.j2 b/make_post_sell/templates/product_edit.j2 index d4e634e..7702e67 100644 --- a/make_post_sell/templates/product_edit.j2 +++ b/make_post_sell/templates/product_edit.j2 @@ -289,11 +289,28 @@ Your cover (thumbnail1) will show up on search pages. {% if torrent_enabled %} - Torrent Distribution - - — generated automatically when you upload the product file - + Torrent Distribution + + {% if torrent_seeded_but_private %} + + Active torrent — file cannot be recalled. + This product is no longer public, so the magnet link is hidden from visitors. + However, the file is already seeded in the BitTorrent network and will remain + available to anyone who already has the magnet link. + + {% endif %} + + + + Seed this product via BitTorrent + + Once seeded, the file persists in the BitTorrent network permanently — + it cannot be recalled even if you disable this or make the product private. + Only enable for files you intend to distribute publicly forever. + + {% if torrent_magnet_link %} thumbnail1) will show up on search pages. {% endif %} + {% elif torrent_opt_in %} + + Seeding enabled — magnet link will be generated when you upload the product file. + {% else %} - Upload the product file to generate the magnet link and .torrent automatically. + Enable seeding above, then upload the product file to generate a magnet link. {% endif %} diff --git a/make_post_sell/views/content.py b/make_post_sell/views/content.py index 2d8c551..d64d129 100644 --- a/make_post_sell/views/content.py +++ b/make_post_sell/views/content.py @@ -110,6 +110,16 @@ 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, - "torrent_file_url": product.torrent_file_url if product.shop.torrent_enabled else None, + # Only show torrent links on the public page when product is public. + # Private/unlisted products must not spread magnet links to visitors. + "torrent_magnet_link": ( + product.torrent_magnet_link + if product.shop.torrent_enabled and product.visibility == 1 + else None + ), + "torrent_file_url": ( + product.torrent_file_url + if product.shop.torrent_enabled and product.visibility == 1 + else None + ), } diff --git a/make_post_sell/views/product.py b/make_post_sell/views/product.py index 9d59685..9b5df8e 100644 --- a/make_post_sell/views/product.py +++ b/make_post_sell/views/product.py @@ -291,13 +291,39 @@ def product_edit(request): request.shop_bucket_name, ) request.session.flash(("You updated the product's visibility.", "success")) + # Warn if switching away from public while a torrent is already seeded. + if visibility != 1 and product.torrent_magnet_link: + request.session.flash(( + "Warning: this product has an active torrent. " + "The magnet link is no longer shown to visitors, " + "but the file persists in the BitTorrent network and cannot be recalled.", + "warning", + )) if price != product.price: product_modified = True request.dbsession.add(product.set_price(price)) request.session.flash(("You updated the product's price.", "success")) - # torrent_magnet_link is auto-generated on upload — no manual save needed + # torrent_opt_in — explicit per-product seeding consent + if product.shop.torrent_enabled and "submit" in request.params: + new_opt_in = request.params.get("torrent_opt_in", "off") == "on" + if new_opt_in != product.torrent_opt_in: + product_modified = True + product.torrent_opt_in = new_opt_in + if new_opt_in: + request.session.flash(( + "Torrent seeding enabled for this product. " + "The file will be seeded when you upload or re-upload. " + "Warning: once seeded, files cannot be recalled from the BitTorrent network.", + "warning", + )) + else: + request.session.flash(( + "Torrent seeding disabled. " + "Note: if already seeded, the file persists in the BitTorrent network.", + "success", + )) if s3_webhook_key and s3_webhook_bucket and s3_webhook_etag: # Check if the file exists and has a non-zero size @@ -381,8 +407,17 @@ def product_edit(request): product.shop.id, request.registry["dbsession_factory"] ) - # Auto-generate .torrent + magnet link for the main product file - if file_key == "product" and product.shop.torrent_enabled: + # Generate .torrent + magnet link only when: + # - torrent distribution is enabled for the shop + # - the owner explicitly opted this product in (torrent_opt_in) + # - the product is public (visibility == 1) + # Torrents are permanent — files persist in the swarm once seeded. + if ( + file_key == "product" + and product.shop.torrent_enabled + and product.torrent_opt_in + and product.visibility == 1 + ): from ..lib.torrent import generate_torrent_async generate_torrent_async( request.shop_uploads_client, @@ -527,8 +562,12 @@ def product_edit(request): "inventories": inventories if product.is_physical else {}, "price_history": price_history, "torrent_enabled": product.shop.torrent_enabled, + "torrent_opt_in": product.torrent_opt_in, "torrent_magnet_link": product.torrent_magnet_link or "", "torrent_file_url": product.torrent_file_url or "", + "torrent_seeded_but_private": bool( + product.torrent_magnet_link and product.visibility != 1 + ), } diff --git a/make_post_sell/views/shop.py b/make_post_sell/views/shop.py index 9da394d..d48c147 100644 --- a/make_post_sell/views/shop.py +++ b/make_post_sell/views/shop.py @@ -79,6 +79,10 @@ def _torrent_backfill_async(request, shop): continue # already generated if "product" not in product.originals: continue # no product file uploaded yet + if not product.torrent_opt_in: + continue # owner has not explicitly opted this product in + if product.visibility != 1: + continue # torrents are permanent — only seed public products generate_torrent_async( s3_client, bucket, @@ -1513,7 +1517,12 @@ def torrent_backfill_status(request): .all() ) - eligible = [p for p in products if "product" in p.originals] + eligible = [ + p for p in products + if "product" in p.originals + and p.torrent_opt_in + and p.visibility == 1 + ] done = [p for p in eligible if p.torrent_magnet_link] total = len(eligible)
thumbnail1
+ Active torrent — file cannot be recalled. + This product is no longer public, so the magnet link is hidden from visitors. + However, the file is already seeded in the BitTorrent network and will remain + available to anyone who already has the magnet link. +
+ Once seeded, the file persists in the BitTorrent network permanently — + it cannot be recalled even if you disable this or make the product private. + Only enable for files you intend to distribute publicly forever. +
+ Seeding enabled — magnet link will be generated when you upload the product file. +
- Upload the product file to generate the magnet link and .torrent automatically. + Enable seeding above, then upload the product file to generate a magnet link.