2022-01-20 18:27:56 +00:00
|
|
|
/* @license This file Copyright (C) 2020-2022 Mnemosyne LLC.
|
2022-02-07 16:25:02 +00:00
|
|
|
It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
2022-01-20 18:27:56 +00:00
|
|
|
or any future license endorsed by Mnemosyne LLC.
|
|
|
|
License text can be found in the licenses/ folder. */
|
2020-10-24 01:04:25 +00:00
|
|
|
|
|
|
|
import { FileRow } from './file-row.js';
|
|
|
|
import { Formatter } from './formatter.js';
|
|
|
|
import { Torrent } from './torrent.js';
|
|
|
|
import {
|
|
|
|
OutsideClickListener,
|
|
|
|
Utils,
|
|
|
|
createTabsContainer,
|
|
|
|
setTextContent,
|
|
|
|
} from './utils.js';
|
|
|
|
|
|
|
|
const peer_column_classes = [
|
|
|
|
'encryption',
|
|
|
|
'speed-up',
|
|
|
|
'speed-down',
|
|
|
|
'percent-done',
|
|
|
|
'status',
|
|
|
|
'peer-address',
|
|
|
|
'peer-app-name',
|
|
|
|
];
|
|
|
|
|
|
|
|
export class Inspector extends EventTarget {
|
|
|
|
constructor(controller) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.closed = false;
|
|
|
|
this.controller = controller;
|
|
|
|
this.elements = this._create();
|
|
|
|
this.current_page = this.elements.info.root;
|
|
|
|
this.interval = setInterval(this._refreshTorrents.bind(this), 3000);
|
|
|
|
this.name = 'inspector';
|
|
|
|
this.selection_listener = (event_) => this._setTorrents(event_.selected);
|
|
|
|
this.torrent_listener = () => this._updateCurrentPage();
|
|
|
|
this.torrents = [];
|
|
|
|
this.file_torrent = null;
|
|
|
|
this.file_torrent_n = null;
|
|
|
|
this.file_rows = null;
|
|
|
|
this.outside = new OutsideClickListener(this.elements.root);
|
|
|
|
this.outside.addEventListener('click', () => this.close());
|
|
|
|
Object.seal(this);
|
|
|
|
|
|
|
|
controller.addEventListener(
|
|
|
|
'torrent-selection-changed',
|
|
|
|
this.selection_listener
|
|
|
|
);
|
|
|
|
this._setTorrents(this.controller.getSelectedTorrents());
|
|
|
|
|
|
|
|
document.body.append(this.elements.root);
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
if (!this.closed) {
|
|
|
|
this.outside.stop();
|
|
|
|
clearInterval(this.interval);
|
|
|
|
this._setTorrents([]);
|
|
|
|
this.elements.root.remove();
|
|
|
|
this.controller.removeEventListener(
|
|
|
|
'torrent-selection-changed',
|
|
|
|
this.selection_listener
|
|
|
|
);
|
|
|
|
this.dispatchEvent(new Event('close'));
|
|
|
|
for (const property of Object.keys(this)) {
|
|
|
|
this[property] = null;
|
|
|
|
}
|
|
|
|
this.closed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createInfoPage() {
|
|
|
|
const root = document.createElement('div');
|
|
|
|
root.classList.add('inspector-info-page');
|
|
|
|
const elements = { root };
|
|
|
|
|
|
|
|
const append_section_title = (text) => {
|
|
|
|
const label = document.createElement('div');
|
|
|
|
label.textContent = text;
|
|
|
|
label.classList.add('section-label');
|
|
|
|
root.append(label);
|
|
|
|
};
|
|
|
|
|
|
|
|
const append_row = (text) => {
|
|
|
|
const lhs = document.createElement('label');
|
|
|
|
setTextContent(lhs, text);
|
|
|
|
root.append(lhs);
|
|
|
|
|
|
|
|
const rhs = document.createElement('label');
|
|
|
|
root.append(rhs);
|
|
|
|
return rhs;
|
|
|
|
};
|
|
|
|
|
|
|
|
append_section_title('Activity');
|
|
|
|
let rows = [
|
|
|
|
['have', 'Have:'],
|
|
|
|
['availability', 'Availability:'],
|
|
|
|
['uploaded', 'Uploaded:'],
|
|
|
|
['downloaded', 'Downloaded:'],
|
|
|
|
['state', 'State:'],
|
|
|
|
['running_time', 'Running time:'],
|
|
|
|
['remaining_time', 'Remaining:'],
|
|
|
|
['last_activity', 'Last activity:'],
|
|
|
|
['error', 'Error:'],
|
|
|
|
];
|
|
|
|
for (const [name, text] of rows) {
|
|
|
|
elements[name] = append_row(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
append_section_title('Details');
|
|
|
|
rows = [
|
|
|
|
['size', 'Size:'],
|
|
|
|
['location', 'Location:'],
|
|
|
|
['hash', 'Hash:'],
|
|
|
|
['privacy', 'Privacy:'],
|
|
|
|
['origin', 'Origin:'],
|
2022-02-09 16:28:48 +00:00
|
|
|
['magnetLink', 'Magnet:'],
|
2020-10-24 01:04:25 +00:00
|
|
|
['comment', 'Comment:'],
|
2022-02-10 15:25:11 +00:00
|
|
|
['labels', 'Labels:'],
|
2020-10-24 01:04:25 +00:00
|
|
|
];
|
|
|
|
for (const [name, text] of rows) {
|
|
|
|
elements[name] = append_row(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
return elements;
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createListPage(list_type, list_id) {
|
|
|
|
const root = document.createElement('div');
|
|
|
|
const list = document.createElement(list_type);
|
|
|
|
list.id = list_id;
|
|
|
|
root.append(list);
|
|
|
|
return { list, root };
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createTiersPage() {
|
|
|
|
return Inspector._createListPage('div', 'inspector-tiers-list');
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createFilesPage() {
|
|
|
|
return Inspector._createListPage('ul', 'inspector-file-list');
|
|
|
|
}
|
|
|
|
|
|
|
|
static _createPeersPage() {
|
|
|
|
const table = document.createElement('table');
|
|
|
|
table.classList.add('peer-list');
|
|
|
|
const thead = document.createElement('thead');
|
|
|
|
const tr = document.createElement('tr');
|
|
|
|
const names = ['', 'Up', 'Down', 'Done', 'Status', 'Address', 'Client'];
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const [index, name] of names.entries()) {
|
2020-10-24 01:04:25 +00:00
|
|
|
const th = document.createElement('th');
|
|
|
|
const classname = peer_column_classes[index];
|
|
|
|
if (classname === 'encryption') {
|
|
|
|
th.dataset.encrypted = true;
|
|
|
|
}
|
|
|
|
th.classList.add(classname);
|
|
|
|
setTextContent(th, name);
|
|
|
|
tr.append(th);
|
2021-05-19 13:43:46 +00:00
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
const tbody = document.createElement('tbody');
|
|
|
|
thead.append(tr);
|
|
|
|
table.append(thead);
|
|
|
|
table.append(tbody);
|
|
|
|
return {
|
|
|
|
root: table,
|
|
|
|
tbody,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_create() {
|
|
|
|
const pages = {
|
|
|
|
files: Inspector._createFilesPage(),
|
|
|
|
info: Inspector._createInfoPage(),
|
|
|
|
peers: Inspector._createPeersPage(),
|
|
|
|
tiers: Inspector._createTiersPage(),
|
|
|
|
};
|
|
|
|
|
|
|
|
const on_activated = (page) => {
|
|
|
|
this.current_page = page;
|
|
|
|
this._updateCurrentPage();
|
|
|
|
};
|
|
|
|
|
|
|
|
const elements = createTabsContainer(
|
|
|
|
'inspector',
|
|
|
|
[
|
|
|
|
['inspector-tab-info', pages.info.root],
|
|
|
|
['inspector-tab-peers', pages.peers.root],
|
|
|
|
['inspector-tab-tiers', pages.tiers.root],
|
|
|
|
['inspector-tab-files', pages.files.root],
|
|
|
|
],
|
|
|
|
on_activated.bind(this)
|
|
|
|
);
|
|
|
|
|
|
|
|
return { ...elements, ...pages };
|
|
|
|
}
|
|
|
|
|
|
|
|
_setTorrents(torrents) {
|
|
|
|
// update the inspector when a selected torrent's data changes.
|
|
|
|
const key = 'dataChanged';
|
|
|
|
const callback = this.torrent_listener;
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const t of this.torrents) {
|
|
|
|
t.removeEventListener(key, callback);
|
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
this.torrents = [...torrents];
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const t of this.torrents) {
|
|
|
|
t.addEventListener(key, callback);
|
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
|
|
|
|
this._refreshTorrents();
|
|
|
|
this._updateCurrentPage();
|
|
|
|
}
|
|
|
|
|
|
|
|
static _needsExtraInfo(torrents) {
|
|
|
|
return torrents.some((tor) => !tor.hasExtraInfo());
|
|
|
|
}
|
|
|
|
|
|
|
|
_refreshTorrents() {
|
|
|
|
const { controller, torrents } = this;
|
|
|
|
const ids = torrents.map((t) => t.getId());
|
|
|
|
|
|
|
|
if (ids && ids.length > 0) {
|
|
|
|
const fields = ['id', ...Torrent.Fields.StatsExtra];
|
|
|
|
if (Inspector._needsExtraInfo(torrents)) {
|
|
|
|
fields.push(...Torrent.Fields.InfoExtra);
|
|
|
|
}
|
|
|
|
|
|
|
|
controller.updateTorrents(ids, fields);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateCurrentPage() {
|
2021-05-19 13:43:46 +00:00
|
|
|
const { current_page, elements } = this;
|
|
|
|
switch (current_page) {
|
2020-10-24 01:04:25 +00:00
|
|
|
case elements.files.root:
|
|
|
|
this._updateFiles();
|
|
|
|
break;
|
|
|
|
case elements.info.root:
|
|
|
|
this._updateInfo();
|
|
|
|
break;
|
|
|
|
case elements.peers.root:
|
|
|
|
this._updatePeers();
|
|
|
|
break;
|
|
|
|
case elements.tiers.root:
|
|
|
|
this._updateTiers();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.warn('unexpected page');
|
2021-05-19 13:43:46 +00:00
|
|
|
console.log(current_page);
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateInfo() {
|
|
|
|
const none = 'None';
|
|
|
|
const mixed = 'Mixed';
|
|
|
|
const unknown = 'Unknown';
|
|
|
|
const fmt = Formatter;
|
|
|
|
const now = Date.now();
|
2021-05-19 13:43:46 +00:00
|
|
|
const { elements: e, torrents } = this;
|
2020-10-24 01:04:25 +00:00
|
|
|
const sizeWhenDone = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getSizeWhenDone(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
|
|
|
|
// state
|
|
|
|
let string = null;
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else if (torrents.every((t) => t.isFinished())) {
|
|
|
|
string = 'Finished';
|
|
|
|
} else if (torrents.every((t) => t.isStopped())) {
|
|
|
|
string = 'Paused';
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getStateString();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
|
|
|
}
|
|
|
|
setTextContent(e.info.state, string);
|
|
|
|
const stateString = string;
|
|
|
|
|
|
|
|
// have
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const verified = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getHaveValid(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
const unverified = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getHaveUnchecked(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
const leftUntilDone = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getLeftUntilDone(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
|
|
|
|
const d =
|
|
|
|
100 *
|
|
|
|
(sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
|
|
|
|
string = fmt.percentString(d);
|
|
|
|
|
|
|
|
if (!unverified && !leftUntilDone) {
|
|
|
|
string = `${fmt.size(verified)} (100%)`;
|
|
|
|
} else if (!unverified) {
|
|
|
|
string = `${fmt.size(verified)} of ${fmt.size(
|
|
|
|
sizeWhenDone
|
|
|
|
)} (${string}%)`;
|
|
|
|
} else {
|
|
|
|
string = `${fmt.size(verified)} of ${fmt.size(
|
|
|
|
sizeWhenDone
|
|
|
|
)} (${string}%), ${fmt.size(unverified)} Unverified`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTextContent(e.info.have, string);
|
|
|
|
|
|
|
|
// availability
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else if (sizeWhenDone === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const available = torrents.reduce(
|
|
|
|
(accumulator, t) => t.getHave() + t.getDesiredAvailable(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
string = `${fmt.percentString((100 * available) / sizeWhenDone)}%`;
|
|
|
|
}
|
|
|
|
setTextContent(e.info.availability, string);
|
|
|
|
|
|
|
|
// downloaded
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const d = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getDownloadedEver(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
const f = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getFailedEver(),
|
|
|
|
0
|
|
|
|
);
|
2022-02-11 07:15:10 +00:00
|
|
|
string = f
|
|
|
|
? `${fmt.size(d)} (+${fmt.size(f)} discarded after failed checksum)`
|
|
|
|
: fmt.size(d);
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
setTextContent(e.info.downloaded, string);
|
|
|
|
|
|
|
|
// uploaded
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
2022-03-17 18:41:48 +00:00
|
|
|
const uploaded = torrents.reduce(
|
2020-10-24 01:04:25 +00:00
|
|
|
(accumulator, t) => accumulator + t.getUploadedEver(),
|
|
|
|
0
|
|
|
|
);
|
2022-03-17 18:41:48 +00:00
|
|
|
const denominator =
|
2020-10-24 01:04:25 +00:00
|
|
|
torrents.reduce(
|
2022-03-17 18:41:48 +00:00
|
|
|
(accumulator, t) => accumulator + t.getSizeWhenDone(),
|
2020-10-24 01:04:25 +00:00
|
|
|
0
|
|
|
|
) ||
|
|
|
|
torrents.reduce((accumulator, t) => accumulator + t.getHaveValid(), 0);
|
2022-03-17 18:41:48 +00:00
|
|
|
string = `${fmt.size(uploaded)} (Ratio: ${fmt.ratioString(
|
|
|
|
Utils.ratio(uploaded, denominator)
|
|
|
|
)})`;
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
setTextContent(e.info.uploaded, string);
|
|
|
|
|
|
|
|
// running time
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else if (torrents.every((t) => t.isStopped())) {
|
|
|
|
string = stateString; // paused || finished}
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getStartDate();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
string = !torrents.every((t) => get(t) === first)
|
|
|
|
? mixed
|
|
|
|
: fmt.timeInterval(now / 1000 - first);
|
|
|
|
}
|
|
|
|
setTextContent(e.info.running_time, string);
|
|
|
|
|
|
|
|
// remaining time
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getETA();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
if (!torrents.every((t) => get(t) === first)) {
|
|
|
|
string = mixed;
|
|
|
|
} else if (first < 0) {
|
|
|
|
string = unknown;
|
|
|
|
} else {
|
|
|
|
string = fmt.timeInterval(first);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTextContent(e.info.remaining_time, string);
|
|
|
|
|
|
|
|
// last active at
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const latest = torrents.reduce(
|
|
|
|
(accumulator, t) => Math.max(accumulator, t.getLastActivity()),
|
|
|
|
-1
|
|
|
|
);
|
|
|
|
const now_seconds = Math.floor(now / 1000);
|
|
|
|
if (0 < latest && latest <= now_seconds) {
|
|
|
|
const idle_secs = now_seconds - latest;
|
|
|
|
string =
|
|
|
|
idle_secs < 5 ? 'Active now' : `${fmt.timeInterval(idle_secs)} ago`;
|
|
|
|
} else {
|
|
|
|
string = none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTextContent(e.info.last_activity, string);
|
|
|
|
|
|
|
|
// error
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getErrorString();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
|
|
|
}
|
|
|
|
setTextContent(e.info.error, string || none);
|
|
|
|
|
|
|
|
// size
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const size = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getTotalSize(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
if (!size) {
|
|
|
|
string = 'None';
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getPieceSize();
|
|
|
|
const pieceCount = torrents.reduce(
|
|
|
|
(accumulator, t) => accumulator + t.getPieceCount(),
|
|
|
|
0
|
|
|
|
);
|
|
|
|
const pieceString = fmt.number(pieceCount);
|
|
|
|
const pieceSize = get(torrents[0]);
|
|
|
|
string = torrents.every((t) => get(t) === pieceSize)
|
|
|
|
? `${fmt.size(size)} (${pieceString} pieces @ ${fmt.mem(pieceSize)})`
|
|
|
|
: `${fmt.size(size)} (${pieceString} pieces)`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTextContent(e.info.size, string);
|
|
|
|
|
|
|
|
// hash
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getHashString();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
|
|
|
}
|
|
|
|
setTextContent(e.info.hash, string);
|
|
|
|
|
|
|
|
// privacy
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getPrivateFlag();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
if (!torrents.every((t) => get(t) === first)) {
|
|
|
|
string = mixed;
|
|
|
|
} else if (first) {
|
|
|
|
string = 'Private to this tracker -- DHT and PEX disabled';
|
|
|
|
} else {
|
|
|
|
string = 'Public torrent';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTextContent(e.info.privacy, string);
|
|
|
|
|
|
|
|
// comment
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getComment();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
|
|
|
}
|
|
|
|
string = string || none;
|
|
|
|
if (string.startsWith('https://') || string.startsWith('http://')) {
|
|
|
|
string = encodeURI(string);
|
|
|
|
Utils.setInnerHTML(
|
|
|
|
e.info.comment,
|
|
|
|
`<a href="${string}" target="_blank" >${string}</a>`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
setTextContent(e.info.comment, string);
|
|
|
|
}
|
|
|
|
|
2022-02-10 15:25:11 +00:00
|
|
|
// labels
|
|
|
|
string = torrents.length === 0 ? none : torrents[0].getLabels().join(', ');
|
|
|
|
setTextContent(e.info.labels, string);
|
|
|
|
|
2020-10-24 01:04:25 +00:00
|
|
|
// origin
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
let get = (t) => t.getCreator();
|
|
|
|
const creator = get(torrents[0]);
|
|
|
|
const mixed_creator = !torrents.every((t) => get(t) === creator);
|
|
|
|
|
|
|
|
get = (t) => t.getDateCreated();
|
|
|
|
const date = get(torrents[0]);
|
|
|
|
const mixed_date = !torrents.every((t) => get(t) === date);
|
|
|
|
|
2021-05-19 13:43:46 +00:00
|
|
|
const empty_creator = !creator || creator.length === 0;
|
2020-10-24 01:04:25 +00:00
|
|
|
const empty_date = !date;
|
|
|
|
if (mixed_creator || mixed_date) {
|
|
|
|
string = mixed;
|
|
|
|
} else if (empty_creator && empty_date) {
|
|
|
|
string = unknown;
|
|
|
|
} else if (empty_date && !empty_creator) {
|
|
|
|
string = `Created by ${creator}`;
|
|
|
|
} else if (empty_creator && !empty_date) {
|
|
|
|
string = `Created on ${new Date(date * 1000).toDateString()}`;
|
|
|
|
} else {
|
|
|
|
string = `Created by ${creator} on ${new Date(
|
|
|
|
date * 1000
|
|
|
|
).toDateString()}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setTextContent(e.info.origin, string);
|
|
|
|
|
|
|
|
// location
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
string = none;
|
|
|
|
} else {
|
|
|
|
const get = (t) => t.getDownloadDir();
|
|
|
|
const first = get(torrents[0]);
|
|
|
|
string = torrents.every((t) => get(t) === first) ? first : mixed;
|
|
|
|
}
|
|
|
|
setTextContent(e.info.location, string);
|
2022-02-09 16:28:48 +00:00
|
|
|
|
|
|
|
// magnetLink
|
|
|
|
if (torrents.length === 0) {
|
|
|
|
setTextContent(e.info.magnetLink, none);
|
|
|
|
} else if (torrents.length > 1) {
|
|
|
|
setTextContent(e.info.magnetLink, mixed);
|
|
|
|
} else {
|
|
|
|
const link = torrents[0].getMagnetLink();
|
|
|
|
Utils.setInnerHTML(
|
|
|
|
e.info.magnetLink,
|
|
|
|
`<a class="inspector-info-magnet" href="${link}"><button></button></a>`
|
|
|
|
);
|
2022-02-09 16:30:18 +00:00
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// PEERS PAGE
|
|
|
|
|
|
|
|
static _peerStatusTitle(flag_string) {
|
|
|
|
const texts = Object.seal({
|
|
|
|
'?': "We unchoked this peer, but they're not interested",
|
|
|
|
D: 'Downloading from this peer',
|
|
|
|
E: 'Encrypted Connection',
|
|
|
|
H: 'Peer was discovered through Distributed Hash Table (DHT)',
|
|
|
|
I: 'Peer is an incoming connection',
|
|
|
|
K: "Peer has unchoked us, but we're not interested",
|
|
|
|
O: 'Optimistic unchoke',
|
|
|
|
T: 'Peer is connected via uTP',
|
|
|
|
U: 'Uploading to peer',
|
|
|
|
X: 'Peer was discovered through Peer Exchange (PEX)',
|
|
|
|
d: "We would download from this peer if they'd let us",
|
|
|
|
u: "We would upload to this peer if they'd ask",
|
|
|
|
});
|
|
|
|
|
|
|
|
return [...flag_string]
|
|
|
|
.filter((ch) => texts[ch])
|
|
|
|
.map((ch) => `${ch}: ${texts[ch]}`)
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
_updatePeers() {
|
|
|
|
const fmt = Formatter;
|
2021-05-19 13:43:46 +00:00
|
|
|
const { elements, torrents } = this;
|
|
|
|
const { tbody } = elements.peers;
|
2020-10-24 01:04:25 +00:00
|
|
|
|
|
|
|
const cell_setters = [
|
|
|
|
(peer, td) => {
|
|
|
|
td.dataset.encrypted = peer.isEncrypted;
|
|
|
|
},
|
|
|
|
(peer, td) =>
|
|
|
|
setTextContent(
|
|
|
|
td,
|
|
|
|
peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''
|
|
|
|
),
|
|
|
|
(peer, td) =>
|
|
|
|
setTextContent(
|
|
|
|
td,
|
|
|
|
peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''
|
|
|
|
),
|
|
|
|
(peer, td) => setTextContent(td, `${Math.floor(peer.progress * 100)}%`),
|
|
|
|
(peer, td) => {
|
|
|
|
setTextContent(td, peer.flagStr);
|
|
|
|
td.setAttribute('title', Inspector._peerStatusTitle(peer.flagStr));
|
|
|
|
},
|
|
|
|
(peer, td) => setTextContent(td, peer.address),
|
|
|
|
(peer, td) => setTextContent(td, peer.clientName),
|
|
|
|
];
|
|
|
|
|
|
|
|
const rows = [];
|
|
|
|
for (const tor of torrents) {
|
|
|
|
// torrent name
|
|
|
|
const tortr = document.createElement('tr');
|
|
|
|
tortr.classList.add('torrent-row');
|
|
|
|
const tortd = document.createElement('td');
|
|
|
|
tortd.setAttribute('colspan', cell_setters.length);
|
|
|
|
setTextContent(tortd, tor.getName());
|
|
|
|
tortr.append(tortd);
|
|
|
|
rows.push(tortr);
|
|
|
|
|
|
|
|
// peers
|
|
|
|
for (const peer of tor.getPeers()) {
|
|
|
|
const tr = document.createElement('tr');
|
|
|
|
tr.classList.add('peer-row');
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const [index, setter] of cell_setters.entries()) {
|
2020-10-24 01:04:25 +00:00
|
|
|
const td = document.createElement('td');
|
|
|
|
td.classList.add(peer_column_classes[index]);
|
|
|
|
setter(peer, td);
|
|
|
|
tr.append(td);
|
2021-05-19 13:43:46 +00:00
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
rows.push(tr);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: modify instead of rebuilding wholesale?
|
|
|
|
while (tbody.firstChild) {
|
|
|
|
tbody.firstChild.remove();
|
|
|
|
}
|
|
|
|
tbody.append(...rows);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// TRACKERS PAGE
|
|
|
|
|
|
|
|
static getAnnounceState(tracker) {
|
|
|
|
switch (tracker.announceState) {
|
|
|
|
case Torrent._TrackerActive:
|
|
|
|
return 'Announce in progress';
|
|
|
|
case Torrent._TrackerWaiting: {
|
|
|
|
const timeUntilAnnounce = Math.max(
|
|
|
|
0,
|
2021-05-19 13:43:46 +00:00
|
|
|
tracker.nextAnnounceTime - Date.now() / 1000
|
2020-10-24 01:04:25 +00:00
|
|
|
);
|
|
|
|
return `Next announce in ${Formatter.timeInterval(timeUntilAnnounce)}`;
|
|
|
|
}
|
|
|
|
case Torrent._TrackerQueued:
|
|
|
|
return 'Announce is queued';
|
|
|
|
case Torrent._TrackerInactive:
|
|
|
|
return tracker.isBackup
|
|
|
|
? 'Tracker will be used as a backup'
|
|
|
|
: 'Announce not scheduled';
|
|
|
|
default:
|
|
|
|
return `unknown announce state: ${tracker.announceState}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static lastAnnounceStatus(tracker) {
|
|
|
|
let lastAnnounceLabel = 'Last Announce';
|
|
|
|
let lastAnnounce = ['N/A'];
|
|
|
|
|
|
|
|
if (tracker.hasAnnounced) {
|
|
|
|
const lastAnnounceTime = Formatter.timestamp(tracker.lastAnnounceTime);
|
|
|
|
if (tracker.lastAnnounceSucceeded) {
|
|
|
|
lastAnnounce = [
|
|
|
|
lastAnnounceTime,
|
|
|
|
' (got ',
|
|
|
|
Formatter.countString('peer', 'peers', tracker.lastAnnouncePeerCount),
|
|
|
|
')',
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
lastAnnounceLabel = 'Announce error';
|
|
|
|
lastAnnounce = [
|
|
|
|
tracker.lastAnnounceResult ? `${tracker.lastAnnounceResult} - ` : '',
|
|
|
|
lastAnnounceTime,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
label: lastAnnounceLabel,
|
|
|
|
value: lastAnnounce.join(''),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static lastScrapeStatus(tracker) {
|
|
|
|
let lastScrapeLabel = 'Last Scrape';
|
|
|
|
let lastScrape = 'N/A';
|
|
|
|
|
|
|
|
if (tracker.hasScraped) {
|
|
|
|
const lastScrapeTime = Formatter.timestamp(tracker.lastScrapeTime);
|
|
|
|
if (tracker.lastScrapeSucceeded) {
|
|
|
|
lastScrape = lastScrapeTime;
|
|
|
|
} else {
|
|
|
|
lastScrapeLabel = 'Scrape error';
|
|
|
|
lastScrape =
|
|
|
|
(tracker.lastScrapeResult ? `${tracker.lastScrapeResult} - ` : '') +
|
|
|
|
lastScrapeTime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
label: lastScrapeLabel,
|
|
|
|
value: lastScrape,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateTiers() {
|
|
|
|
const na = 'N/A';
|
|
|
|
const { list } = this.elements.tiers;
|
|
|
|
const { torrents } = this;
|
|
|
|
|
|
|
|
const rows = [];
|
|
|
|
for (const tor of torrents) {
|
|
|
|
const group = document.createElement('div');
|
|
|
|
group.classList.add('inspector-group');
|
|
|
|
rows.push(group);
|
|
|
|
|
|
|
|
// if >1 torrent to be shown, give a title
|
|
|
|
if (torrents.length > 1) {
|
|
|
|
const title = document.createElement('div');
|
|
|
|
title.classList.add('tier-list-torrent');
|
|
|
|
setTextContent(title, tor.getName());
|
|
|
|
rows.push(title);
|
|
|
|
}
|
|
|
|
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const [index, tracker] of tor.getTrackers().entries()) {
|
2020-10-24 01:04:25 +00:00
|
|
|
const announceState = Inspector.getAnnounceState(tracker);
|
|
|
|
const lastAnnounceStatusHash = Inspector.lastAnnounceStatus(tracker);
|
|
|
|
const lastScrapeStatusHash = Inspector.lastScrapeStatus(tracker);
|
|
|
|
|
|
|
|
const tier_div = document.createElement('div');
|
|
|
|
tier_div.classList.add('tier-list-row', index % 2 ? 'odd' : 'even');
|
|
|
|
|
|
|
|
let element = document.createElement('div');
|
|
|
|
element.classList.add('tier-list-tracker');
|
|
|
|
setTextContent(
|
|
|
|
element,
|
2022-02-12 17:30:27 +00:00
|
|
|
`${tracker.sitename || tracker.host || tracker.announce} - tier ${
|
2020-10-24 01:04:25 +00:00
|
|
|
tracker.tier + 1
|
|
|
|
}`
|
|
|
|
);
|
|
|
|
element.setAttribute('title', tracker.announce);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
element = document.createElement('div');
|
|
|
|
element.classList.add('tier-announce');
|
|
|
|
setTextContent(
|
|
|
|
element,
|
|
|
|
`${lastAnnounceStatusHash.label}: ${lastAnnounceStatusHash.value}`
|
|
|
|
);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
element = document.createElement('div');
|
|
|
|
element.classList.add('tier-seeders');
|
|
|
|
setTextContent(
|
|
|
|
element,
|
|
|
|
`Seeders: ${tracker.seederCount > -1 ? tracker.seederCount : na}`
|
|
|
|
);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
element = document.createElement('div');
|
|
|
|
element.classList.add('tier-state');
|
|
|
|
setTextContent(element, announceState);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
element = document.createElement('div');
|
|
|
|
element.classList.add('tier-leechers');
|
|
|
|
setTextContent(
|
|
|
|
element,
|
|
|
|
`Leechers: ${tracker.leecherCount > -1 ? tracker.leecherCount : na}`
|
|
|
|
);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
element = document.createElement('div');
|
|
|
|
element.classList.add('tier-scrape');
|
|
|
|
setTextContent(
|
|
|
|
element,
|
|
|
|
`${lastScrapeStatusHash.label}: ${lastScrapeStatusHash.value}`
|
|
|
|
);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
element = document.createElement('div');
|
|
|
|
element.classList.add('tier-downloads');
|
|
|
|
setTextContent(
|
|
|
|
element,
|
|
|
|
`Downloads: ${
|
|
|
|
tracker.downloadCount > -1 ? tracker.downloadCount : na
|
|
|
|
}`
|
|
|
|
);
|
|
|
|
tier_div.append(element);
|
|
|
|
|
|
|
|
rows.push(tier_div);
|
2021-05-19 13:43:46 +00:00
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: modify instead of rebuilding wholesale?
|
|
|
|
while (list.firstChild) {
|
|
|
|
list.firstChild.remove();
|
|
|
|
}
|
|
|
|
list.append(...rows);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// FILES PAGE
|
|
|
|
|
|
|
|
_changeFileCommand(fileIndices, command) {
|
|
|
|
const { controller, file_torrent } = this;
|
|
|
|
const torrentId = file_torrent.getId();
|
|
|
|
controller.changeFileCommand(torrentId, fileIndices, command);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onFileWantedToggled(event_) {
|
|
|
|
const { indices, wanted } = event_;
|
|
|
|
this._changeFileCommand(
|
|
|
|
indices,
|
|
|
|
wanted ? 'files-wanted' : 'files-unwanted'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
_onFilePriorityToggled(event_) {
|
|
|
|
const { indices, priority } = event_;
|
|
|
|
|
|
|
|
let command = null;
|
|
|
|
switch (priority) {
|
|
|
|
case -1:
|
|
|
|
command = 'priority-low';
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
command = 'priority-high';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
command = 'priority-normal';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._changeFileCommand(indices, command);
|
|
|
|
}
|
|
|
|
|
|
|
|
_clearFileList() {
|
|
|
|
const { list } = this.elements.files;
|
|
|
|
while (list.firstChild) {
|
|
|
|
list.firstChild.remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.file_torrent = null;
|
|
|
|
this.file_torrent_n = null;
|
|
|
|
this.file_rows = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
static createFileTreeModel(tor) {
|
|
|
|
const leaves = [];
|
|
|
|
const tree = {
|
|
|
|
children: {},
|
|
|
|
file_indices: [],
|
|
|
|
};
|
|
|
|
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const [index, file] of tor.getFiles().entries()) {
|
2020-10-24 01:04:25 +00:00
|
|
|
const { name } = file;
|
|
|
|
const tokens = name.split('/');
|
|
|
|
let walk = tree;
|
|
|
|
for (const [index_, token] of tokens.entries()) {
|
|
|
|
let sub = walk.children[token];
|
|
|
|
if (!sub) {
|
|
|
|
walk.children[token] = sub = {
|
|
|
|
children: {},
|
|
|
|
depth: index_,
|
|
|
|
file_indices: [],
|
|
|
|
name: token,
|
|
|
|
parent: walk,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
walk = sub;
|
|
|
|
}
|
|
|
|
walk.file_index = index;
|
|
|
|
delete walk.children;
|
|
|
|
leaves.push(walk);
|
2021-05-19 13:43:46 +00:00
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
|
|
|
|
for (const leaf of leaves) {
|
|
|
|
const { file_index } = leaf;
|
|
|
|
let walk = leaf;
|
|
|
|
do {
|
|
|
|
walk.file_indices.push(file_index);
|
|
|
|
walk = walk.parent;
|
|
|
|
} while (walk);
|
|
|
|
}
|
|
|
|
|
|
|
|
return tree;
|
|
|
|
}
|
|
|
|
|
|
|
|
addNodeToView(tor, parent, sub, index) {
|
|
|
|
const row = new FileRow(
|
|
|
|
tor,
|
|
|
|
sub.depth,
|
|
|
|
sub.name,
|
|
|
|
sub.file_indices,
|
|
|
|
index % 2
|
|
|
|
);
|
|
|
|
row.addEventListener('wantedToggled', this._onFileWantedToggled.bind(this));
|
|
|
|
row.addEventListener(
|
|
|
|
'priorityToggled',
|
|
|
|
this._onFilePriorityToggled.bind(this)
|
|
|
|
);
|
|
|
|
this.file_rows.push(row);
|
|
|
|
parent.append(row.getElement());
|
|
|
|
}
|
|
|
|
|
|
|
|
addSubtreeToView(tor, parent, sub, index) {
|
|
|
|
if (sub.parent) {
|
|
|
|
this.addNodeToView(tor, parent, sub, index++);
|
|
|
|
}
|
|
|
|
if (sub.children) {
|
|
|
|
for (const value of Object.values(sub.children)) {
|
|
|
|
index = this.addSubtreeToView(tor, parent, value, index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateFiles() {
|
|
|
|
const { list } = this.elements.files;
|
2021-05-19 13:43:46 +00:00
|
|
|
const { file_rows, file_torrent, file_torrent_n, torrents } = this;
|
2020-10-24 01:04:25 +00:00
|
|
|
|
|
|
|
// only show one torrent at a time
|
|
|
|
if (torrents.length !== 1) {
|
|
|
|
this._clearFileList();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const [tor] = torrents;
|
|
|
|
const n = tor.getFiles().length;
|
2021-05-19 13:43:46 +00:00
|
|
|
if (tor !== file_torrent || n !== file_torrent_n) {
|
2020-10-24 01:04:25 +00:00
|
|
|
// rebuild the file list...
|
|
|
|
this._clearFileList();
|
|
|
|
this.file_torrent = tor;
|
|
|
|
this.file_torrent_n = n;
|
|
|
|
this.file_rows = [];
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
const tree = Inspector.createFileTreeModel(tor);
|
|
|
|
this.addSubtreeToView(tor, fragment, tree, 0);
|
|
|
|
list.append(fragment);
|
|
|
|
} else {
|
|
|
|
// ...refresh the already-existing file list
|
2021-05-19 13:43:46 +00:00
|
|
|
for (const row of file_rows) {
|
|
|
|
row.refresh();
|
|
|
|
}
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|