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.
- package/dist/action.js +344 -224
- 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 +93 -71
- 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 +50 -90
- package/dist/config.template.cjs.map +1 -1
- package/dist/configSchema.js +117 -33
- 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} - 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
|
-
|
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,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
|
-
|
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(searchee), 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
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
287
|
-
|
288
|
-
|
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)}: ${
|
358
|
+
message: `Failed to link files for ${getLogString(newMeta)} from ${getLogString(searchee)}: ${res.unwrapErr().message}`,
|
291
359
|
});
|
292
|
-
const
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
304
|
-
|
305
|
-
|
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(
|
380
|
+
await saveTorrentFile(tracker, getMediaType(newMeta), newMeta);
|
309
381
|
}
|
310
382
|
}
|
311
|
-
else if (
|
312
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
428
|
-
if (!srcFile)
|
429
|
-
|
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
|