cross-seed 6.11.2 → 6.12.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/action.js +339 -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 +26 -6
  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(client, 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,266 @@ export async function linkAllFilesInMetafile(searchee, newMeta, tracker, decisio
232
113
  }
233
114
  }
234
115
  }
116
+ return resultOf(undefined);
117
+ }
118
+ let savePath;
119
+ if (searchee.savePath) {
120
+ const refreshedSearchee = (await client.getClientSearchees({
121
+ newSearcheesOnly: true,
122
+ refresh: [searchee.infoHash],
123
+ })).newSearchees.find((s) => s.infoHash === searchee.infoHash);
124
+ if (!refreshedSearchee)
125
+ return resultOfErr("TORRENT_NOT_FOUND");
126
+ for (const [key, value] of Object.entries(refreshedSearchee)) {
127
+ searchee[key] = value;
128
+ }
129
+ if (!(await client.isTorrentComplete(searchee.infoHash)).orElse(false)) {
130
+ return resultOfErr("TORRENT_NOT_COMPLETE");
131
+ }
132
+ savePath = searchee.savePath;
133
+ }
134
+ else {
135
+ const downloadDirResult = await client.getDownloadDir(searchee, { onlyCompleted: options.onlyCompleted });
136
+ if (downloadDirResult.isErr()) {
137
+ return downloadDirResult.mapErr((e) => e === "NOT_FOUND" || e === "UNKNOWN_ERROR"
138
+ ? "TORRENT_NOT_FOUND"
139
+ : e);
140
+ }
141
+ savePath = downloadDirResult.unwrap();
142
+ }
143
+ const rootFolder = getRootFolder(searchee.files[0]);
144
+ const sourceRootOrSavePath = searchee.files.length === 1
145
+ ? join(savePath, searchee.files[0].path)
146
+ : rootFolder
147
+ ? join(savePath, rootFolder)
148
+ : savePath;
149
+ if (!fs.existsSync(sourceRootOrSavePath)) {
150
+ logger.error({
151
+ label: searchee.label,
152
+ message: `Linking failed, ${sourceRootOrSavePath} not found.`,
153
+ });
154
+ return resultOfErr("INVALID_DATA");
155
+ }
156
+ return resultOf(savePath);
157
+ }
158
+ async function getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker) {
159
+ const { flatLinking, linkType } = getRuntimeConfig();
160
+ if (!client) {
161
+ let srcPath;
162
+ let srcDev;
163
+ try {
164
+ srcPath = !savePath
165
+ ? searchee.files.find((f) => fs.existsSync(f.path)).path
166
+ : join(savePath, searchee.files.find((f) => fs.existsSync(join(savePath, f.path))).path);
167
+ srcDev = fs.statSync(srcPath).dev;
168
+ }
169
+ catch (e) {
170
+ logger.debug(e);
171
+ return null;
172
+ }
173
+ let error;
174
+ for (const testClient of getClients().filter((c) => !c.readonly)) {
175
+ const torrentSavePaths = new Set((await testClient.getAllDownloadDirs({
176
+ metas: [],
177
+ onlyCompleted: false,
178
+ })).values());
179
+ for (const torrentSavePath of torrentSavePaths) {
180
+ try {
181
+ if (srcDev && fs.statSync(torrentSavePath).dev === srcDev) {
182
+ client = testClient;
183
+ break;
184
+ }
185
+ const testPath = join(torrentSavePath, linkTestName);
186
+ linkFile(srcPath, testPath, linkType === LinkType.REFLINK
187
+ ? linkType
188
+ : LinkType.HARDLINK);
189
+ fs.rmSync(testPath);
190
+ client = testClient;
191
+ break;
192
+ }
193
+ catch (e) {
194
+ error = e;
195
+ }
196
+ }
197
+ if (client)
198
+ break;
199
+ }
200
+ if (!client) {
201
+ logger.debug(error);
202
+ return null;
203
+ }
235
204
  }
205
+ let destinationDir;
236
206
  const clientSavePathRes = await client.getDownloadDir(newMeta, {
237
207
  onlyCompleted: false,
238
208
  });
239
- let destinationDir = null;
240
209
  if (clientSavePathRes.isOk()) {
241
210
  destinationDir = clientSavePathRes.unwrap();
242
211
  }
243
212
  else {
244
- const linkDir = sourceRoot
245
- ? getLinkDir(sourceRoot)
213
+ if (clientSavePathRes.unwrapErr() === "INVALID_DATA") {
214
+ return null;
215
+ }
216
+ const linkDir = savePath
217
+ ? getLinkDir(savePath)
246
218
  : getLinkDirVirtual(searchee);
247
219
  if (!linkDir)
248
- return resultOfErr("MISSING_DATA");
220
+ return null;
249
221
  destinationDir = flatLinking ? linkDir : join(linkDir, tracker);
250
222
  }
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
- }));
223
+ return { client, destinationDir };
224
+ }
225
+ function logActionResult(result, newMeta, searchee, tracker, decision) {
226
+ const metaLog = getLogString(newMeta, chalk.green.bold);
227
+ const searcheeLog = getLogString(searchee, chalk.magenta.bold);
228
+ const source = `${getSearcheeSource(searchee)} (${searcheeLog})`;
229
+ const foundBy = `Found ${metaLog} on ${chalk.bold(tracker)} by`;
230
+ let infoOrVerbose = logger.info;
231
+ let warnOrVerbose = logger.warn;
232
+ if (searchee.label === Label.INJECT) {
233
+ infoOrVerbose = logger.verbose;
234
+ warnOrVerbose = logger.verbose;
260
235
  }
261
- else {
262
- return resultOf(linkFuzzyTree(searchee, newMeta, destinationDir, sourceRoot, {
263
- ignoreMissing: !options.onlyCompleted,
264
- }));
236
+ switch (result) {
237
+ case SaveResult.SAVED:
238
+ infoOrVerbose({
239
+ label: searchee.label,
240
+ message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - saved`,
241
+ });
242
+ break;
243
+ case InjectionResult.SUCCESS:
244
+ infoOrVerbose({
245
+ label: searchee.label,
246
+ message: `${foundBy} ${chalk.green.bold(decision)} from ${source} - injected`,
247
+ });
248
+ break;
249
+ case InjectionResult.ALREADY_EXISTS:
250
+ infoOrVerbose({
251
+ label: searchee.label,
252
+ message: `${foundBy} ${chalk.yellow(decision)} from ${source} - exists`,
253
+ });
254
+ break;
255
+ case InjectionResult.TORRENT_NOT_COMPLETE:
256
+ warnOrVerbose({
257
+ label: searchee.label,
258
+ message: `${foundBy} ${chalk.yellow(decision)} from ${source} - source is incomplete, saving...`,
259
+ });
260
+ break;
261
+ case InjectionResult.FAILURE:
262
+ default:
263
+ logger.error({
264
+ label: searchee.label,
265
+ message: `${foundBy} ${chalk.red(decision)} from ${source} - failed to inject, saving...`,
266
+ });
267
+ break;
265
268
  }
266
269
  }
267
270
  export async function performAction(newMeta, decision, searchee, tracker) {
271
+ return withMutex(Mutex.CLIENT_INJECTION, async () => {
272
+ return performActionWithoutMutex(newMeta, decision, searchee, tracker);
273
+ }, { useQueue: true });
274
+ }
275
+ export async function performActionWithoutMutex(newMeta, decision, searchee, tracker, injectClient, options = { onlyCompleted: true }) {
268
276
  const { action, linkDirs } = getRuntimeConfig();
269
277
  if (action === Action.SAVE) {
270
- await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
271
278
  logActionResult(SaveResult.SAVED, newMeta, searchee, tracker, decision);
272
- return { actionResult: SaveResult.SAVED, linkedNewFiles: false };
279
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
280
+ return { actionResult: SaveResult.SAVED };
273
281
  }
282
+ let savePath;
274
283
  let destinationDir;
275
284
  let unlinkOk = false;
276
285
  let linkedNewFiles = false;
286
+ const warnOrVerbose = searchee.label !== Label.INJECT ? logger.warn : logger.verbose;
287
+ const clients = getClients();
288
+ let client = clients.length === 1
289
+ ? clients[0]
290
+ : clients.find((c) => c.clientHost === searchee.clientHost && !c.readonly);
277
291
  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;
292
+ const savePathRes = await getSavePath(client, searchee, options);
293
+ if (savePathRes.isErr()) {
294
+ const result = savePathRes.unwrapErr();
288
295
  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
- });
296
+ const actionResult = InjectionResult.TORRENT_NOT_COMPLETE;
297
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
298
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
299
+ return { client, actionResult, linkedNewFiles };
297
300
  }
301
+ const actionResult = InjectionResult.FAILURE;
302
+ logger.error({
303
+ label: searchee.label,
304
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${result}`,
305
+ });
298
306
  logActionResult(actionResult, newMeta, searchee, tracker, decision);
299
307
  await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
300
308
  return { actionResult, linkedNewFiles };
301
309
  }
310
+ savePath = savePathRes.unwrap();
311
+ const res = await getClientAndDestinationDir(client, searchee, savePath, newMeta, tracker);
312
+ if (res) {
313
+ client = res.client;
314
+ destinationDir = res.destinationDir;
315
+ }
316
+ else {
317
+ client = undefined;
318
+ }
319
+ }
320
+ if (!client) {
321
+ logger.error({
322
+ label: searchee.label,
323
+ message: `Failed to find a torrent client for ${getLogString(searchee)}`,
324
+ });
325
+ const actionResult = InjectionResult.FAILURE;
326
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
327
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
328
+ return { actionResult, linkedNewFiles };
329
+ }
330
+ for (const otherClient of clients) {
331
+ if (otherClient.clientHost === client.clientHost)
332
+ continue;
333
+ if ((await otherClient.isTorrentComplete(newMeta.infoHash)).isErr()) {
334
+ continue;
335
+ }
336
+ warnOrVerbose({
337
+ label: searchee.label,
338
+ message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - already exists in ${otherClient.clientHost}`,
339
+ });
340
+ const actionResult = InjectionResult.FAILURE;
341
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
342
+ return { actionResult, linkedNewFiles };
343
+ }
344
+ if (injectClient && injectClient.clientHost !== client.clientHost) {
345
+ warnOrVerbose({
346
+ label: searchee.label,
347
+ message: `Skipping ${getLogString(newMeta)} injection into ${client.clientHost} - existing match is using ${injectClient.clientHost}`,
348
+ });
349
+ const actionResult = InjectionResult.FAILURE;
350
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
351
+ return { actionResult, linkedNewFiles };
352
+ }
353
+ if (linkDirs.length) {
354
+ const res = linkAllFilesInMetafile(searchee, newMeta, decision, destinationDir, { savePath, ignoreMissing: !options.onlyCompleted });
355
+ if (res.isErr()) {
356
+ logger.error({
357
+ label: searchee.label,
358
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${res.unwrapErr().message}`,
359
+ });
360
+ const actionResult = InjectionResult.FAILURE;
361
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
362
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
363
+ return { actionResult, linkedNewFiles };
364
+ }
365
+ const linkResult = res.unwrap();
366
+ unlinkOk = !linkResult.alreadyExisted;
367
+ linkedNewFiles = linkResult.linkedNewFiles;
302
368
  }
