- Automation tab: settings form (define_settings) beside the editor, stacking above it when narrow; script/settings save split. - Set location modal: current location + reset checkbox. - saveScriptSettings/setSavePath API wiring; plugins panel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
473 lines
16 KiB
JavaScript
473 lines
16 KiB
JavaScript
// Mock data store for the torrent UI.
|
|
// Generates a realistic "fleet" of torrents with full per-torrent detail
|
|
// (trackers, peers, files, piece map) plus categories, tags, RSS and search stubs.
|
|
|
|
const KiB = 1024;
|
|
const MiB = 1024 * KiB;
|
|
const GiB = 1024 * MiB;
|
|
|
|
// ---- deterministic-ish PRNG so reloads look stable but varied ----
|
|
let _seed = 1337;
|
|
function rng() {
|
|
_seed = (_seed * 1103515245 + 12345) & 0x7fffffff;
|
|
return _seed / 0x7fffffff;
|
|
}
|
|
function pick(arr) { return arr[Math.floor(rng() * arr.length)]; }
|
|
function between(min, max) { return min + rng() * (max - min); }
|
|
function intBetween(min, max) { return Math.floor(between(min, max + 1)); }
|
|
function chance(p) { return rng() < p; }
|
|
|
|
function fakeHash() {
|
|
const hex = '0123456789abcdef';
|
|
let s = '';
|
|
for (let i = 0; i < 40; i++) s += hex[Math.floor(rng() * 16)];
|
|
return s;
|
|
}
|
|
|
|
const COUNTRIES = ['US', 'DE', 'NL', 'FR', 'GB', 'SE', 'CA', 'JP', 'AU', 'RU', 'BR', 'CN', 'IN', 'PL', 'UA', 'CH'];
|
|
const CLIENTS = [
|
|
'qBittorrent 4.6.5', 'qBittorrent 5.0.0', 'Transmission 4.0.5', 'Deluge 2.1.1',
|
|
'libtorrent 2.0.10', 'Vuze 5.7.7', 'rTorrent 0.9.8', 'BiglyBT 3.6.0',
|
|
'µTorrent 3.6.0', 'WebTorrent 2.1.0', 'Tixati 3.27',
|
|
];
|
|
const PEER_FLAGS = ['D', 'U', 'O', 'S', 'I', 'E', 'X', 'H', 'P', 'K', '?'];
|
|
|
|
const CATEGORIES = [
|
|
{ name: 'Linux ISOs', savePath: '/data/iso' },
|
|
{ name: 'Movies', savePath: '/data/media/movies' },
|
|
{ name: 'TV', savePath: '/data/media/tv' },
|
|
{ name: 'Music', savePath: '/data/media/music' },
|
|
{ name: 'Books', savePath: '/data/books' },
|
|
{ name: 'Datasets', savePath: '/data/datasets' },
|
|
{ name: 'Games', savePath: '/data/games' },
|
|
{ name: '', savePath: '/data/downloads' }, // uncategorized
|
|
];
|
|
|
|
const TAGS = ['archive', 'public', 'private', 'seed-forever', 'hit-and-run', 'verified', '4k', 'remux', 'incomplete', 'priority'];
|
|
|
|
const TRACKER_HOSTS = [
|
|
'tracker.opentrackr.org:1337', 'open.demonii.com:1337', 'tracker.torrent.eu.org:451',
|
|
'exodus.desync.com:6969', 'tracker.openbittorrent.com:6969', 'private.tracker.lan:2710',
|
|
'tracker.dler.org:6969', 'open.stealth.si:80',
|
|
];
|
|
|
|
const NAME_TEMPLATES = [
|
|
'debian-12.5.0-amd64-netinst.iso',
|
|
'ubuntu-24.04-desktop-amd64.iso',
|
|
'archlinux-2024.06.01-x86_64.iso',
|
|
'Fedora-Workstation-Live-x86_64-40.iso',
|
|
'NixOS-24.05-x86_64-linux.iso',
|
|
'The.Expanse.S01.2160p.UHD.BluRay.REMUX.HDR.DV',
|
|
'Cosmos.A.Spacetime.Odyssey.S01.1080p.BluRay',
|
|
'Blender.Open.Movies.Collection.2006-2023',
|
|
'Big.Buck.Bunny.4K.60fps.HDR',
|
|
'Sintel.2010.2160p.BluRay.x265',
|
|
'MIT.OCW.6.006.Introduction.to.Algorithms.2020',
|
|
'Stanford.CS231n.2017.Lectures',
|
|
'Wikipedia.en.all.maxi.2024-05.zim',
|
|
'OpenStreetMap.planet-240603.osm.pbf',
|
|
'Common.Voice.Corpus.17.0.en',
|
|
'ImageNet.ILSVRC2012.tar',
|
|
'GPL.Source.Mirror.linux-6.9.4',
|
|
'Project.Gutenberg.2024.Snapshot',
|
|
'FreeBSD-14.0-RELEASE-amd64-dvd1.iso',
|
|
'TempleOS.Distro.5.03',
|
|
'Public.Domain.Jazz.Collection.FLAC',
|
|
'NASA.Apollo.Archive.4K.Scans',
|
|
'OpenStax.Textbook.Bundle.2024',
|
|
'Rocky.Linux.9.4.x86_64.dvd.iso',
|
|
];
|
|
|
|
const STATES = {
|
|
downloading: 'downloading',
|
|
stalledDL: 'stalledDL',
|
|
uploading: 'uploading',
|
|
stalledUP: 'stalledUP',
|
|
pausedDL: 'pausedDL',
|
|
pausedUP: 'pausedUP',
|
|
checkingDL: 'checkingDL',
|
|
queuedDL: 'queuedDL',
|
|
forcedDL: 'forcedDL',
|
|
forcedUP: 'forcedUP',
|
|
metaDL: 'metaDL',
|
|
error: 'error',
|
|
missingFiles: 'missingFiles',
|
|
moving: 'moving',
|
|
};
|
|
|
|
function makeFiles(name, totalSize) {
|
|
// multi-file torrents get a tree; single-file otherwise.
|
|
const isMulti = chance(0.55);
|
|
if (!isMulti) {
|
|
return [{
|
|
name,
|
|
size: totalSize,
|
|
progress: 1,
|
|
priority: 1,
|
|
availability: 1,
|
|
}];
|
|
}
|
|
const folder = name.replace(/\.[a-z0-9]+$/i, '');
|
|
const count = intBetween(2, 9);
|
|
const files = [];
|
|
let remaining = totalSize;
|
|
const subdirs = ['', '', 'extras/', 'subs/', 'sample/'];
|
|
for (let i = 0; i < count; i++) {
|
|
const portion = i === count - 1 ? remaining : Math.floor(remaining * between(0.1, 0.5));
|
|
remaining -= portion;
|
|
const ext = pick(['.mkv', '.mp4', '.flac', '.iso', '.pdf', '.tar', '.zim', '.nfo', '.srt']);
|
|
const prio = pick([0, 1, 1, 1, 6, 7]); // 0=do not download,1=normal,6=high,7=max
|
|
files.push({
|
|
name: `${folder}/${pick(subdirs)}part${i + 1}${ext}`,
|
|
size: Math.max(portion, MiB),
|
|
progress: prio === 0 ? 0 : between(0.2, 1),
|
|
priority: prio,
|
|
availability: between(0.8, 2.5),
|
|
});
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function makePieces(count, progress) {
|
|
// 0 = missing, 1 = downloading, 2 = done
|
|
const arr = new Array(count).fill(0);
|
|
const done = Math.floor(count * progress);
|
|
// make completed pieces somewhat scattered to look realistic
|
|
let filled = 0;
|
|
for (let i = 0; i < count && filled < done; i++) {
|
|
if (chance(0.85)) { arr[i] = 2; filled++; }
|
|
}
|
|
// remaining done pieces fill from the front
|
|
for (let i = 0; i < count && filled < done; i++) {
|
|
if (arr[i] === 0) { arr[i] = 2; filled++; }
|
|
}
|
|
// a few in-flight
|
|
if (progress < 1) {
|
|
for (let i = 0; i < count; i++) {
|
|
if (arr[i] === 0 && chance(0.02)) arr[i] = 1;
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function makeTrackers(seeds, peers) {
|
|
const n = intBetween(2, 4);
|
|
const hosts = [...TRACKER_HOSTS];
|
|
const trackers = [];
|
|
// always include DHT/PeX/LSD pseudo-trackers
|
|
for (const pseudo of ['** [DHT] **', '** [PeX] **', '** [LSD] **']) {
|
|
trackers.push({
|
|
url: pseudo,
|
|
tier: -1,
|
|
status: 'working',
|
|
seeds: pseudo.includes('DHT') ? intBetween(0, seeds) : -1,
|
|
peers: pseudo.includes('DHT') ? intBetween(0, peers) : -1,
|
|
leeches: -1,
|
|
downloaded: -1,
|
|
message: '',
|
|
});
|
|
}
|
|
for (let i = 0; i < n; i++) {
|
|
const url = `udp://${pick(hosts)}/announce`;
|
|
const working = chance(0.78);
|
|
trackers.push({
|
|
url,
|
|
tier: i,
|
|
status: working ? 'working' : pick(['not contacted', 'updating', 'error']),
|
|
seeds: working ? intBetween(0, seeds + 50) : 0,
|
|
peers: working ? intBetween(0, peers + 30) : 0,
|
|
leeches: working ? intBetween(0, peers + 30) : 0,
|
|
downloaded: working ? intBetween(100, 50000) : 0,
|
|
message: working ? '' : pick(['Connection timed out', 'Host not found', 'Not working', 'unregistered torrent']),
|
|
});
|
|
}
|
|
return trackers;
|
|
}
|
|
|
|
function makePeers(count, dlActive) {
|
|
const peers = [];
|
|
const realCount = Math.min(count, intBetween(0, 40));
|
|
for (let i = 0; i < realCount; i++) {
|
|
const flags = [];
|
|
if (dlActive && chance(0.5)) flags.push('D');
|
|
if (chance(0.4)) flags.push('U');
|
|
if (chance(0.3)) flags.push('O'); // optimistic unchoke
|
|
if (chance(0.2)) flags.push('I'); // incoming
|
|
if (chance(0.5)) flags.push('E'); // encrypted
|
|
if (chance(0.1)) flags.push('X'); // PEX
|
|
if (chance(0.1)) flags.push('H'); // DHT
|
|
const prog = between(0, 1);
|
|
peers.push({
|
|
ip: `${intBetween(1, 254)}.${intBetween(0, 254)}.${intBetween(0, 254)}.${intBetween(1, 254)}`,
|
|
port: intBetween(1024, 65535),
|
|
country: pick(COUNTRIES),
|
|
client: pick(CLIENTS),
|
|
flags: flags.join(' '),
|
|
progress: prog,
|
|
dlspeed: flags.includes('D') ? intBetween(10 * KiB, 4 * MiB) : 0,
|
|
upspeed: flags.includes('U') ? intBetween(1 * KiB, 800 * KiB) : 0,
|
|
downloaded: intBetween(0, 500 * MiB),
|
|
uploaded: intBetween(0, 500 * MiB),
|
|
relevance: prog,
|
|
connection: pick(['µTP', 'BT', 'BT', 'WEB']),
|
|
});
|
|
}
|
|
return peers;
|
|
}
|
|
|
|
function makeTorrent(name, idx) {
|
|
const size = Math.floor(between(150 * MiB, 60 * GiB));
|
|
const pieceSizeOptions = [256 * KiB, 512 * KiB, 1 * MiB, 2 * MiB, 4 * MiB, 8 * MiB, 16 * MiB];
|
|
const pieceSize = pick(pieceSizeOptions);
|
|
const pieceCount = Math.max(8, Math.min(2400, Math.ceil(size / pieceSize)));
|
|
|
|
// pick a plausible state
|
|
const stateRoll = rng();
|
|
let state, progress;
|
|
if (stateRoll < 0.30) { state = STATES.downloading; progress = between(0.05, 0.95); }
|
|
else if (stateRoll < 0.40) { state = STATES.stalledDL; progress = between(0.0, 0.6); }
|
|
else if (stateRoll < 0.62) { state = STATES.uploading; progress = 1; }
|
|
else if (stateRoll < 0.74) { state = STATES.stalledUP; progress = 1; }
|
|
else if (stateRoll < 0.82) { state = STATES.pausedUP; progress = 1; }
|
|
else if (stateRoll < 0.87) { state = STATES.pausedDL; progress = between(0.1, 0.8); }
|
|
else if (stateRoll < 0.90) { state = STATES.queuedDL; progress = between(0, 0.3); }
|
|
else if (stateRoll < 0.93) { state = STATES.checkingDL; progress = between(0.3, 0.99); }
|
|
else if (stateRoll < 0.95) { state = STATES.forcedUP; progress = 1; }
|
|
else if (stateRoll < 0.97) { state = STATES.metaDL; progress = 0; }
|
|
else if (stateRoll < 0.99) { state = STATES.error; progress = between(0, 0.9); }
|
|
else { state = STATES.missingFiles; progress = between(0.5, 1); }
|
|
|
|
const isDL = ['downloading', 'forcedDL', 'metaDL'].includes(state);
|
|
const isUP = ['uploading', 'forcedUP'].includes(state);
|
|
|
|
const seedsTotal = intBetween(0, 800);
|
|
const peersTotal = intBetween(0, 600);
|
|
const seeds = Math.min(seedsTotal, intBetween(0, 50));
|
|
const peers = Math.min(peersTotal, intBetween(0, 40));
|
|
|
|
const dlspeed = isDL ? intBetween(50 * KiB, 18 * MiB) : 0;
|
|
const upspeed = (isUP || isDL) ? intBetween(0, 6 * MiB) : 0;
|
|
|
|
const downloaded = Math.floor(size * progress + intBetween(0, 200 * MiB));
|
|
const ratio = between(0, 8.5);
|
|
const uploaded = Math.floor(downloaded * ratio);
|
|
|
|
const now = Date.now();
|
|
const addedOn = now - intBetween(60, 60 * 60 * 24 * 90) * 1000;
|
|
const completionOn = progress >= 1 ? addedOn + intBetween(60, 60 * 60 * 24) * 1000 : -1;
|
|
|
|
const cat = pick(CATEGORIES);
|
|
const tagSet = new Set();
|
|
const tagCount = intBetween(0, 3);
|
|
for (let i = 0; i < tagCount; i++) tagSet.add(pick(TAGS));
|
|
|
|
const eta = isDL && dlspeed > 0
|
|
? Math.floor((size - downloaded) / dlspeed)
|
|
: 8640000; // ∞ sentinel (100 days)
|
|
|
|
const files = makeFiles(name, size);
|
|
const isPrivate = chance(0.35);
|
|
|
|
return {
|
|
hash: fakeHash(),
|
|
name,
|
|
size,
|
|
progress: Math.min(1, progress),
|
|
dlspeed,
|
|
upspeed,
|
|
state,
|
|
eta,
|
|
seeds, seedsTotal,
|
|
peers, peersTotal,
|
|
ratio,
|
|
ratioLimit: chance(0.3) ? between(1, 4) : -1,
|
|
category: cat.name,
|
|
tags: [...tagSet],
|
|
savePath: cat.savePath,
|
|
contentPath: `${cat.savePath}/${name}`,
|
|
addedOn,
|
|
completionOn,
|
|
lastActivity: now - intBetween(0, 60 * 60 * 12) * 1000,
|
|
seenComplete: completionOn,
|
|
downloaded,
|
|
uploaded,
|
|
downloadedSession: Math.floor(downloaded * between(0.01, 0.3)),
|
|
uploadedSession: Math.floor(uploaded * between(0.01, 0.3)),
|
|
availability: state.includes('paused') ? 0 : between(0.5, 4),
|
|
priority: ['queuedDL', 'queuedUP'].includes(state) ? intBetween(1, 12) : 0,
|
|
seqDl: chance(0.15),
|
|
superSeeding: progress >= 1 && chance(0.1),
|
|
autoTMM: chance(0.5),
|
|
forceStart: state.startsWith('forced'),
|
|
pieceSize,
|
|
pieceCount,
|
|
pieces: makePieces(pieceCount, Math.min(1, progress)),
|
|
downLimit: chance(0.2) ? intBetween(50 * KiB, 5 * MiB) : 0,
|
|
upLimit: chance(0.2) ? intBetween(20 * KiB, 2 * MiB) : 0,
|
|
timeActive: intBetween(60, 60 * 60 * 24 * 60),
|
|
comment: chance(0.4) ? pick(['Verified release', 'Please seed!', 'Official mirror', 'See README for checksums']) : '',
|
|
createdBy: pick(['mktorrent 1.1', 'qBittorrent v5.0.0', 'Transmission/4.0.5', 'libtorrent']),
|
|
creationDate: addedOn - intBetween(60 * 60 * 24, 60 * 60 * 24 * 365) * 1000,
|
|
private: isPrivate,
|
|
magnetUri: `magnet:?xt=urn:btih:${''}`,
|
|
files,
|
|
trackers: makeTrackers(seeds, peers),
|
|
peersList: makePeers(peers, isDL),
|
|
};
|
|
}
|
|
|
|
function buildFleet() {
|
|
_seed = 1337;
|
|
const torrents = NAME_TEMPLATES.map((n, i) => makeTorrent(n, i));
|
|
for (const t of torrents) t.magnetUri = `magnet:?xt=urn:btih:${t.hash}&dn=${encodeURIComponent(t.name)}`;
|
|
return torrents;
|
|
}
|
|
|
|
const torrents = buildFleet();
|
|
|
|
const rssFeeds = [
|
|
{
|
|
uid: 'feed-1',
|
|
name: 'Linux ISO Tracker',
|
|
url: 'https://tracker.example.org/rss/linux',
|
|
lastUpdate: Date.now() - 8 * 60 * 1000,
|
|
articles: [
|
|
{ title: 'debian-12.6.0-amd64-netinst.iso', date: Date.now() - 60 * 60 * 1000, size: 660 * MiB, isRead: false },
|
|
{ title: 'ubuntu-24.04.1-desktop-amd64.iso', date: Date.now() - 5 * 60 * 60 * 1000, size: 5.9 * GiB, isRead: true },
|
|
{ title: 'archlinux-2024.07.01-x86_64.iso', date: Date.now() - 26 * 60 * 60 * 1000, size: 1.1 * GiB, isRead: true },
|
|
],
|
|
},
|
|
{
|
|
uid: 'feed-2',
|
|
name: 'Public Domain Media',
|
|
url: 'https://media.example.org/rss',
|
|
lastUpdate: Date.now() - 22 * 60 * 1000,
|
|
articles: [
|
|
{ title: 'Sintel.2010.2160p.BluRay.x265', date: Date.now() - 2 * 60 * 60 * 1000, size: 4.2 * GiB, isRead: false },
|
|
{ title: 'Big.Buck.Bunny.4K.60fps.HDR', date: Date.now() - 9 * 60 * 60 * 1000, size: 2.8 * GiB, isRead: false },
|
|
],
|
|
},
|
|
];
|
|
|
|
const rssRules = [
|
|
{
|
|
name: 'New Debian stable',
|
|
enabled: true,
|
|
mustContain: 'debian amd64 netinst',
|
|
mustNotContain: 'rc beta alpha',
|
|
useRegex: false,
|
|
episodeFilter: '',
|
|
affectedFeeds: ['feed-1'],
|
|
assignedCategory: 'Linux ISOs',
|
|
savePath: '/data/iso',
|
|
addPaused: false,
|
|
lastMatch: Date.now() - 60 * 60 * 1000,
|
|
},
|
|
{
|
|
name: '4K Public Domain',
|
|
enabled: true,
|
|
mustContain: '2160p',
|
|
mustNotContain: 'cam ts',
|
|
useRegex: false,
|
|
episodeFilter: '',
|
|
affectedFeeds: ['feed-2'],
|
|
assignedCategory: 'Movies',
|
|
savePath: '/data/media/movies',
|
|
addPaused: true,
|
|
lastMatch: Date.now() - 2 * 60 * 60 * 1000,
|
|
},
|
|
{
|
|
name: 'Ubuntu LTS only',
|
|
enabled: false,
|
|
mustContain: 'ubuntu.*desktop.*amd64',
|
|
mustNotContain: 'daily beta',
|
|
useRegex: true,
|
|
episodeFilter: '',
|
|
affectedFeeds: ['feed-1'],
|
|
assignedCategory: 'Linux ISOs',
|
|
savePath: '/data/iso',
|
|
addPaused: false,
|
|
lastMatch: -1,
|
|
},
|
|
];
|
|
|
|
const searchPlugins = [
|
|
{ name: 'LinuxTracker', enabled: true, url: 'https://linuxtracker.org' },
|
|
{ name: 'PublicMediaDB', enabled: true, url: 'https://media.example.org' },
|
|
{ name: 'AcademicTorrents', enabled: true, url: 'https://academictorrents.com' },
|
|
{ name: 'LegacyIndexer', enabled: false, url: 'https://legacy.example.net' },
|
|
];
|
|
|
|
function runSearch(query) {
|
|
const q = (query || '').toLowerCase().trim();
|
|
const pool = [
|
|
...NAME_TEMPLATES,
|
|
'OpenWRT-23.05.3-x86-64-generic.img',
|
|
'KDE.neon.User.Edition.2024.iso',
|
|
'Manjaro.KDE.24.0.iso',
|
|
'Public.Domain.Films.1920s.Collection',
|
|
'LibreOffice.24.2.SDK.docs',
|
|
];
|
|
return pool
|
|
.filter((n) => !q || n.toLowerCase().includes(q))
|
|
.map((n) => ({
|
|
name: n,
|
|
size: Math.floor(between(100 * MiB, 50 * GiB)),
|
|
seeds: intBetween(0, 1200),
|
|
leeches: intBetween(0, 600),
|
|
engine: pick(searchPlugins.filter((p) => p.enabled).map((p) => p.name)),
|
|
pubDate: Date.now() - intBetween(60, 60 * 60 * 24 * 400) * 1000,
|
|
descrLink: 'https://example.org/details',
|
|
}))
|
|
.sort((a, b) => b.seeds - a.seeds);
|
|
}
|
|
|
|
export const db = {
|
|
torrents,
|
|
categories: CATEGORIES,
|
|
tags: TAGS,
|
|
rssFeeds,
|
|
rssRules,
|
|
searchPlugins,
|
|
runSearch,
|
|
script: {
|
|
loaded: true,
|
|
path: 'examples/anime_sort.lua',
|
|
source: `function on_file_complete(event)
|
|
naut.move_file(event.torrent_id, event.index, event.path .. ".sorted")
|
|
end
|
|
`,
|
|
queued: 12,
|
|
handled: 12,
|
|
dropped: 0,
|
|
errors: 0,
|
|
move_requests: 3,
|
|
last_error: '',
|
|
},
|
|
// server preferences (subset, advanced)
|
|
preferences: {
|
|
dl_limit: 0,
|
|
up_limit: 0,
|
|
alt_dl_limit: 1 * MiB,
|
|
alt_up_limit: 256 * KiB,
|
|
alt_speed_enabled: false,
|
|
max_connec: 500,
|
|
max_connec_per_torrent: 100,
|
|
max_uploads: 20,
|
|
max_active_downloads: 5,
|
|
max_active_uploads: 10,
|
|
max_active_torrents: 12,
|
|
dht: true,
|
|
pex: true,
|
|
lsd: true,
|
|
encryption: 1, // 0=prefer,1=force on,2=force off
|
|
utp: true,
|
|
listen_port: 6881,
|
|
upnp: true,
|
|
queueing_enabled: true,
|
|
save_path: '/data/downloads',
|
|
scan_interval: 15,
|
|
},
|
|
};
|
|
|
|
export const constants = { KiB, MiB, GiB, STATES };
|