cross-seed 6.11.2 → 6.12.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.
Files changed (51) hide show
  1. package/dist/action.js +357 -222
  2. package/dist/action.js.map +1 -1
  3. package/dist/clients/Deluge.js +100 -55
  4. package/dist/clients/Deluge.js.map +1 -1
  5. package/dist/clients/QBittorrent.js +147 -50
  6. package/dist/clients/QBittorrent.js.map +1 -1
  7. package/dist/clients/RTorrent.js +86 -56
  8. package/dist/clients/RTorrent.js.map +1 -1
  9. package/dist/clients/TorrentClient.js +60 -28
  10. package/dist/clients/TorrentClient.js.map +1 -1
  11. package/dist/clients/Transmission.js +67 -33
  12. package/dist/clients/Transmission.js.map +1 -1
  13. package/dist/cmd.js +10 -13
  14. package/dist/cmd.js.map +1 -1
  15. package/dist/config.template.cjs +17 -29
  16. package/dist/config.template.cjs.map +1 -1
  17. package/dist/configSchema.js +104 -30
  18. package/dist/configSchema.js.map +1 -1
  19. package/dist/configuration.js.map +1 -1
  20. package/dist/constants.js +2 -1
  21. package/dist/constants.js.map +1 -1
  22. package/dist/dataFiles.js +14 -11
  23. package/dist/dataFiles.js.map +1 -1
  24. package/dist/db.js +10 -7
  25. package/dist/db.js.map +1 -1
  26. package/dist/decide.js +13 -16
  27. package/dist/decide.js.map +1 -1
  28. package/dist/diff.js +5 -26
  29. package/dist/diff.js.map +1 -1
  30. package/dist/inject.js +99 -98
  31. package/dist/inject.js.map +1 -1
  32. package/dist/logger.js +26 -15
  33. package/dist/logger.js.map +1 -1
  34. package/dist/pipeline.js +127 -75
  35. package/dist/pipeline.js.map +1 -1
  36. package/dist/preFilter.js +2 -35
  37. package/dist/preFilter.js.map +1 -1
  38. package/dist/pushNotifier.js +3 -0
  39. package/dist/pushNotifier.js.map +1 -1
  40. package/dist/runtimeConfig.js.map +1 -1
  41. package/dist/searchee.js +136 -91
  42. package/dist/searchee.js.map +1 -1
  43. package/dist/startup.js +3 -2
  44. package/dist/startup.js.map +1 -1
  45. package/dist/torrent.js +132 -98
  46. package/dist/torrent.js.map +1 -1
  47. package/dist/torznab.js +5 -5
  48. package/dist/torznab.js.map +1 -1
  49. package/dist/utils.js +28 -8
  50. package/dist/utils.js.map +1 -1
  51. package/package.json +1 -1
package/dist/action.js CHANGED
@@ -1,142 +1,54 @@
1
1
  import chalk from "chalk";
2
2
  import fs from "fs";
3
3
  import { dirname, join, resolve } from "path";
4
- import { getClient, shouldRecheck } from "./clients/TorrentClient.js";
4
+ import { getClients, shouldRecheck, } from "./clients/TorrentClient.js";
5
5
  import { Action, ALL_EXTENSIONS, Decision, InjectionResult, LinkType, SaveResult, } from "./constants.js";
6
6
  import { CrossSeedError } from "./errors.js";
7
7
  import { Label, logger } from "./logger.js";
8
8
  import { resultOf, resultOfErr } from "./Result.js";
9
9
  import { getRuntimeConfig } from "./runtimeConfig.js";
10
- import { createSearcheeFromPath, getAbsoluteFilePath, getRootFolder, getSearcheeSource, getSourceRoot, } from "./searchee.js";
10
+ import { createSearcheeFromPath, getRoot, getRootFolder, getSearcheeSource, } from "./searchee.js";
11
11
  import { saveTorrentFile } from "./torrent.js";
12
- import { findAFileWithExt, formatAsList, getLogString, getMediaType, } from "./utils.js";
12
+ import { findAFileWithExt, formatAsList, getLogString, getMediaType, Mutex, withMutex, } from "./utils.js";
13
+ const srcTestName = "test.cross-seed";
13
14
  const linkTestName = "cross-seed.test";