303
369
  else if (searchee.path) {
304
370
  destinationDir = dirname(searchee.path);
305
371
  }
306
- const actionResult = await getClient().inject(newMeta, searchee, decision, destinationDir);
372
+ const actionResult = await client.inject(newMeta, searchee, decision, {
373
+ onlyCompleted: options.onlyCompleted,
374
+ destinationDir,
375
+ });
307
376
  logActionResult(actionResult, newMeta, searchee, tracker, decision);
308
377
  if (actionResult === InjectionResult.SUCCESS) {
309
378
  // cross-seed may need to process these with the inject job
@@ -311,13 +380,29 @@ export async function performAction(newMeta, decision, searchee, tracker) {
311
380
  await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
312
381
  }
313
382
  }
314
- else if (actionResult !== InjectionResult.ALREADY_EXISTS) {
383
+ else if (actionResult === InjectionResult.ALREADY_EXISTS) {
384
+ if (linkedNewFiles) {
385
+ logger.info({
386
+ label: client.label,
387
+ message: `Rechecking ${getLogString(newMeta)} as new files were linked from ${getLogString(searchee)}`,
388
+ });
389
+ await client.recheckTorrent(newMeta.infoHash);
390
+ client.resumeInjection(newMeta.infoHash, decision, {
391
+ checkOnce: false,
392
+ });
393
+ }
394
+ }
395
+ else {
315
396
  await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
316
397
  if (unlinkOk && destinationDir) {
317
398
  unlinkMetafile(newMeta, destinationDir);
399
+ linkedNewFiles = false;
318
400
  }
319
401
  }
320
- return { actionResult, linkedNewFiles };
402
+ if (actionResult === InjectionResult.FAILURE) {
403
+ return { actionResult, linkedNewFiles };
404
+ }
405
+ return { client, actionResult, linkedNewFiles };
321
406
  }
