MovixOpenSource/API/Mainapi/utils/__tests__/helpers/fakeVipInvoicePool.js
Movix 3a7ab5ad42 feat: synchronise le repo privé — Cast, sources, API et mobile 2.5.7
Gros lot de travail synchronisé depuis le repo privé. Cette mise à jour
regroupe la refonte du lecteur et du Cast, de nouvelles sources VOD/Live TV,
le durcissement de l’API et des proxies, ainsi qu’une nouvelle version de
l’application mobile.

Lecteur, sources et Live TV

- Refonte du lecteur HLS avec sélection et mémorisation de la qualité,
  détection des résolutions, scan des liens, choix des pistes audio et
  fallback contrôlé de 1080p vers 720p.
- Ajoute une garde contre les boucles de changement de source, notamment
  lors des erreurs vidéo et des réponses 429.
- Revoit SeekStreaming de bout en bout : validation des embeds, prise en
  charge de plusieurs candidats, regroupement par serveur, règles de headers
  persistantes et cache d’extraction raccourci.
- Ajoute SwiftFlow pour les films et séries, avec lecteurs VF/VOSTFR, cache
  stale-while-revalidate, déduplication des requêtes et limite partagée côté API.
- Adapte Coflix à son nouveau domaine et à son nouveau thème, ajoute un kill
  switch, développe les wrappers 1jour1film et actualise le domaine Wiflix.
- Ajoute NorthLive et Vavoo à la Live TV, regroupe les variantes d’une même
  chaîne et permet de choisir le serveur ou la qualité.
- Enrichit les Top 10 avec des filtres par période et par algorithme, ainsi
  que de nouvelles métriques de visionnage.

Cast et application mobile

- Unifie le contrôle Cast entre le Web Cast SDK et le pont Android :
  état distant, lecture, pause, seek, arrêt et reprise après reconnexion.
- Ajoute sur Android un relais média LAN sécurisé, limité au Chromecast et
  au réseau Wi-Fi sélectionnés, avec sessions opaques et service foreground.
- Prépare les sources avant diffusion et prend en charge les playlists HLS
  TS/fMP4, les flux progressifs, les sous-titres VTT, SeekStreaming et les
  segments MPEG-TS encapsulés dans des PNG.
- Renforce le proxy média local : support Range, validation des redirections,
  liaison DNS/socket au réseau Cast et meilleure gestion des headers Fsvid.
- Sécurise le bridge WebView avec contrôle de l’origine top-frame, capacités
  propres à chaque document et invalidation lors des navigations.
- Ajoute le Picture-in-Picture Android avec lecture/pause et seek de
  10 secondes depuis la fenêtre flottante.
- Maintient désormais l’écran allumé uniquement pendant une lecture active.
- Publie l’application Android 2.5.7, build 16, avec le nouvel APK et ses
  métadonnées d’intégrité.

API, base de données et paiements

- Remplace 13 migrations SQL isolées par un manifeste de schéma MySQL
  centralisé, additif et capable de détecter les divergences de structure.
- Ajoute le workflow db:init avec prévisualisation, dry-run, verrou MySQL,
  confirmations pour les tables Wrapped et couverture de tests dédiée.
- Rend configurables les limites du pool MySQL et fiabilise les advisory locks.
- Revoit entièrement PayGate : références de callback séparées, nonces,
  validation des wallets et transactions, états monotones, déduplication des
  paiements et protection contre les courses ou réutilisations de txid.
- Renforce l’annulation, la réconciliation et la livraison idempotente des
  factures VIP, avec réponses no-store sur les routes sensibles.
- Distingue les JWT invalides des indisponibilités MySQL afin de renvoyer
  correctement 401 ou 503 sans provoquer de déconnexions inutiles.
- Utilise ZZAPI/Claude Haiku pour la modération, avec OpenRouter/DeepSeek en
  solution de secours.
- Améliore la gestion HLS et Range dans proxiesembed.

Frontend, profils et infrastructure

- Protège la sélection et l’hydratation des profils contre les réponses
  concurrentes, les intentions obsolètes et l’effacement accidentel du
  stockage local.