14
- function logActionResult(result, newMeta, searchee, tracker, decision) {
15
- const metaLog = getLogString(newMeta, chalk.green.bold);
16
- const searcheeLog = getLogString(searchee, chalk.magenta.bold);
17
- const source = `${getSearcheeSource(searchee)} (${searcheeLog})`;
18
- const foundBy = `Found ${metaLog} on ${chalk.bold(tracker)} by`;
19
- let infoOrVerbose = logger.info;
20
- let warnOrVerbose = logger.warn;
21
- if (searchee.label === Label.INJECT) {
22
- infoOrVerbose = logger.verbose;
23
- warnOrVerbose = logger.verbose;
24
- }
25
- switch (result) {
26
- case SaveResult.SAVED:
27
- infoOrVerbose({
28
- label: searchee.label,
29
- message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - saved`,
30
- });
31
- break;
32
- case InjectionResult.SUCCESS:
33
- infoOrVerbose({
34
- label: searchee.label,
35
- message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - injected`,
36
- });
37
- break;
38
- case InjectionResult.ALREADY_EXISTS:
39
- infoOrVerbose({
40
- label: searchee.label,
41
- message: `${foundBy} ${chalk.yellow(decision)} from ${source} - exists`,
42
- });
43
- break;
44
- case InjectionResult.TORRENT_NOT_COMPLETE:
45
- warnOrVerbose({
46
- label: searchee.label,
47
- message: `${foundBy} ${chalk.yellow(decision)} from ${source} - source is incomplete, saving...`,
48
- });
49
- break;
50
- case InjectionResult.FAILURE:
51
- default:
52
- logger.error({
53
- label: searchee.label,
54
- message: `${foundBy} ${chalk.red(decision)} from ${source} - failed to inject, saving...`,
55
- });
56
- break;
57
- }
58
- }
59
- /**
60
- * @return the root of linked files.
61
- */
62
- function linkExactTree(newMeta, destinationDir, sourceRoot, options) {
63
- let alreadyExisted = false;
64
- let linkedNewFiles = false;
65
- for (const newFile of newMeta.files) {
66
- const srcFilePath = getAbsoluteFilePath(sourceRoot, newFile.path);
67
- const destFilePath = join(destinationDir, newFile.path);
68
- if (fs.existsSync(destFilePath)) {
69
- alreadyExisted = true;
70
- continue;
71
- }
72
- if (options.ignoreMissing && !fs.existsSync(srcFilePath))
73
- continue;
74
- const destFileParentPath = dirname(destFilePath);
75
- if (!fs.existsSync(destFileParentPath)) {
76
- fs.mkdirSync(destFileParentPath, { recursive: true });
77
- }
78
- if (linkFile(srcFilePath, destFilePath)) {
79
- linkedNewFiles = true;
80
- }
81
- }
82
- const contentPath = join(destinationDir, newMeta.name);
83
- return { contentPath, alreadyExisted, linkedNewFiles };
84
- }
85
- /**
86
- * @return the root of linked files.
87
- */
88
- function linkFuzzyTree(searchee, newMeta, destinationDir, sourceRoot, options) {
89
- let alreadyExisted = false;
90
- let linkedNewFiles = false;
15
+ function linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, options) {
91
16
  const availableFiles = searchee.files.slice();
92
- for (const newFile of newMeta.files) {
93
- let matchedSearcheeFiles = availableFiles.filter((searcheeFile) => searcheeFile.length === newFile.length);
94
- if (matchedSearcheeFiles.length > 1) {
95
- matchedSearcheeFiles = matchedSearcheeFiles.filter((searcheeFile) => searcheeFile.name === newFile.name);
96
- }
97
- if (matchedSearcheeFiles.length) {
98
- const srcFilePath = getAbsoluteFilePath(sourceRoot, matchedSearcheeFiles[0].path);
99
- const destFilePath = join(destinationDir, newFile.path);
17
+ const paths = decision === Decision.MATCH && options.savePath
18
+ ? newMeta.files.map((file) => [
19
+ join(options.savePath, file.path),
20
+ join(destinationDir, file.path),
21
+ ])
22
+ : newMeta.files.reduce((acc, newFile) => {
23
+ let matchedSearcheeFiles = availableFiles.filter((searcheeFile) => searcheeFile.length === newFile.length);
24
+ if (matchedSearcheeFiles.length > 1) {
25
+ matchedSearcheeFiles = matchedSearcheeFiles.filter((searcheeFile) => searcheeFile.name === newFile.name);
26
+ }
27
+ if (!matchedSearcheeFiles.length)
28
+ return acc;
100
29
  const index = availableFiles.indexOf(matchedSearcheeFiles[0]);
101
30
  availableFiles.splice(index, 1);
102
- if (fs.existsSync(destFilePath)) {
103
- alreadyExisted = true;
104
- continue;
105
- }
106
- if (options.ignoreMissing && !fs.existsSync(srcFilePath))
107
- continue;
108
- const destFileParentPath = dirname(destFilePath);
109
- if (!fs.existsSync(destFileParentPath)) {
110
- fs.mkdirSync(destFileParentPath, { recursive: true });
111
- }
112
- if (linkFile(srcFilePath, destFilePath)) {
113
- linkedNewFiles = true;
114
- }
115
- }
116
- }
117
- const contentPath = join(destinationDir, newMeta.name);
118
- return { contentPath, alreadyExisted, linkedNewFiles };
119
- }
120
- function linkVirtualSearchee(searchee, newMeta, destinationDir, options) {
31
+ const srcFilePath = options.savePath
32
+ ? join(options.savePath, matchedSearcheeFiles[0].path)
33
+ : matchedSearcheeFiles[0].path; // Absolute path
34
+ acc.push([srcFilePath, join(destinationDir, newFile.path)]);
35
+ return acc;
36
+ }, []);
121
37
  let alreadyExisted = false;
122
38
  let linkedNewFiles = false;
123
- const availableFiles = searchee.files.slice();
124
- for (const newFile of newMeta.files) {
125
- let matchedSearcheeFiles = availableFiles.filter((searcheeFile) => searcheeFile.length === newFile.length);
126
- if (matchedSearcheeFiles.length > 1) {
127
- matchedSearcheeFiles = matchedSearcheeFiles.filter((searcheeFile) => searcheeFile.name === newFile.name);
128
- }
129
- if (matchedSearcheeFiles.length) {
130
- const srcFilePath = matchedSearcheeFiles[0].path; // Absolute path
131
- const destFilePath = join(destinationDir, newFile.path);
132
- const index = availableFiles.indexOf(matchedSearcheeFiles[0]);
133
- availableFiles.splice(index, 1);
39
+ try {
40
+ const validPaths = paths.filter(([srcFilePath, destFilePath]) => {
134
41
  if (fs.existsSync(destFilePath)) {
135
42
  alreadyExisted = true;
136
- continue;
43
+ return false;
137
44
  }
138
- if (options.ignoreMissing && !fs.existsSync(srcFilePath))
139
- continue;
45
+ if (fs.existsSync(srcFilePath))
46
+ return true;
47
+ if (options.ignoreMissing)
48
+ return false;
49
+ throw new Error(`Linking failed, ${srcFilePath} not found.`);
50
+ });
51
+ for (const [srcFilePath, destFilePath] of validPaths) {
140
52
  const destFileParentPath = dirname(destFilePath);
141
53
  if (!fs.existsSync(destFileParentPath)) {
142
54
  fs.mkdirSync(destFileParentPath, { recursive: true });
@@ -146,66 +58,35 @@ function linkVirtualSearchee(searchee, newMeta, destinationDir, options) {
146
58
  }
147
59
  }
148
60
  }
149
- const contentPath = join(destinationDir, newMeta.name);
150
- return { contentPath, alreadyExisted, linkedNewFiles };
61
+ catch (e) {
62
+ return resultOfErr(e);
63
+ }
64
+ return resultOf({ alreadyExisted, linkedNewFiles });
151
65
  }
152
66
  function unlinkMetafile(meta, destinationDir) {
153
- const fullPath = join(destinationDir, getRootFolder(meta.files[0]));
154
- if (!fs.existsSync(fullPath))
155
- return;
156
- if (!fullPath.startsWith(destinationDir))
157
- return; // assert: fullPath is within destinationDir
158
- if (fs.statSync(fullPath).ino === fs.statSync(destinationDir).ino)
159
- return; // assert: fullPath is not destinationDir
160
- logger.verbose(`Unlinking ${fullPath}`);
161
- fs.rmSync(fullPath, { recursive: true });
162
- }
163
- export async function linkAllFilesInMetafile(searchee, newMeta, tracker, decision, options) {
164
- const { flatLinking } = getRuntimeConfig();
165
- const client = getClient();
166
- let sourceRoot;
167
- if (searchee.infoHash) {
168
- let savePath;
169
- if (searchee.savePath) {
170
- const refreshedSearchee = (await client.getClientSearchees({
171
- newSearcheesOnly: true,
172
- refresh: [searchee.infoHash],
173
- })).newSearchees.find((s) => s.infoHash === searchee.infoHash);
174
- if (!refreshedSearchee)
175
- return resultOfErr("TORRENT_NOT_FOUND");
176
- for (const [key, value] of Object.entries(refreshedSearchee)) {
177
- searchee[key] = value;
178
- }
179
- if (!(await client.isTorrentComplete(searchee.infoHash)).orElse(false)) {
180
- return resultOfErr("TORRENT_NOT_COMPLETE");
181
- }
182
- savePath = searchee.savePath;
183
- }
184
- else {
185
- const downloadDirResult = await client.getDownloadDir(searchee, { onlyCompleted: options.onlyCompleted });
186
- if (downloadDirResult.isErr()) {
187
- return downloadDirResult.mapErr((e) => e === "NOT_FOUND" || e === "UNKNOWN_ERROR"
188
- ? "TORRENT_NOT_FOUND"
189
- : e);
190
- }
191
- savePath = downloadDirResult.unwrap();
192
- }
193
- sourceRoot = getSourceRoot(searchee, savePath);
194
- if (!fs.existsSync(sourceRoot)) {
195
- logger.error({
196
- label: searchee.label,
197
- message: `Linking failed, ${sourceRoot} not found. Make sure Docker volume mounts are set up properly.`,
198
- });
199
- return resultOfErr("MISSING_DATA");
200
- }
67
+ const destinationDirIno = fs.statSync(destinationDir).ino;
68
+ const roots = meta.files.map((file) => join(destinationDir, getRoot(file)));
69
+ for (const root of roots) {
70
+ if (!fs.existsSync(root))
71
+ continue;
72
+ if (!root.startsWith(destinationDir))
73
+ continue; // assert: root is within destinationDir
74
+ if (resolve(root) === resolve(destinationDir))
75
+ continue; // assert: root is not destinationDir
76
+ if (fs.statSync(root).ino === destinationDirIno)
77
+ continue; // assert: root is not destinationDir
78
+ logger.verbose(`Unlinking ${root}`);
79
+ fs.rmSync(root, { recursive: true });
201
80
  }
202
- else if (searchee.path) {
81
+ }
82
+ async function getSavePath(searchee, options) {
83
+ if (searchee.path) {
203
84
  if (!fs.existsSync(searchee.path)) {
204
85
  logger.error({
205
86
  label: searchee.label,
206
- message: `Linking failed, ${searchee.path} not found. Make sure Docker volume mounts are set up properly.`,
87
+ message: `Linking failed, ${searchee.path} not found.`,
207
88
  });
208
- return resultOfErr("MISSING_DATA");
89
+ return resultOfErr("INVALID_DATA");
209
90
  }
210
91
  const result = await createSearcheeFromPath(searchee.path);
211
92
  if (result.isErr()) {
@@ -217,13 +98,13 @@ export async function linkAllFilesInMetafile(searchee, newMeta, tracker, decisio
217
98
  searchee.length !== refreshedSearchee.length)) {
218
99
  return resultOfErr("TORRENT_NOT_COMPLETE");
219
100
  }
220
- sourceRoot = searchee.path;
101
+ return resultOf(dirname(searchee.path));
221
102
  }
222
- else {
103
+ else if (!searchee.infoHash) {
223
104
  for (const file of searchee.files) {
224
105
  if (!fs.existsSync(file.path)) {
225
- logger.error(`Linking failed, ${file.path} not found. Make sure Docker volume mounts are set up properly.`);
226
- return resultOfErr("MISSING_DATA");
106
+ logger.error(`Linking failed, ${file.path} not found.`);
107
+ return resultOfErr("INVALID_DATA");
227
108
  }
228
109
  if (options.onlyCompleted) {
229
110
  const f = fs.statSync(file.path);
@@ -232,78 +113,284 @@ export async function linkAllFilesInMetafile(searchee, newMeta, tracker, decisio
232
113
  }
233
114
  }
234
115
  }
116
+ return resultOf(undefined);
117
+ }
118
+ const clients = getClients();
119
+ const client = clients.length === 1
120
+ ? clients[0]
121
+ : clients.find((c) => c.clientHost === searchee.clientHost);
122
+ let savePath;
123
+ if (searchee.savePath) {
124
+ const refreshedSearchee = (await client.getClientSearchees({
125
+ newSearcheesOnly: true,
126
+ refresh: [searchee.infoHash],
127
+ })).newSearchees.find((s) => s.infoHash === searchee.infoHash);
128
+ if (!refreshedSearchee)
129
+ return resultOfErr("TORRENT_NOT_FOUND");
130
+ for (const [key, value] of Object.entries(refreshedSearchee)) {
131
+ searchee[key] = value;
132
+ }
133
+ if (!(await client.isTorrentComplete(searchee.infoHash)).orElse(false)) {
134
+ return resultOfErr("TORRENT_NOT_COMPLETE");
135
+ }
136
+ savePath = searchee.savePath;
137
+ }
138
+ else {
139
+ const downloadDirResult = await client.getDownloadDir(searchee, { onlyCompleted: options.onlyCompleted });
140
+ if (downloadDirResult.isErr()) {
141
+ return downloadDirResult.mapErr((e) => e === "NOT_FOUND" || e === "UNKNOWN_ERROR"
142
+ ? "TORRENT_NOT_FOUND"
143
+ : e);
144
+ }
145
+ savePath = downloadDirResult.unwrap();
146
+ }
147
+ const rootFolder = getRootFolder(searchee.files[0]);
148
+ const sourceRootOrSavePath = searchee.files.length === 1
149
+ ? join(savePath, searchee.files[0].path)
150
+ : rootFolder
151
+ ? join(savePath, rootFolder)
152
+ : savePath;
153
+ if (!fs.existsSync(sourceRootOrSavePath)) {
154
+ logger.error({
155
+ label: searchee.label,
156
+ message: `Linking failed, ${sourceRootOrSavePath} not found.`,
157
+ });
158
+ return resultOfErr("INVALID_DATA");
159
+ }
160
+ return resultOf(savePath);
161
+ }
162
+ async function getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker) {
163
+ const { flatLinking, linkType } = getRuntimeConfig();
164
+ if (!client) {
165
+ let srcPath;
166
+ let srcDev;
167
+ try {
168
+ srcPath = !savePath
169
+ ? searchee.files.find((f) => fs.existsSync(f.path)).path
170
+ : join(savePath, searchee.files.find((f) => fs.existsSync(join(savePath, f.path))).path);
171
+ srcDev = fs.statSync(srcPath).dev;
172
+ }
173
+ catch (e) {
174
+ logger.debug(e);
175
+ return null;
176
+ }
177
+ let error;
178
+ for (const testClient of getClients().filter((c) => !c.readonly)) {
179
+ const torrentSavePaths = new Set((await testClient.getAllDownloadDirs({
180
+ metas: [],
181
+ onlyCompleted: false,
182
+ })).values());
183
+ for (const torrentSavePath of torrentSavePaths) {
184
+ try {
185
+ if (srcDev && fs.statSync(torrentSavePath).dev === srcDev) {
186
+ client = testClient;
187
+ break;
188
+ }
189
+ const testPath = join(torrentSavePath, linkTestName);
190
+ linkFile(srcPath, testPath, linkType === LinkType.REFLINK
191
+ ? linkType
192
+ : LinkType.HARDLINK);
193
+ fs.rmSync(testPath);
194
+ client = testClient;
195
+ break;
196
+ }
197
+ catch (e) {
198
+ error = e;
199
+ }
200
+ }
201
+ if (client)
202
+ break;
203
+ }
204
+ if (!client) {
205
+ logger.debug(error);
206
+ return null;
207
+ }
235
208
  }
209
+ let destinationDir;
236
210
  const clientSavePathRes = await client.getDownloadDir(newMeta, {
237
211
  onlyCompleted: false,
238
212
  });
239
- let destinationDir = null;
240
213
  if (clientSavePathRes.isOk()) {
241
214
  destinationDir = clientSavePathRes.unwrap();
242
215
  }
243
216
  else {
244
- const linkDir = sourceRoot
245
- ? getLinkDir(sourceRoot)
217
+ if (clientSavePathRes.unwrapErr() === "INVALID_DATA") {
218
+ return null;
219
+ }
220
+ const linkDir = savePath
221
+ ? getLinkDir(savePath)
246
222
  : getLinkDirVirtual(searchee);
247
223
  if (!linkDir)
248
- return resultOfErr("MISSING_DATA");
224
+ return null;
249
225
  destinationDir = flatLinking ? linkDir : join(linkDir, tracker);
250
226
  }
251
- if (!sourceRoot) {
252
- return resultOf(linkVirtualSearchee(searchee, newMeta, destinationDir, {
253
- ignoreMissing: !options.onlyCompleted,
254
- }));
255
- }
256
- else if (decision === Decision.MATCH) {
257
- return resultOf(linkExactTree(newMeta, destinationDir, sourceRoot, {
258
- ignoreMissing: !options.onlyCompleted,
259
- }));
227
+ return { client, destinationDir };
228
+ }
229
+ function logActionResult(result, newMeta, searchee, tracker, decision) {
230
+ const metaLog = getLogString(newMeta, chalk.green.bold);
231
+ const searcheeLog = getLogString(searchee, chalk.magenta.bold);
232
+ const source = `${getSearcheeSource(searchee)} (${searcheeLog})`;
233
+ const foundBy = `Found ${metaLog} on ${chalk.bold(tracker)} by`;
234
+ let infoOrVerbose = logger.info;
235
+ let warnOrVerbose = logger.warn;
236
+ if (searchee.label === Label.INJECT) {
237
+ infoOrVerbose = logger.verbose;
238
+ warnOrVerbose = logger.verbose;
260
239
  }
261
- else {
262
- return resultOf(linkFuzzyTree(searchee, newMeta, destinationDir, sourceRoot, {
263
- ignoreMissing: !options.onlyCompleted,
264
- }));
240
+ switch (result) {
241
+ case SaveResult.SAVED:
242
+ infoOrVerbose({
243
+ label: searchee.label,
244
+ message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - saved`,
245
+ });
246
+ break;
247
+ case InjectionResult.SUCCESS:
248
+ infoOrVerbose({
249
+ label: searchee.label,
250
+ message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - injected`,
251
+ });
252
+ break;
253
+ case InjectionResult.ALREADY_EXISTS:
254
+ infoOrVerbose({
255
+ label: searchee.label,
256
+ message: `${foundBy} ${chalk.yellow(decision)} from ${source} - exists`,
257
+ });
258
+ break;
259
+ case InjectionResult.TORRENT_NOT_COMPLETE:
260
+ warnOrVerbose({
261
+ label: searchee.label,
262
+ message: `${foundBy} ${chalk.yellow(decision)} from ${source} - source is incomplete, saving...`,
263
+ });
264
+ break;
265
+ case InjectionResult.FAILURE:
266
+ default:
267
+ logger.error({
268
+ label: searchee.label,
269
+ message: `${foundBy} ${chalk.red(decision)} from ${source} - failed to inject, saving...`,
270
+ });
271
+ break;
265
272
  }
266
273
  }
267
274
  export async function performAction(newMeta, decision, searchee, tracker) {
275
+ return withMutex(Mutex.CLIENT_INJECTION, async () => {
276
+ return performActionWithoutMutex(newMeta, decision, searchee, tracker);
277
+ }, { useQueue: true });
278
+ }
279
+ export async function performActionWithoutMutex(newMeta, decision, searchee, tracker, injectClient, options = { onlyCompleted: true }) {
268
280
  const { action, linkDirs } = getRuntimeConfig();
269
281
  if (action === Action.SAVE) {
270
- await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
271
282
  logActionResult(SaveResult.SAVED, newMeta, searchee, tracker, decision);
272
- return { actionResult: SaveResult.SAVED, linkedNewFiles: false };
283
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
284
+ return { actionResult: SaveResult.SAVED };
273
285
  }
286
+ const savePathRes = await getSavePath(searchee, options);
287
+ const savePath = savePathRes.orElse(undefined);
274
288
  let destinationDir;
275
289
  let unlinkOk = false;
276
290
  let linkedNewFiles = false;
291
+ const warnOrVerbose = searchee.label !== Label.INJECT ? logger.warn : logger.verbose;
292
+ const clients = getClients();
293
+ let client = clients.length === 1
294
+ ? clients[0]
295
+ : clients.find((c) => c.clientHost === searchee.clientHost && !c.readonly);
296
+ const readonlySource = !client && !!searchee.clientHost;
277
297
  if (linkDirs.length) {
278
- const linkedFilesRootResult = await linkAllFilesInMetafile(searchee, newMeta, tracker, decision, { onlyCompleted: true });
279
- if (linkedFilesRootResult.isOk()) {
280
- const linkResult = linkedFilesRootResult.unwrap();
281
- destinationDir = dirname(linkResult.contentPath);
282
- unlinkOk = !linkResult.alreadyExisted;
283
- linkedNewFiles = linkResult.linkedNewFiles;
284
- }
285
- else {
286
- const result = linkedFilesRootResult.unwrapErr();
287
- let actionResult;
298
+ if (savePathRes.isErr()) {
299
+ const result = savePathRes.unwrapErr();
288
300
  if (result === "TORRENT_NOT_COMPLETE") {
289
- actionResult = InjectionResult.TORRENT_NOT_COMPLETE;
290
- }
291
- else {
292
- actionResult = InjectionResult.FAILURE;
293
- logger.error({
294
- label: searchee.label,
295
- message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${result}`,
296
- });
301
+ const actionResult = InjectionResult.TORRENT_NOT_COMPLETE;
302
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
303
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
304
+ return { client, actionResult, linkedNewFiles };
297
305
  }
306
+ const actionResult = InjectionResult.FAILURE;
307
+ logger.error({
308
+ label: searchee.label,
309
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${result}`,
310
+ });
298
311
  logActionResult(actionResult, newMeta, searchee, tracker, decision);
299
312
  await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
300
313
  return { actionResult, linkedNewFiles };
301
314
  }
315
+ const res = await getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker);
316
+ if (res) {
317
+ client = res.client;
318
+ destinationDir = res.destinationDir;
319
+ }
320
+ else {
321
+ client = undefined;
322
+ }
323
+ }
324
+ if (!client) {
325
+ logger.error({
326
+ label: searchee.label,
327
+ message: `Failed to find a torrent client for ${getLogString(searchee)}`,
328
+ });
329
+ const actionResult = InjectionResult.FAILURE;
330
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
331
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
332
+ return { actionResult, linkedNewFiles };
333
+ }
334
+ for (const otherClient of clients) {
335
+ if (otherClient.clientHost === client.clientHost)
336
+ continue;
337
+ if ((await otherClient.isTorrentComplete(newMeta.infoHash)).isErr()) {
338
+ continue;
339
+ }
340
+ warnOrVerbose({
341
+ label: searchee.label,
342
+ message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - already exists in ${otherClient.clientHost}`,
343
+ });
344
+ const actionResult = InjectionResult.FAILURE;
345
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
346
+ return { actionResult, linkedNewFiles };
347
+ }
348
+ if (injectClient && injectClient.clientHost !== client.clientHost) {
349
+ warnOrVerbose({
350
+ label: searchee.label,
351
+ message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - existing match is using ${injectClient.clientHost}`,
352
+ });
353
+ const actionResult = InjectionResult.FAILURE;
354
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
355
+ return { actionResult, linkedNewFiles };
356
+ }
357
+ if (linkDirs.length) {
358
+ const res = linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, { savePath, ignoreMissing: !options.onlyCompleted });
359
+ if (res.isErr()) {
360
+ logger.error({
361
+ label: searchee.label,
362
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${res.unwrapErr().message}`,
363
+ });
364
+ const actionResult = InjectionResult.FAILURE;
365
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
366
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
367
+ return { actionResult, linkedNewFiles };
368
+ }
369
+ const linkResult = res.unwrap();
370
+ unlinkOk = !linkResult.alreadyExisted;
371
+ linkedNewFiles = linkResult.linkedNewFiles;
302
372
  }
303
373
  else if (searchee.path) {
304
374
  destinationDir = dirname(searchee.path);
305
375
  }
306
- const actionResult = await getClient().inject(newMeta, searchee, decision, destinationDir);
376
+ else if (readonlySource) {
377
+ if (!savePath) {
378
+ logger.error({
379
+ label: searchee.label,
380
+ message: `Failed to find a save path for ${getLogString(searchee)}`,
381
+ });
382
+ const actionResult = InjectionResult.FAILURE;
383
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
384
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
385
+ return { actionResult, linkedNewFiles };
386
+ }
387
+ destinationDir = savePath;
388
+ }
389
+ const actionResult = await client.inject(newMeta, readonlySource ? { ...searchee, infoHash: undefined } : searchee, // treat as data-based
390
+ decision, {
391
+ onlyCompleted: options.onlyCompleted,
392
+ destinationDir,
393
+ });
307
394
  logActionResult(actionResult, newMeta, searchee, tracker, decision);
308
395
  if (actionResult === InjectionResult.SUCCESS) {
309
396
  // cross-seed may need to process these with the inject job
@@ -311,13 +398,29 @@ export async function performAction(newMeta, decision, searchee, tracker) {
311
398
  await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
312
399
  }
313
400
  }
314
- else if (actionResult !== InjectionResult.ALREADY_EXISTS) {
401
+ else if (actionResult === InjectionResult.ALREADY_EXISTS) {
402
+ if (linkedNewFiles) {
403
+ logger.info({
404
+ label: client.label,
405
+ message: `Rechecking ${getLogString(newMeta)} as new files were linked from ${getLogString(searchee)}`,
406
+ });
407
+ await client.recheckTorrent(newMeta.infoHash);
408
+ client.resumeInjection(newMeta.infoHash, decision, {
409
+ checkOnce: false,
410
+ });
411
+ }
412
+ }
413
+ else {
315
414
  await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
316
415
  if (unlinkOk && destinationDir) {
317
416
  unlinkMetafile(newMeta, destinationDir);
417
+ linkedNewFiles = false;
318
418
  }
319
419
  }
320
- return { actionResult, linkedNewFiles };
420
+ if (actionResult === InjectionResult.FAILURE) {
421
+ return { actionResult, linkedNewFiles };
422
+ }
423
+ return { client, actionResult, linkedNewFiles };
321
424
  }
322
425
  export async function performActions(searchee, matches) {
323
426
  const results = [];
@@ -337,11 +440,24 @@ export function getLinkDir(pathStr) {
337
440
  return linkDir;
338
441
  }
339
442
  }
340
- const srcFile = pathStat.isFile()
443
+ let srcFile = pathStat.isFile()
341
444
  ? pathStr
342
445
  : pathStat.isDirectory()
343
446
  ? findAFileWithExt(pathStr, ALL_EXTENSIONS)
344
447
  : null;
448
+ let tempFile;
449
+ if (!srcFile) {
450
+ tempFile = pathStat.isDirectory()
451
+ ? join(pathStr, srcTestName)
452
+ : join(dirname(pathStr), srcTestName);
453
+ try {
454
+ fs.writeFileSync(tempFile, "");
455
+ srcFile = tempFile;
456
+ }
457
+ catch (e) {
458
+ logger.debug(e);
459
+ }
460
+ }
345
461
  if (srcFile) {
346
462
  for (const linkDir of linkDirs) {
347
463
  try {
@@ -350,6 +466,8 @@ export function getLinkDir(pathStr) {
350
466
  ? linkType
351
467
  : LinkType.HARDLINK);
352
468
  fs.rmSync(testPath);
469
+ if (tempFile && fs.existsSync(tempFile))
470
+ fs.rmSync(tempFile);
353
471
  return linkDir;
354
472
  }
355
473
  catch {
@@ -357,6 +475,8 @@ export function getLinkDir(pathStr) {
357
475
  }
358
476
  }
359
477
  }
478
+ if (tempFile && fs.existsSync(tempFile))
479
+ fs.rmSync(tempFile);
360
480
  if (linkType !== LinkType.SYMLINK) {
361
481
  logger.error(`Cannot find any linkDir from linkDirs on the same drive to ${linkType} ${pathStr}`);
362
482
  return null;
@@ -426,10 +546,21 @@ function unwrapSymlinks(path) {
426
546
  */
427
547
  export function testLinking(srcDir) {
428
548
  const { linkDirs, linkType } = getRuntimeConfig();
549
+ let tempFile;
429
550
  try {
430
- const srcFile = findAFileWithExt(srcDir, ALL_EXTENSIONS);
431
- if (!srcFile)
432
- return;
551
+ let srcFile = findAFileWithExt(srcDir, ALL_EXTENSIONS);
552
+ if (!srcFile) {
553
+ tempFile = join(srcDir, srcTestName);
554
+ try {
555
+ fs.writeFileSync(tempFile, "");
556
+ srcFile = tempFile;
557
+ }
558
+ catch (e) {
559
+ logger.error(e);
560
+ logger.error(`Failed to create test file in ${srcDir}, cross-seed is unable to verify linking for this path.`);
561
+ return;
562
+ }
563
+ }
433
564
  const linkDir = getLinkDir(srcDir);
434
565
  if (!linkDir)
435
566
  throw new Error(`No valid linkDir found for ${srcDir}`);
@@ -441,5 +572,9 @@ export function testLinking(srcDir) {
441
572
  logger.error(e);
442
573
  throw new CrossSeedError(`Failed to create a test ${linkType} from ${srcDir} in any linkDirs: [${formatAsList(linkDirs.map((d) => `"${d}"`), { sort: false, style: "short", type: "unit" })}]. Ensure that ${linkType} is supported between these paths (hardlink/reflink requires same drive, partition, and volume).`);
443
574
  }
575
+ finally {
576
+ if (tempFile && fs.existsSync(tempFile))
577
+ fs.rmSync(tempFile);
578
+ }
444
579
  }
445
580
  //# sourceMappingURL=action.js.map