322
407
  export async function performActions(searchee, matches) {
323
408
  const results = [];
@@ -337,11 +422,24 @@ export function getLinkDir(pathStr) {
337
422
  return linkDir;
338
423
  }
339
424
  }
340
- const srcFile = pathStat.isFile()
425
+ let srcFile = pathStat.isFile()
341
426
  ? pathStr
342
427
  : pathStat.isDirectory()
343
428
  ? findAFileWithExt(pathStr, ALL_EXTENSIONS)
344
429
  : null;
430
+ let tempFile;
431
+ if (!srcFile) {
432
+ tempFile = pathStat.isDirectory()
433
+ ? join(pathStr, srcTestName)
434
+ : join(dirname(pathStr), srcTestName);
435
+ try {
436
+ fs.writeFileSync(tempFile, "");
437
+ srcFile = tempFile;
438
+ }
439
+ catch (e) {
440
+ logger.debug(e);
441
+ }
442
+ }
345
443
  if (srcFile) {
346
444
  for (const linkDir of linkDirs) {
347
445
  try {
@@ -350,6 +448,8 @@ export function getLinkDir(pathStr) {
350
448
  ? linkType
351
449
  : LinkType.HARDLINK);
352
450
  fs.rmSync(testPath);
451
+ if (tempFile && fs.existsSync(tempFile))
452
+ fs.rmSync(tempFile);
353
453
  return linkDir;
354
454
  }
355
455
  catch {
@@ -357,6 +457,8 @@ export function getLinkDir(pathStr) {
357
457
  }
358
458
  }
359
459
  }
460
+ if (tempFile && fs.existsSync(tempFile))
461
+ fs.rmSync(tempFile);
360
462
  if (linkType !== LinkType.SYMLINK) {
361
463
  logger.error(`Cannot find any linkDir from linkDirs on the same drive to ${linkType} ${pathStr}`);
362
464
  return null;
@@ -426,10 +528,21 @@ function unwrapSymlinks(path) {
426
528
  */
427
529
  export function testLinking(srcDir) {
428
530
  const { linkDirs, linkType } = getRuntimeConfig();
531
+ let tempFile;
429
532
  try {
430
- const srcFile = findAFileWithExt(srcDir, ALL_EXTENSIONS);
431
- if (!srcFile)
432
- return;
533
+ let srcFile = findAFileWithExt(srcDir, ALL_EXTENSIONS);
534
+ if (!srcFile) {
535
+ tempFile = join(srcDir, srcTestName);
536
+ try {
537
+ fs.writeFileSync(tempFile, "");
538
+ srcFile = tempFile;
539
+ }
540
+ catch (e) {
541
+ logger.error(e);
542
+ logger.error(`Failed to create test file in ${srcDir}, cross-seed is unable to verify linking for this path.`);
543
+ return;
544
+ }
545
+ }
433
546
  const linkDir = getLinkDir(srcDir);
434
547
  if (!linkDir)
435
548
  throw new Error(`No valid linkDir found for ${srcDir}`);
@@ -441,5 +554,9 @@ export function testLinking(srcDir) {
441
554
  logger.error(e);
442
555
  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
556
  }
557
+ finally {
558
+ if (tempFile && fs.existsSync(tempFile))
559
+ fs.rmSync(tempFile);
560
+ }
444
561
  }
445
562
  //# sourceMappingURL=action.js.map