- Rend VITE_SITE_URL obligatoire et l’injecte dans le runtime, les données
  structurées, le service worker, robots.txt et sitemap.xml.
- Optimise les probes réseau du service worker avec cache court et single-flight.
- Charge dynamiquement les traductions anglaises, plusieurs routes secondaires
  et les données Search/TMDB uniquement lorsqu’elles deviennent nécessaires.
- Ajoute des caches mémoire/session pour les images et contenus TMDB et réduit
  les animations, calculs et rendus hors écran.
- Ajoute la page publique des frais, les coins carrés persistants, une nouvelle
  intro Movix, les avertissements de publicités adultes et de nombreux textes
  Cast, qualité, Live TV et sources.
- Met à jour les extensions Chrome 1.3.12 et Firefox 1.5.9, ainsi que le
  userscript, avec la nouvelle extraction SeekStreaming et la préparation Cast.
- Ajoute une large couverture de tests Kotlin et JavaScript pour le Cast, le
  proxy média, le PiP, le bridge, la base de données, PayGate et le lecteur.
- Ajoute bootstrap-icons et patch-package, régénère les lockfiles npm et
  actualise la documentation d’installation et d’initialisation MySQL.

Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
2026-08-02 12:09:37 +02:00

217 lines
6.8 KiB
JavaScript

