diff --git a/scripts/adduser.sh b/scripts/bash/adduser.sh
similarity index 100%
rename from scripts/adduser.sh
rename to scripts/bash/adduser.sh
diff --git a/scripts/dock.sh b/scripts/bash/dock.sh
similarity index 100%
rename from scripts/dock.sh
rename to scripts/bash/dock.sh
diff --git a/scripts/movefiles.sh b/scripts/bash/movefiles.sh
similarity index 100%
rename from scripts/movefiles.sh
rename to scripts/bash/movefiles.sh
diff --git a/scripts/rclone-sync.sh b/scripts/bash/rclone-sync.sh
similarity index 100%
rename from scripts/rclone-sync.sh
rename to scripts/bash/rclone-sync.sh
diff --git a/scripts/toggle-metal-hud.sh b/scripts/bash/toggle-metal-hud.sh
similarity index 100%
rename from scripts/toggle-metal-hud.sh
rename to scripts/bash/toggle-metal-hud.sh
diff --git a/scripts/toggle-ota.sh b/scripts/bash/toggle-ota.sh
similarity index 100%
rename from scripts/toggle-ota.sh
rename to scripts/bash/toggle-ota.sh
diff --git a/scripts/update-forgejo.sh b/scripts/bash/update-forgejo.sh
similarity index 100%
rename from scripts/update-forgejo.sh
rename to scripts/bash/update-forgejo.sh
diff --git a/scripts/javascript/privatehd-seed-timer.user.js b/scripts/javascript/privatehd-seed-timer.user.js
new file mode 100644
index 0000000..fda7820
--- /dev/null
+++ b/scripts/javascript/privatehd-seed-timer.user.js
@@ -0,0 +1,131 @@
+// ==UserScript==
+// @name PrivateHD Seed Timer
+// @namespace https://privatehd.to/
+// @version 1.0.0
+// @description Shows required seed time and whether you can stop seeding each torrent, based on file size.
+// @author you
+// @match https://privatehd.to/profile/*/active*
+// @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);
+ });
+
+})();
diff --git a/scripts/yBzDuMp.pl b/scripts/perl/yBzDuMp.pl
similarity index 100%
rename from scripts/yBzDuMp.pl
rename to scripts/perl/yBzDuMp.pl
|