cross-seed 7.0.0-1 → 7.0.0-2
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/dist/Result.d.ts +27 -0
- package/dist/Result.js +64 -0
- package/dist/Result.js.map +1 -0
- package/dist/action.d.ts +34 -0
- package/dist/action.js +694 -0
- package/dist/action.js.map +1 -0
- package/dist/arr.d.ts +31 -0
- package/dist/arr.js +267 -0
- package/dist/arr.js.map +1 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +28 -0
- package/dist/auth.js.map +1 -0
- package/dist/clients/Deluge.d.ts +153 -0
- package/dist/clients/Deluge.js +698 -0
- package/dist/clients/Deluge.js.map +1 -0
- package/dist/clients/QBittorrent.d.ts +218 -0
- package/dist/clients/QBittorrent.js +785 -0
- package/dist/clients/QBittorrent.js.map +1 -0
- package/dist/clients/RTorrent.d.ts +43 -0
- package/dist/clients/RTorrent.js +657 -0
- package/dist/clients/RTorrent.js.map +1 -0
- package/dist/clients/TorrentClient.d.ts +108 -0
- package/dist/clients/TorrentClient.js +341 -0
- package/dist/clients/TorrentClient.js.map +1 -0
- package/dist/clients/Transmission.d.ts +43 -0
- package/dist/clients/Transmission.js +404 -0
- package/dist/clients/Transmission.js.map +1 -0
- package/dist/cmd.d.ts +2 -0
- package/dist/cmd.js +128 -0
- package/dist/cmd.js.map +1 -0
- package/dist/configSchema.d.ts +1 -0
- package/dist/configSchema.js +2 -0
- package/dist/configSchema.js.map +1 -0
- package/dist/configuration.d.ts +63 -0
- package/dist/configuration.js +321 -0
- package/dist/configuration.js.map +1 -0
- package/dist/constants.d.ts +108 -0
- package/dist/constants.js +251 -0
- package/dist/constants.js.map +1 -0
- package/dist/dataFiles.d.ts +8 -0
- package/dist/dataFiles.js +223 -0
- package/dist/dataFiles.js.map +1 -0
- package/dist/db.d.ts +3 -0
- package/dist/db.js +216 -0
- package/dist/db.js.map +1 -0
- package/dist/dbConfig.d.ts +4 -0
- package/dist/dbConfig.js +67 -0
- package/dist/dbConfig.js.map +1 -0
- package/dist/decide.d.ts +25 -0
- package/dist/decide.js +553 -0
- package/dist/decide.js.map +1 -0
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +24 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -0
- package/dist/indexers.d.ts +105 -0
- package/dist/indexers.js +248 -0
- package/dist/indexers.js.map +1 -0
- package/dist/inject.d.ts +2 -0
- package/dist/inject.js +594 -0
- package/dist/inject.js.map +1 -0
- package/dist/jobs.d.ts +29 -0
- package/dist/jobs.js +151 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.js +157 -0
- package/dist/logger.js.map +1 -0
- package/dist/migrations/00-initialSchema.d.ts +9 -0
- package/dist/migrations/00-initialSchema.js +30 -0
- package/dist/migrations/00-initialSchema.js.map +1 -0
- package/dist/migrations/01-jobs.d.ts +9 -0
- package/dist/migrations/01-jobs.js +12 -0
- package/dist/migrations/01-jobs.js.map +1 -0
- package/dist/migrations/02-timestamps.d.ts +9 -0
- package/dist/migrations/02-timestamps.js +21 -0
- package/dist/migrations/02-timestamps.js.map +1 -0
- package/dist/migrations/03-rateLimits.d.ts +9 -0
- package/dist/migrations/03-rateLimits.js +14 -0
- package/dist/migrations/03-rateLimits.js.map +1 -0
- package/dist/migrations/04-auth.d.ts +9 -0
- package/dist/migrations/04-auth.js +13 -0
- package/dist/migrations/04-auth.js.map +1 -0
- package/dist/migrations/05-caps.d.ts +9 -0
- package/dist/migrations/05-caps.js +16 -0
- package/dist/migrations/05-caps.js.map +1 -0
- package/dist/migrations/06-uniqueDecisions.d.ts +9 -0
- package/dist/migrations/06-uniqueDecisions.js +29 -0
- package/dist/migrations/06-uniqueDecisions.js.map +1 -0
- package/dist/migrations/07-limits.d.ts +9 -0
- package/dist/migrations/07-limits.js +12 -0
- package/dist/migrations/07-limits.js.map +1 -0
- package/dist/migrations/08-rss.d.ts +9 -0
- package/dist/migrations/08-rss.js +15 -0
- package/dist/migrations/08-rss.js.map +1 -0
- package/dist/migrations/09-clientAndDataSearchees.d.ts +9 -0
- package/dist/migrations/09-clientAndDataSearchees.js +34 -0
- package/dist/migrations/09-clientAndDataSearchees.js.map +1 -0
- package/dist/migrations/10-indexerNameAudioBookCaps.d.ts +9 -0
- package/dist/migrations/10-indexerNameAudioBookCaps.js +18 -0
- package/dist/migrations/10-indexerNameAudioBookCaps.js.map +1 -0
- package/dist/migrations/11-trackers.d.ts +9 -0
- package/dist/migrations/11-trackers.js +38 -0
- package/dist/migrations/11-trackers.js.map +1 -0
- package/dist/migrations/12-user-auth.d.ts +9 -0
- package/dist/migrations/12-user-auth.js +22 -0
- package/dist/migrations/12-user-auth.js.map +1 -0
- package/dist/migrations/13-settings.d.ts +9 -0
- package/dist/migrations/13-settings.js +23 -0
- package/dist/migrations/13-settings.js.map +1 -0
- package/dist/migrations/14-indexer-enabled-flag.d.ts +9 -0
- package/dist/migrations/14-indexer-enabled-flag.js +12 -0
- package/dist/migrations/14-indexer-enabled-flag.js.map +1 -0
- package/dist/migrations/15-remove-url-unique-constraint.d.ts +9 -0
- package/dist/migrations/15-remove-url-unique-constraint.js +14 -0
- package/dist/migrations/15-remove-url-unique-constraint.js.map +1 -0
- package/dist/migrations/16-prune-inactive-indexers.d.ts +9 -0
- package/dist/migrations/16-prune-inactive-indexers.js +17 -0
- package/dist/migrations/16-prune-inactive-indexers.js.map +1 -0
- package/dist/migrations/migrations.d.ts +13 -0
- package/dist/migrations/migrations.js +41 -0
- package/dist/migrations/migrations.js.map +1 -0
- package/dist/parseTorrent.d.ts +53 -0
- package/dist/parseTorrent.js +128 -0
- package/dist/parseTorrent.js.map +1 -0
- package/dist/pipeline.d.ts +41 -0
- package/dist/pipeline.js +574 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/preFilter.d.ts +25 -0
- package/dist/preFilter.js +250 -0
- package/dist/preFilter.js.map +1 -0
- package/dist/problems/linking.d.ts +2 -0
- package/dist/problems/linking.js +80 -0
- package/dist/problems/linking.js.map +1 -0
- package/dist/problems/path.d.ts +22 -0
- package/dist/problems/path.js +96 -0
- package/dist/problems/path.js.map +1 -0
- package/dist/problems.d.ts +13 -0
- package/dist/problems.js +48 -0
- package/dist/problems.js.map +1 -0
- package/dist/pushNotifier.d.ts +19 -0
- package/dist/pushNotifier.js +137 -0
- package/dist/pushNotifier.js.map +1 -0
- package/dist/routes/baseApi.d.ts +2 -0
- package/dist/routes/baseApi.js +354 -0
- package/dist/routes/baseApi.js.map +1 -0
- package/dist/routes/indexerApi.d.ts +6 -0
- package/dist/routes/indexerApi.js +165 -0
- package/dist/routes/indexerApi.js.map +1 -0
- package/dist/routes/staticFrontendPlugin.d.ts +4 -0
- package/dist/routes/staticFrontendPlugin.js +61 -0
- package/dist/routes/staticFrontendPlugin.js.map +1 -0
- package/dist/runtimeConfig.d.ts +6 -0
- package/dist/runtimeConfig.js +27 -0
- package/dist/runtimeConfig.js.map +1 -0
- package/dist/searchee.d.ts +108 -0
- package/dist/searchee.js +689 -0
- package/dist/searchee.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +65 -0
- package/dist/server.js.map +1 -0
- package/dist/services/indexerService.d.ts +96 -0
- package/dist/services/indexerService.js +287 -0
- package/dist/services/indexerService.js.map +1 -0
- package/dist/sessionCookies.d.ts +5 -0
- package/dist/sessionCookies.js +27 -0
- package/dist/sessionCookies.js.map +1 -0
- package/dist/startup.d.ts +25 -0
- package/dist/startup.js +157 -0
- package/dist/startup.js.map +1 -0
- package/dist/torrent.d.ts +69 -0
- package/dist/torrent.js +641 -0
- package/dist/torrent.js.map +1 -0
- package/dist/torznab.d.ts +60 -0
- package/dist/torznab.js +711 -0
- package/dist/torznab.js.map +1 -0
- package/dist/trpc/fastifyAdapter.d.ts +2 -0
- package/dist/trpc/fastifyAdapter.js +9 -0
- package/dist/trpc/fastifyAdapter.js.map +1 -0
- package/dist/trpc/index.d.ts +49 -0
- package/dist/trpc/index.js +53 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/trpc/routers/auth.d.ts +43 -0
- package/dist/trpc/routers/auth.js +116 -0
- package/dist/trpc/routers/auth.js.map +1 -0
- package/dist/trpc/routers/clients.d.ts +21 -0
- package/dist/trpc/routers/clients.js +65 -0
- package/dist/trpc/routers/clients.js.map +1 -0
- package/dist/trpc/routers/health.d.ts +14 -0
- package/dist/trpc/routers/health.js +20 -0
- package/dist/trpc/routers/health.js.map +1 -0
- package/dist/trpc/routers/index.d.ts +391 -0
- package/dist/trpc/routers/index.js +23 -0
- package/dist/trpc/routers/index.js.map +1 -0
- package/dist/trpc/routers/indexers.d.ts +75 -0
- package/dist/trpc/routers/indexers.js +79 -0
- package/dist/trpc/routers/indexers.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +33 -0
- package/dist/trpc/routers/jobs.js +84 -0
- package/dist/trpc/routers/jobs.js.map +1 -0
- package/dist/trpc/routers/logs.d.ts +27 -0
- package/dist/trpc/routers/logs.js +91 -0
- package/dist/trpc/routers/logs.js.map +1 -0
- package/dist/trpc/routers/searchees.d.ts +51 -0
- package/dist/trpc/routers/searchees.js +156 -0
- package/dist/trpc/routers/searchees.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +83 -0
- package/dist/trpc/routers/settings.js +92 -0
- package/dist/trpc/routers/settings.js.map +1 -0
- package/dist/trpc/routers/stats.d.ts +42 -0
- package/dist/trpc/routers/stats.js +102 -0
- package/dist/trpc/routers/stats.js.map +1 -0
- package/dist/userAuth.d.ts +21 -0
- package/dist/userAuth.js +86 -0
- package/dist/userAuth.js.map +1 -0
- package/dist/utils/authUtils.d.ts +10 -0
- package/dist/utils/authUtils.js +24 -0
- package/dist/utils/authUtils.js.map +1 -0
- package/dist/utils/logWatcher.d.ts +28 -0
- package/dist/utils/logWatcher.js +218 -0
- package/dist/utils/logWatcher.js.map +1 -0
- package/dist/utils/object.d.ts +1 -0
- package/dist/utils/object.js +4 -0
- package/dist/utils/object.js.map +1 -0
- package/dist/utils.d.ts +175 -0
- package/dist/utils.js +660 -0
- package/dist/utils.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { readdir } from "fs/promises";
|
|
2
|
+
import ms from "ms";
|
|
3
|
+
import { basename } from "path";
|
|
4
|
+
import { inspect } from "util";
|
|
5
|
+
import { InjectionResult, TORRENT_CATEGORY_SUFFIX, TORRENT_TAG, USER_AGENT, } from "../constants.js";
|
|
6
|
+
import { db } from "../db.js";
|
|
7
|
+
import { CrossSeedError } from "../errors.js";
|
|
8
|
+
import { Label, logger } from "../logger.js";
|
|
9
|
+
import { resultOf, resultOfErr } from "../Result.js";
|
|
10
|
+
import { getRuntimeConfig } from "../runtimeConfig.js";
|
|
11
|
+
import { createSearcheeFromDB, parseTitle, updateSearcheeClientDB, } from "../searchee.js";
|
|
12
|
+
import { extractCredentialsFromUrl, getLogString, humanReadableSize, sanitizeInfoHash, wait, } from "../utils.js";
|
|
13
|
+
import { shouldResumeFromNonRelevantFiles, clientSearcheeModified, getMaxRemainingBytes, getResumeStopTime, organizeTrackers, resumeErrSleepTime, resumeSleepTime, shouldRecheck, } from "./TorrentClient.js";
|
|
14
|
+
var DelugeErrorCode;
|
|
15
|
+
(function (DelugeErrorCode) {
|
|
16
|
+
DelugeErrorCode[DelugeErrorCode["NO_AUTH"] = 1] = "NO_AUTH";
|
|
17
|
+
DelugeErrorCode[DelugeErrorCode["BAD_METHOD"] = 2] = "BAD_METHOD";
|
|
18
|
+
DelugeErrorCode[DelugeErrorCode["CALL_ERR"] = 3] = "CALL_ERR";
|
|
19
|
+
DelugeErrorCode[DelugeErrorCode["RPC_FAIL"] = 4] = "RPC_FAIL";
|
|
20
|
+
DelugeErrorCode[DelugeErrorCode["BAD_JSON"] = 5] = "BAD_JSON";
|
|
21
|
+
})(DelugeErrorCode || (DelugeErrorCode = {}));
|
|
22
|
+
export default class Deluge {
|
|
23
|
+
url;
|
|
24
|
+
clientHost;
|
|
25
|
+
clientPriority;
|
|
26
|
+
clientType = Label.DELUGE;
|
|
27
|
+
readonly;
|
|
28
|
+
label;
|
|
29
|
+
delugeCookie = null;
|
|
30
|
+
delugeLabel = TORRENT_TAG;
|
|
31
|
+
delugeLabelSuffix = TORRENT_CATEGORY_SUFFIX;
|
|
32
|
+
isLabelEnabled;
|
|
33
|
+
delugeRequestId = 0;
|
|
34
|
+
constructor(url, clientHost, priority, readonly) {
|
|
35
|
+
this.url = url;
|
|
36
|
+
this.clientHost = clientHost;
|
|
37
|
+
this.clientPriority = priority;
|
|
38
|
+
this.readonly = readonly;
|
|
39
|
+
this.label = `${this.clientType}@${this.clientHost}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* validates the login and host for deluge webui
|
|
43
|
+
*/
|
|
44
|
+
async validateConfig() {
|
|
45
|
+
const { torrentDir } = getRuntimeConfig();
|
|
46
|
+
await this.authenticate();
|
|
47
|
+
this.isLabelEnabled = await this.labelEnabled();
|
|
48
|
+
logger.info({
|
|
49
|
+
label: this.label,
|
|
50
|
+
message: `Logged in successfully${this.readonly ? " (readonly)" : ""}`,
|
|
51
|
+
});
|
|
52
|
+
if (!torrentDir)
|
|
53
|
+
return;
|
|
54
|
+
if (!(await readdir(torrentDir)).some((f) => f.endsWith(".state"))) {
|
|
55
|
+
throw new CrossSeedError(`[${this.label}] Invalid torrentDir, if no torrents are in client set to null for now: https://www.cross-seed.org/docs/basics/options#torrentdir`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* connects and authenticates to the webui
|
|
60
|
+
*/
|
|
61
|
+
async authenticate() {
|
|
62
|
+
const { href, password } = extractCredentialsFromUrl(this.url).unwrapOrThrow(new CrossSeedError(`[${this.label}] delugeRpcUrl must be percent-encoded`));
|
|
63
|
+
if (!password) {
|
|
64
|
+
throw new CrossSeedError(`[${this.label}] You need to define a password in the delugeRpcUrl. (e.g. http://:<PASSWORD>@localhost:8112)`);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const authResponse = (await this.call("auth.login", [password], 0)).unwrapOrThrow(new Error(`[${this.label}] failed to connect for authentication`));
|
|
68
|
+
if (!authResponse) {
|
|
69
|
+
throw new CrossSeedError(`[${this.label}] Reached Deluge, but failed to authenticate: ${href}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (networkError) {
|
|
73
|
+
throw new CrossSeedError(networkError);
|
|
74
|
+
}
|
|
75
|
+
const isConnectedResponse = await this.call("web.connected", [], 0);
|
|
76
|
+
if (isConnectedResponse.isOk() && !isConnectedResponse.unwrap()) {
|
|
77
|
+
logger.warn({
|
|
78
|
+
label: this.label,
|
|
79
|
+
message: "Deluge WebUI disconnected from daemon...attempting to reconnect.",
|
|
80
|
+
});
|
|
81
|
+
const webuiHostList = (await this.call("web.get_hosts", [], 0)).unwrapOrThrow(new Error(`[${this.label}] failed to get host-list for reconnect`));
|
|
82
|
+
const connectResponse = await this.call("web.connect", [webuiHostList[0][0]], 0);
|
|
83
|
+
if (connectResponse.isOk() && connectResponse.unwrap()) {
|
|
84
|
+
logger.info({
|
|
85
|
+
label: this.label,
|
|
86
|
+
message: "Deluge WebUI connected to the daemon.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw new CrossSeedError(`[${this.label}] Unable to connect WebUI to Deluge daemon. Connect to the WebUI to resolve this.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* ensures authentication and sends JSON-RPC calls to deluge
|
|
96
|
+
* @param method RPC method to send (usually prefaced with module name)
|
|
97
|
+
* @param params parameters for the method (usually in an array)
|
|
98
|
+
* @param retries specify a retry count (optional)
|
|
99
|
+
* @return a promised Result of the specified ResultType or an ErrorType
|
|
100
|
+
*/
|
|
101
|
+
async call(method, params, retries = 1) {
|
|
102
|
+
const msg = `Calling method ${method} with params ${inspect(params, { depth: null, compact: true })}`;
|
|
103
|
+
const message = msg.length > 1000 ? `${msg.slice(0, 1000)}...` : msg;
|
|
104
|
+
logger.verbose({ label: this.label, message });
|
|
105
|
+
const { href } = extractCredentialsFromUrl(this.url).unwrapOrThrow(new CrossSeedError(`[${this.label}] delugeRpcUrl must be percent-encoded`));
|
|
106
|
+
const headers = new Headers({
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"User-Agent": USER_AGENT,
|
|
109
|
+
});
|
|
110
|
+
if (this.delugeCookie)
|
|
111
|
+
headers.set("Cookie", this.delugeCookie);
|
|
112
|
+
let response, json;
|
|
113
|
+
try {
|
|
114
|
+
response = await fetch(href, {
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
method,
|
|
117
|
+
params,
|
|
118
|
+
id: this.delugeRequestId++,
|
|
119
|
+
}),
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers,
|
|
122
|
+
signal: AbortSignal.timeout(ms("10 seconds")),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (networkError) {
|
|
126
|
+
if (networkError.name === "AbortError" ||
|
|
127
|
+
networkError.name === "TimeoutError") {
|
|
128
|
+
throw new Error(`[${this.label}] Deluge method ${method} timed out after 10 seconds`);
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`[${this.label}] Failed to connect to Deluge at ${href}`, {
|
|
131
|
+
cause: networkError,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
json = (await response.json());
|
|
136
|
+
}
|
|
137
|
+
catch (jsonParseError) {
|
|
138
|
+
throw new Error(`[${this.label}] Deluge method ${method} response was non-JSON ${jsonParseError}`);
|
|
139
|
+
}
|
|
140
|
+
if (json.error?.code === DelugeErrorCode.NO_AUTH && retries > 0) {
|
|
141
|
+
this.delugeCookie = null;
|
|
142
|
+
await this.authenticate();
|
|
143
|
+
if (this.delugeCookie) {
|
|
144
|
+
return this.call(method, params, 0);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
throw new Error(`[${this.label}] Connection lost with Deluge. Re-authentication failed.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
this.handleResponseHeaders(response.headers);
|
|
151
|
+
if (json.error) {
|
|
152
|
+
return resultOfErr(json.error);
|
|
153
|
+
}
|
|
154
|
+
return resultOf(json.result);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* parses the set-cookie header and updates stored value
|
|
158
|
+
* @param headers the headers from a request
|
|
159
|
+
*/
|
|
160
|
+
handleResponseHeaders(headers) {
|
|
161
|
+
if (headers.has("Set-Cookie")) {
|
|
162
|
+
this.delugeCookie = headers.get("Set-Cookie").split(";")[0];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* checks enabled plugins for "Label"
|
|
167
|
+
* @return boolean declaring whether the "Label" plugin is enabled
|
|
168
|
+
*/
|
|
169
|
+
async labelEnabled() {
|
|
170
|
+
const enabledPlugins = await this.call("core.get_enabled_plugins", []);
|
|
171
|
+
if (enabledPlugins.isOk()) {
|
|
172
|
+
return enabledPlugins.unwrap().includes("Label");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* checks the status of an infohash in the client and resumes if/when criteria is met
|
|
180
|
+
* @param meta MetaFile containing torrent being resumed
|
|
181
|
+
* @param decision decision by which the newTorrent was matched
|
|
182
|
+
* @param options options object for extra flags
|
|
183
|
+
* @param options.checkOnce boolean to only check for resuming once
|
|
184
|
+
* @param options.meta metafile object containing the torrent data
|
|
185
|
+
*/
|
|
186
|
+
async resumeInjection(meta, decision, options) {
|
|
187
|
+
const infoHash = meta.infoHash;
|
|
188
|
+
let sleepTime = resumeSleepTime;
|
|
189
|
+
const stopTime = getResumeStopTime();
|
|
190
|
+
let stop = false;
|
|
191
|
+
while (Date.now() < stopTime) {
|
|
192
|
+
if (options.checkOnce) {
|
|
193
|
+
if (stop)
|
|
194
|
+
return;
|
|
195
|
+
stop = true;
|
|
196
|
+
}
|
|
197
|
+
await wait(sleepTime);
|
|
198
|
+
let torrentInfo;
|
|
199
|
+
let torrentLog;
|
|
200
|
+
try {
|
|
201
|
+
torrentInfo = await this.getTorrentInfo(infoHash);
|
|
202
|
+
if (torrentInfo.state === "Checking") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
torrentLog = `${torrentInfo.name} [${sanitizeInfoHash(infoHash)}]`;
|
|
206
|
+
if (torrentInfo.state !== "Paused") {
|
|
207
|
+
logger.warn({
|
|
208
|
+
label: this.label,
|
|
209
|
+
message: `Will not resume ${torrentLog}: state is ${torrentInfo.state}`,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const maxRemainingBytes = getMaxRemainingBytes(meta, decision, {
|
|
214
|
+
torrentLog,
|
|
215
|
+
label: this.label,
|
|
216
|
+
});
|
|
217
|
+
if (torrentInfo.total_remaining > maxRemainingBytes) {
|
|
218
|
+
if (!shouldResumeFromNonRelevantFiles(meta, torrentInfo.total_remaining, decision, { torrentLog, label: this.label })) {
|
|
219
|
+
logger.warn({
|
|
220
|
+
label: this.label,
|
|
221
|
+
message: `autoResumeMaxDownload will not resume ${torrentLog}: remainingSize ${humanReadableSize(torrentInfo.total_remaining, { binary: true })} > ${humanReadableSize(maxRemainingBytes, { binary: true })} limit`,
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
sleepTime = resumeErrSleepTime; // Dropping connections or restart
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
logger.info({
|
|
232
|
+
label: this.label,
|
|
233
|
+
message: `Resuming ${torrentLog}: ${humanReadableSize(torrentInfo.total_remaining, { binary: true })} remaining`,
|
|
234
|
+
});
|
|
235
|
+
await this.call("core.resume_torrent", [[infoHash]]);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
logger.warn({
|
|
239
|
+
label: this.label,
|
|
240
|
+
message: `Will not resume torrent ${infoHash}: timeout`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* generates the label for injection based on searchee and torrentInfo
|
|
245
|
+
* @param searchee Searchee that contains the originating torrent
|
|
246
|
+
* @param torrentInfo TorrentInfo from the searchee
|
|
247
|
+
* @return string with the label for the newTorrent
|
|
248
|
+
*/
|
|
249
|
+
calculateLabel(searchee, torrentInfo) {
|
|
250
|
+
const { linkCategory, duplicateCategories } = getRuntimeConfig();
|
|
251
|
+
if (!searchee.infoHash || !torrentInfo.label) {
|
|
252
|
+
return this.delugeLabel;
|
|
253
|
+
}
|
|
254
|
+
const ogLabel = torrentInfo.label;
|
|
255
|
+
if (!duplicateCategories) {
|
|
256
|
+
return ogLabel;
|
|
257
|
+
}
|
|
258
|
+
const shouldSuffixLabel = !ogLabel.endsWith(this.delugeLabelSuffix) && // no .cross-seed
|
|
259
|
+
ogLabel !== linkCategory; // not data
|
|
260
|
+
return !searchee.infoHash
|
|
261
|
+
? (linkCategory ?? "")
|
|
262
|
+
: shouldSuffixLabel
|
|
263
|
+
? `${ogLabel}${this.delugeLabelSuffix}`
|
|
264
|
+
: ogLabel;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* if Label plugin is loaded, adds (if necessary)
|
|
268
|
+
* and sets the label based on torrent hash.
|
|
269
|
+
* @param newTorrent the searchee of the newTorrent
|
|
270
|
+
* @param label the destination label for the newTorrent/searchee
|
|
271
|
+
*/
|
|
272
|
+
async setLabel(newTorrent, label) {
|
|
273
|
+
let setResult;
|
|
274
|
+
if (!this.isLabelEnabled)
|
|
275
|
+
return;
|
|
276
|
+
try {
|
|
277
|
+
const getCurrentLabels = await this.call("label.get_labels", []);
|
|
278
|
+
if (getCurrentLabels.isErr()) {
|
|
279
|
+
this.isLabelEnabled = false;
|
|
280
|
+
throw new Error("Labels have been disabled.");
|
|
281
|
+
}
|
|
282
|
+
if (getCurrentLabels.unwrap().includes(label)) {
|
|
283
|
+
setResult = await this.call("label.set_torrent", [
|
|
284
|
+
newTorrent.infoHash,
|
|
285
|
+
label,
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
await this.call("label.add", [label]);
|
|
290
|
+
await wait(300);
|
|
291
|
+
setResult = await this.call("label.set_torrent", [
|
|
292
|
+
newTorrent.infoHash,
|
|
293
|
+
label,
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
if (setResult.isErr()) {
|
|
297
|
+
throw new Error(setResult.unwrapErr().message);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
logger.warn({
|
|
302
|
+
label: this.label,
|
|
303
|
+
message: `Failed to label ${getLogString(newTorrent)} as ${label}: ${e.message}`,
|
|
304
|
+
});
|
|
305
|
+
logger.debug(e);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* injects a torrent into deluge client
|
|
310
|
+
* @param newTorrent injected candidate torrent
|
|
311
|
+
* @param searchee originating torrent (searchee)
|
|
312
|
+
* @param decision decision by which the newTorrent was matched
|
|
313
|
+
* @param options.onlyCompleted boolean to only inject completed torrents
|
|
314
|
+
* @param options.destinationDir location of the linked files (optional)
|
|
315
|
+
* @return InjectionResult of the newTorrent's injection
|
|
316
|
+
*/
|
|
317
|
+
async inject(newTorrent, searchee, decision, options) {
|
|
318
|
+
try {
|
|
319
|
+
const existsRes = await this.isTorrentInClient(newTorrent.infoHash);
|
|
320
|
+
if (existsRes.isErr())
|
|
321
|
+
return InjectionResult.FAILURE;
|
|
322
|
+
if (existsRes.unwrap())
|
|
323
|
+
return InjectionResult.ALREADY_EXISTS;
|
|
324
|
+
let torrentInfo;
|
|
325
|
+
if (options.onlyCompleted && searchee.infoHash) {
|
|
326
|
+
torrentInfo = await this.getTorrentInfo(searchee.infoHash);
|
|
327
|
+
if (!torrentInfo.complete)
|
|
328
|
+
return InjectionResult.TORRENT_NOT_COMPLETE;
|
|
329
|
+
}
|
|
330
|
+
if (!options.destinationDir &&
|
|
331
|
+
(!searchee.infoHash || !torrentInfo)) {
|
|
332
|
+
logger.debug({
|
|
333
|
+
label: this.label,
|
|
334
|
+
message: `Injection failure: ${getLogString(searchee)} was missing critical data.`,
|
|
335
|
+
});
|
|
336
|
+
return InjectionResult.FAILURE;
|
|
337
|
+
}
|
|
338
|
+
const torrentFileName = `${newTorrent.getFileSystemSafeName()}.cross-seed.torrent`;
|
|
339
|
+
const encodedTorrentData = newTorrent.encode().toString("base64");
|
|
340
|
+
const destinationDir = options.destinationDir
|
|
341
|
+
? options.destinationDir
|
|
342
|
+
: torrentInfo.save_path;
|
|
343
|
+
const toRecheck = shouldRecheck(newTorrent, decision);
|
|
344
|
+
const params = this.formatData(torrentFileName, encodedTorrentData, destinationDir, toRecheck);
|
|
345
|
+
const addResponse = await this.call("core.add_torrent_file", params);
|
|
346
|
+
if (addResponse.isErr()) {
|
|
347
|
+
const addResponseError = addResponse.unwrapErr();
|
|
348
|
+
if (addResponseError.message.includes("already")) {
|
|
349
|
+
return InjectionResult.ALREADY_EXISTS;
|
|
350
|
+
}
|
|
351
|
+
else if (addResponseError) {
|
|
352
|
+
logger.debug({
|
|
353
|
+
label: this.label,
|
|
354
|
+
message: `Injection failed: ${addResponseError.message}`,
|
|
355
|
+
});
|
|
356
|
+
return InjectionResult.FAILURE;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
logger.debug({
|
|
360
|
+
label: this.label,
|
|
361
|
+
message: `Unknown injection failure: ${getLogString(newTorrent)}`,
|
|
362
|
+
});
|
|
363
|
+
return InjectionResult.FAILURE;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (addResponse.isOk()) {
|
|
367
|
+
await this.setLabel(newTorrent, this.calculateLabel(searchee, torrentInfo));
|
|
368
|
+
if (toRecheck) {
|
|
369
|
+
// when paused, libtorrent doesnt start rechecking
|
|
370
|
+
// leaves torrent ready to download - ~99%
|
|
371
|
+
await wait(1000);
|
|
372
|
+
await this.recheckTorrent(newTorrent.infoHash);
|
|
373
|
+
void this.resumeInjection(newTorrent, decision, {
|
|
374
|
+
checkOnce: false,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
logger.error({
|
|
381
|
+
label: this.label,
|
|
382
|
+
message: `Injection failed: ${error}`,
|
|
383
|
+
});
|
|
384
|
+
logger.debug(error);
|
|
385
|
+
return InjectionResult.FAILURE;
|
|
386
|
+
}
|
|
387
|
+
return InjectionResult.SUCCESS;
|
|
388
|
+
}
|
|
389
|
+
async recheckTorrent(infoHash) {
|
|
390
|
+
// Pause first as it may resume after recheck automatically
|
|
391
|
+
await this.call("core.pause_torrent", [[infoHash]]);
|
|
392
|
+
await this.call("core.force_recheck", [[infoHash]]);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* formats the json for rpc calls to inject
|
|
396
|
+
* @param filename filename for the injecting torrent file
|
|
397
|
+
* @param filedump string with encoded torrent file
|
|
398
|
+
* @param destinationDir path to the torrent data
|
|
399
|
+
* @param toRecheck boolean to recheck the torrent
|
|
400
|
+
*/
|
|
401
|
+
formatData(filename, filedump, destinationDir, toRecheck) {
|
|
402
|
+
return [
|
|
403
|
+
filename,
|
|
404
|
+
filedump,
|
|
405
|
+
{
|
|
406
|
+
add_paused: toRecheck,
|
|
407
|
+
seed_mode: !toRecheck,
|
|
408
|
+
download_location: destinationDir,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* returns directory of an infohash in deluge as a string
|
|
414
|
+
* @param meta SearcheeWithInfoHash or Metafile for torrent to lookup in client
|
|
415
|
+
* @param options options object for extra flags
|
|
416
|
+
* @param options.onlyCompleted boolean to only return a completed torrent
|
|
417
|
+
* @return Result containing either a string with path or reason it was not provided
|
|
418
|
+
*/
|
|
419
|
+
async getDownloadDir(meta, options) {
|
|
420
|
+
let response;
|
|
421
|
+
const params = [["save_path", "progress"], { hash: meta.infoHash }];
|
|
422
|
+
try {
|
|
423
|
+
response = await this.call("web.update_ui", params);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return resultOfErr("UNKNOWN_ERROR");
|
|
427
|
+
}
|
|
428
|
+
if (!response.isOk()) {
|
|
429
|
+
return resultOfErr("UNKNOWN_ERROR");
|
|
430
|
+
}
|
|
431
|
+
const torrentResponse = response.unwrap().torrents;
|
|
432
|
+
if (!torrentResponse) {
|
|
433
|
+
return resultOfErr("UNKNOWN_ERROR");
|
|
434
|
+
}
|
|
435
|
+
const torrent = torrentResponse[meta.infoHash];
|
|
436
|
+
if (!torrent) {
|
|
437
|
+
return resultOfErr("NOT_FOUND");
|
|
438
|
+
}
|
|
439
|
+
if (options.onlyCompleted && torrent.progress !== 100) {
|
|
440
|
+
return resultOfErr("TORRENT_NOT_COMPLETE");
|
|
441
|
+
}
|
|
442
|
+
return resultOf(torrent.save_path);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* returns map of hashes and download directories for all torrents
|
|
446
|
+
* @param options.metas array of SearcheeWithInfoHash or Metafile for torrents to lookup in client
|
|
447
|
+
* @param options.onlyCompleted boolean to only return completed torrents
|
|
448
|
+
* @return Promise of a Map with hashes and download directories
|
|
449
|
+
*/
|
|
450
|
+
async getAllDownloadDirs(options) {
|
|
451
|
+
const dirs = new Map();
|
|
452
|
+
let response;
|
|
453
|
+
const params = [["save_path", "progress"], {}];
|
|
454
|
+
try {
|
|
455
|
+
response = await this.call("web.update_ui", params);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return dirs;
|
|
459
|
+
}
|
|
460
|
+
if (!response.isOk()) {
|
|
461
|
+
return dirs;
|
|
462
|
+
}
|
|
463
|
+
const torrentResponse = response.unwrap().torrents;
|
|
464
|
+
if (!torrentResponse) {
|
|
465
|
+
return dirs;
|
|
466
|
+
}
|
|
467
|
+
for (const [hash, torrent] of Object.entries(torrentResponse)) {
|
|
468
|
+
if (options.onlyCompleted && torrent.progress !== 100)
|
|
469
|
+
continue;
|
|
470
|
+
dirs.set(hash, torrent.save_path);
|
|
471
|
+
}
|
|
472
|
+
return dirs;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* checks if a torrent exists in deluge
|
|
476
|
+
* @param inputHash the infoHash of the torrent to check
|
|
477
|
+
* @return Result containing either a boolean or reason it was not provided
|
|
478
|
+
*/
|
|
479
|
+
async isTorrentInClient(inputHash) {
|
|
480
|
+
const infoHash = inputHash.toLowerCase();
|
|
481
|
+
try {
|
|
482
|
+
const torrentsRes = await this.call("web.update_ui", [[], {}]);
|
|
483
|
+
if (torrentsRes.isErr()) {
|
|
484
|
+
const err = torrentsRes.unwrapErr();
|
|
485
|
+
throw new Error(`${err.code ? err.code + ": " : ""}${err.message}`);
|
|
486
|
+
}
|
|
487
|
+
const torrents = torrentsRes.unwrap().torrents;
|
|
488
|
+
if (!torrents)
|
|
489
|
+
throw new Error("No torrents found");
|
|
490
|
+
for (const hash of Object.keys(torrents)) {
|
|
491
|
+
if (hash.toLowerCase() === infoHash)
|
|
492
|
+
return resultOf(true);
|
|
493
|
+
}
|
|
494
|
+
return resultOf(false);
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
return resultOfErr(e);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* checks if a torrent is complete in deluge
|
|
502
|
+
* @param infoHash the infoHash of the torrent to check
|
|
503
|
+
* @return Result containing either a boolean or reason it was not provided
|
|
504
|
+
*/
|
|
505
|
+
async isTorrentComplete(infoHash) {
|
|
506
|
+
try {
|
|
507
|
+
const torrentInfo = await this.getTorrentInfo(infoHash, {
|
|
508
|
+
useVerbose: true,
|
|
509
|
+
});
|
|
510
|
+
return torrentInfo.complete ? resultOf(true) : resultOf(false);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return resultOfErr("NOT_FOUND");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* checks if a torrent is checking in deluge
|
|
518
|
+
* @param infoHash the infoHash of the torrent to check
|
|
519
|
+
* @return Result containing either a boolean or reason it was not provided
|
|
520
|
+
*/
|
|
521
|
+
async isTorrentChecking(infoHash) {
|
|
522
|
+
try {
|
|
523
|
+
const torrentInfo = await this.getTorrentInfo(infoHash, {
|
|
524
|
+
useVerbose: true,
|
|
525
|
+
});
|
|
526
|
+
return resultOf(torrentInfo.state === "Checking");
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return resultOfErr("NOT_FOUND");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* @return All torrents in the client
|
|
534
|
+
*/
|
|
535
|
+
async getAllTorrents() {
|
|
536
|
+
const params = [["hash", "label"], {}];
|
|
537
|
+
const response = await this.call("web.update_ui", params);
|
|
538
|
+
if (!response.isOk()) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
const torrents = response.unwrap().torrents;
|
|
542
|
+
if (!torrents) {
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
return Object.entries(torrents).map(([hash, torrent]) => ({
|
|
546
|
+
infoHash: hash,
|
|
547
|
+
category: torrent.label ?? "",
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Get all searchees from the client and update the db
|
|
552
|
+
* @param options.newSearcheesOnly only return searchees that are not in the db
|
|
553
|
+
* @param options.refresh undefined uses the cache, [] refreshes all searchees, or a list of infoHashes to refresh
|
|
554
|
+
* @return an object containing all searchees and new searchees (refreshed searchees are considered new)
|
|
555
|
+
*/
|
|
556
|
+
async getClientSearchees(options) {
|
|
557
|
+
const searchees = [];
|
|
558
|
+
const newSearchees = [];
|
|
559
|
+
const infoHashes = new Set();
|
|
560
|
+
const torrentsRes = await this.call("web.update_ui", [
|
|
561
|
+
["name", "label", "save_path", "total_size", "files", "trackers"],
|
|
562
|
+
{},
|
|
563
|
+
]);
|
|
564
|
+
if (torrentsRes.isErr()) {
|
|
565
|
+
logger.error({
|
|
566
|
+
label: this.label,
|
|
567
|
+
message: "Failed to get torrents from client",
|
|
568
|
+
});
|
|
569
|
+
logger.debug(torrentsRes.unwrapErr());
|
|
570
|
+
return { searchees, newSearchees };
|
|
571
|
+
}
|
|
572
|
+
const torrents = torrentsRes.unwrap().torrents;
|
|
573
|
+
if (!torrents || !Object.keys(torrents).length) {
|
|
574
|
+
logger.verbose({
|
|
575
|
+
label: this.label,
|
|
576
|
+
message: "No torrents found in client",
|
|
577
|
+
});
|
|
578
|
+
return { searchees, newSearchees };
|
|
579
|
+
}
|
|
580
|
+
for (const [hash, torrent] of Object.entries(torrents)) {
|
|
581
|
+
const infoHash = hash.toLowerCase();
|
|
582
|
+
infoHashes.add(infoHash);
|
|
583
|
+
const dbTorrent = await db("client_searchee")
|
|
584
|
+
.where("info_hash", infoHash)
|
|
585
|
+
.where("client_host", this.clientHost)
|
|
586
|
+
.first();
|
|
587
|
+
const name = torrent.name;
|
|
588
|
+
const savePath = torrent.save_path;
|
|
589
|
+
const category = torrent.label ?? "";
|
|
590
|
+
const modified = clientSearcheeModified(this.label, dbTorrent, name, savePath, {
|
|
591
|
+
category,
|
|
592
|
+
});
|
|
593
|
+
const refresh = options?.refresh === undefined
|
|
594
|
+
? false
|
|
595
|
+
: options.refresh.length === 0
|
|
596
|
+
? true
|
|
597
|
+
: options.refresh.includes(infoHash);
|
|
598
|
+
if (!modified && !refresh) {
|
|
599
|
+
if (!options?.newSearcheesOnly) {
|
|
600
|
+
searchees.push(createSearcheeFromDB(dbTorrent));
|
|
601
|
+
}
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const files = torrent.files.map((file) => ({
|
|
605
|
+
name: basename(file.path),
|
|
606
|
+
path: file.path,
|
|
607
|
+
length: file.size,
|
|
608
|
+
}));
|
|
609
|
+
if (!files.length) {
|
|
610
|
+
logger.verbose({
|
|
611
|
+
label: this.label,
|
|
612
|
+
message: `No files found for ${torrent.name} [${sanitizeInfoHash(infoHash)}]: skipping`,
|
|
613
|
+
});
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const trackers = organizeTrackers(torrent.trackers);
|
|
617
|
+
const title = parseTitle(name, files) ?? name;
|
|
618
|
+
const length = torrent.total_size;
|
|
619
|
+
const searchee = {
|
|
620
|
+
infoHash,
|
|
621
|
+
name,
|
|
622
|
+
title,
|
|
623
|
+
files,
|
|
624
|
+
length,
|
|
625
|
+
clientHost: this.clientHost,
|
|
626
|
+
savePath,
|
|
627
|
+
category,
|
|
628
|
+
trackers,
|
|
629
|
+
};
|
|
630
|
+
newSearchees.push(searchee);
|
|
631
|
+
searchees.push(searchee);
|
|
632
|
+
}
|
|
633
|
+
await updateSearcheeClientDB(this.clientHost, newSearchees, infoHashes);
|
|
634
|
+
return { searchees, newSearchees };
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* returns information needed to complete/validate injection
|
|
638
|
+
* @return Promise of TorrentInfo type
|
|
639
|
+
* @param infoHash infohash to query for in the client
|
|
640
|
+
* @param options options object for extra flags
|
|
641
|
+
* @param options.useVerbose use verbose instead of error logging
|
|
642
|
+
* @return Promise of TorrentInfo type
|
|
643
|
+
*/
|
|
644
|
+
async getTorrentInfo(infoHash, options) {
|
|
645
|
+
let torrent;
|
|
646
|
+
try {
|
|
647
|
+
const params = [
|
|
648
|
+
[
|
|
649
|
+
"name",
|
|
650
|
+
"state",
|
|
651
|
+
"progress",
|
|
652
|
+
"save_path",
|
|
653
|
+
"label",
|
|
654
|
+
"total_remaining",
|
|
655
|
+
],
|
|
656
|
+
{ hash: infoHash },
|
|
657
|
+
];
|
|
658
|
+
const response = (await this.call("web.update_ui", params)).unwrapOrThrow(new Error("failed to fetch the torrent list"));
|
|
659
|
+
if (response.torrents) {
|
|
660
|
+
torrent = response.torrents?.[infoHash];
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
throw new Error("Client returned unexpected response (object missing)");
|
|
664
|
+
}
|
|
665
|
+
if (torrent === undefined) {
|
|
666
|
+
throw new Error(`Torrent not found in client (${infoHash})`);
|
|
667
|
+
}
|
|
668
|
+
const completedTorrent = (torrent.state === "Paused" &&
|
|
669
|
+
(torrent.progress === 100 || !torrent.total_remaining)) ||
|
|
670
|
+
torrent.state === "Seeding" ||
|
|
671
|
+
torrent.progress === 100 ||
|
|
672
|
+
!torrent.total_remaining;
|
|
673
|
+
const torrentLabel = this.isLabelEnabled && torrent.label.length != 0
|
|
674
|
+
? torrent.label
|
|
675
|
+
: undefined;
|
|
676
|
+
return {
|
|
677
|
+
name: torrent.name,
|
|
678
|
+
complete: completedTorrent,
|
|
679
|
+
state: torrent.state,
|
|
680
|
+
save_path: torrent.save_path,
|
|
681
|
+
label: torrentLabel,
|
|
682
|
+
total_remaining: torrent.total_remaining,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
const log = options?.useVerbose ? logger.verbose : logger.error;
|
|
687
|
+
log({
|
|
688
|
+
label: this.label,
|
|
689
|
+
message: `Failed to fetch torrent data for ${infoHash}: ${e.message}`,
|
|
690
|
+
});
|
|
691
|
+
logger.debug(e);
|
|
692
|
+
throw new Error(`[${this.label}] web.update_ui: failed to fetch data from client`, {
|
|
693
|
+
cause: e,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=Deluge.js.map
|