// ==UserScript==
// @name MZ - Exportar Transferências (CSV)
// @namespace MZTools
// @version 0.1.0
// @description Exporta jogadores no mercado (todas as páginas) em CSV com atributos e metadados. Robusto a temas/idiomas, ignora Forma.
// @author MZ Tools
// @match *://*.managerzone.com/*
// @icon https://www.managerzone.com/favicon.ico
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/** ==========================
* CONFIG / LABELS / HELPERS
* ==========================
*/
// Mapeamento de labels dos atributos → chave no CSV
// Aceita variações em PT/EN/ES e ignora maiúsc/minúsc.
const ATTR_LABELS = {
// Físicos / mentais (ajuste se seu idioma estiver diferente)
'velocidade': 'Velocidade',
'speed': 'Velocidade',
'resist': 'Resistência',
'stamina': 'Resistência',
'intelig': 'Inteligência',
'intelligence': 'Inteligência',
'passe': 'Passe',
'passing': 'Passe',
'chute': 'Chute',
'shooting': 'Chute',
'cabece': 'Cabeceio',
'heading': 'Cabeceio',
'controle': 'Controle de Bola',
'ball control': 'Controle de Bola',
'marcação': 'Marcação',
'marking': 'Marcação',
'posicion': 'Posicionamento',
'positioning': 'Posicionamento',
'tackle': 'Desarme',
'desarme': 'Desarme',
'corrida com a bola': 'Corrida com Bola',
'dribbling': 'Corrida com Bola',
'finalização': 'Finalização',
'finishing': 'Finalização',
'cruzamento': 'Cruzamento',
'crossing': 'Cruzamento',
'experi': 'Experiência',
'experience': 'Experiência',
// goleiro (se aparecer nos cards)
'goleiro': 'Goleiro',
'keeper': 'Goleiro'
};
// Labels para estrelas (alto1, baixo2) e velocidade de treino
const STAR_LABELS = {
// “alto1” e “baixo2” são nomes de categorias no seu fluxo; aqui salvamos como colunas
alto1: ['alto', 'high', 'alto1'],
baixo2: ['baixo', 'low', 'baixo2'],
treino: ['velocidade de treino', 'training speed', 'velocidad de entrenamiento']
};
// Funções auxiliares
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const normText = (t) => (t || '').replace(/\s+/g, ' ').trim();
const onlyDigits = (t) => (t || '').replace(/[^\d]/g, '');
const toNumber = (t) => {
if (t == null) return null;
const n = String(t).replace(/[^\d.,-]/g, '').replace(/\./g, '').replace(',', '.');
const v = parseFloat(n);
return Number.isFinite(v) ? v : null;
};
// CSV safe
const csvCell = (v) => {
if (v == null) return '';
const s = String(v);
return /[;"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
};
// Tenta ler número de "bolas" (total de pontos/níveis) exibidos no card
function parseTotalBolas(container) {
// Heurística 1: badges/bolinhas como <span class="ball ..."> repetidas
const balls = $$('.ball', container);
if (balls.length) return balls.length;
// Heurística 2: texto tipo "Total de bolas: 123"
const byText = $$('.player-attr, .small, .tiny, .text-muted', container)
.map(el => normText(el.textContent))
.find(t => /total.*bolas|balls total|total.*pontos/i.test(t));
if (byText) {
const n = toNumber(onlyDigits(byText));
if (n) return n;
}
return '';
}
// Conta estrelas num bloco (checa <i class="fa-star">, <img alt="*">, etc.)
function countStarsIn(node) {
if (!node) return '';
const starLike = $$('i[class*="star"], svg[class*="star"], img[alt*="star"], img[src*="star"]', node);
if (starLike.length) {
// Se houver half-stars, ajuste aqui (ex.: class*="half"); por ora, contamos inteiras.
return starLike.length;
}
// Fallback: procura “★”
const txt = normText(node.textContent);
const stars = (txt.match(/★/g) || []).length;
return stars || '';
}
// Encontra um bloco próximo cujo label contem determinado termo
function findBlockByLabel(root, labelCandidates) {
const nodes = $$('*', root).filter(el => {
const t = normText(el.textContent).toLowerCase();
return labelCandidates.some(lab => t.includes(lab));
});
return nodes[0] || null;
}
// Lê “alto/baixo” determinante a partir de rótulos contendo (1) ou (2)
function parseAltoBaixoDeterminate(attrList) {
// attrList: array de {label, value, rawLabel}
// Regra: se o label contém "1", determinante = 'alto1'; se contém "2", 'baixo2'.
// Salva também o NOME do atributo que carregou o 1 ou 2.
let det = '';
let atributo = '';
for (const row of attrList) {
const raw = (row.rawLabel || '').toLowerCase();
if (/\b1\b|\(1\)/.test(raw)) { det = '1'; atributo = row.label; break; }
if (/\b2\b|\(2\)/.test(raw)) { det = '2'; atributo = row.label; break; }
}
return { alto_baixo: det, atributo_determinante: atributo };
}
// Extrai ID do jogador a partir de links
function extractPlayerId(container) {
const a = $('a[href*="player_id="]', container) || $('a[href*="players&"]', container);
if (!a) return '';
const m = a.href.match(/player_id=(\d+)/);
return m ? m[1] : '';
}
// Extrai nome (a partir do link do jogador ou título do card)
function extractName(container) {
const a = $('a[href*="player_id="]', container);
if (a && a.textContent) return normText(a.textContent);
// fallback: o primeiro H-tag com texto
const h = $('h3, h4, .player-name, .name', container);
return h ? normText(h.textContent) : '';
}
// Extrai idade e temporada de nascimento a partir de textos frequentes no card
function extractAgeAndSeason(container) {
const textNodes = $$('.small, .tiny, .text-muted, .player-meta, .details', container)
.map(el => normText(el.textContent));
let idade = '';
let temporada = '';
for (const t of textNodes) {
// Exemplos: "Idade: 23 (Nasc.: temp 95)", "Age: 23 (Born: season 95)"
const mAge = t.match(/idade[:\s]*?(\d{1,2})|age[:\s]*?(\d{1,2})/i);
if (mAge) idade = mAge[1] || mAge[2] || idade;
const mSeason = t.match(/temp(?:orad[ao])?[:\s]*?(\d{2,3})|season[:\s]*?(\d{2,3})/i);
if (mSeason) temporada = mSeason[1] || mSeason[2] || temporada;
}
return { idade, temporada_nascimento: temporada };
}
// Extrai prazo final (deadline) e valor solicitado
function extractDeadlineAndPrice(container) {
const txts = $$('*', container).map(el => normText(el.textContent));
// Prazo final
const tPrazo = txts.find(t => /prazo|deadline|fecha.*em|ends/i.test(t)) || '';
// ex.: "Prazo final: 2025-09-30 18:00", "Deadline: in 3h 14m", etc.
let prazo_final = '';
const mDate = tPrazo.match(/\b(\d{4}-\d{2}-\d{2}.*?)$/) || tPrazo.match(/(\d{1,2}[:h]\d{2}.*)/);
if (mDate) prazo_final = mDate[1];
// Valor solicitado
const tPreco = txts.find(t => /valor|preço|asking|ask/i.test(t)) || '';
// números com milhar/ponto e decimal/vírgula
const mMoney = tPreco.match(/([\d\.\,]+)/);
const valor_solicitado = mMoney ? mMoney[1] : '';
return { prazo_final, valor_solicitado };
}
// Extrai tabela/lista de atributos do card
function extractAttributes(container) {
const rows = [];
// Pega linhas que parecem "Label: valor" ou estão lado a lado
const possibleRows = $$('li, .row, .attr, .attribute, tr', container);
for (const r of possibleRows) {
const t = normText(r.textContent).toLowerCase();
// encontre um label conhecido
const key = Object.keys(ATTR_LABELS).find(k => t.includes(k));
if (!key) continue;
// label “cru” para detecção de (1)/(2)
const rawLabelNode = $('*', r) || r;
const rawLabel = normText(rawLabelNode.textContent);
// tenta achar número do atributo no mesmo bloco
let val = '';
// 1) números explícitos
const m = rawLabel.match(/(\d{1,2})\s*$/) || normText(r.textContent).match(/(\d{1,2})\s*$/);
if (m) val = m[1];
// 2) bolinhas (nível visual)
if (!val) {
const dots = $$('i[class*="dot"], span[class*="dot"], .ball', r).length;
if (dots) val = String(dots);
}
rows.push({
label: ATTR_LABELS[key],
value: val,
rawLabel: rawLabel
});
}
// Consolida num objeto {Atributo: valor}
const out = {};
for (const row of rows) {
out[row.label] = row.value;
}
// Determina alto/baixo e atributo determinante
const det = parseAltoBaixoDeterminate(rows);
return { attrs: out, ...det };
}
// Extrai estrelas para alto1, baixo2, velocidade de treino
function extractStars(container) {
const findAndCount = (labels) => {
const node = findBlockByLabel(container, labels);
return countStarsIn(node);
};
return {
estrelas_alto1: findAndCount(STAR_LABELS.alto1.map(s => s.toLowerCase())),
estrelas_baixo2: findAndCount(STAR_LABELS.baixo2.map(s => s.toLowerCase())),
estrelas_vel_treino: findAndCount(STAR_LABELS.treino.map(s => s.toLowerCase()))
};
}
// Seleciona “cards/linhas” de jogadores nos resultados
function selectPlayerCards() {
// Heurísticas comuns:
// - tabela: linhas com link ?player_id=...
// - cards: divs com link do jogador
const byTable = $$('tr').filter(tr => $('a[href*="player_id="]', tr));
if (byTable.length) return byTable;
const byCard = $$('div, li').filter(div => $('a[href*="player_id="]', div));
return byCard;
}
// Extrai 1 jogador a partir de um card/linha
function scrapeOne(container) {
const id = extractPlayerId(container);
const nome = extractName(container);
const { idade, temporada_nascimento } = extractAgeAndSeason(container);
const total_bolas = parseTotalBolas(container);
const { prazo_final, valor_solicitado } = extractDeadlineAndPrice(container);
const { attrs, alto_baixo, atributo_determinante } = extractAttributes(container);
const { estrelas_alto1, estrelas_baixo2, estrelas_vel_treino } = extractStars(container);
return {
ID: id,
Nome: nome,
Idade: idade,
'Temporada Nascimento': temporada_nascimento,
'Total de Bolas': total_bolas,
'Prazo Final': prazo_final,
'Valor Solicitado': valor_solicitado,
// Atributos (ignorando Forma)
Velocidade: attrs['Velocidade'] || '',
'Resistência': attrs['Resistência'] || '',
Inteligência: attrs['Inteligência'] || '',
Passe: attrs['Passe'] || '',
Chute: attrs['Chute'] || '',
Cabeceio: attrs['Cabeceio'] || '',
'Controle de Bola': attrs['Controle de Bola'] || '',
Marcação: attrs['Marcação'] || '',
Posicionamento: attrs['Posicionamento'] || '',
Desarme: attrs['Desarme'] || '',
'Corrida com Bola': attrs['Corrida com Bola'] || '',
Finalização: attrs['Finalização'] || '',
Cruzamento: attrs['Cruzamento'] || '',
Experiência: attrs['Experiência'] || '',
// Estrelas
'Estrelas Alto1': estrelas_alto1,
'Estrelas Baixo2': estrelas_baixo2,
'Velocidade de Treinamento (★)': estrelas_vel_treino,
// Determinante alto/baixo
'Alto/Baixo (1/2)': alto_baixo,
'Atributo Determinante': atributo_determinante
};
}
// Constrói CSV com separador ';'
function toCSV(rows) {
const headers = [
'ID','Nome','Idade','Temporada Nascimento','Total de Bolas','Prazo Final','Valor Solicitado',
'Velocidade','Resistência','Inteligência','Passe','Chute','Cabeceio','Controle de Bola','Marcação',
'Posicionamento','Desarme','Corrida com Bola','Finalização','Cruzamento','Experiência',
'Estrelas Alto1','Estrelas Baixo2','Velocidade de Treinamento (★)',
'Alto/Baixo (1/2)','Atributo Determinante'
];
const lines = [
headers.map(csvCell).join(';'),
...rows.map(r => headers.map(h => csvCell(r[h])).join(';'))
];
return lines.join('\n');
}
// Dispara download
function download(name, text) {
const blob = new Blob([text], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
setTimeout(() => {
URL.revokeObjectURL(url);
a.remove();
}, 250);
}
/** ==========================
* SCRAPER (PÁGINA ATUAL)
* ==========================
*/
function scrapeCurrentPage() {
const cards = selectPlayerCards();
const rows = [];
for (const c of cards) {
try {
const row = scrapeOne(c);
if (row.ID || row.Nome) rows.push(row);
} catch (e) {
// silencioso, segue o baile
}
}
return rows;
}
/** ==========================
* PAGINAÇÃO (TODAS AS PÁGINAS)
* ==========================
*
* Estratégia: usamos ajaxSearchOffset se existir; caso não exista no escopo,
* simulamos cliques nos links de paginação que tenham offset.
*/
async function scrapeAllPages() {
const aggregate = [];
const seen = new Set();
// Se a função global existir, iteramos offsets razoáveis (até 1000 registros)
const ajaxFn = window.ajaxSearchOffset;
if (typeof ajaxFn === 'function') {
// Detecta quantos offsets existem na paginação atual (lê links)
const getOffsetsFromDOM = () =>
$$('a[href*="offset="], a.page-link, .pagination a')
.map(a => {
const m = (a.getAttribute('onclick') || '').match(/ajaxSearchOffset\((\d+)\)/) ||
(a.href || '').match(/offset=(\d+)/);
return m ? parseInt(m[1], 10) : null;
})
.filter(v => Number.isInteger(v));
const offsets = Array.from(new Set([0, ...getOffsetsFromDOM()])).sort((a,b)=>a-b);
for (const off of offsets) {
await goOffset(off);
await waitForResultsMutation();
const pageRows = scrapeCurrentPage();
for (const r of pageRows) {
const key = r.ID || r.Nome + '|' + r['Prazo Final'] + '|' + r['Valor Solicitado'];
if (!seen.has(key)) {
seen.add(key);
aggregate.push(r);
}
}
}
return aggregate;
}
// Fallback: sem ajaxSearchOffset — tentamos clicar nos links de paginação
const pagerLinks = $$('a[href*="offset="], a.page-link, .pagination a');
if (!pagerLinks.length) {
// Sem paginador → só atual
return scrapeCurrentPage();
}
// Primeira página
aggregate.push(...scrapeCurrentPage());
// Demais páginas (abrindo em AJAX se for SPA, ou navegando)
for (const a of pagerLinks) {
if (a.click) a.click();
await waitForResultsMutation();
aggregate.push(...scrapeCurrentPage());
}
return dedupe(aggregate);
function dedupe(arr) {
const out = [];
const s = new Set();
for (const r of arr) {
const key = r.ID || r.Nome + '|' + r['Prazo Final'] + '|' + r['Valor Solicitado'];
if (!s.has(key)) {
s.add(key);
out.push(r);
}
}
return out;
}
function waitForResultsMutation() {
return new Promise(resolve => {
const container = document.body;
const obs = new MutationObserver((muts) => {
// heurística: quando houver muitos links de players, deve ter carregado
const count = $$('a[href*="player_id="]').length;
if (count > 0) {
obs.disconnect();
setTimeout(resolve, 100); // pequeno buffer
}
});
obs.observe(container, { childList: true, subtree: true });
// timeout de segurança
setTimeout(() => { obs.disconnect(); resolve(); }, 4000);
});
}
async function goOffset(n) {
try {
const ret = ajaxFn(n);
// Se ajaxFn não retornar Promise, aguarda um pequeno tempo
if (ret && typeof ret.then === 'function') await ret;
await new Promise(r => setTimeout(r, 200));
} catch (e) {
await new Promise(r => setTimeout(r, 300));
}
}
}
/** ==========================
* UI (PAINEL FLUTUANTE)
* ==========================
*/
function injectPanel() {
if ($('#mz-export-panel')) return;
const panel = document.createElement('div');
panel.id = 'mz-export-panel';
panel.innerHTML = `
<div style="
position: fixed; right: 14px; bottom: 14px; z-index: 99999;
background: rgba(15, 17, 23, 0.9); color: #fff; padding: 12px 14px;
border-radius: 12px; box-shadow: 0 6px 18px rgba(0,0,0,.25); font: 13px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, sans-serif;">
<div style="font-weight:600;margin-bottom:8px;">MZ • Exportar Transferências</div>
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px;">
<button id="mz-btn-page" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;">Coletar (página)</button>
<button id="mz-btn-all" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;">Coletar (todas páginas)</button>
<button id="mz-btn-dl" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;" disabled>Baixar CSV</button>
</div>
<div id="mz-status" style="opacity:.85">Aguardando…</div>
</div>
`;
document.body.appendChild(panel);
let buffer = [];
$('#mz-btn-page').addEventListener('click', () => {
try {
const rows = scrapeCurrentPage();
buffer = rows;
$('#mz-status').textContent = `Coletados (página): ${rows.length}`;
$('#mz-btn-dl').disabled = rows.length === 0;
} catch (e) {
$('#mz-status').textContent = `Erro: ${e.message || e}`;
}
});
$('#mz-btn-all').addEventListener('click', async () => {
$('#mz-status').textContent = 'Coletando todas as páginas…';
try {
buffer = await scrapeAllPages();
$('#mz-status').textContent = `Coletados (todas páginas): ${buffer.length}`;
$('#mz-btn-dl').disabled = buffer.length === 0;
} catch (e) {
$('#mz-status').textContent = `Erro: ${e.message || e}`;
}
});
$('#mz-btn-dl').addEventListener('click', () => {
if (!buffer.length) return;
const csv = toCSV(buffer);
const stamp = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-');
download(`mz_transferencias_${stamp}.csv`, csv);
});
}
/** ==========================
* BOOT
* ==========================
*/
function isTransferPage() {
const href = location.href;
return /[?&]p=transfer(&|$)/i.test(href) || /\/transfer/i.test(href);
}
function boot() {
if (!isTransferPage()) return;
injectPanel();
}
// Espera DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();