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