diff --git a/app/main.py b/app/main.py index b9483c1..04438a9 100644 --- a/app/main.py +++ b/app/main.py @@ -1806,6 +1806,33 @@ async def pay_status(payment_id: str): return {"status": db_status, "purpose": purpose} +@app.post("/api/paywall/check") +async def paywall_check(payload: dict, request: Request): + """Предпроверка пейволла БЕЗ загрузки файла. body: {size_mb?, pages?, user_id?}. + Возвращает решение сервера, чтобы фронт показал пейволл сразу.""" + if not isinstance(payload, dict): + payload = {} + try: + size_mb = float(payload.get("size_mb") or 0) + except (TypeError, ValueError): + size_mb = 0.0 + try: + pages = int(payload.get("pages") or 0) + except (TypeError, ValueError): + pages = 0 + account = _resolve_account(request, payload.get("user_id")) + sub_active = _db_sub_active(account) + over = size_mb > AUTH_REQUIRED_MB or pages > AUTH_REQUIRED_PAGES + requires_payment = bool(PAYWALL_ENABLED and over and not sub_active) + return { + "paywall_enabled": bool(PAYWALL_ENABLED), + "sub_active": sub_active, + "requires_payment": requires_payment, + "file_price": FILE_PRICE_RUB, + "sub_price": SUB_PRICE_RUB, + } + + # ══════════════════════════════════════════════════════════════════════════════ # Аккаунты, сессии, OAuth-вход (Yandex ID) # ══════════════════════════════════════════════════════════════════════════════ diff --git a/app/static/index.html b/app/static/index.html index a6096da..ed668ce 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -1303,6 +1303,9 @@ pay_canceled: 'Платёж отменён', paywall_title: 'Файл больше бесплатного лимита', paywall_desc: 'Бесплатно — до 50 МБ и 100 страниц. Для файлов больше — разовая оплата или подписка.', + paywall_batch_title: 'Пакетная обработка — по подписке', + paywall_batch_desc: 'Обработка нескольких файлов сразу — по подписке. Оформите подписку, затем загрузите файлы.', + pay_uploading: 'Загружаем файл…', paywall_pay_file: 'Оплатить файл', paywall_pay_sub: 'Подписка', paywall_month: 'мес', @@ -1430,6 +1433,9 @@ pay_canceled: 'Payment canceled', paywall_title: 'File exceeds the free limit', paywall_desc: 'Free up to 50 MB and 100 pages. Larger files — one-time payment or subscription.', + paywall_batch_title: 'Batch processing — subscription', + paywall_batch_desc: 'Processing multiple files at once requires a subscription. Subscribe, then upload your files.', + pay_uploading: 'Uploading file…', paywall_pay_file: 'Pay for file', paywall_pay_sub: 'Subscription', paywall_month: 'mo', @@ -1557,6 +1563,9 @@ pay_canceled: 'Zahlung storniert', paywall_title: 'Datei über dem Gratis-Limit', paywall_desc: 'Kostenlos bis 50 MB und 100 Seiten. Größere Dateien — Einmalzahlung oder Abo.', + paywall_batch_title: 'Stapelverarbeitung — im Abo', + paywall_batch_desc: 'Mehrere Dateien gleichzeitig erfordern ein Abo. Abonnieren Sie und laden Sie dann Ihre Dateien hoch.', + pay_uploading: 'Datei wird hochgeladen…', paywall_pay_file: 'Datei bezahlen', paywall_pay_sub: 'Abo', paywall_month: 'Mon', @@ -1684,6 +1693,9 @@ pay_canceled: 'Paiement annulé', paywall_title: 'Fichier au-delà de la limite gratuite', paywall_desc: 'Gratuit : 50 Mo et 100 pages max. Au-delà — paiement unique ou abonnement.', + paywall_batch_title: 'Traitement par lots — sur abonnement', + paywall_batch_desc: 'Traiter plusieurs fichiers à la fois nécessite un abonnement. Abonnez-vous, puis chargez vos fichiers.', + pay_uploading: 'Téléversement du fichier…', paywall_pay_file: 'Payer le fichier', paywall_pay_sub: 'Abonnement', paywall_month: 'mois', @@ -1811,6 +1823,9 @@ pay_canceled: 'Pagamento annullato', paywall_title: 'File oltre il limite gratuito', paywall_desc: 'Gratis fino a 50 MB e 100 pagine. File più grandi — pagamento singolo o abbonamento.', + paywall_batch_title: 'Elaborazione multipla — in abbonamento', + paywall_batch_desc: 'Elaborare più file insieme richiede un abbonamento. Abbonati e poi carica i file.', + pay_uploading: 'Caricamento file…', paywall_pay_file: 'Paga file', paywall_pay_sub: 'Abbonamento', paywall_month: 'mese', @@ -1938,6 +1953,9 @@ pay_canceled: 'Pago cancelado', paywall_title: 'Archivo supera el límite gratis', paywall_desc: 'Gratis hasta 50 MB y 100 páginas. Archivos mayores — pago único o suscripción.', + paywall_batch_title: 'Procesamiento por lotes — con suscripción', + paywall_batch_desc: 'Procesar varios archivos a la vez requiere suscripción. Suscríbete y luego sube tus archivos.', + pay_uploading: 'Subiendo archivo…', paywall_pay_file: 'Pagar archivo', paywall_pay_sub: 'Suscripción', paywall_month: 'mes', @@ -2065,6 +2083,9 @@ pay_canceled: 'Pagamento cancelado', paywall_title: 'Arquivo acima do limite grátis', paywall_desc: 'Grátis até 50 MB e 100 páginas. Arquivos maiores — pagamento único ou assinatura.', + paywall_batch_title: 'Processamento em lote — por assinatura', + paywall_batch_desc: 'Processar vários arquivos de uma vez requer assinatura. Assine e depois envie seus arquivos.', + pay_uploading: 'Enviando arquivo…', paywall_pay_file: 'Pagar arquivo', paywall_pay_sub: 'Assinatura', paywall_month: 'mês', @@ -2202,29 +2223,63 @@ } catch (e) { msg.textContent = t('pay_error'); btn.disabled = false; } } - // ── Пейволл больших файлов ──────────────────────────────────────────────── - let paywallJob = null, paywallFilePrice = 99, paywallSubPrice = 299; - function showPaywall(jobId, filePrice, subPrice) { + // ── Пейволл больших файлов / батча ──────────────────────────────────────── + let paywallJob = null, paywallFilePrice = 99, paywallSubPrice = 299, paywallMode = 'file'; + + // Предпроверка на сервере БЕЗ загрузки файла + async function checkPaywall(sizeMb, pages) { + try { + const body = { size_mb: sizeMb, pages: pages }; + if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0; + const r = await fetch('/api/paywall/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + return await r.json(); + } catch (e) { return { requires_payment: false, sub_active: false, paywall_enabled: false, file_price: 99, sub_price: 299 }; } + } + + function showPaywall(jobId, filePrice, subPrice, mode) { + paywallMode = mode || 'file'; paywallJob = jobId; paywallFilePrice = filePrice || 99; paywallSubPrice = subPrice || 299; + const isBatch = paywallMode === 'batch'; document.getElementById('paywall-file-price').textContent = paywallFilePrice; document.getElementById('paywall-sub-price').textContent = paywallSubPrice; + document.getElementById('paywall-file-btn').style.display = isBatch ? 'none' : ''; + const titleEl = document.querySelector('#paywall-card [data-i18n="paywall_title"]'); + const descEl = document.querySelector('#paywall-card [data-i18n="paywall_desc"]'); + if (titleEl) titleEl.textContent = t(isBatch ? 'paywall_batch_title' : 'paywall_title'); + if (descEl) descEl.textContent = t(isBatch ? 'paywall_batch_desc' : 'paywall_desc'); const emailEl = document.getElementById('paywall-email'); if (sessionUser && sessionUser.email && !emailEl.value) emailEl.value = sessionUser.email; + document.getElementById('paywall-file-btn').disabled = false; + document.getElementById('paywall-sub-btn').disabled = false; + document.getElementById('paywall-msg').textContent = ''; document.getElementById('progress-card').style.display = 'none'; document.getElementById('result-card').style.display = 'none'; + document.getElementById('batch-card').style.display = 'none'; submitBtn.style.display = 'none'; document.getElementById('paywall-card').style.display = 'block'; + document.getElementById('paywall-card').scrollIntoView({ behavior: 'smooth', block: 'center' }); } + async function _payViaKassa(purpose) { const email = (document.getElementById('paywall-email').value || '').trim(); const msg = document.getElementById('paywall-msg'); if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = t('pay_need_email'); return; } - const body = { purpose, job_id: paywallJob, email }; - if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0; const fb = document.getElementById('paywall-file-btn'), sb = document.getElementById('paywall-sub-btn'); - fb.disabled = true; sb.disabled = true; msg.textContent = t('pay_creating'); + fb.disabled = true; sb.disabled = true; + // Если файл ещё не загружен (пейволл показан ДО загрузки) — грузим сейчас + if (!paywallJob && paywallMode !== 'batch' && selectedFile) { + msg.textContent = t('pay_uploading'); + try { + const data = await uploadFileChunked(selectedFile, collectSettings(), null); + paywallJob = data.job_id; + } catch (e) { msg.textContent = (e && e.message) ? e.message : t('err_network'); fb.disabled = false; sb.disabled = false; return; } + } + const body = { purpose, email }; + if (paywallJob) body.job_id = paywallJob; + if (tgUser) body.user_id = tgUser.user_id || tgUser.id || 0; + msg.textContent = t('pay_creating'); try { const r = await fetch('/api/pay/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const d = await r.json(); @@ -2569,14 +2624,23 @@ acceptFiles(gatherFiles(folderInput.files)); }); + // Пакетная обработка (2+ файла / папка / ZIP) — только по подписке. + function requireSubForBatch(proceed) { + checkPaywall(0, 0).then(chk => { + if (chk.paywall_enabled && !chk.sub_active) { + showPaywall(null, chk.file_price, chk.sub_price, 'batch'); + } else { proceed(); } + }); + } + // Один файл → классический одиночный поток; несколько → батч со списком и ZIP. function acceptFiles(arr) { if (!arr || !arr.length) return; hideError(); hideInfo(); hideDonateCard(); submitBtn.style.display = 'block'; // мог быть скрыт после прошлого батча - // ZIP-архив: распаковывается на сервере, каждый вложенный файл — отдельное задание. + // ZIP-архив (форма батча) — по подписке. const zips = arr.filter(f => /\.zip$/i.test(f.name)); - if (zips.length) { handleZip(zips[0]); return; } + if (zips.length) { requireSubForBatch(() => handleZip(zips[0])); return; } let files = arr; if (files.length > MAX_BATCH) { showError(t('err_too_many')(MAX_BATCH)); @@ -2587,12 +2651,15 @@ document.getElementById('batch-card').style.display = 'none'; handleFile(files[0]); } else { - batchMode = true; - selectedFile = null; - fileBadge.style.display = 'none'; - document.getElementById('result-card').style.display = 'none'; - document.getElementById('progress-card').style.display = 'none'; - handleFiles(files); + // Пакет из нескольких файлов — гейт по подписке + requireSubForBatch(() => { + batchMode = true; + selectedFile = null; + fileBadge.style.display = 'none'; + document.getElementById('result-card').style.display = 'none'; + document.getElementById('progress-card').style.display = 'none'; + handleFiles(files); + }); } } @@ -2999,8 +3066,15 @@ } } - function submitJob() { + async function submitJob() { if (!selectedFile) { showError(t('err_no_file')); return; } + // Пейволл БЕЗ загрузки: только для потенциально больших файлов (мелкие не дёргают сервер) + if (fileSizeMb > AUTH_MAX_MB || filePages > AUTH_MAX_PAGES) { + submitBtn.disabled = true; + const chk = await checkPaywall(fileSizeMb, filePages); + submitBtn.disabled = false; + if (chk.requires_payment) { showPaywall(null, chk.file_price, chk.sub_price, 'file'); return; } + } // Крупные файлы — через чанковую загрузку (туннель не тянет большое тело за раз) if (selectedFile.size > CHUNK_SIZE) { submitJobChunked(); return; } hideError();