'use strict';
function clone(value) {
return value === undefined ? undefined : structuredClone(value);
}
function normalizeSql(sql) {
return String(sql).replace(/\s+/g, ' ').trim();
}
function createFakeVipInvoicePool(initialInvoices, options = {}) {
const rows = new Map(initialInvoices.map((invoice) => [Number(invoice.id), clone(invoice)]));
const events = [];
const calls = [];
const stats = {
updates: 0,
identityUpdates: 0,
commits: 0,
rollbacks: 0
};
let transactionTail = Promise.resolve();
function findBy(field, value) {
return [...rows.values()].find((row) => row[field] === value) || null;
}
async function read(sql, params) {
const normalized = normalizeSql(sql);
if (/WHERE paygate_callback_reference = \?/i.test(normalized)) {
return [[clone(findBy('paygate_callback_reference', params[0]))].filter(Boolean), []];
}
if (/WHERE public_id = \?/i.test(normalized)) {
const invoice = findBy('public_id', params[0]);
if (/paygate_callback_reference IS NULL/i.test(normalized) && invoice?.paygate_callback_reference) {
return [[], []];
}
return [[clone(invoice)].filter(Boolean), []];
}
if (/WHERE id = \?/i.test(normalized) && /SELECT \*/i.test(normalized)) {
return [[clone(rows.get(Number(params[0])))].filter(Boolean), []];
}
if (/SELECT id FROM vip_invoices WHERE paygate_paid_txid_in = \?/i.test(normalized)) {
const owner = [...rows.values()].find((row) => (
row.paygate_paid_txid_in === params[0] && Number(row.id) !== Number(params[1])
));
return [[owner ? { id: owner.id } : null].filter(Boolean), []];
}
if (/SELECT key_value FROM access_keys WHERE key_value = \?/i.test(normalized)) {
return [[], []];
}
return null;
}
async function executeSql(sql, params, scope) {
const normalized = normalizeSql(sql);
calls.push({ scope, sql: normalized, params: clone(params) });
const selected = await read(normalized, params);
if (selected) return selected;
if (/UPDATE vip_invoices SET status = \?/i.test(normalized)) {
const [
nextStatus,
txHash,
markPaidAt,
paidCoin,
paidValue,
valueCoin,
txidIn,
txidOut,
_statusForNextCheck,
invoiceId
] = params;
const invoice = rows.get(Number(invoiceId));
if (!invoice || !['awaiting_payment', 'partial_payment', 'expired'].includes(invoice.status)) {
return [{ affectedRows: 0 }, []];
}
invoice.status = nextStatus;
invoice.tx_hash = invoice.tx_hash || txHash;
invoice.paid_at = markPaidAt ? (invoice.paid_at || new Date().toISOString()) : invoice.paid_at;
invoice.paygate_paid_coin = paidCoin;
invoice.paygate_paid_value = paidValue;
invoice.amount_crypto_received = valueCoin;
invoice.paygate_paid_txid_in = invoice.paygate_paid_txid_in || txidIn;
invoice.paygate_paid_txid = invoice.paygate_paid_txid || txidOut;
stats.updates += 1;
return [{ affectedRows: 1 }, []];
}
if (/UPDATE vip_invoices SET paygate_paid_txid_in = \?/i.test(normalized)) {
if (options.failTxidClaim) throw options.failTxidClaim;
const [txidIn, invoiceId] = params;
const invoice = rows.get(Number(invoiceId));
if (
!invoice
|| invoice.status === 'cancelled'
|| invoice.paygate_paid_txid_in
) {
return [{ affectedRows: 0 }, []];
}
const duplicateOwner = [...rows.values()].find((row) => (
Number(row.id) !== Number(invoiceId)
&& row.paygate_paid_txid_in === txidIn
));
if (duplicateOwner) {
const error = new Error('Duplicate entry');
error.code = 'ER_DUP_ENTRY';
error.errno = 1062;
throw error;
}
invoice.paygate_paid_txid_in = txidIn;
stats.identityUpdates += 1;
return [{ affectedRows: 1 }, []];
}
if (/UPDATE vip_invoices SET status = 'paid'/i.test(normalized)) {
const paymentMethod = params[0];
const invoiceId = Number(params.at(-1));
const invoice = rows.get(invoiceId);
if (!invoice) {
return [{ affectedRows: 0 }, []];
}
invoice.status = 'paid';
invoice.paid_at = invoice.paid_at || new Date().toISOString();
if (paymentMethod === 'paygate_hosted') {
invoice.paygate_paid_coin = invoice.paygate_paid_coin || 'manual';
invoice.paygate_paid_value = invoice.paygate_paid_value ?? invoice.amount_usd;
}
stats.updates += 1;
return [{ affectedRows: 1 }, []];
}
if (/UPDATE vip_invoices SET status = 'delivered'/i.test(normalized)) {
const invoiceId = Number(params.at(-1));
const invoice = rows.get(invoiceId);
if (!invoice || invoice.status !== 'paid' || invoice.vip_key_value) {
return [{ affectedRows: 0 }, []];
}
invoice.status = 'delivered';
invoice.vip_key_value = params[0];
return [{ affectedRows: 1 }, []];
}
if (/INSERT INTO vip_invoice_events/i.test(normalized)) {
if (options.failEvent) throw options.failEvent;
events.push({
invoiceId: params[0],
eventType: params[1],
actorType: params[2],
actorId: params[3],
message: params[4],
payload: params[5] ? JSON.parse(params[5]) : null
});
return [{ insertId: events.length, affectedRows: 1 }, []];
}
return [{ affectedRows: 1 }, []];
}
const pool = {
async execute(sql, params = []) {
return executeSql(sql, params, 'pool');
},
async getConnection() {
let unlock = null;
let snapshot = null;
let eventLength = 0;
return {
async beginTransaction() {
const previous = transactionTail;
transactionTail = new Promise((resolve) => {
unlock = resolve;
});
await previous;
snapshot = new Map([...rows].map(([id, row]) => [id, clone(row)]));
eventLength = events.length;
},
async execute(sql, params = []) {
return executeSql(sql, params, 'connection');
},
async commit() {
stats.commits += 1;
unlock?.();
unlock = null;
},
async rollback() {
stats.rollbacks += 1;
rows.clear();
for (const [id, row] of snapshot || []) rows.set(id, row);
events.length = eventLength;
unlock?.();
unlock = null;
},
release() {
unlock?.();
unlock = null;
}
};
}
};
return {
pool,
events,
calls,
stats,
getInvoice(id) {
return clone(rows.get(Number(id)));
},
mutateInvoice(id, changes) {
Object.assign(rows.get(Number(id)), clone(changes));
}
};
}
module.exports = {
createFakeVipInvoicePool
};