cross-seed 6.11.1 → 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 +344 -224
  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 +93 -71
  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 +50 -90
  16. package/dist/config.template.cjs.map +1 -1
  17. package/dist/configSchema.js +117 -33
  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} - incomplete torrent, 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,89 +113,296 @@ export async function linkAllFilesInMetafile(searchee, newMeta, tracker, decisio
232
113
  }
233
114
  }
234
115
  }
116
+ return resultOf(undefined);
235
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
+ }
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(searchee), 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;
292
+ const savePathRes = await getSavePath(client, searchee, options);
293
+ if (savePathRes.isErr()) {
294
+ const result = savePathRes.unwrapErr();
295
+ if (result === "TORRENT_NOT_COMPLETE") {
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 };
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
+ });
306
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
307
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
308
+ return { actionResult, linkedNewFiles };
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;
284
315
  }
285
316
  else {
286
- const result = linkedFilesRootResult.unwrapErr();
287
- const warnOrVerbose = searchee.label !== Label.INJECT ? logger.warn : logger.verbose;
288
- warnOrVerbose({
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({
289
357
  label: searchee.label,
290
- message: `Failed to link files for ${getLogString(newMeta)}: ${result}`,
358
+ message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${res.unwrapErr().message}`,
291
359
  });
292
- const injectionResult = result === "TORRENT_NOT_COMPLETE"
293
- ? InjectionResult.TORRENT_NOT_COMPLETE
294
- : InjectionResult.FAILURE;
295
- logActionResult(injectionResult, newMeta, searchee, tracker, decision);
296
- await saveTorrentFile(tracker, getMediaType(searchee), newMeta);
297
- return { actionResult: injectionResult, linkedNewFiles };
360
+ const actionResult = InjectionResult.FAILURE;
361
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
362
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
363
+ return { actionResult, linkedNewFiles };
298
364
  }
365
+ const linkResult = res.unwrap();
366
+ unlinkOk = !linkResult.alreadyExisted;
367
+ linkedNewFiles = linkResult.linkedNewFiles;
299
368
  }
300
369
  else if (searchee.path) {
301
370
  destinationDir = dirname(searchee.path);
302
371
  }
303
- const result = await getClient().inject(newMeta, searchee, decision, destinationDir);
304
- logActionResult(result, newMeta, searchee, tracker, decision);
305
- if (result === InjectionResult.SUCCESS) {
372
+ const actionResult = await client.inject(newMeta, searchee, decision, {
373
+ onlyCompleted: options.onlyCompleted,
374
+ destinationDir,
375
+ });
376
+ logActionResult(actionResult, newMeta, searchee, tracker, decision);
377
+ if (actionResult === InjectionResult.SUCCESS) {
306
378
  // cross-seed may need to process these with the inject job
307
379
  if (shouldRecheck(searchee, decision) || !searchee.infoHash) {
308
- await saveTorrentFile(tracker, getMediaType(searchee), newMeta);
380
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
309
381
  }
310
382
  }
311
- else if (result !== InjectionResult.ALREADY_EXISTS) {
312
- await saveTorrentFile(tracker, getMediaType(searchee), newMeta);
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 {
396
+ await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
313
397
  if (unlinkOk && destinationDir) {
314
398
  unlinkMetafile(newMeta, destinationDir);
399
+ linkedNewFiles = false;
315
400
  }
316
401
  }
317
- return { actionResult: result, linkedNewFiles };
402
+ if (actionResult === InjectionResult.FAILURE) {
403
+ return { actionResult, linkedNewFiles };
404
+ }
405
+ return { client, actionResult, linkedNewFiles };
318
406
  }
319
407
  export async function performActions(searchee, matches) {
320
408
  const results = [];
@@ -334,11 +422,24 @@ export function getLinkDir(pathStr) {
334
422
  return linkDir;
335
423
  }
336
424
  }
337
- const srcFile = pathStat.isFile()
425
+ let srcFile = pathStat.isFile()
338
426
  ? pathStr
339
427
  : pathStat.isDirectory()
340
428
  ? findAFileWithExt(pathStr, ALL_EXTENSIONS)
341
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
+ }
342
443
  if (srcFile) {
343
444
  for (const linkDir of linkDirs) {
344
445
  try {
@@ -347,6 +448,8 @@ export function getLinkDir(pathStr) {
347
448
  ? linkType
348
449
  : LinkType.HARDLINK);
349
450
  fs.rmSync(testPath);
451
+ if (tempFile && fs.existsSync(tempFile))
452
+ fs.rmSync(tempFile);
350
453
  return linkDir;
351
454
  }
352
455
  catch {
@@ -354,6 +457,8 @@ export function getLinkDir(pathStr) {
354
457
  }
355
458
  }
356
459
  }
460
+ if (tempFile && fs.existsSync(tempFile))
461
+ fs.rmSync(tempFile);
357
462
  if (linkType !== LinkType.SYMLINK) {
358
463
  logger.error(`Cannot find any linkDir from linkDirs on the same drive to ${linkType} ${pathStr}`);
359
464
  return null;
@@ -423,10 +528,21 @@ function unwrapSymlinks(path) {
423
528
  */
424
529
  export function testLinking(srcDir) {
425
530
  const { linkDirs, linkType } = getRuntimeConfig();
531
+ let tempFile;
426
532
  try {
427
- const srcFile = findAFileWithExt(srcDir, ALL_EXTENSIONS);
428
- if (!srcFile)
429
- 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
+ }
430
546
  const linkDir = getLinkDir(srcDir);
431
547
  if (!linkDir)
432
548
  throw new Error(`No valid linkDir found for ${srcDir}`);
@@ -438,5 +554,9 @@ export function testLinking(srcDir) {
438
554
  logger.error(e);
439
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).`);
440
556
  }
557
+ finally {
558
+ if (tempFile && fs.existsSync(tempFile))
559
+ fs.rmSync(tempFile);
560
+ }
441
561
  }
442
562
  //# sourceMappingURL=action.js.map