NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Tibia Charbazar Auctions History
// @namespace ziirgg
// @description This script improves Tibia.com Charbazar Auctions History with additional filters.
// @match https://www.tibia.com/charactertrade/*subtopic=pastcharactertrades*
// @run-at document-end
// @license MIT
// @version 1.0.0
// ==/UserScript==
const AUCTION_CONTAINER_SELECTOR = '.Auction';
const AUCTION_HEADER_SELECTOR = '.AuctionHeader';
const AUCTION_CHARACTER_NAME_LINK_SELECTOR = '.AuctionCharacterName a';
const AUCTION_CHARACTER_WORLD_LINK_SELECTOR = 'a[href*=worlds]';
const AUCTION_DETAILS_LINK_SELECTOR = 'a[href*=auctionid]';
const AUCTION_WINNING_BID_SELECTOR = '.ShortAuctionDataValue b';
const AUCTION_DATES_SELECTOR = '.ShortAuctionDataValue';
const RESULTS_ADJACENT_NODE_SELECTOR = '.InnerTableContainer table tbody';
const FILTERS_CONTAINER_SELECTOR = '.HintBox';
const DEFAULT_NUMBER_OF_PAGES = 10;
const DEFAULT_CONCURRENT_WORKERS = 2;
const ORIGIN = 'https://www.tibia.com';
const BASE_PATH = '/charactertrade/?subtopic=pastcharactertrades';
const PAGE_PARAM_NAME = 'currentpage';
const MONTHS = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
const FIELDS = [{
label: 'Number Of Pages',
name: 'numberOfPages',
defaultValue: DEFAULT_NUMBER_OF_PAGES,
placeholder: DEFAULT_NUMBER_OF_PAGES,
},
{
label: 'Number Of Workers',
name: 'numberOfWorkers',
defaultValue: DEFAULT_CONCURRENT_WORKERS,
placeholder: DEFAULT_CONCURRENT_WORKERS,
},
{
label: 'Minimum Level',
name: 'minLevel',
placeholder: '8'
},
{
label: 'Maximum Level',
name: 'maxLevel',
placeholder: '100'
},
{
label: 'World',
name: 'world',
placeholder: 'Adra'
},
{
label: 'Vocation',
name: 'vocation',
placeholder: 'Sorcerer'
},
];
function processAuctionNode(node) {
const row =
node.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode
.parentNode.parentNode;
const header = node.querySelector(AUCTION_HEADER_SELECTOR);
const name = header.querySelector(AUCTION_CHARACTER_NAME_LINK_SELECTOR)
.innerText;
const summary = Array.from(header.childNodes).find(
(child) => child.nodeType === Node.TEXT_NODE
).textContent;
let [level, vocation, gender] = summary
.split(' | ')
.map((str) => str.split(': ').pop());
level = Number.parseInt(level, 10);
const world = header.querySelector(AUCTION_CHARACTER_WORLD_LINK_SELECTOR)
.textContent;
const bidContainer = node.querySelector(AUCTION_WINNING_BID_SELECTOR);
const bid =
(bidContainer &&
Number.parseInt(bidContainer.textContent.replace(',', ''))) ||
Number.NaN;
const details = node.querySelector(AUCTION_DETAILS_LINK_SELECTOR).href;
const [start, end] = Array.from(node.querySelectorAll(AUCTION_DATES_SELECTOR))
.slice(0, 2)
.map(({
textContent
}) => {
let [date, time] = textContent.split(',');
let [month, day, year] = date.split(date.split('')[3]);
month = MONTHS.indexOf(month);
const hour = time.trim().split(':')[0];
return new Date(year, month, day, hour);
});
return {
name,
level,
vocation,
gender,
world,
bid,
start,
end,
details,
node,
row,
};
}
function processDocument(doc) {
console.log('Processing document...');
const auctionNodes = Array.from(
doc.querySelectorAll(AUCTION_CONTAINER_SELECTOR)
);
return auctionNodes.map(processAuctionNode);
}
const worker = (next_, results, fn) => async () => {
let next;
while ((next = next_())) {
const result = await fn(next);
results.push(result);
}
};
function fetchAndParseHTML(url) {
console.log('Fetching page...');
return fetch(url)
.then((res) => res.text())
.then((html) => new DOMParser().parseFromString(html, 'text/html'))
.catch((err) => console.error(err));
}
let auctions = null;
let adjacentNode = null;
let resultsContainer = null;
async function handleFiltersFiltersFormSubmit(evt) {
evt.preventDefault();
const form = evt.target;
const button = form.querySelector('button');
button.setAttribute('disabled', true);
const params = {};
for (let [key, value] of new FormData(form)) {
params[key] = value;
}
const {
numberOfPages,
numberOfWorkers
} = params;
const pages = Array.from(
new Array(Number.parseInt(numberOfPages)),
(_, i) => `${ORIGIN}${BASE_PATH}&${PAGE_PARAM_NAME}=${i + 1}`
);
let documents = [];
const workers = [];
console.log(
`Preparing ${numberOfWorkers} workers, to fetch & parse ${pages.length} pages.`
);
for (let i = 0; i < Number.parseInt(numberOfWorkers); i++) {
workers.push(worker(pages.pop.bind(pages), documents, fetchAndParseHTML)());
}
await Promise.all(workers);
console.log(`Workers are finished.`);
auctions = documents.reduce((acc, doc) => {
let pageAuctions;
try {
pageAuctions = processDocument(doc);
}
catch (err) {
console.error(err);
// pass
}
if (pageAuctions) acc.push.apply(acc, pageAuctions);
return acc;
}, []);
console.log(`Extracted ${auctions.length} auctions.`);
const {
world,
minLevel,
maxLevel,
vocation
} = params;
const filtered = auctions
.filter((auction) => !world || auction.world === world)
.filter(
(auction) => !vocation || new RegExp(vocation, 'i').test(auction.vocation)
)
.filter(
(auction) =>
minLevel === '' || auction.level >= Number.parseInt(minLevel, 10)
)
.filter(
(auction) =>
maxLevel === '' || auction.level <= Number.parseInt(maxLevel, 10)
);
console.log(`Filtered ${filtered.length} auctions`);
try {
adjacentNode =
adjacentNode || document.querySelector(RESULTS_ADJACENT_NODE_SELECTOR);
const results = filtered
.map((auction) => auction.row)
.reduce((acc, row) => {
acc += row.outerHTML;
return acc;
}, '');
resultsContainer = resultsContainer || adjacentNode.parentElement;
resultsContainer.innerHTML = results;
button.removeAttribute('disabled');
}
catch (err) {
console.error(err);
}
}
let filtersContainer = document.querySelector(FILTERS_CONTAINER_SELECTOR);
filtersContainer.outerHTML =
'<div class="HintBox" style="text-align: right;"></div>';
filtersContainer = document.querySelector(FILTERS_CONTAINER_SELECTOR);
const filtersForm = document.createElement('form');
FIELDS.forEach(({
label,
name,
defaultValue,
placeholder
}) => {
const field = document.createElement('div');
field.style =
'display: flex; justify-content: space-between; align-items: center;';
const input = document.createElement('input');
input.name = name;
input.placeholder = placeholder;
input.id = `charbazar-history-${name}`;
input.value = defaultValue || '';
const labelEl = document.createElement('label');
labelEl.textContent = label || name;
labelEl.setAttribute('for', input.id);
field.append(labelEl, input);
filtersForm.appendChild(field);
});
const submitButton = document.createElement('button');
submitButton.textContent = 'Filter';
filtersForm.appendChild(submitButton);
filtersContainer.appendChild(filtersForm);
filtersForm.addEventListener('submit', handleFiltersFiltersFormSubmit);