pmtiles-swarm 0.30.0 β 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +1 -0
- package/docs/engines.md +41 -3
- package/package.json +1 -1
- package/src/api.js +15 -0
- package/src/engines/composite.js +44 -0
- package/src/engines/libtorrent.js +27 -0
- package/src/engines/qbittorrent.js +14 -0
- package/src/engines/types.js +1 -0
- package/src/library.js +62 -0
- package/src/web/index.html +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.31.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- **A Recheck files button, for when an archive's progress and its files disagree.** Every figure a
|
|
13
|
+
node can give you about how much of an archive is present is derived from something written down
|
|
14
|
+
earlier: the catalog's `complete` flag, the engine's resume data, the `seedOnly` claim made when
|
|
15
|
+
the torrent was added. When one of those is wrong there is no path back on its own β an archive
|
|
16
|
+
built on this node whose entry says `complete: false` is re-added without `seedOnly`, so the
|
|
17
|
+
engine goes looking for bytes that are already under its nose and sits at 0% beside a finished
|
|
18
|
+
file. Every restart reaches the same conclusion.
|
|
19
|
+
|
|
20
|
+
`POST /api/torrents/<infohash>/recheck` hashes every piece against the torrent and the result
|
|
21
|
+
wins. libtorrent does it with `force_recheck` through the sidecar (pmtiles-torrent 0.5.1 or
|
|
22
|
+
newer); qBittorrent has the same operation in its WebUI API. WebTorrent has none β it verifies on
|
|
23
|
+
add and never again β so it is re-added with the "the data is already here" claim withheld, which
|
|
24
|
+
is reported as `method: "readd"` rather than dressed up as the same mechanism.
|
|
25
|
+
|
|
26
|
+
Answers as soon as the check is under way, because hashing a planet build is tens of minutes and
|
|
27
|
+
no request should be held open for it. The archive reports state `checking` with progress as the
|
|
28
|
+
fraction hashed; progress running backwards during that is the operation working. Nothing is
|
|
29
|
+
deleted, and nothing is written to the catalog β the answer arrives where progress always does,
|
|
30
|
+
and the completion sweep already acts on it.
|
|
31
|
+
|
|
32
|
+
With two engines both are asked, since each keeps its own belief about the same file and a stale
|
|
33
|
+
one on the secondary is why a browser peer would find nothing while the primary seeds happily.
|
|
34
|
+
|
|
35
|
+
- _...Add new stuff here..._
|
|
36
|
+
|
|
37
|
+
### π Bug fixes
|
|
38
|
+
- _...Add new stuff here..._
|
|
39
|
+
|
|
10
40
|
## 0.30.0
|
|
11
41
|
### β¨ Features and improvements
|
|
12
42
|
- **The style URL now carries the `.torrent` URL as well as the magnet.** Piece hashes reach a
|
package/README.md
CHANGED
|
@@ -753,6 +753,7 @@ which the endpoint answers 501.
|
|
|
753
753
|
| `PATCH` | `/api/torrents/:infoHash/categories` | Set, add or remove categories |
|
|
754
754
|
| `PATCH` | `/api/torrents/:infoHash/seeding` | Per-archive seeding limit, or "use the global one" |
|
|
755
755
|
| `PATCH` `GET` | `/api/torrents/:infoHash/location` | Move the data; poll the move |
|
|
756
|
+
| `POST` | `/api/torrents/:infoHash/recheck` | Hash what is on disk again and believe the result β for an archive whose progress and whose files disagree |
|
|
756
757
|
| `POST` | `/api/torrents/:infoHash/pause`, `/resume` | Stop offering it, without forgetting it |
|
|
757
758
|
| `POST` | `/api/torrents/:infoHash/webseeds` | Add web seeds β does not change the infohash |
|
|
758
759
|
| `POST` | `/api/torrents/:infoHash/warm` | Pre-fetch a region (`GET` for progress, `DELETE` to cancel) |
|
package/docs/engines.md
CHANGED
|
@@ -344,6 +344,43 @@ Trackers live outside the torrent's `info` dictionary, so adding them **does not
|
|
|
344
344
|
infohash** β but it only applies to torrents created after the change. An existing archive keeps
|
|
345
345
|
announcing where its own torrent says to.
|
|
346
346
|
|
|
347
|
+
## Rechecking what is on disk
|
|
348
|
+
|
|
349
|
+
Every figure a node can give you about how much of an archive is present is derived from something
|
|
350
|
+
written down earlier: the catalog's `complete` flag, the engine's resume data, the `seedOnly` claim
|
|
351
|
+
made when the torrent was added. When one of those is wrong there is no path back on its own β the
|
|
352
|
+
archive sits at 0% beside a finished file, downloading bytes it already has, and every restart
|
|
353
|
+
repeats the same conclusion.
|
|
354
|
+
|
|
355
|
+
**Recheck files** in the console (`POST /api/torrents/<infohash>/recheck`) is the one operation
|
|
356
|
+
that goes and looks. It hashes every piece against the torrent and the result wins.
|
|
357
|
+
|
|
358
|
+
| Engine | How |
|
|
359
|
+
| ----------- | --------------------------------------------------------------- |
|
|
360
|
+
| libtorrent | `force_recheck`, via the sidecar. Needs pmtiles-torrent β₯ 0.5.1 |
|
|
361
|
+
| qBittorrent | `POST /api/v2/torrents/recheck` β the same library underneath |
|
|
362
|
+
| WebTorrent | no such operation; re-added with `seedOnly` withheld instead |
|
|
363
|
+
|
|
364
|
+
It answers as soon as the check is under way. Hashing a planet build is tens of minutes, which is
|
|
365
|
+
longer than any request should be held open for, so the archive reports state `checking` with
|
|
366
|
+
progress as the fraction hashed. **Progress running backwards during that is the operation
|
|
367
|
+
working**, not a fault. Nothing is deleted at any point.
|
|
368
|
+
|
|
369
|
+
WebTorrent's route is cruder and is reported as `method: "readd"` rather than dressed up as the
|
|
370
|
+
same thing: it verifies on add and never again, so the only way to make it look is to add the
|
|
371
|
+
torrent a second time with the "the data is already here" claim withheld. It also refuses on a
|
|
372
|
+
paused archive, since a re-add would start it.
|
|
373
|
+
|
|
374
|
+
Nothing is written to the catalog when the check begins. The answer arrives where progress always
|
|
375
|
+
does β the engine's own figures, which the completion sweep already reads. Note the one thing this
|
|
376
|
+
does not repair on its own: the sweep promotes and never demotes, so a recheck finding _less_ than
|
|
377
|
+
the record claims shows the truth in the console but leaves `complete: true` in place. Demoting on
|
|
378
|
+
a progress figure would strip the finished name off any archive that happened to be mid-check.
|
|
379
|
+
|
|
380
|
+
With two engines both are asked. Each keeps its own belief about the same file, so a stale one on
|
|
381
|
+
the secondary is why a browser peer finds nothing while the primary seeds happily. A secondary that
|
|
382
|
+
cannot check is reported and does not fail the operation; the primary failing does.
|
|
383
|
+
|
|
347
384
|
## Marking incomplete files
|
|
348
385
|
|
|
349
386
|
An archive that is not whole yet is written under a marked name and renamed when
|
|
@@ -375,9 +412,10 @@ checked and the unreadable ones are joined by magnet instead.
|
|
|
375
412
|
|
|
376
413
|
## Writing another engine
|
|
377
414
|
|
|
378
|
-
Implement `connect`, `add`, `remove`, `list`, `get`, `destroy`, and optionally `peers
|
|
379
|
-
`reachability`. Anything optional that is missing is simply not offered β an engine
|
|
380
|
-
`reachability` hides the indicator rather than reporting a node unreachable
|
|
415
|
+
Implement `connect`, `add`, `remove`, `list`, `get`, `destroy`, and optionally `peers`,
|
|
416
|
+
`reachability` and `recheck`. Anything optional that is missing is simply not offered β an engine
|
|
417
|
+
without `reachability` hides the indicator rather than reporting a node unreachable, and one
|
|
418
|
+
without `recheck` is rechecked by re-adding instead.
|
|
381
419
|
The interface is deliberately small; see
|
|
382
420
|
[`src/engines/types.js`](../src/engines/types.js) for the contract and
|
|
383
421
|
[`src/engines/webtorrent.js`](../src/engines/webtorrent.js) for the shortest example.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/api.js
CHANGED
|
@@ -1040,6 +1040,21 @@ export function createApp({
|
|
|
1040
1040
|
}),
|
|
1041
1041
|
);
|
|
1042
1042
|
|
|
1043
|
+
// When the record and the disk disagree, this is the only thing that goes and
|
|
1044
|
+
// looks. Answers as soon as the check is under way -- hashing a planet build
|
|
1045
|
+
// is tens of minutes, and no request should be held open for it.
|
|
1046
|
+
app.post(
|
|
1047
|
+
'/api/torrents/:infoHash/recheck',
|
|
1048
|
+
route(async (req, res) => {
|
|
1049
|
+
try {
|
|
1050
|
+
const result = await library.recheck(req.params.infoHash);
|
|
1051
|
+
res.status(202).json(result);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
res.status(error.status ?? 500).json({ error: error.message });
|
|
1054
|
+
}
|
|
1055
|
+
}),
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1043
1058
|
// Joining defaults to cache, deliberately. This is how that is changed
|
|
1044
1059
|
// afterwards, without re-adding the archive by hand.
|
|
1045
1060
|
app.patch(
|
package/src/engines/composite.js
CHANGED
|
@@ -549,6 +549,50 @@ export class CompositeEngine {
|
|
|
549
549
|
return this.#primary.resume ? this.#primary.resume(infoHash) : false;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Hashes what is on disk again, on every engine that can.
|
|
554
|
+
*
|
|
555
|
+
* Each engine keeps its own belief about the same file, so a stale one on the
|
|
556
|
+
* secondary is the same fault as a stale one on the primary -- it is why a
|
|
557
|
+
* browser peer would find nothing while the primary seeds happily. Both are
|
|
558
|
+
* asked.
|
|
559
|
+
*
|
|
560
|
+
* They hash concurrently, which sounds worse than it is: the second pass over
|
|
561
|
+
* a file the first just read is mostly page cache, and a recheck is a rare
|
|
562
|
+
* thing somebody asked for rather than something that runs on a timer.
|
|
563
|
+
*
|
|
564
|
+
* An engine without the operation is skipped rather than worked around. The
|
|
565
|
+
* re-add that WebTorrent needs instead is the library's to do, because it
|
|
566
|
+
* takes the catalog entry -- see Library.recheck.
|
|
567
|
+
* @param {string} infoHash - The archive to verify.
|
|
568
|
+
* @returns {Promise<object>} - The primary's answer, plus one row per engine.
|
|
569
|
+
*/
|
|
570
|
+
async recheck(infoHash) {
|
|
571
|
+
const engines = [];
|
|
572
|
+
for (const engine of [this.#primary, ...this.#secondaries]) {
|
|
573
|
+
if (!engine.recheck) {
|
|
574
|
+
engines.push({ engine: engine.name, rechecking: false, skipped: true });
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const result = await engine.recheck(infoHash);
|
|
579
|
+
engines.push({ engine: engine.name, ...result });
|
|
580
|
+
} catch (error) {
|
|
581
|
+
// A secondary that cannot verify must not fail the primary's check.
|
|
582
|
+
// The primary holds the data the tiles are served from.
|
|
583
|
+
engines.push({
|
|
584
|
+
engine: engine.name,
|
|
585
|
+
rechecking: false,
|
|
586
|
+
error: error.message,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const primary = engines[0];
|
|
592
|
+
if (primary.error) throw new Error(primary.error);
|
|
593
|
+
return { ...primary, engines };
|
|
594
|
+
}
|
|
595
|
+
|
|
552
596
|
/**
|
|
553
597
|
* Tells every engine about a web seed.
|
|
554
598
|
* @param {string} infoHash - The archive.
|
|
@@ -303,6 +303,33 @@ export class LibtorrentEngine {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Hashes what is on disk again and believes the result over the record.
|
|
308
|
+
*
|
|
309
|
+
* Returns as soon as the check is under way, not when it finishes: a planet
|
|
310
|
+
* archive is tens of minutes of disk. The torrent reports state `checking`
|
|
311
|
+
* while it runs, with progress as the fraction hashed.
|
|
312
|
+
* @param {string} infoHash - The archive to verify.
|
|
313
|
+
* @returns {Promise<object>} - `{rechecking, wasPaused}`.
|
|
314
|
+
*/
|
|
315
|
+
async recheck(infoHash) {
|
|
316
|
+
try {
|
|
317
|
+
return await this.#call('recheck', { infoHash });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// An older sidecar answers "unknown op", which is true and useless: it
|
|
320
|
+
// reads as a bug in the request rather than as a package that needs
|
|
321
|
+
// updating. Said plainly instead, because this is a button somebody just
|
|
322
|
+
// pressed and the next thing they do depends on which it is.
|
|
323
|
+
if (/unknown op/i.test(error.message)) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
'this sidecar cannot recheck; pmtiles-torrent 0.5.1 or newer is needed',
|
|
326
|
+
{ cause: error },
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
306
333
|
async list() {
|
|
307
334
|
// A node that is shutting down still has a console polling it and a sweep
|
|
308
335
|
// or two in flight. Answering "the sidecar exited" to each of them fills
|
|
@@ -211,6 +211,20 @@ export class QBittorrentEngine {
|
|
|
211
211
|
await this.#request('/api/v2/torrents/delete', { method: 'POST', body });
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Hashes what is on disk again and believes the result over the record.
|
|
216
|
+
*
|
|
217
|
+
* qBittorrent starts the check and answers immediately, the same as the
|
|
218
|
+
* libtorrent engine does -- which is the same library underneath.
|
|
219
|
+
* @param {string} infoHash - The archive to verify.
|
|
220
|
+
* @returns {Promise<object>} - `{rechecking: true}`.
|
|
221
|
+
*/
|
|
222
|
+
async recheck(infoHash) {
|
|
223
|
+
const body = new URLSearchParams({ hashes: infoHash.toLowerCase() });
|
|
224
|
+
await this.#request('/api/v2/torrents/recheck', { method: 'POST', body });
|
|
225
|
+
return { rechecking: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
214
228
|
/**
|
|
215
229
|
* Lists every torrent qBittorrent holds.
|
|
216
230
|
* @returns {Promise<import('./types.js').TorrentStatus[]>} - Normalised statuses.
|
package/src/engines/types.js
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
* @property {(filePath: string, options?: object) => Promise<object>} [createTorrent] - Builds a torrent from a local file, where the engine can do it better than the default β libtorrent produces hybrid v1+v2, which create-torrent cannot.
|
|
61
61
|
* @property {(infoHash: string) => Promise<object[]>} [trackerStatus] - Per-tracker announce results, where the engine keeps them.
|
|
62
62
|
* @property {(infoHash: string) => Promise<Uint8Array | null>} [metadata] - The torrent's metainfo once known, so an archive joined by magnet can be written down rather than re-fetched over BEP 9 on every start.
|
|
63
|
+
* @property {(infoHash: string) => Promise<object>} [recheck] - Hash what is on disk again and believe the result over the stored state -- resume data, or the `seedOnly` claim made when the torrent was added. Returns once the check is under way, not when it finishes: a planet archive is tens of minutes of disk. An engine without this is rechecked by re-adding it instead, which the library does.
|
|
63
64
|
* @property {() => Promise<object | null>} [reachability] - Whether peers can open a connection to this node, or only the reverse. Three states: `open` (something has connected inward), `unproven` (listening, but nothing ever has) and `offline` (not listening at all). The middle is deliberately not called firewalled -- on a node with no peers, blocked and untried are the same observation.
|
|
64
65
|
* @property {() => Promise<void>} destroy - Releases resources.
|
|
65
66
|
*/
|
package/src/library.js
CHANGED
|
@@ -1657,6 +1657,68 @@ export class Library {
|
|
|
1657
1657
|
return this.#catalog.put({ infoHash, paused: true });
|
|
1658
1658
|
}
|
|
1659
1659
|
|
|
1660
|
+
/**
|
|
1661
|
+
* Hashes what is on disk again and believes the result over the record.
|
|
1662
|
+
*
|
|
1663
|
+
* Every other answer about how much of an archive is here comes from
|
|
1664
|
+
* something written down earlier -- the catalog's `complete` flag, resume
|
|
1665
|
+
* data, the `seedOnly` claim made when it was added. When one of those is
|
|
1666
|
+
* wrong there is no path back on its own: an archive built here whose entry
|
|
1667
|
+
* says `complete: false` is re-added without `seedOnly`, so the engine goes
|
|
1668
|
+
* looking for bytes that are already under its nose and sits at 0% beside a
|
|
1669
|
+
* finished file. This is the way out.
|
|
1670
|
+
*
|
|
1671
|
+
* Nothing is written to the catalog here. The check runs for as long as it
|
|
1672
|
+
* takes to hash the archive -- tens of minutes for a planet build -- so the
|
|
1673
|
+
* answer arrives long after this returns, and it arrives where it always
|
|
1674
|
+
* does: the engine's own progress, which the completion sweep already reads
|
|
1675
|
+
* and acts on. Recording a guess now would only have to be corrected later.
|
|
1676
|
+
*
|
|
1677
|
+
* Note what this cannot fix on its own. The sweep promotes, it never demotes,
|
|
1678
|
+
* so a recheck that finds *less* than the record claims shows the truth in
|
|
1679
|
+
* the console but leaves `complete: true` in place. That is deliberate: a
|
|
1680
|
+
* torrent reports progress below 1 for perfectly ordinary reasons while it is
|
|
1681
|
+
* checking, and demoting on that would strip the finished name off an archive
|
|
1682
|
+
* that is merely being verified.
|
|
1683
|
+
* @param {string} infoHash - The archive to verify.
|
|
1684
|
+
* @returns {Promise<object>} - `{rechecking, method}`.
|
|
1685
|
+
*/
|
|
1686
|
+
async recheck(infoHash) {
|
|
1687
|
+
const entry = this.#catalog.get(infoHash);
|
|
1688
|
+
if (!entry) {
|
|
1689
|
+
const error = new Error('unknown archive');
|
|
1690
|
+
error.status = 404;
|
|
1691
|
+
throw error;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (this.#engine.recheck) {
|
|
1695
|
+
const result = await this.#engine.recheck(infoHash);
|
|
1696
|
+
return { ...result, rechecking: true, method: 'recheck' };
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// WebTorrent has no recheck at all: it verifies on add and never again. So
|
|
1700
|
+
// the only way to make it look is to make it add the torrent again, with
|
|
1701
|
+
// the claim that the data is already there withheld -- `seedOnly` is
|
|
1702
|
+
// precisely "do not verify this", and it is computed from `complete`, so a
|
|
1703
|
+
// copy of the entry saying otherwise is what turns the check on.
|
|
1704
|
+
//
|
|
1705
|
+
// Nothing is deleted and the catalog is not touched; this re-adds the same
|
|
1706
|
+
// torrent against the same files. It is a slower and cruder mechanism than
|
|
1707
|
+
// force_recheck, and it is reported as a different one rather than dressed
|
|
1708
|
+
// up as the same thing.
|
|
1709
|
+
if (entry.paused) {
|
|
1710
|
+
const error = new Error(
|
|
1711
|
+
'this engine rechecks by re-adding the archive, which a paused ' +
|
|
1712
|
+
'archive cannot do. Resume it first.',
|
|
1713
|
+
);
|
|
1714
|
+
error.status = 409;
|
|
1715
|
+
throw error;
|
|
1716
|
+
}
|
|
1717
|
+
await this.#engine.remove(infoHash, { deleteData: false }).catch(() => {});
|
|
1718
|
+
await this.#readd({ ...entry, complete: false });
|
|
1719
|
+
return { rechecking: true, method: 'readd' };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1660
1722
|
/**
|
|
1661
1723
|
* Starts offering a paused archive again.
|
|
1662
1724
|
* @param {string} infoHash - The archive.
|
package/src/web/index.html
CHANGED
|
@@ -1641,6 +1641,7 @@
|
|
|
1641
1641
|
<button id="set-location">Set locationβ¦</button>
|
|
1642
1642
|
<button id="add-seed">Add web seed</button>
|
|
1643
1643
|
<button id="clear-cache" ${mode === 'cache' ? '' : 'disabled title="only cache-mode archives have a cache to clear"'}>Clear cache</button>
|
|
1644
|
+
<button id="recheck" title="Hash the files on disk again and believe the result. For an archive that reads 0% next to a file that is plainly there, or one that claims to be complete and is not β every other figure comes from something written down earlier, and this is the only thing that goes and looks.">Recheck files</button>
|
|
1644
1645
|
<button id="pause">${entry.paused ? 'Resume' : 'Pause'}</button>
|
|
1645
1646
|
<button id="remove" class="danger">Remove</button>
|
|
1646
1647
|
</div>
|
|
@@ -1949,6 +1950,38 @@
|
|
|
1949
1950
|
}
|
|
1950
1951
|
};
|
|
1951
1952
|
|
|
1953
|
+
$('recheck').onclick = async () => {
|
|
1954
|
+
if (
|
|
1955
|
+
!window.confirm(
|
|
1956
|
+
`Recheck ${entry.name}.
|
|
1957
|
+
|
|
1958
|
+
Every piece is hashed against the ` +
|
|
1959
|
+
'torrent, which for a large archive is minutes to tens of ' +
|
|
1960
|
+
'minutes of disk. Nothing is deleted, and the archive keeps ' +
|
|
1961
|
+
'serving whatever it can while the check runs.',
|
|
1962
|
+
)
|
|
1963
|
+
) {
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
const result = await api(`/api/torrents/${infoHash}/recheck`, {
|
|
1968
|
+
method: 'POST',
|
|
1969
|
+
});
|
|
1970
|
+
// Said rather than left to be inferred: this returns as soon as the
|
|
1971
|
+
// check is under way, and the progress bar going backwards for the
|
|
1972
|
+
// next twenty minutes is the operation working, not a fault.
|
|
1973
|
+
toast(
|
|
1974
|
+
result.method === 'readd'
|
|
1975
|
+
? 'rechecking β this engine verifies by re-adding, so it will start from 0%'
|
|
1976
|
+
: 'rechecking β progress shows the fraction hashed',
|
|
1977
|
+
);
|
|
1978
|
+
refresh();
|
|
1979
|
+
renderDetail(infoHash);
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
toast(error.message);
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1952
1985
|
$('pause').onclick = async () => {
|
|
1953
1986
|
const action = entry.paused ? 'resume' : 'pause';
|
|
1954
1987
|
try {
|