Skip to content

Commit ed507b5

Browse files
committed
feat(userscript): add xdccsearch.com and click-to-dismiss notices
Add a handler for xdccsearch.com results tables (network/channel/bot/ slot columns) and make [DCCBOT] notices dismiss on click instead of only timing out.
1 parent ab08a77 commit ed507b5

4 files changed

Lines changed: 158 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ A Violentmonkey userscript is provided to add download buttons to popular XDCC s
133133
* xdcc-search.com
134134
* xdcc.info
135135
* skullxdcc.com
136+
* xdccsearch.com
136137

137138
#### Installation
138139

‎tests/js/userscript/nibl.test.js‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,31 @@ describe("userscript on nibl.co.uk", () => {
5454
});
5555
});
5656

57+
test("clicking a notice dismisses it before the timeout", () => {
58+
jest.useFakeTimers();
59+
try {
60+
loadScript();
61+
const btn = Array.from(document.querySelectorAll("button")).find(
62+
(b) => b.textContent.trim() === "Down"
63+
);
64+
btn.click();
65+
66+
// GM_xmlhttpRequest stub fires onerror -> error notice appears
67+
const notice = document.querySelector(".dccbot-notice");
68+
expect(notice).not.toBeNull();
69+
expect(notice.textContent).toContain("[DCCBOT] Failed");
70+
71+
notice.click();
72+
expect(document.querySelector(".dccbot-notice")).toBeNull();
73+
74+
// no stray timer should recreate or error after dismissal
75+
jest.advanceTimersByTime(10000);
76+
expect(document.querySelector(".dccbot-notice")).toBeNull();
77+
} finally {
78+
jest.useRealTimers();
79+
}
80+
});
81+
5782
test("batch download groups checked packs per bot", () => {
5883
loadScript();
5984
document.getElementById("copy-as-batch").click();
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @jest-environment-options {"url": "https://xdccsearch.com/"}
3+
*/
4+
5+
const {
6+
installGmStubs,
7+
loadScript,
8+
dccbotButtons,
9+
} = require("./helpers");
10+
11+
const ROW = (network, channel, bot, slot) => `
12+
<tr>
13+
<td>${network}</td>
14+
<td>${channel}</td>
15+
<td>${bot}</td>
16+
<td class="cursor-pointer">#${slot}</td>
17+
<td>7x</td><td>[1.8MB]</td><td>25s</td>
18+
<td>file-${slot}.tar</td>
19+
</tr>`;
20+
21+
const PAGE = (rows) => `
22+
<div id="root">
23+
<div class="overflow-x-auto">
24+
<table class="min-w-full text-sm">
25+
<thead><tr><th>Network</th><th>Channel</th><th>Bot</th><th>Slot</th>
26+
<th>Gets</th><th>Size</th><th>Seen</th><th>Filename</th></tr></thead>
27+
<tbody>${rows}</tbody>
28+
</table>
29+
</div>
30+
</div>`;
31+
32+
describe("userscript on xdccsearch.com", () => {
33+
beforeEach(() => {
34+
document.body.innerHTML = "";
35+
installGmStubs();
36+
});
37+
38+
test("appends a Down button cell per row and a header cell", () => {
39+
document.body.innerHTML = PAGE(
40+
ROW("rizon", "#elitewarez", "[ewg]k4rl", "8") +
41+
ROW("abjects", "#beast-xdcc", "NewBot", "55")
42+
);
43+
loadScript();
44+
45+
const buttons = dccbotButtons();
46+
expect(buttons).toHaveLength(2);
47+
expect(buttons[0].dataset).toMatchObject({
48+
server: "irc.rizon.net",
49+
channel: "#elitewarez",
50+
bot: "[ewg]k4rl",
51+
pack: "8",
52+
});
53+
expect(buttons[1].dataset.server).toBe("irc.abjects.net");
54+
expect(document.querySelector("th.dccbot-action")).not.toBeNull();
55+
expect(buttons[0].parentElement.tagName).toBe("TD");
56+
expect(buttons[0].parentElement.className).toBe("dccbot-action");
57+
});
58+
59+
test("re-processes rows after the results view re-renders", async () => {
60+
document.body.innerHTML = PAGE(
61+
ROW("rizon", "#elitewarez", "[ewg]k4rl", "8")
62+
);
63+
loadScript();
64+
expect(dccbotButtons()).toHaveLength(1);
65+
66+
// A new search replaces the results table entirely
67+
const wrapper = document.querySelector(".overflow-x-auto");
68+
wrapper.innerHTML = PAGE(ROW("terrachat", "#x", "TBot", "3"));
69+
await new Promise((r) => setTimeout(r, 0));
70+
71+
const buttons = dccbotButtons();
72+
expect(buttons).toHaveLength(1);
73+
expect(buttons[0].dataset).toMatchObject({
74+
server: "irc.terrachat.cl",
75+
channel: "#x",
76+
bot: "TBot",
77+
pack: "3",
78+
});
79+
expect(document.querySelector("th.dccbot-action")).not.toBeNull();
80+
});
81+
});

‎userscript/add-dccbot-btn.js‎

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// @name add-dccbot-btn
33
// @namespace https://github.com/luni/dccbot/
44
// @website https://github.com/luni/dccbot/
5-
// @version 2026-09-20-1
5+
// @version 2026-09-20-3
66
// @description Add button for DCCbot to automate downloads.
77
// @author luni
88
// @match https://www.xdcc.eu/search.php*
@@ -15,6 +15,8 @@
1515
// @match https://www.xdcc.info/*
1616
// @match https://skullxdcc.com/*
1717
// @match https://www.skullxdcc.com/*
18+
// @match https://xdccsearch.com/*
19+
// @match https://www.xdccsearch.com/*
1820
// @downloadURL https://raw.githubusercontent.com/luni/dccbot/refs/heads/main/userscript/add-dccbot-btn.js
1921
// @connect *
2022
// @grant GM_xmlhttpRequest
@@ -169,6 +171,7 @@
169171
if (!message) return;
170172
const container = getNoticeContainer();
171173
const note = document.createElement('div');
174+
note.className = 'dccbot-notice';
172175
note.textContent = message;
173176
note.style.padding = '8px 10px';
174177
note.style.borderRadius = '6px';
@@ -177,6 +180,8 @@
177180
note.style.wordBreak = 'break-word';
178181
note.style.boxShadow = '0 4px 14px rgba(0,0,0,0.22)';
179182
note.style.color = '#fff';
183+
note.style.cursor = 'pointer';
184+
note.title = 'Click to dismiss';
180185
if (kind === 'error') {
181186
note.style.background = '#b81d13';
182187
} else if (kind === 'success') {
@@ -185,12 +190,18 @@
185190
note.style.background = '#2e3238';
186191
}
187192
container.appendChild(note);
188-
setTimeout(function () {
193+
let dismissed = false;
194+
const dismiss = function () {
195+
if (dismissed) return;
196+
dismissed = true;
197+
clearTimeout(timer);
189198
note.remove();
190199
if (container.childElementCount === 0) {
191200
container.remove();
192201
}
193-
}, timeoutMs || 5000);
202+
};
203+
const timer = setTimeout(dismiss, timeoutMs || 5000);
204+
note.addEventListener('click', dismiss);
194205
}
195206

196207
function formatMsgSummary(server, channel, user, message) {
@@ -1185,6 +1196,40 @@
11851196
observeMutations(document.body, processSkullRows);
11861197
}
11871198

1199+
function add_button_xdccsearch() {
1200+
function processSearchRows() {
1201+
document.querySelectorAll('table').forEach(function (table) {
1202+
const headRow = table.querySelector('thead tr');
1203+
if (headRow && !headRow.querySelector('th.dccbot-action')) {
1204+
const th = document.createElement('th');
1205+
th.className = 'dccbot-action';
1206+
th.textContent = 'DL';
1207+
headRow.appendChild(th);
1208+
}
1209+
});
1210+
1211+
processRowsWithButtons('table tbody tr', function (row) {
1212+
const cells = row.querySelectorAll('td');
1213+
if (cells.length < 8) return null;
1214+
return {
1215+
server: mapNetworkToServer(cells[0].textContent),
1216+
channel: normalizeChannel(cells[1].textContent),
1217+
bot: (cells[2].textContent || '').trim(),
1218+
pack: extractPackNumber(cells[3].textContent)
1219+
};
1220+
}, function (row, btn) {
1221+
const btnCell = document.createElement('td');
1222+
btnCell.className = 'dccbot-action';
1223+
btnCell.appendChild(btn);
1224+
row.appendChild(btnCell);
1225+
});
1226+
}
1227+
1228+
processSearchRows();
1229+
// Observe <body>: results render inside a chat view that re-renders.
1230+
observeMutations(document.body, processSearchRows);
1231+
}
1232+
11881233
const hostHandlers = {
11891234
'www.xdcc.eu': add_button_xdcc_eu,
11901235
'nibl.co.uk': add_button_nibl,
@@ -1196,7 +1241,9 @@
11961241
'xdcc.info': add_button_xdcc_info,
11971242
'www.xdcc.info': add_button_xdcc_info,
11981243
'skullxdcc.com': add_button_skullxdcc,
1199-
'www.skullxdcc.com': add_button_skullxdcc
1244+
'www.skullxdcc.com': add_button_skullxdcc,
1245+
'xdccsearch.com': add_button_xdccsearch,
1246+
'www.xdccsearch.com': add_button_xdccsearch
12001247
};
12011248

12021249
const handler = hostHandlers[window.location.hostname];

0 commit comments

Comments
 (0)