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.
- package/dist/action.js +339 -222
- package/dist/action.js.map +1 -1
- package/dist/clients/Deluge.js +100 -55
- package/dist/clients/Deluge.js.map +1 -1
- package/dist/clients/QBittorrent.js +147 -50
- package/dist/clients/QBittorrent.js.map +1 -1
- package/dist/clients/RTorrent.js +86 -56
- package/dist/clients/RTorrent.js.map +1 -1
- package/dist/clients/TorrentClient.js +60 -28
- package/dist/clients/TorrentClient.js.map +1 -1
- package/dist/clients/Transmission.js +67 -33
- package/dist/clients/Transmission.js.map +1 -1
- package/dist/cmd.js +10 -13
- package/dist/cmd.js.map +1 -1
- package/dist/config.template.cjs +17 -29
- package/dist/config.template.cjs.map +1 -1
- package/dist/configSchema.js +104 -30
- package/dist/configSchema.js.map +1 -1
- package/dist/configuration.js.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/dataFiles.js +14 -11
- package/dist/dataFiles.js.map +1 -1
- package/dist/db.js +10 -7
- package/dist/db.js.map +1 -1
- package/dist/decide.js +13 -16
- package/dist/decide.js.map +1 -1
- package/dist/diff.js +5 -26
- package/dist/diff.js.map +1 -1
- package/dist/inject.js +99 -98
- package/dist/inject.js.map +1 -1
- package/dist/logger.js +26 -15
- package/dist/logger.js.map +1 -1
- package/dist/pipeline.js +127 -75
- package/dist/pipeline.js.map +1 -1
- package/dist/preFilter.js +2 -35
- package/dist/preFilter.js.map +1 -1
- package/dist/pushNotifier.js +3 -0
- package/dist/pushNotifier.js.map +1 -1
- package/dist/runtimeConfig.js.map +1 -1
- package/dist/searchee.js +136 -91
- package/dist/searchee.js.map +1 -1
- package/dist/startup.js +3 -2
- package/dist/startup.js.map +1 -1
- package/dist/torrent.js +132 -98
- package/dist/torrent.js.map +1 -1
- package/dist/torznab.js +5 -5
- package/dist/torznab.js.map +1 -1
- package/dist/utils.js +26 -6
- package/dist/utils.js.map +1 -1
- 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 {
|
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,
|
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
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
124
|
-
|
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
|
-
|
43
|
+
return false;
|
137
44
|
}
|
138
|
-
if (
|
139
|
-
|
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
|
-
|
150
|
-
|
61
|
+
catch (e) {
|
62
|
+
return resultOfErr(e);
|
63
|
+
}
|
64
|
+
return resultOf({ alreadyExisted, linkedNewFiles });
|
151
65
|
}
|
152
66
|
function unlinkMetafile(meta, destinationDir) {
|
153
|
-
const
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
87
|
+
message: `Linking failed, ${searchee.path} not found.`,
|
207
88
|
});
|
208
|
-
return resultOfErr("
|
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
|
-
|
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
|
226
|
-
return resultOfErr("
|
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
|
-
|
245
|
-
|
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
|
220
|
+
return null;
|
249
221
|
destinationDir = flatLinking ? linkDir : join(linkDir, tracker);
|
250
222
|
}
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
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
|
279
|
-
if (
|
280
|
-
const
|
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
|
-
|
292
|
-
actionResult
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
431
|
-
if (!srcFile)
|
432
|
-
|
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
|