// ==UserScript== // @name PrivateHD Seed Timer // @namespace https://git.okseby.com/okseby/crunchbang // @version 1.0.0 // @description Shows required seed time and whether you can stop seeding each torrent, based on file size. // @author seby // @match https://privatehd.to/profile/*/active* // @updateURL https://git.okseby.com/okseby/crunchbang/raw/branch/main/scripts/javascript/privatehd-seed-timer.user.js // @downloadURL https://git.okseby.com/okseby/crunchbang/raw/branch/main/scripts/javascript/privatehd-seed-timer.user.js // @grant none // ==/UserScript== (function () { "use strict"; // --------------------------------------------------------------------------- // Formula: required seed time in hours, where x = size in GB // x <= 1 → 72h // 1 < x < 50 → 72 + 2x h // x >= 50 → 100 * ln(x) - 219.2023 h // --------------------------------------------------------------------------- function requiredHours(sizeGB) { if (sizeGB <= 1) return 72; if (sizeGB < 50) return 72 + 2 * sizeGB; return 100 * Math.log(sizeGB) - 219.2023; } // --------------------------------------------------------------------------- // Parse a size string like "32.62 GB", "512.00 MB", "1.20 TB" → GB (float) // --------------------------------------------------------------------------- function parseSizeToGB(str) { const m = str.trim().match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i); if (!m) return null; const val = parseFloat(m[1]); const unit = m[2].toUpperCase(); const factors = { B: 1e-9, KB: 1e-6, MB: 1e-3, GB: 1, TB: 1e3 }; return val * (factors[unit] ?? 1); } // --------------------------------------------------------------------------- // Parse elapsed time from the "For" column (last in each row). // Examples: "23h", "2d", "5d 3h", "1h 30m", "45m" // Returns elapsed hours as a float. // --------------------------------------------------------------------------- function parseElapsedHours(str) { let hours = 0; const days = str.match(/(\d+)\s*d/); const hrs = str.match(/(\d+)\s*h/); const mins = str.match(/(\d+)\s*m/); if (days) hours += parseInt(days[1], 10) * 24; if (hrs) hours += parseInt(hrs[1], 10); if (mins) hours += parseInt(mins[1], 10) / 60; return hours; } // --------------------------------------------------------------------------- // Format hours nicely: "72h 00m" etc. // --------------------------------------------------------------------------- function fmtHours(h) { const days = Math.floor(h / 24); const hrs = Math.floor(h % 24); const mins = Math.round((h % 1) * 60); if (days > 0) return `${days}d ${hrs}h`; return `${hrs}h ${String(mins).padStart(2, "0")}m`; } // --------------------------------------------------------------------------- // Add a "Seed Status" column header // --------------------------------------------------------------------------- const thead = document.querySelector("table thead tr"); if (!thead) return; const th = document.createElement("th"); th.textContent = "Seed Status"; th.style.whiteSpace = "nowrap"; thead.appendChild(th); // --------------------------------------------------------------------------- // Process each data row // --------------------------------------------------------------------------- const rows = document.querySelectorAll("table tbody tr"); rows.forEach((row) => { const cells = row.querySelectorAll("td"); // Column layout (0-indexed): // 0: icon | 1: name+stats | 2: progress | 3: status | 4: client // 5: upload | 6: download | 7: left | 8: ratio | 9: started | 10: updated | 11: For const nameCell = cells[1]; const forCell = cells[11]; // "For" — how long you've been seeding if (!nameCell || !forCell) { const td = document.createElement("td"); td.textContent = "—"; row.appendChild(td); return; } // File size lives inside the name cell: fa-database span const sizeSpan = nameCell.querySelector(".fa-database"); const sizeText = sizeSpan ? sizeSpan.textContent.trim() : null; const sizeGB = sizeText ? parseSizeToGB(sizeText) : null; const elapsedText = forCell.textContent.trim(); const elapsed = parseElapsedHours(elapsedText); const required = sizeGB !== null ? requiredHours(sizeGB) : null; const td = document.createElement("td"); td.style.whiteSpace = "nowrap"; td.style.verticalAlign = "middle"; if (required === null) { td.textContent = "No size data"; td.style.color = "#999"; } else { const remaining = required - elapsed; const canStop = remaining <= 0; if (canStop) { td.innerHTML = `✔ Can stop
` + `Req: ${fmtHours(required)} | Done: ${fmtHours(elapsed)}`; } else { td.innerHTML = `✘ Keep seeding
` + `${fmtHours(remaining)} left of ${fmtHours(required)}`; } } row.appendChild(td); }); })();