Greasy Fork is available in English.
Monitors the transfer market for players from a specific country
当前为
// ==UserScript==
// @name MZ - Country Transfer Monitor
// @namespace douglaskampl
// @version 1.3
// @description Monitors the transfer market for players from a specific country
// @author Douglas
// @match https://www.managerzone.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @resource SWEETALERT2_CSS https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
GM_addStyle(GM_getResourceText('SWEETALERT2_CSS'));
GM_addStyle(`
#country-monitor-wrapper {
display: flex;
align-items: center;
margin-left: 15px;
position: relative;
}
#country-monitor-flag {
width: 22px;
height: 17px;
object-fit: contain;
vertical-align: middle;
border: 1px solid #ddd;
border-radius: 2px;
transition: transform 0.2s ease;
cursor: pointer;
}
#country-monitor-flag:hover {
transform: scale(1.1);
}
#country-monitor-name {
margin-left: 5px;
font-size: 12px;
color: #666;
display: none;
}
#rare-finder-btn {
margin-left: 10px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #3498db;
color: white;
border-radius: 50%;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
font-size: 14px;
}
#rare-finder-btn:hover {
background: #2980b9;
transform: scale(1.1);
}
#rare-countries-results {
display: flex;
flex-wrap: wrap;
margin-left: 10px;
align-items: center;
}
.rare-country-result {
display: flex;
align-items: center;
margin-right: 10px;
background: rgba(52, 152, 219, 0.1);
padding: 4px 8px;
border-radius: 12px;
cursor: pointer;
transition: background 0.2s;
}
.rare-country-result:hover {
background: rgba(52, 152, 219, 0.2);
}
.rare-country-result img {
width: 16px;
height: 12px;
border: 1px solid #ddd;
margin-right: 5px;
}
.rare-country-result span {
font-size: 11px;
color: #e74c3c;
font-weight: bold;
}
#country-selector {
position: absolute;
top: 100%;
right: 0;
z-index: 9999;
background: #2c3e50;
color: #fff;
border: 1px solid #34495e;
border-radius: 4px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
width: 300px;
max-height: 400px;
overflow-y: auto;
padding: 10px;
display: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
#country-selector.visible {
opacity: 1;
transform: translateY(0);
}
#country-selector::-webkit-scrollbar {
width: 8px;
}
#country-selector::-webkit-scrollbar-track {
background: #34495e;
border-radius: 4px;
}
#country-selector::-webkit-scrollbar-thumb {
background: #7f8c8d;
border-radius: 4px;
}
#country-selector::-webkit-scrollbar-thumb:hover {
background: #95a5a6;
}
#country-search {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #34495e;
background-color: #34495e;
color: white;
border-radius: 4px;
}
#country-search::placeholder {
color: #bdc3c7;
}
.country-option {
display: flex;
align-items: center;
padding: 6px 10px;
cursor: pointer;
border-radius: 4px;
color: #ecf0f1;
}
.country-option:hover {
background: #34495e;
}
.country-option img {
width: 24px;
height: 16px;
margin-right: 10px;
object-fit: contain;
border: 1px solid #34495e;
}
.country-option.selected {
background: #3498db;
}
#search-status {
margin-left: 10px;
font-size: 12px;
color: #95a5a6;
}
.rare-country-item {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
border-radius: 4px;
background: rgba(52, 73, 94, 0.1);
cursor: pointer;
}
.rare-country-item:hover {
background: rgba(52, 73, 94, 0.2);
}
.rare-country-flag {
width: 24px;
height: 16px;
margin-right: 10px;
border: 1px solid #ddd;
}
.rare-country-name {
flex-grow: 1;
}
.rare-country-count {
font-weight: bold;
color: #e74c3c;
margin-left: 10px;
}
.swal2-html-container {
max-height: 400px;
overflow-y: auto;
}
`);
const CONFIG = {
CHECK_INTERVAL_HOURS: 6,
TOAST_DURATION: 5000,
DEFAULT_COUNTRY_NAME: 'Country',
DEFAULT_COUNTRY_CID: null,
COUNTRIES_JSON_URL: 'https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/countries.json',
MAX_RARE_PLAYERS: 15
};
let countries = [];
let selectedCountry = {
cid: GM_getValue('selectedCountryCid', CONFIG.DEFAULT_COUNTRY_CID),
name: GM_getValue('selectedCountryName', CONFIG.DEFAULT_COUNTRY_NAME),
code: GM_getValue('selectedCountryCode', '')
};
let knownPlayers = GM_getValue('knownPlayers', {});
let lastChecked = GM_getValue('lastChecked', 0);
let isScanning = false;
function getFlagUrl(code) {
const upperCode = (code || '').toUpperCase();
if (upperCode === 'SC') {
return 'https://cdn.jsdelivr.net/gh/lipis/flag-icons/flags/4x3/gb-sct.svg';
} else if (upperCode === 'WL') {
return 'https://cdn.jsdelivr.net/gh/lipis/flag-icons/flags/4x3/gb-wls.svg';
} else if (upperCode === 'NI') {
return 'https://cdn.jsdelivr.net/gh/lipis/flag-icons/flags/4x3/gb-nir.svg';
} else if (upperCode === 'EN') {
return 'https://cdn.jsdelivr.net/gh/lipis/flag-icons/flags/4x3/gb-eng.svg';
} else if (upperCode === 'DC') {
return 'https://placehold.co/16x12/gray/white?text=MZ';
}
return `https://flagcdn.com/16x12/${(code || '').toLowerCase()}.png`;
}
function cleanupKnownPlayers() {
const now = Date.now();
const threeDaysInMs = 3 * 24 * 60 * 60 * 1000;
let modified = false;
Object.keys(knownPlayers).forEach(playerId => {
if (now - knownPlayers[playerId].firstSeen > threeDaysInMs) {
delete knownPlayers[playerId];
modified = true;
}
});
if (modified) {
GM_setValue('knownPlayers', knownPlayers);
}
}
function fetchCountries() {
fetch(CONFIG.COUNTRIES_JSON_URL)
.then(response => response.json())
.then(data => {
countries = data;
countries.sort((a, b) => a.name.localeCompare(b.name));
buildUI();
})
.catch(error => {});
}
function buildUI() {
if (!isTransferPage()) return;
const searchButton = document.getElementById('tds');
if (!searchButton) return;
const monitorWrapper = document.createElement('div');
monitorWrapper.id = 'country-monitor-wrapper';
monitorWrapper.style.display = 'inline-block';
monitorWrapper.style.marginLeft = '10px';
const flagImg = document.createElement('img');
flagImg.id = 'country-monitor-flag';
flagImg.src = selectedCountry.code ? getFlagUrl(selectedCountry.code) : 'https://placehold.co/16x12/gray/white?text=?';
flagImg.alt = selectedCountry.name;
flagImg.title = "Select country to monitor";
const rareFinderBtn = document.createElement('div');
rareFinderBtn.id = 'rare-finder-btn';
rareFinderBtn.innerHTML = '🔍';
rareFinderBtn.title = 'Find countries with < 5 19-year-olds';
const rareCountriesResults = document.createElement('div');
rareCountriesResults.id = 'rare-countries-results';
const searchStatus = document.createElement('span');
searchStatus.id = 'search-status';
searchStatus.style.display = 'none';
const countryName = document.createElement('span');
countryName.id = 'country-monitor-name';
countryName.textContent = selectedCountry.name;
monitorWrapper.appendChild(flagImg);
monitorWrapper.appendChild(countryName);
monitorWrapper.appendChild(rareFinderBtn);
monitorWrapper.appendChild(searchStatus);
monitorWrapper.appendChild(rareCountriesResults);
const selector = document.createElement('div');
selector.id = 'country-selector';
const search = document.createElement('input');
search.id = 'country-search';
search.type = 'text';
search.placeholder = 'Search country...';
selector.appendChild(search);
countries.forEach(country => {
const option = document.createElement('div');
option.className = 'country-option';
if (selectedCountry.cid === country.cid) {
option.classList.add('selected');
}
const optionImg = document.createElement('img');
optionImg.src = getFlagUrl(country.code);
optionImg.alt = country.name;
const optionName = document.createElement('span');
optionName.textContent = country.name;
option.appendChild(optionImg);
option.appendChild(optionName);
option.addEventListener('click', () => {
selectCountry(country);
hideSelector(selector);
});
selector.appendChild(option);
});
search.addEventListener('input', function() {
const query = this.value.toLowerCase();
Array.from(selector.querySelectorAll('.country-option')).forEach(option => {
const name = option.querySelector('span').textContent.toLowerCase();
option.style.display = name.includes(query) ? 'flex' : 'none';
});
});
monitorWrapper.appendChild(selector);
flagImg.addEventListener('click', (e) => {
if (selector.classList.contains('visible')) {
hideSelector(selector);
} else {
showSelector(selector);
search.focus();
}
});
rareFinderBtn.addEventListener('click', () => {
findRareCountriesInline();
});
document.addEventListener('click', (e) => {
if (!monitorWrapper.contains(e.target) || (e.target !== flagImg && !selector.contains(e.target))) {
hideSelector(selector);
}
});
searchButton.insertAdjacentElement('afterend', monitorWrapper);
if (selectedCountry.cid) {
checkTransferMarket(false);
}
}
function showSelector(selector) {
selector.style.display = 'block';
selector.offsetHeight;
selector.classList.add('visible');
}
function hideSelector(selector) {
selector.classList.remove('visible');
setTimeout(() => {
if (!selector.classList.contains('visible')) {
selector.style.display = 'none';
}
}, 300);
}
function selectCountry(country) {
selectedCountry = {
cid: country.cid,
name: country.name,
code: country.code
};
GM_setValue('selectedCountryCid', country.cid);
GM_setValue('selectedCountryName', country.name);
GM_setValue('selectedCountryCode', country.code);
const flagImg = document.getElementById('country-monitor-flag');
if (flagImg) {
flagImg.src = getFlagUrl(country.code);
flagImg.alt = country.name;
flagImg.title = "Monitor players from: " + country.name;
}
const countryName = document.getElementById('country-monitor-name');
if (countryName) {
countryName.textContent = country.name;
}
checkTransferMarket(true);
}
function isTransferPage() {
return window.location.href.includes('p=transfer');
}
function isTransferSearchPage19() {
return window.location.href.includes('p=transfer') &&
window.location.href.includes('sub=search') &&
window.location.href.includes('agea=19') &&
window.location.href.includes('ageb=19');
}
function checkTransferMarket(forced = false) {
if (!selectedCountry.cid) return;
const now = Date.now();
const checkIntervalMs = CONFIG.CHECK_INTERVAL_HOURS * 60 * 60 * 1000;
if (!forced && (now - lastChecked < checkIntervalMs)) {
return;
}
cleanupKnownPlayers();
fetch(`https://www.managerzone.com/ajax.php?p=transfer&sub=transfer-search&sport=soccer&issearch=true&u=&nationality=${selectedCountry.cid}&deadline=0&category=&valuea=&valueb=&bida=&bidb=&agea=19&ageb=37&birth_season_low=56&birth_season_high=74&tot_low=0&tot_high=110&s0a=0&s0b=10&s1a=0&s1b=10&s2a=0&s2b=10&s3a=0&s3b=10&s4a=0&s4b=10&s5a=0&s5b=10&s6a=0&s6b=10&s7a=0&s7b=10&s8a=0&s8b=10&s9a=0&s9b=10&s10a=0&s10b=10&s11a=0&s11b=10&s12a=0&s12b=10`)
.then(response => response.json())
.then(data => {
lastChecked = now;
GM_setValue('lastChecked', lastChecked);
if (data.totalHits && parseInt(data.totalHits) > 0) {
processPlayers(data);
}
})
.catch(error => {});
}
function processPlayers(data) {
if (!data.players || data.players.includes("No players found")) {
return;
}
const playersData = data.players;
const pidRegex = /href=["'].*?pid=(\d+)/g;
let pidMatches = [...playersData.matchAll(pidRegex)];
const playerIdSpanRegex = /player_id_(\d+)/g;
let playerIdSpanMatches = [...playersData.matchAll(playerIdSpanRegex)];
const idRegex = /id: (\d+)/g;
let idMatches = [...playersData.matchAll(idRegex)];
let allPlayerIds = new Set([
...pidMatches.map(match => match[1]),
...playerIdSpanMatches.map(match => match[1]),
...idMatches.map(match => match[1])
]);
const playerNameRegex1 = /"([^"]+)"\s+([^<]+)<\/span>/g;
const playerNameRegex2 = /player_name">([^<]+)<\/span>/g;
let nameMatches1 = [...playersData.matchAll(playerNameRegex1)];
let nameMatches2 = [...playersData.matchAll(playerNameRegex2)];
let players = [];
if (nameMatches1.length > 0) {
nameMatches1.forEach((match, index) => {
if (index < allPlayerIds.size) {
const playerName = match[2] ? match[2].trim() : match[1].trim();
const playerId = Array.from(allPlayerIds)[index];
players.push({ id: playerId, name: playerName });
}
});
} else if (nameMatches2.length > 0) {
nameMatches2.forEach((match, index) => {
if (index < allPlayerIds.size) {
const playerName = match[1].trim();
const playerId = Array.from(allPlayerIds)[index];
players.push({ id: playerId, name: playerName });
}
});
} else if (allPlayerIds.size > 0) {
Array.from(allPlayerIds).forEach(id => {
players.push({ id: id, name: id });
});
}
if (players.length === 0) {
try {
const jsonString = JSON.stringify(data);
const jsonIdRegex = /\d{9}/g;
const jsonIdMatches = [...new Set([...jsonString.matchAll(jsonIdRegex)].map(m => m[0]))];
jsonIdMatches.forEach(id => {
players.push({ id: id, name: id });
});
} catch (e) {}
}
if (!isScanning) {
const newPlayers = players.filter(player => !knownPlayers[player.id]);
if (newPlayers.length > 0) {
showPlayerNotification(newPlayers);
newPlayers.forEach(player => {
knownPlayers[player.id] = { k: player.name, firstSeen: Date.now() };
});
GM_setValue('knownPlayers', knownPlayers);
}
}
return players.length;
}
function showPlayerNotification(players) {
const htmlContent = `<p>New players from ${selectedCountry.name} found (${players.length}). Click "View" to see them on the transfer market.</p>`;
Swal.fire({
title: 'New Players!',
html: htmlContent,
icon: 'info',
showCancelButton: true,
confirmButtonText: 'View',
cancelButtonText: 'Close'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = `https://www.managerzone.com/?p=transfer&sub=search&sport=soccer&nationality=${selectedCountry.cid}`;
}
});
}
function countPlayersFromCountry(country) {
return new Promise((resolve, reject) => {
fetch(`https://www.managerzone.com/ajax.php?p=transfer&sub=transfer-search&sport=soccer&issearch=true&u=&nationality=${country.cid}&deadline=0&category=&valuea=&valueb=&bida=&bidb=&agea=19&ageb=19&birth_season_low=56&birth_season_high=74&tot_low=0&tot_high=110&s0a=0&s0b=10&s1a=0&s1b=10&s2a=0&s2b=10&s3a=0&s3b=10&s4a=0&s4b=10&s5a=0&s5b=10&s6a=0&s6b=10&s7a=0&s7b=10&s8a=0&s8b=10&s9a=0&s9b=10&s10a=0&s10b=10&s11a=0&s11b=10&s12a=0&s12b=10`)
.then(response => response.json())
.then(data => {
let playerCount = 0;
if (data.totalHits) {
playerCount = parseInt(data.totalHits);
}
resolve({
cid: country.cid,
name: country.name,
code: country.code,
playerCount: playerCount
});
})
.catch(() => {
resolve({
cid: country.cid,
name: country.name,
code: country.code,
playerCount: 0
});
});
});
}
function findRareCountriesInline() {
if (isScanning) return;
isScanning = true;
const rareCountriesResults = document.getElementById('rare-countries-results');
const searchStatus = document.getElementById('search-status');
rareCountriesResults.innerHTML = '';
searchStatus.textContent = 'Scanning countries...';
searchStatus.style.display = 'inline';
const rareCountries = [];
let processed = 0;
async function runBatch(startIdx, batchSize) {
const endIdx = Math.min(startIdx + batchSize, countries.length);
const batch = countries.slice(startIdx, endIdx);
const promises = batch.map(country => countPlayersFromCountry(country));
try {
const results = await Promise.all(promises);
processed += results.length;
searchStatus.textContent = `Scanned ${processed}/${countries.length}...`;
results.forEach(result => {
if (result.playerCount > 0 && result.playerCount < CONFIG.MAX_RARE_PLAYERS) {
rareCountries.push(result);
const countryItem = document.createElement('div');
countryItem.className = 'rare-country-result';
countryItem.title = result.name;
countryItem.dataset.cid = result.cid;
const flagImg = document.createElement('img');
flagImg.src = getFlagUrl(result.code);
flagImg.alt = result.name;
const countSpan = document.createElement('span');
countSpan.textContent = result.playerCount;
countryItem.appendChild(flagImg);
countryItem.appendChild(countSpan);
countryItem.addEventListener('click', () => {
window.open(`https://www.managerzone.com/?p=transfer&sub=search&sport=soccer&nationality=${result.cid}&agea=19&ageb=19`, '_blank');
});
rareCountriesResults.appendChild(countryItem);
}
});
if (endIdx < countries.length) {
setTimeout(() => runBatch(endIdx, batchSize), 1000);
} else {
finishScanning(rareCountries);
}
} catch (error) {
if (endIdx < countries.length) {
setTimeout(() => runBatch(endIdx, batchSize), 1000);
} else {
finishScanning(rareCountries);
}
}
}
function finishScanning(rareCountries) {
isScanning = false;
if (rareCountries.length === 0) {
searchStatus.textContent = 'No rare countries found';
setTimeout(() => {
searchStatus.style.display = 'none';
}, 3000);
} else {
searchStatus.textContent = `Found ${rareCountries.length} countries`;
setTimeout(() => {
searchStatus.style.display = 'none';
}, 3000);
}
}
runBatch(0, 5);
}
function findRareCountries() {
if (isScanning) return;
isScanning = true;
Swal.fire({
title: 'Scanning Countries',
html: 'Looking for countries with fewer than 5 19-year-old players on the market.<br>This may take a while...',
allowOutsideClick: false,
position: 'top-end',
didOpen: () => {
Swal.showLoading();
}
});
const rareCountries = [];
let processed = 0;
async function runBatch(startIdx, batchSize) {
const endIdx = Math.min(startIdx + batchSize, countries.length);
const batch = countries.slice(startIdx, endIdx);
const promises = batch.map(country => countPlayersFromCountry(country));
try {
const results = await Promise.all(promises);
processed += results.length;
Swal.getHtmlContainer().innerHTML = `Scanned ${processed}/${countries.length} countries...<br>Found ${rareCountries.length} rare countries so far.`;
results.forEach(result => {
if (result.playerCount > 0 && result.playerCount < CONFIG.MAX_RARE_PLAYERS) {
rareCountries.push(result);
}
});
if (endIdx < countries.length) {
setTimeout(() => runBatch(endIdx, batchSize), 1000);
} else {
displayRareCountries(rareCountries);
isScanning = false;
}
} catch (error) {
if (endIdx < countries.length) {
setTimeout(() => runBatch(endIdx, batchSize), 1000);
} else {
displayRareCountries(rareCountries);
isScanning = false;
}
}
}
runBatch(0, 5);
}
function displayRareCountries(rareCountries) {
if (rareCountries.length === 0) {
Swal.fire({
title: 'No Rare Countries Found',
text: 'Could not find any countries with fewer than 5 19-year-old players on the market.',
icon: 'info',
position: 'top-end'
});
return;
}
rareCountries.sort((a, b) => a.playerCount - b.playerCount);
let htmlContent = `<p>Found ${rareCountries.length} countries with fewer than 5 19-year-old players:</p><div style="margin-top: 15px;">`;
rareCountries.forEach(country => {
htmlContent += `
<div class="rare-country-item" data-cid="${country.cid}">
<img src="${getFlagUrl(country.code)}" class="rare-country-flag" alt="${country.name}">
<span class="rare-country-name">${country.name}</span>
<span class="rare-country-count">${country.playerCount}</span>
</div>
`;
});
htmlContent += '</div>';
Swal.fire({
title: 'Rare Countries - 19-Year-Olds',
html: htmlContent,
icon: 'success',
confirmButtonText: 'Close',
width: 400,
position: 'top-end',
didOpen: () => {
document.querySelectorAll('.rare-country-item').forEach(item => {
item.addEventListener('click', () => {
const cid = item.getAttribute('data-cid');
const url = `https://www.managerzone.com/?p=transfer&sub=search&sport=soccer&nationality=${cid}&agea=19&ageb=19`;
window.open(url, '_blank');
});
});
}
});
}
function initAgeButtonClick() {
if (!isTransferSearchPage19()) return;
const x = 30;
setTimeout(() => {
const ageButton = document.getElementById('ageButton19');
if (ageButton) {
console.log('Clicking age button');
ageButton.click();
} else {
console.log('Age button not found after timeout');
}
}, x * 1000);
}
function checkPageAndInit() {
if (isTransferPage()) {
fetchCountries();
}
if (isTransferSearchPage19()) {
initAgeButtonClick();
}
}
checkPageAndInit();
setInterval(checkTransferMarket, 30 * 60 * 1000);
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
setTimeout(checkPageAndInit, 500);
}
}).observe(document, {subtree: true, childList: true});
})();