cross-seed 6.13.6-2 → 7.0.0-1
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/README.md +12 -13
- package/dist/webui/assets/FieldInfo-Bxj_j8SJ.js +1 -0
- package/dist/webui/assets/Page-C3rteCZt.js +1 -0
- package/dist/webui/assets/array-field-DVSC6nHP.js +1 -0
- package/dist/webui/assets/badge-DTZMtS0e.js +1 -0
- package/dist/webui/assets/check-Bu3ldi63.js +1 -0
- package/dist/webui/assets/chevron-down-CRy8M0kJ.js +1 -0
- package/dist/webui/assets/clients-CW8oEZoQ.js +1 -0
- package/dist/webui/assets/connect-YBNsnjWT.js +1 -0
- package/dist/webui/assets/debug-mz8-WYZj.js +1 -0
- package/dist/webui/assets/directories-BSK28RgR.js +1 -0
- package/dist/webui/assets/duration-field-C6xoSlJg.js +1 -0
- package/dist/webui/assets/general-lJJxZhH7.js +1 -0
- package/dist/webui/assets/health-CXbsVrie.js +1 -0
- package/dist/webui/assets/index-Bi48hI2z.js +54 -0
- package/dist/webui/assets/index-C-Ul7GNg.css +1 -0
- package/dist/webui/assets/index-C2cH1Gst.js +1 -0
- package/dist/webui/assets/index-Cc5bDmJr.js +1 -0
- package/dist/webui/assets/jobs-CxmNab9w.js +1 -0
- package/dist/webui/assets/library-vaj2W8sE.js +1 -0
- package/dist/webui/assets/loader-circle-M0gu1gZ-.js +1 -0
- package/dist/webui/assets/logs-Cu9RyKS0.js +1 -0
- package/dist/webui/assets/search-2R5sIdT8.js +1 -0
- package/dist/webui/assets/select-field-BCqNLDrJ.js +1 -0
- package/dist/webui/assets/select-zHgqMzLj.js +1 -0
- package/dist/webui/assets/settings-CMYjpTbZ.js +1 -0
- package/dist/webui/assets/submit-button-BtcnyggQ.js +1 -0
- package/dist/webui/assets/switch-G0W3uJVN.js +1 -0
- package/dist/webui/assets/switch-field-IBd9ORNq.js +1 -0
- package/dist/webui/assets/table-DvgJU7Gh.js +1 -0
- package/dist/webui/assets/test-tube-BIwmoM45.js +1 -0
- package/dist/webui/assets/text-field-DruSbGhy.js +1 -0
- package/dist/webui/assets/time-BSMZjmyW.js +1 -0
- package/dist/webui/assets/trackers-D-OpAe63.js +7 -0
- package/dist/webui/assets/use-form-validation-context-BkAfWAh0.js +1 -0
- package/dist/webui/assets/use-settings-form-submit-CDRh-E9U.js +2 -0
- package/dist/webui/assets/useQuery-A4Hv_4uX.js +1 -0
- package/dist/webui/index.html +13 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.d.ts +261 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.d.ts.map +1 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.js +53 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.js.map +1 -0
- package/node_modules/@cross-seed/shared/dist/constants.d.ts +122 -0
- package/node_modules/@cross-seed/shared/dist/constants.d.ts.map +1 -0
- package/node_modules/@cross-seed/shared/dist/constants.js +127 -0
- package/node_modules/@cross-seed/shared/dist/constants.js.map +1 -0
- package/node_modules/@cross-seed/shared/dist/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@cross-seed/shared/dist/utils.d.ts +6 -0
- package/node_modules/@cross-seed/shared/dist/utils.d.ts.map +1 -0
- package/node_modules/@cross-seed/shared/dist/utils.js +9 -0
- package/node_modules/@cross-seed/shared/dist/utils.js.map +1 -0
- package/node_modules/@cross-seed/shared/package.json +22 -0
- package/package.json +35 -11
- package/dist/Result.js +0 -64
- package/dist/Result.js.map +0 -1
- package/dist/action.js +0 -693
- package/dist/action.js.map +0 -1
- package/dist/arr.js +0 -199
- package/dist/arr.js.map +0 -1
- package/dist/auth.js +0 -25
- package/dist/auth.js.map +0 -1
- package/dist/clients/Deluge.js +0 -698
- package/dist/clients/Deluge.js.map +0 -1
- package/dist/clients/QBittorrent.js +0 -785
- package/dist/clients/QBittorrent.js.map +0 -1
- package/dist/clients/RTorrent.js +0 -654
- package/dist/clients/RTorrent.js.map +0 -1
- package/dist/clients/TorrentClient.js +0 -272
- package/dist/clients/TorrentClient.js.map +0 -1
- package/dist/clients/Transmission.js +0 -404
- package/dist/clients/Transmission.js.map +0 -1
- package/dist/cmd.js +0 -196
- package/dist/cmd.js.map +0 -1
- package/dist/config.template.cjs +0 -353
- package/dist/config.template.cjs.map +0 -1
- package/dist/configSchema.js +0 -667
- package/dist/configSchema.js.map +0 -1
- package/dist/configuration.js +0 -82
- package/dist/configuration.js.map +0 -1
- package/dist/constants.js +0 -281
- package/dist/constants.js.map +0 -1
- package/dist/dataFiles.js +0 -208
- package/dist/dataFiles.js.map +0 -1
- package/dist/db.js +0 -216
- package/dist/db.js.map +0 -1
- package/dist/decide.js +0 -553
- package/dist/decide.js.map +0 -1
- package/dist/diff.js +0 -24
- package/dist/diff.js.map +0 -1
- package/dist/errors.js +0 -16
- package/dist/errors.js.map +0 -1
- package/dist/indexers.js +0 -180
- package/dist/indexers.js.map +0 -1
- package/dist/inject.js +0 -594
- package/dist/inject.js.map +0 -1
- package/dist/jobs.js +0 -146
- package/dist/jobs.js.map +0 -1
- package/dist/logger.js +0 -143
- package/dist/logger.js.map +0 -1
- package/dist/migrations/00-initialSchema.js +0 -30
- package/dist/migrations/00-initialSchema.js.map +0 -1
- package/dist/migrations/01-jobs.js +0 -12
- package/dist/migrations/01-jobs.js.map +0 -1
- package/dist/migrations/02-timestamps.js +0 -21
- package/dist/migrations/02-timestamps.js.map +0 -1
- package/dist/migrations/03-rateLimits.js +0 -14
- package/dist/migrations/03-rateLimits.js.map +0 -1
- package/dist/migrations/04-auth.js +0 -13
- package/dist/migrations/04-auth.js.map +0 -1
- package/dist/migrations/05-caps.js +0 -16
- package/dist/migrations/05-caps.js.map +0 -1
- package/dist/migrations/06-uniqueDecisions.js +0 -29
- package/dist/migrations/06-uniqueDecisions.js.map +0 -1
- package/dist/migrations/07-limits.js +0 -12
- package/dist/migrations/07-limits.js.map +0 -1
- package/dist/migrations/08-rss.js +0 -15
- package/dist/migrations/08-rss.js.map +0 -1
- package/dist/migrations/09-clientAndDataSearchees.js +0 -34
- package/dist/migrations/09-clientAndDataSearchees.js.map +0 -1
- package/dist/migrations/10-indexerNameAudioBookCaps.js +0 -18
- package/dist/migrations/10-indexerNameAudioBookCaps.js.map +0 -1
- package/dist/migrations/11-trackers.js +0 -38
- package/dist/migrations/11-trackers.js.map +0 -1
- package/dist/migrations/migrations.js +0 -31
- package/dist/migrations/migrations.js.map +0 -1
- package/dist/parseTorrent.js +0 -128
- package/dist/parseTorrent.js.map +0 -1
- package/dist/pipeline.js +0 -527
- package/dist/pipeline.js.map +0 -1
- package/dist/preFilter.js +0 -250
- package/dist/preFilter.js.map +0 -1
- package/dist/pushNotifier.js +0 -137
- package/dist/pushNotifier.js.map +0 -1
- package/dist/runtimeConfig.js +0 -11
- package/dist/runtimeConfig.js.map +0 -1
- package/dist/searchee.js +0 -658
- package/dist/searchee.js.map +0 -1
- package/dist/server.js +0 -456
- package/dist/server.js.map +0 -1
- package/dist/startup.js +0 -203
- package/dist/startup.js.map +0 -1
- package/dist/torrent.js +0 -637
- package/dist/torrent.js.map +0 -1
- package/dist/torznab.js +0 -777
- package/dist/torznab.js.map +0 -1
- package/dist/utils.js +0 -637
- package/dist/utils.js.map +0 -1
package/dist/clients/RTorrent.js
DELETED
|
@@ -1,654 +0,0 @@
|
|
|
1
|
-
import { readdir, stat } from "fs/promises";
|
|
2
|
-
import { basename, dirname, join, resolve, sep } from "path";
|
|
3
|
-
import { inspect } from "util";
|
|
4
|
-
import xmlrpc from "xmlrpc";
|
|
5
|
-
import { InjectionResult, TORRENT_TAG, } from "../constants.js";
|
|
6
|
-
import { db } from "../db.js";
|
|
7
|
-
import { CrossSeedError } from "../errors.js";
|
|
8
|
-
import { Label, logger } from "../logger.js";
|
|
9
|
-
import { Metafile } from "../parseTorrent.js";
|
|
10
|
-
import { resultOf, resultOfErr } from "../Result.js";
|
|
11
|
-
import { getRuntimeConfig } from "../runtimeConfig.js";
|
|
12
|
-
import { createSearcheeFromDB, parseTitle, updateSearcheeClientDB, } from "../searchee.js";
|
|
13
|
-
import { extractCredentialsFromUrl, fromBatches, humanReadableSize, isTruthy, mapAsync, sanitizeInfoHash, wait, } from "../utils.js";
|
|
14
|
-
import { shouldResumeFromNonRelevantFiles, clientSearcheeModified, getMaxRemainingBytes, getResumeStopTime, organizeTrackers, resumeErrSleepTime, resumeSleepTime, shouldRecheck, } from "./TorrentClient.js";
|
|
15
|
-
const COULD_NOT_FIND_INFO_HASH = "Could not find info-hash.";
|
|
16
|
-
async function createLibTorrentResumeTree(meta, basePath) {
|
|
17
|
-
async function getFileResumeData(file) {
|
|
18
|
-
const filePathWithoutFirstSegment = file.path
|
|
19
|
-
.split(sep)
|
|
20
|
-
.slice(1)
|
|
21
|
-
.join(sep);
|
|
22
|
-
const resolvedFilePath = resolve(basePath, filePathWithoutFirstSegment);
|
|
23
|
-
const fileStat = await stat(resolvedFilePath).catch(() => ({ isFile: () => false }));
|
|
24
|
-
if (!fileStat.isFile() || fileStat.size !== file.length) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
completed: Math.ceil(file.length / meta.pieceLength),
|
|
29
|
-
mtime: Math.trunc(fileStat.mtimeMs / 1000),
|
|
30
|
-
priority: 1,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
const fileResumes = await mapAsync(meta.files, getFileResumeData);
|
|
34
|
-
return {
|
|
35
|
-
bitfield: Math.ceil(meta.length / meta.pieceLength),
|
|
36
|
-
files: fileResumes.filter(isTruthy),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export default class RTorrent {
|
|
40
|
-
client;
|
|
41
|
-
clientHost;
|
|
42
|
-
clientPriority;
|
|
43
|
-
clientType = Label.RTORRENT;
|
|
44
|
-
readonly;
|
|
45
|
-
label;
|
|
46
|
-
batchSize = 500;
|
|
47
|
-
constructor(url, clientHost, priority, readonly) {
|
|
48
|
-
this.clientHost = clientHost;
|
|
49
|
-
this.clientPriority = priority;
|
|
50
|
-
this.readonly = readonly;
|
|
51
|
-
this.label = `${this.clientType}@${this.clientHost}`;
|
|
52
|
-
const { href, username, password } = extractCredentialsFromUrl(url).unwrapOrThrow(new CrossSeedError(`[${this.label}] rTorrent url must be percent-encoded`));
|
|
53
|
-
const clientCreator = new URL(href).protocol === "https:"
|
|
54
|
-
? xmlrpc.createSecureClient
|
|
55
|
-
: xmlrpc.createClient;
|
|
56
|
-
const shouldUseAuth = Boolean(username && password);
|
|
57
|
-
this.client = clientCreator({
|
|
58
|
-
url: href,
|
|
59
|
-
basic_auth: shouldUseAuth
|
|
60
|
-
? { user: username, pass: password }
|
|
61
|
-
: undefined,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
async methodCallP(method, args) {
|
|
65
|
-
const msg = `Calling method ${method} with params ${inspect(args, { depth: null, compact: true })}`;
|
|
66
|
-
const message = msg.length > 1000 ? `${msg.slice(0, 1000)}...` : msg;
|
|
67
|
-
logger.verbose({ label: this.label, message });
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
this.client.methodCall(method, args, (err, data) => {
|
|
70
|
-
if (err)
|
|
71
|
-
return reject(err);
|
|
72
|
-
return resolve(data);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
async isTorrentInClient(inputHash) {
|
|
77
|
-
const infoHash = inputHash.toLowerCase();
|
|
78
|
-
try {
|
|
79
|
-
const downloadList = await this.methodCallP("download_list", []);
|
|
80
|
-
for (const hash of downloadList) {
|
|
81
|
-
if (hash.toLowerCase() === infoHash)
|
|
82
|
-
return resultOf(true);
|
|
83
|
-
}
|
|
84
|
-
return resultOf(false);
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
return resultOfErr(e);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async checkOriginalTorrent(infoHash, options) {
|
|
91
|
-
const hash = infoHash.toUpperCase();
|
|
92
|
-
let response;
|
|
93
|
-
const args = [
|
|
94
|
-
[
|
|
95
|
-
{
|
|
96
|
-
methodName: "d.name",
|
|
97
|
-
params: [hash],
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
methodName: "d.directory",
|
|
101
|
-
params: [hash],
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
methodName: "d.left_bytes",
|
|
105
|
-
params: [hash],
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
methodName: "d.hashing",
|
|
109
|
-
params: [hash],
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
methodName: "d.complete",
|
|
113
|
-
params: [hash],
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
methodName: "d.is_multi_file",
|
|
117
|
-
params: [hash],
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
methodName: "d.is_active",
|
|
121
|
-
params: [hash],
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
];
|
|
125
|
-
try {
|
|
126
|
-
response = await this.methodCallP("system.multicall", args);
|
|
127
|
-
}
|
|
128
|
-
catch (e) {
|
|
129
|
-
logger.debug({ label: this.label, message: e });
|
|
130
|
-
return resultOfErr("FAILURE");
|
|
131
|
-
}
|
|
132
|
-
function isFault(response) {
|
|
133
|
-
return "faultString" in response[0];
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
if (isFault(response)) {
|
|
137
|
-
if (response[0].faultString === COULD_NOT_FIND_INFO_HASH) {
|
|
138
|
-
return resultOfErr("NOT_FOUND");
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
throw new Error("Unknown rTorrent fault while checking original torrent");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const [[name], [directoryBase], [bytesLeftStr], [hashingStr], [isCompleteStr], [isMultiFileStr], [isActiveStr],] = response;
|
|
145
|
-
const isComplete = Boolean(Number(isCompleteStr));
|
|
146
|
-
if (options.onlyCompleted && !isComplete) {
|
|
147
|
-
return resultOfErr("TORRENT_NOT_COMPLETE");
|
|
148
|
-
}
|
|
149
|
-
return resultOf({
|
|
150
|
-
name,
|
|
151
|
-
directoryBase,
|
|
152
|
-
bytesLeft: Number(bytesLeftStr),
|
|
153
|
-
hashing: Number(hashingStr),
|
|
154
|
-
isMultiFile: Boolean(Number(isMultiFileStr)),
|
|
155
|
-
isActive: Boolean(Number(isActiveStr)),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch (e) {
|
|
159
|
-
logger.error({ label: this.label, message: e });
|
|
160
|
-
logger.debug("Failure caused by server response below:");
|
|
161
|
-
logger.debug(inspect(response));
|
|
162
|
-
return resultOfErr("FAILURE");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async getDownloadLocation(meta, searchee, options) {
|
|
166
|
-
if (options.destinationDir) {
|
|
167
|
-
// resolve to absolute because we send the path to rTorrent
|
|
168
|
-
const basePath = resolve(options.destinationDir, meta.name);
|
|
169
|
-
const directoryBase = meta.isSingleFileTorrent
|
|
170
|
-
? options.destinationDir
|
|
171
|
-
: basePath;
|
|
172
|
-
return resultOf({
|
|
173
|
-
downloadDir: options.destinationDir,
|
|
174
|
-
basePath,
|
|
175
|
-
directoryBase,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
const result = await this.checkOriginalTorrent(searchee.infoHash, {
|
|
180
|
-
onlyCompleted: options.onlyCompleted,
|
|
181
|
-
});
|
|
182
|
-
return result.mapOk(({ directoryBase }) => ({
|
|
183
|
-
directoryBase,
|
|
184
|
-
downloadDir: meta.isSingleFileTorrent
|
|
185
|
-
? directoryBase
|
|
186
|
-
: dirname(directoryBase),
|
|
187
|
-
basePath: meta.isSingleFileTorrent
|
|
188
|
-
? join(directoryBase, searchee.name)
|
|
189
|
-
: directoryBase,
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async validateConfig() {
|
|
194
|
-
const { torrentDir } = getRuntimeConfig();
|
|
195
|
-
try {
|
|
196
|
-
await this.methodCallP("download_list", []);
|
|
197
|
-
}
|
|
198
|
-
catch (e) {
|
|
199
|
-
logger.debug({ label: this.label, message: e });
|
|
200
|
-
throw new CrossSeedError(`[${this.label}] Failed to reach rTorrent at ${this.clientHost}: ${e.message}`);
|
|
201
|
-
}
|
|
202
|
-
logger.info({
|
|
203
|
-
label: this.label,
|
|
204
|
-
message: `Logged in successfully${this.readonly ? " (readonly)" : ""}`,
|
|
205
|
-
});
|
|
206
|
-
if (!torrentDir)
|
|
207
|
-
return;
|
|
208
|
-
if (!(await readdir(torrentDir)).some((f) => f.endsWith("_resume"))) {
|
|
209
|
-
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`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async getDownloadDir(meta, options) {
|
|
213
|
-
const existsRes = await this.isTorrentInClient(meta.infoHash);
|
|
214
|
-
if (existsRes.isErr())
|
|
215
|
-
return resultOfErr("UNKNOWN_ERROR");
|
|
216
|
-
if (!existsRes.unwrap())
|
|
217
|
-
return resultOfErr("NOT_FOUND");
|
|
218
|
-
const result = await this.checkOriginalTorrent(meta.infoHash, options);
|
|
219
|
-
return result
|
|
220
|
-
.mapOk(({ directoryBase, isMultiFile }) => {
|
|
221
|
-
return isMultiFile ? dirname(directoryBase) : directoryBase;
|
|
222
|
-
})
|
|
223
|
-
.mapErr((error) => (error === "FAILURE" ? "UNKNOWN_ERROR" : error));
|
|
224
|
-
}
|
|
225
|
-
async getAllDownloadDirs(options) {
|
|
226
|
-
const hashes = await this.methodCallP("download_list", []);
|
|
227
|
-
function isFault(response) {
|
|
228
|
-
return "faultString" in response[0];
|
|
229
|
-
}
|
|
230
|
-
let numMethods = 0;
|
|
231
|
-
const results = await fromBatches(hashes, async (batch) => {
|
|
232
|
-
const args = [
|
|
233
|
-
batch.flatMap((hash) => {
|
|
234
|
-
const arg = [
|
|
235
|
-
{
|
|
236
|
-
methodName: "d.directory",
|
|
237
|
-
params: [hash],
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
methodName: "d.is_multi_file",
|
|
241
|
-
params: [hash],
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
methodName: "d.complete",
|
|
245
|
-
params: [hash],
|
|
246
|
-
},
|
|
247
|
-
];
|
|
248
|
-
numMethods = arg.length;
|
|
249
|
-
return arg;
|
|
250
|
-
}),
|
|
251
|
-
];
|
|
252
|
-
try {
|
|
253
|
-
const res = await this.methodCallP("system.multicall", args);
|
|
254
|
-
if (isFault(res)) {
|
|
255
|
-
logger.error({
|
|
256
|
-
label: this.label,
|
|
257
|
-
message: "Fault while getting download directories for all torrents",
|
|
258
|
-
});
|
|
259
|
-
logger.debug(inspect(res));
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
return res;
|
|
263
|
-
}
|
|
264
|
-
catch (e) {
|
|
265
|
-
logger.error({
|
|
266
|
-
label: this.label,
|
|
267
|
-
message: `Failed to get download directories for all torrents: ${e.message}`,
|
|
268
|
-
});
|
|
269
|
-
logger.debug(e);
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
}, { batchSize: this.batchSize });
|
|
273
|
-
if (!results.length || results.length !== hashes.length * numMethods) {
|
|
274
|
-
logger.error({
|
|
275
|
-
label: this.label,
|
|
276
|
-
message: `Unexpected number of results: ${results.length} for ${hashes.length} hashes`,
|
|
277
|
-
});
|
|
278
|
-
return new Map();
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
return hashes.reduce((infoHashSavePathMap, hash, index) => {
|
|
282
|
-
const directory = results[index * numMethods][0];
|
|
283
|
-
const isMultiFile = Boolean(Number(results[index * numMethods + 1][0]));
|
|
284
|
-
const isComplete = Boolean(Number(results[index * numMethods + 2][0]));
|
|
285
|
-
if (!options.onlyCompleted || isComplete) {
|
|
286
|
-
infoHashSavePathMap.set(hash, isMultiFile ? dirname(directory) : directory);
|
|
287
|
-
}
|
|
288
|
-
return infoHashSavePathMap;
|
|
289
|
-
}, new Map());
|
|
290
|
-
}
|
|
291
|
-
catch (e) {
|
|
292
|
-
logger.error({
|
|
293
|
-
label: this.label,
|
|
294
|
-
message: `Error parsing response for all torrents: ${e.message}`,
|
|
295
|
-
});
|
|
296
|
-
logger.debug(e);
|
|
297
|
-
return new Map();
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
async isTorrentComplete(infoHash) {
|
|
301
|
-
try {
|
|
302
|
-
const response = await this.methodCallP("d.complete", [
|
|
303
|
-
infoHash,
|
|
304
|
-
]);
|
|
305
|
-
if (response.length === 0) {
|
|
306
|
-
return resultOfErr("NOT_FOUND");
|
|
307
|
-
}
|
|
308
|
-
return resultOf(Boolean(Number(response[0])));
|
|
309
|
-
}
|
|
310
|
-
catch (e) {
|
|
311
|
-
return resultOfErr("NOT_FOUND");
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async isTorrentChecking(infoHash) {
|
|
315
|
-
try {
|
|
316
|
-
const response = await this.methodCallP("d.hashing", [
|
|
317
|
-
infoHash,
|
|
318
|
-
]);
|
|
319
|
-
if (response.length === 0) {
|
|
320
|
-
return resultOfErr("NOT_FOUND");
|
|
321
|
-
}
|
|
322
|
-
return resultOf(Boolean(Number(response[0])));
|
|
323
|
-
}
|
|
324
|
-
catch (e) {
|
|
325
|
-
return resultOfErr("NOT_FOUND");
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
async getAllTorrents() {
|
|
329
|
-
const hashes = await this.methodCallP("download_list", []);
|
|
330
|
-
function isFault(response) {
|
|
331
|
-
return "faultString" in response[0];
|
|
332
|
-
}
|
|
333
|
-
const results = await fromBatches(hashes, async (batch) => {
|
|
334
|
-
const args = [
|
|
335
|
-
batch.map((hash) => {
|
|
336
|
-
return {
|
|
337
|
-
methodName: "d.custom1",
|
|
338
|
-
params: [hash],
|
|
339
|
-
};
|
|
340
|
-
}),
|
|
341
|
-
];
|
|
342
|
-
try {
|
|
343
|
-
const res = await this.methodCallP("system.multicall", args);
|
|
344
|
-
if (isFault(res)) {
|
|
345
|
-
logger.error({
|
|
346
|
-
label: this.label,
|
|
347
|
-
message: "Fault while getting torrent info for all torrents",
|
|
348
|
-
});
|
|
349
|
-
logger.debug(inspect(res));
|
|
350
|
-
return [];
|
|
351
|
-
}
|
|
352
|
-
return res;
|
|
353
|
-
}
|
|
354
|
-
catch (e) {
|
|
355
|
-
logger.error({
|
|
356
|
-
label: this.label,
|
|
357
|
-
message: `Failed to get torrent info for all torrents: ${e.message}`,
|
|
358
|
-
});
|
|
359
|
-
logger.debug(e);
|
|
360
|
-
return [];
|
|
361
|
-
}
|
|
362
|
-
}, { batchSize: this.batchSize });
|
|
363
|
-
if (results.length !== hashes.length) {
|
|
364
|
-
logger.error({
|
|
365
|
-
label: this.label,
|
|
366
|
-
message: `Unexpected number of results: ${results.length} for ${hashes.length} hashes`,
|
|
367
|
-
});
|
|
368
|
-
return [];
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
// response: [ [tag1], [tag2], ... ], assuming infoHash order is preserved
|
|
372
|
-
return hashes.map((hash, index) => ({
|
|
373
|
-
infoHash: hash.toLowerCase(),
|
|
374
|
-
tags: results[index].length !== 1
|
|
375
|
-
? results[index]
|
|
376
|
-
: results[index][0].length
|
|
377
|
-
? decodeURIComponent(results[index][0])
|
|
378
|
-
.split(",")
|
|
379
|
-
.map((tag) => tag.trim())
|
|
380
|
-
: [],
|
|
381
|
-
}));
|
|
382
|
-
}
|
|
383
|
-
catch (e) {
|
|
384
|
-
logger.error({
|
|
385
|
-
label: this.label,
|
|
386
|
-
message: `Error parsing response for all torrents: ${e.message}`,
|
|
387
|
-
});
|
|
388
|
-
logger.debug(e);
|
|
389
|
-
return [];
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
async getClientSearchees(options) {
|
|
393
|
-
const searchees = [];
|
|
394
|
-
const newSearchees = [];
|
|
395
|
-
const hashes = await this.methodCallP("download_list", []);
|
|
396
|
-
function isFault(response) {
|
|
397
|
-
return "faultString" in response[0];
|
|
398
|
-
}
|
|
399
|
-
let numMethods = 0;
|
|
400
|
-
const results = await fromBatches(hashes, async (batch) => {
|
|
401
|
-
const args = [
|
|
402
|
-
batch.flatMap((hash) => {
|
|
403
|
-
const arg = [
|
|
404
|
-
{
|
|
405
|
-
methodName: "d.name",
|
|
406
|
-
params: [hash],
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
methodName: "d.size_bytes",
|
|
410
|
-
params: [hash],
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
methodName: "d.directory",
|
|
414
|
-
params: [hash],
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
methodName: "d.is_multi_file",
|
|
418
|
-
params: [hash],
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
methodName: "d.custom1",
|
|
422
|
-
params: [hash],
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
methodName: "f.multicall",
|
|
426
|
-
params: [hash, "", "f.path=", "f.size_bytes="],
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
methodName: "t.multicall",
|
|
430
|
-
params: [hash, "", "t.url=", "t.group="],
|
|
431
|
-
},
|
|
432
|
-
];
|
|
433
|
-
numMethods = arg.length;
|
|
434
|
-
return arg;
|
|
435
|
-
}),
|
|
436
|
-
];
|
|
437
|
-
try {
|
|
438
|
-
const res = await this.methodCallP("system.multicall", args);
|
|
439
|
-
if (isFault(res)) {
|
|
440
|
-
logger.error({
|
|
441
|
-
label: this.label,
|
|
442
|
-
message: "Fault while getting client torrents",
|
|
443
|
-
});
|
|
444
|
-
logger.debug(inspect(res));
|
|
445
|
-
return [];
|
|
446
|
-
}
|
|
447
|
-
return res;
|
|
448
|
-
}
|
|
449
|
-
catch (e) {
|
|
450
|
-
logger.error({
|
|
451
|
-
label: this.label,
|
|
452
|
-
message: `Failed to get client torrents: ${e.message}`,
|
|
453
|
-
});
|
|
454
|
-
logger.debug(e);
|
|
455
|
-
return [];
|
|
456
|
-
}
|
|
457
|
-
}, { batchSize: this.batchSize });
|
|
458
|
-
if (!results.length || results.length !== hashes.length * numMethods) {
|
|
459
|
-
logger.error({
|
|
460
|
-
label: this.label,
|
|
461
|
-
message: `Unexpected number of results: ${results.length} for ${hashes.length} hashes`,
|
|
462
|
-
});
|
|
463
|
-
return { searchees, newSearchees };
|
|
464
|
-
}
|
|
465
|
-
const infoHashes = new Set();
|
|
466
|
-
for (let i = 0; i < hashes.length; i++) {
|
|
467
|
-
const infoHash = hashes[i].toLowerCase();
|
|
468
|
-
infoHashes.add(infoHash);
|
|
469
|
-
const dbTorrent = await db("client_searchee")
|
|
470
|
-
.where("info_hash", infoHash)
|
|
471
|
-
.where("client_host", this.clientHost)
|
|
472
|
-
.first();
|
|
473
|
-
const name = results[i * numMethods][0];
|
|
474
|
-
const directory = results[i * numMethods + 2][0];
|
|
475
|
-
const isMultiFile = Boolean(Number(results[i * numMethods + 3][0]));
|
|
476
|
-
const labels = results[i * numMethods + 4][0];
|
|
477
|
-
const savePath = isMultiFile ? dirname(directory) : directory;
|
|
478
|
-
const tags = labels.length
|
|
479
|
-
? decodeURIComponent(labels)
|
|
480
|
-
.split(",")
|
|
481
|
-
.map((tag) => tag.trim())
|
|
482
|
-
: [];
|
|
483
|
-
const modified = clientSearcheeModified(this.label, dbTorrent, name, savePath, {
|
|
484
|
-
tags,
|
|
485
|
-
});
|
|
486
|
-
const refresh = options?.refresh === undefined
|
|
487
|
-
? false
|
|
488
|
-
: options.refresh.length === 0
|
|
489
|
-
? true
|
|
490
|
-
: options.refresh.includes(infoHash);
|
|
491
|
-
if (!modified && !refresh) {
|
|
492
|
-
if (!options?.newSearcheesOnly) {
|
|
493
|
-
searchees.push(createSearcheeFromDB(dbTorrent));
|
|
494
|
-
}
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
const length = Number(results[i * numMethods + 1][0]);
|
|
498
|
-
const files = results[i * numMethods + 5][0].map((arr) => ({
|
|
499
|
-
name: basename(arr[0]),
|
|
500
|
-
path: isMultiFile ? join(basename(directory), arr[0]) : arr[0],
|
|
501
|
-
length: Number(arr[1]),
|
|
502
|
-
}));
|
|
503
|
-
if (!files.length) {
|
|
504
|
-
logger.verbose({
|
|
505
|
-
label: this.label,
|
|
506
|
-
message: `No files found for ${name} [${sanitizeInfoHash(infoHash)}]: skipping`,
|
|
507
|
-
});
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
const trackers = organizeTrackers(results[i * numMethods + 6][0].map((arr) => ({
|
|
511
|
-
url: arr[0],
|
|
512
|
-
tier: Number(arr[1]),
|
|
513
|
-
})));
|
|
514
|
-
const title = parseTitle(name, files) ?? name;
|
|
515
|
-
const searchee = {
|
|
516
|
-
infoHash,
|
|
517
|
-
name,
|
|
518
|
-
title,
|
|
519
|
-
files,
|
|
520
|
-
length,
|
|
521
|
-
clientHost: this.clientHost,
|
|
522
|
-
savePath,
|
|
523
|
-
tags,
|
|
524
|
-
trackers,
|
|
525
|
-
};
|
|
526
|
-
newSearchees.push(searchee);
|
|
527
|
-
searchees.push(searchee);
|
|
528
|
-
}
|
|
529
|
-
await updateSearcheeClientDB(this.clientHost, newSearchees, infoHashes);
|
|
530
|
-
return { searchees, newSearchees };
|
|
531
|
-
}
|
|
532
|
-
async recheckTorrent(infoHash) {
|
|
533
|
-
// Pause first as it may resume after recheck automatically
|
|
534
|
-
await this.methodCallP("d.pause", [infoHash]);
|
|
535
|
-
await this.methodCallP("d.check_hash", [infoHash]);
|
|
536
|
-
}
|
|
537
|
-
async resumeInjection(meta, decision, options) {
|
|
538
|
-
const infoHash = meta.infoHash;
|
|
539
|
-
let sleepTime = resumeSleepTime;
|
|
540
|
-
const stopTime = getResumeStopTime();
|
|
541
|
-
let stop = false;
|
|
542
|
-
while (Date.now() < stopTime) {
|
|
543
|
-
if (options.checkOnce) {
|
|
544
|
-
if (stop)
|
|
545
|
-
return;
|
|
546
|
-
stop = true;
|
|
547
|
-
}
|
|
548
|
-
await wait(sleepTime);
|
|
549
|
-
const torrentInfoRes = await this.checkOriginalTorrent(infoHash, {
|
|
550
|
-
onlyCompleted: false,
|
|
551
|
-
});
|
|
552
|
-
if (torrentInfoRes.isErr()) {
|
|
553
|
-
sleepTime = resumeErrSleepTime; // Dropping connections or restart
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
const torrentInfo = torrentInfoRes.unwrap();
|
|
557
|
-
if (torrentInfo.hashing) {
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
const torrentLog = `${torrentInfo.name} [${sanitizeInfoHash(infoHash)}]`;
|
|
561
|
-
if (torrentInfo.isActive) {
|
|
562
|
-
logger.warn({
|
|
563
|
-
label: this.label,
|
|
564
|
-
message: `Will not resume torrent ${torrentLog}: active`,
|
|
565
|
-
});
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
const maxRemainingBytes = getMaxRemainingBytes(meta, decision, {
|
|
569
|
-
torrentLog,
|
|
570
|
-
label: this.label,
|
|
571
|
-
});
|
|
572
|
-
if (torrentInfo.bytesLeft > maxRemainingBytes) {
|
|
573
|
-
if (!shouldResumeFromNonRelevantFiles(meta, torrentInfo.bytesLeft, decision, { torrentLog, label: this.label })) {
|
|
574
|
-
logger.warn({
|
|
575
|
-
label: this.label,
|
|
576
|
-
message: `autoResumeMaxDownload will not resume ${torrentLog}: remainingSize ${humanReadableSize(torrentInfo.bytesLeft, { binary: true })} > ${humanReadableSize(maxRemainingBytes, { binary: true })} limit`,
|
|
577
|
-
});
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
logger.info({
|
|
582
|
-
label: this.label,
|
|
583
|
-
message: `Resuming torrent ${torrentLog}`,
|
|
584
|
-
});
|
|
585
|
-
await this.methodCallP("d.resume", [infoHash]);
|
|
586
|
-
}
|
|
587
|
-
logger.warn({
|
|
588
|
-
label: this.label,
|
|
589
|
-
message: `Will not resume torrent ${infoHash}: timeout`,
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
async inject(meta, searchee, decision, options) {
|
|
593
|
-
const existsRes = await this.isTorrentInClient(meta.infoHash);
|
|
594
|
-
if (existsRes.isErr())
|
|
595
|
-
return InjectionResult.FAILURE;
|
|
596
|
-
if (existsRes.unwrap())
|
|
597
|
-
return InjectionResult.ALREADY_EXISTS;
|
|
598
|
-
const result = await this.getDownloadLocation(meta, searchee, options);
|
|
599
|
-
if (result.isErr()) {
|
|
600
|
-
switch (result.unwrapErr()) {
|
|
601
|
-
case "NOT_FOUND":
|
|
602
|
-
return InjectionResult.FAILURE;
|
|
603
|
-
case "TORRENT_NOT_COMPLETE":
|
|
604
|
-
return InjectionResult.TORRENT_NOT_COMPLETE;
|
|
605
|
-
case "FAILURE":
|
|
606
|
-
return InjectionResult.FAILURE;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
const { directoryBase, basePath } = result.unwrap();
|
|
610
|
-
const rawWithLibtorrentResume = {
|
|
611
|
-
...meta.raw,
|
|
612
|
-
libtorrent_resume: await createLibTorrentResumeTree(meta, basePath),
|
|
613
|
-
};
|
|
614
|
-
const toRecheck = shouldRecheck(meta, decision);
|
|
615
|
-
const loadType = toRecheck ? "load.raw" : "load.raw_start";
|
|
616
|
-
const retries = 5;
|
|
617
|
-
for (let i = 0; i < retries; i++) {
|
|
618
|
-
try {
|
|
619
|
-
await this.methodCallP(loadType, [
|
|
620
|
-
"",
|
|
621
|
-
new Metafile(rawWithLibtorrentResume).encode(),
|
|
622
|
-
`d.directory_base.set="${directoryBase}"`,
|
|
623
|
-
`d.custom1.set="${TORRENT_TAG}"`,
|
|
624
|
-
`d.custom.set=addtime,${Math.round(Date.now() / 1000)}`,
|
|
625
|
-
toRecheck
|
|
626
|
-
? `d.check_hash=${meta.infoHash.toUpperCase()}`
|
|
627
|
-
: null,
|
|
628
|
-
].filter((e) => e !== null));
|
|
629
|
-
if (toRecheck) {
|
|
630
|
-
void this.resumeInjection(meta, decision, {
|
|
631
|
-
checkOnce: false,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
catch (e) {
|
|
637
|
-
logger.verbose({
|
|
638
|
-
label: this.label,
|
|
639
|
-
message: `Failed to inject torrent ${meta.name} on attempt ${i + 1}/${retries}: ${e.message}`,
|
|
640
|
-
});
|
|
641
|
-
logger.debug(e);
|
|
642
|
-
await wait(1000 * Math.pow(2, i));
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
for (let i = 0; i < 5; i++) {
|
|
646
|
-
if ((await this.isTorrentInClient(meta.infoHash)).orElse(false)) {
|
|
647
|
-
return InjectionResult.SUCCESS;
|
|
648
|
-
}
|
|
649
|
-
await wait(100 * Math.pow(2, i));
|
|
650
|
-
}
|
|
651
|
-
return InjectionResult.FAILURE;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
//# sourceMappingURL=RTorrent.js.map
|