cross-seed 6.13.6 → 7.0.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.
Files changed (147) hide show
  1. package/README.md +12 -13
  2. package/dist/webui/assets/FieldInfo-Bxj_j8SJ.js +1 -0
  3. package/dist/webui/assets/Page-C3rteCZt.js +1 -0
  4. package/dist/webui/assets/array-field-DVSC6nHP.js +1 -0
  5. package/dist/webui/assets/badge-DTZMtS0e.js +1 -0
  6. package/dist/webui/assets/check-Bu3ldi63.js +1 -0
  7. package/dist/webui/assets/chevron-down-CRy8M0kJ.js +1 -0
  8. package/dist/webui/assets/clients-CW8oEZoQ.js +1 -0
  9. package/dist/webui/assets/connect-YBNsnjWT.js +1 -0
  10. package/dist/webui/assets/debug-mz8-WYZj.js +1 -0
  11. package/dist/webui/assets/directories-BSK28RgR.js +1 -0
  12. package/dist/webui/assets/duration-field-C6xoSlJg.js +1 -0
  13. package/dist/webui/assets/general-lJJxZhH7.js +1 -0
  14. package/dist/webui/assets/health-CXbsVrie.js +1 -0
  15. package/dist/webui/assets/index-Bi48hI2z.js +54 -0
  16. package/dist/webui/assets/index-C-Ul7GNg.css +1 -0
  17. package/dist/webui/assets/index-C2cH1Gst.js +1 -0
  18. package/dist/webui/assets/index-Cc5bDmJr.js +1 -0
  19. package/dist/webui/assets/jobs-CxmNab9w.js +1 -0
  20. package/dist/webui/assets/library-vaj2W8sE.js +1 -0
  21. package/dist/webui/assets/loader-circle-M0gu1gZ-.js +1 -0
  22. package/dist/webui/assets/logs-Cu9RyKS0.js +1 -0
  23. package/dist/webui/assets/search-2R5sIdT8.js +1 -0
  24. package/dist/webui/assets/select-field-BCqNLDrJ.js +1 -0
  25. package/dist/webui/assets/select-zHgqMzLj.js +1 -0
  26. package/dist/webui/assets/settings-CMYjpTbZ.js +1 -0
  27. package/dist/webui/assets/submit-button-BtcnyggQ.js +1 -0
  28. package/dist/webui/assets/switch-G0W3uJVN.js +1 -0
  29. package/dist/webui/assets/switch-field-IBd9ORNq.js +1 -0
  30. package/dist/webui/assets/table-DvgJU7Gh.js +1 -0
  31. package/dist/webui/assets/test-tube-BIwmoM45.js +1 -0
  32. package/dist/webui/assets/text-field-DruSbGhy.js +1 -0
  33. package/dist/webui/assets/time-BSMZjmyW.js +1 -0
  34. package/dist/webui/assets/trackers-D-OpAe63.js +7 -0
  35. package/dist/webui/assets/use-form-validation-context-BkAfWAh0.js +1 -0
  36. package/dist/webui/assets/use-settings-form-submit-CDRh-E9U.js +2 -0
  37. package/dist/webui/assets/useQuery-A4Hv_4uX.js +1 -0
  38. package/dist/webui/index.html +13 -0
  39. package/node_modules/@cross-seed/shared/dist/configSchema.d.ts +261 -0
  40. package/node_modules/@cross-seed/shared/dist/configSchema.d.ts.map +1 -0
  41. package/node_modules/@cross-seed/shared/dist/configSchema.js +53 -0
  42. package/node_modules/@cross-seed/shared/dist/configSchema.js.map +1 -0
  43. package/node_modules/@cross-seed/shared/dist/constants.d.ts +122 -0
  44. package/node_modules/@cross-seed/shared/dist/constants.d.ts.map +1 -0
  45. package/node_modules/@cross-seed/shared/dist/constants.js +127 -0
  46. package/node_modules/@cross-seed/shared/dist/constants.js.map +1 -0
  47. package/node_modules/@cross-seed/shared/dist/tsconfig.tsbuildinfo +1 -0
  48. package/node_modules/@cross-seed/shared/dist/utils.d.ts +6 -0
  49. package/node_modules/@cross-seed/shared/dist/utils.d.ts.map +1 -0
  50. package/node_modules/@cross-seed/shared/dist/utils.js +9 -0
  51. package/node_modules/@cross-seed/shared/dist/utils.js.map +1 -0
  52. package/node_modules/@cross-seed/shared/package.json +22 -0
  53. package/package.json +35 -11
  54. package/dist/Result.js +0 -64
  55. package/dist/Result.js.map +0 -1
  56. package/dist/action.js +0 -693
  57. package/dist/action.js.map +0 -1
  58. package/dist/arr.js +0 -199
  59. package/dist/arr.js.map +0 -1
  60. package/dist/auth.js +0 -25
  61. package/dist/auth.js.map +0 -1
  62. package/dist/clients/Deluge.js +0 -698
  63. package/dist/clients/Deluge.js.map +0 -1
  64. package/dist/clients/QBittorrent.js +0 -785
  65. package/dist/clients/QBittorrent.js.map +0 -1
  66. package/dist/clients/RTorrent.js +0 -654
  67. package/dist/clients/RTorrent.js.map +0 -1
  68. package/dist/clients/TorrentClient.js +0 -272
  69. package/dist/clients/TorrentClient.js.map +0 -1
  70. package/dist/clients/Transmission.js +0 -404
  71. package/dist/clients/Transmission.js.map +0 -1
  72. package/dist/cmd.js +0 -196
  73. package/dist/cmd.js.map +0 -1
  74. package/dist/config.template.cjs +0 -353
  75. package/dist/config.template.cjs.map +0 -1
  76. package/dist/configSchema.js +0 -667
  77. package/dist/configSchema.js.map +0 -1
  78. package/dist/configuration.js +0 -82
  79. package/dist/configuration.js.map +0 -1
  80. package/dist/constants.js +0 -281
  81. package/dist/constants.js.map +0 -1
  82. package/dist/dataFiles.js +0 -208
  83. package/dist/dataFiles.js.map +0 -1
  84. package/dist/db.js +0 -216
  85. package/dist/db.js.map +0 -1
  86. package/dist/decide.js +0 -553
  87. package/dist/decide.js.map +0 -1
  88. package/dist/diff.js +0 -24
  89. package/dist/diff.js.map +0 -1
  90. package/dist/errors.js +0 -16
  91. package/dist/errors.js.map +0 -1
  92. package/dist/indexers.js +0 -180
  93. package/dist/indexers.js.map +0 -1
  94. package/dist/inject.js +0 -594
  95. package/dist/inject.js.map +0 -1
  96. package/dist/jobs.js +0 -146
  97. package/dist/jobs.js.map +0 -1
  98. package/dist/logger.js +0 -143
  99. package/dist/logger.js.map +0 -1
  100. package/dist/migrations/00-initialSchema.js +0 -30
  101. package/dist/migrations/00-initialSchema.js.map +0 -1
  102. package/dist/migrations/01-jobs.js +0 -12
  103. package/dist/migrations/01-jobs.js.map +0 -1
  104. package/dist/migrations/02-timestamps.js +0 -21
  105. package/dist/migrations/02-timestamps.js.map +0 -1
  106. package/dist/migrations/03-rateLimits.js +0 -14
  107. package/dist/migrations/03-rateLimits.js.map +0 -1
  108. package/dist/migrations/04-auth.js +0 -13
  109. package/dist/migrations/04-auth.js.map +0 -1
  110. package/dist/migrations/05-caps.js +0 -16
  111. package/dist/migrations/05-caps.js.map +0 -1
  112. package/dist/migrations/06-uniqueDecisions.js +0 -29
  113. package/dist/migrations/06-uniqueDecisions.js.map +0 -1
  114. package/dist/migrations/07-limits.js +0 -12
  115. package/dist/migrations/07-limits.js.map +0 -1
  116. package/dist/migrations/08-rss.js +0 -15
  117. package/dist/migrations/08-rss.js.map +0 -1
  118. package/dist/migrations/09-clientAndDataSearchees.js +0 -34
  119. package/dist/migrations/09-clientAndDataSearchees.js.map +0 -1
  120. package/dist/migrations/10-indexerNameAudioBookCaps.js +0 -18
  121. package/dist/migrations/10-indexerNameAudioBookCaps.js.map +0 -1
  122. package/dist/migrations/11-trackers.js +0 -38
  123. package/dist/migrations/11-trackers.js.map +0 -1
  124. package/dist/migrations/migrations.js +0 -31
  125. package/dist/migrations/migrations.js.map +0 -1
  126. package/dist/parseTorrent.js +0 -128
  127. package/dist/parseTorrent.js.map +0 -1
  128. package/dist/pipeline.js +0 -527
  129. package/dist/pipeline.js.map +0 -1
  130. package/dist/preFilter.js +0 -250
  131. package/dist/preFilter.js.map +0 -1
  132. package/dist/pushNotifier.js +0 -137
  133. package/dist/pushNotifier.js.map +0 -1
  134. package/dist/runtimeConfig.js +0 -11
  135. package/dist/runtimeConfig.js.map +0 -1
  136. package/dist/searchee.js +0 -658
  137. package/dist/searchee.js.map +0 -1
  138. package/dist/server.js +0 -456
  139. package/dist/server.js.map +0 -1
  140. package/dist/startup.js +0 -203
  141. package/dist/startup.js.map +0 -1
  142. package/dist/torrent.js +0 -637
  143. package/dist/torrent.js.map +0 -1
  144. package/dist/torznab.js +0 -786
  145. package/dist/torznab.js.map +0 -1
  146. package/dist/utils.js +0 -637
  147. package/dist/utils.js.map +0 -1
package/dist/searchee.js DELETED
@@ -1,658 +0,0 @@
1
- import { readdir, stat } from "fs/promises";
2
- import { basename, dirname, isAbsolute, join, relative } from "path";
3
- import ms from "ms";
4
- import { byClientHostPriority, getClients, } from "./clients/TorrentClient.js";
5
- import { ABS_WIN_PATH_REGEX, AKA_REGEX, ANIME_GROUP_REGEX, ANIME_REGEX, ARR_DIR_REGEX, AUDIO_EXTENSIONS, BAD_GROUP_PARSE_REGEX, BOOK_EXTENSIONS, EP_REGEX, MediaType, MOVIE_REGEX, parseSource, RELEASE_GROUP_REGEX, REPACK_PROPER_REGEX, RES_STRICT_REGEX, SEASON_REGEX, SONARR_SUBFOLDERS_REGEX, VIDEO_DISC_EXTENSIONS, VIDEO_EXTENSIONS, } from "./constants.js";
6
- import { db } from "./db.js";
7
- import { Label, logger } from "./logger.js";
8
- import { resultOf, resultOfErr } from "./Result.js";
9
- import { getRuntimeConfig } from "./runtimeConfig.js";
10
- import { parseTorrentWithMetadata } from "./torrent.js";
11
- import { comparing, createKeyTitle, extractInt, filesWithExt, flatMapAsync, getLogString, hasExt, humanReadableDate, humanReadableSize, inBatches, isBadTitle, isTruthy, notExists, mapAsync, stripExtension, withMutex, Mutex, } from "./utils.js";
12
- export function hasInfoHash(searchee) {
13
- return searchee.infoHash != null;
14
- }
15
- var SearcheeSource;
16
- (function (SearcheeSource) {
17
- SearcheeSource["CLIENT"] = "torrentClient";
18
- SearcheeSource["TORRENT"] = "torrentFile";
19
- SearcheeSource["DATA"] = "dataDir";
20
- SearcheeSource["VIRTUAL"] = "virtual";
21
- })(SearcheeSource || (SearcheeSource = {}));
22
- export function getSearcheeSource(searchee) {
23
- if (searchee.savePath) {
24
- return SearcheeSource.CLIENT;
25
- }
26
- else if (searchee.infoHash) {
27
- return SearcheeSource.TORRENT;
28
- }
29
- else if (searchee.path) {
30
- return SearcheeSource.DATA;
31
- }
32
- else {
33
- return SearcheeSource.VIRTUAL;
34
- }
35
- }
36
- export function getMediaType({ title, files }) {
37
- switch (true /* eslint-disable no-fallthrough */) {
38
- case EP_REGEX.test(title):
39
- return MediaType.EPISODE;
40
- case SEASON_REGEX.test(title):
41
- return MediaType.SEASON;
42
- case hasExt(files, VIDEO_EXTENSIONS):
43
- if (MOVIE_REGEX.test(title))
44
- return MediaType.MOVIE;
45
- if (ANIME_REGEX.test(title))
46
- return MediaType.ANIME;
47
- return MediaType.VIDEO;
48
- case hasExt(files, VIDEO_DISC_EXTENSIONS):
49
- if (MOVIE_REGEX.test(title))
50
- return MediaType.MOVIE;
51
- return MediaType.VIDEO;
52
- case hasExt(files, [".rar"]):
53
- if (MOVIE_REGEX.test(title))
54
- return MediaType.MOVIE;
55
- default: // Minimally supported media types
56
- if (hasExt(files, AUDIO_EXTENSIONS))
57
- return MediaType.AUDIO;
58
- if (hasExt(files, BOOK_EXTENSIONS))
59
- return MediaType.BOOK;
60
- return MediaType.OTHER;
61
- }
62
- }
63
- export function getFuzzySizeFactor(searchee) {
64
- const { fuzzySizeThreshold, seasonFromEpisodes } = getRuntimeConfig();
65
- return seasonFromEpisodes && !searchee.infoHash && !searchee.path
66
- ? 1 - seasonFromEpisodes
67
- : fuzzySizeThreshold;
68
- }
69
- export function getMinSizeRatio(searchee) {
70
- const { fuzzySizeThreshold, seasonFromEpisodes } = getRuntimeConfig();
71
- return seasonFromEpisodes && !searchee.infoHash && !searchee.path
72
- ? seasonFromEpisodes
73
- : 1 - fuzzySizeThreshold;
74
- }
75
- export function getRoot({ path }, options = { dirname, isAbsolute }) {
76
- if (options.isAbsolute(path) ||
77
- path.startsWith("/") ||
78
- ABS_WIN_PATH_REGEX.test(path)) {
79
- return resultOfErr(new Error(`absolute paths for the torrent file tree are not supported. File tree paths must be relative to the torrent save path: ${path}`));
80
- }
81
- let root = path;
82
- let parent = options.dirname(root);
83
- while (parent !== ".") {
84
- root = parent;
85
- parent = options.dirname(root);
86
- }
87
- return resultOf(root);
88
- }
89
- export function getRootFolder(file) {
90
- const res = getRoot(file);
91
- if (res.isErr())
92
- return res;
93
- const root = res.unwrap();
94
- if (root === file.path)
95
- return resultOf(null);
96
- return resultOf(root);
97
- }
98
- export function getLargestFile(files) {
99
- return files.reduce((a, b) => (a.length > b.length ? a : b));
100
- }
101
- export async function getNewestFileAge(absoluteFilePaths) {
102
- return (await mapAsync(absoluteFilePaths, async (f) => (await stat(f)).mtimeMs)).reduce((a, b) => Math.max(a, b));
103
- }
104
- export async function getSearcheeNewestFileAge(searchee) {
105
- const { path } = searchee;
106
- if (!path) {
107
- return getNewestFileAge(searchee.files.map((file) => file.path));
108
- }
109
- const pathStat = await stat(path);
110
- if (pathStat.isFile())
111
- return pathStat.mtimeMs;
112
- return getNewestFileAge(searchee.files.map((file) => join(dirname(path), file.path)));
113
- }
114
- async function getFileNamesFromRootRec(root, memoizedPaths, isDirHint) {
115
- if (memoizedPaths.has(root))
116
- return memoizedPaths.get(root);
117
- const isDir = isDirHint !== undefined ? isDirHint : (await stat(root)).isDirectory();
118
- const paths = !isDir
119
- ? [root]
120
- : await flatMapAsync(await readdir(root, { withFileTypes: true }), (dirent) => getFileNamesFromRootRec(join(root, dirent.name), memoizedPaths, dirent.isDirectory()));
121
- memoizedPaths.set(root, paths);
122
- return paths;
123
- }
124
- export async function getFilesFromDataRoot(rootPath, memoizedPaths, memoizedLengths) {
125
- const parentDir = dirname(rootPath);
126
- try {
127
- return await mapAsync(await getFileNamesFromRootRec(rootPath, memoizedPaths), async (file) => ({
128
- path: relative(parentDir, file),
129
- name: basename(file),
130
- length: memoizedLengths.get(file) ??
131
- memoizedLengths
132
- .set(file, (await stat(file)).size)
133
- .get(file),
134
- }));
135
- }
136
- catch (e) {
137
- logger.debug(e);
138
- return [];
139
- }
140
- }
141
- /**
142
- * Parse things like the resolution to add to parsed titles for better decisions.
143
- * @param videoFileNames All relavant video file names (e.g episodes for a season)
144
- * @returns Info to add to the title if all files match
145
- */
146
- function parseMetaInfo(videoFileNames) {
147
- let metaInfo = "";
148
- const videoStems = videoFileNames.map((name) => stripExtension(name));
149
- const types = videoStems
150
- .map((stem) => stem.match(REPACK_PROPER_REGEX)?.groups?.type)
151
- .filter(isTruthy);
152
- if (types.length) {
153
- metaInfo += ` REPACK`;
154
- }
155
- const res = videoStems
156
- .map((stem) => stem.match(RES_STRICT_REGEX)?.groups?.res?.trim()?.toLowerCase())
157
- .filter(isTruthy);
158
- if (res.length === videoStems.length && res.every((r) => r === res[0])) {
159
- metaInfo += ` ${res[0]}`;
160
- }
161
- const sources = videoStems
162
- .map((stem) => parseSource(stem))
163
- .filter(isTruthy);
164
- if (sources.length === videoStems.length &&
165
- sources.every((s) => s === sources[0])) {
166
- metaInfo += ` ${sources[0]}`;
167
- }
168
- const groups = videoStems
169
- .map((stem) => getReleaseGroup(stem))
170
- .filter(isTruthy);
171
- if (groups.length === videoStems.length &&
172
- groups.every((g) => g.toLowerCase() === groups[0].toLowerCase())) {
173
- metaInfo += `-${groups[0]}`;
174
- }
175
- return metaInfo;
176
- }
177
- /**
178
- * Parse title from SXX or Season XX. Return null if no title found.
179
- * Also tries to parse titles that are just `Show`, returns `Show` if better not found.
180
- * @param name Original name of the searchee/metafile
181
- * @param files files in the searchee
182
- * @param path if data based, the path to the searchee
183
- */
184
- export function parseTitle(name, files, path) {
185
- const seasonMatch = name.length < 12 ? name.match(SONARR_SUBFOLDERS_REGEX) : null;
186
- if (!seasonMatch &&
187
- (name.match(/\d/) || !hasExt(files, VIDEO_EXTENSIONS))) {
188
- return name;
189
- }
190
- const videoFiles = filesWithExt(files, VIDEO_EXTENSIONS);
191
- for (const videoFile of videoFiles) {
192
- const ep = videoFile.name.match(EP_REGEX);
193
- if (ep) {
194
- const seasonVal = ep.groups.season ??
195
- ep.groups.year ??
196
- seasonMatch?.groups.seasonNum;
197
- const season = seasonVal ? `S${extractInt(seasonVal)}` : "";
198
- const episode = videoFiles.length === 1
199
- ? `E${ep.groups.episode ? extractInt(ep.groups.episode) : `${ep.groups.month}.${ep.groups.day}`}`
200
- : "";
201
- if (season.length || episode.length || !seasonMatch) {
202
- const metaInfo = parseMetaInfo(videoFiles.map((f) => f.name));
203
- return `${ep.groups.title} ${season}${episode}${metaInfo}`.trim();
204
- }
205
- }
206
- if (path && seasonMatch) {
207
- const title = basename(dirname(path)).match(ARR_DIR_REGEX)?.groups
208
- ?.title;
209
- if (title?.length) {
210
- const metaInfo = parseMetaInfo(videoFiles.map((f) => f.name));
211
- return `${title} S${seasonMatch.groups.seasonNum}${metaInfo}`;
212
- }
213
- }
214
- const anime = videoFile.name.match(ANIME_REGEX);
215
- if (anime) {
216
- const season = seasonMatch
217
- ? `S${seasonMatch.groups.seasonNum}`
218
- : "";
219
- if (season.length || !seasonMatch) {
220
- const metaInfo = parseMetaInfo(videoFiles.map((f) => f.name));
221
- return `${anime.groups.title} ${season}${metaInfo}`.trim();
222
- }
223
- }
224
- }
225
- return !seasonMatch ? name : null;
226
- }
227
- export async function updateSearcheeClientDB(clientHost, newSearchees, infoHashes) {
228
- const removedInfoHashes = (await db("client_searchee")
229
- .where("client_host", clientHost)
230
- .select("info_hash"))
231
- .map((t) => t.info_hash)
232
- .filter((infoHash) => !infoHashes.has(infoHash));
233
- await inBatches(removedInfoHashes, async (batch) => {
234
- await db("client_searchee")
235
- .whereIn("info_hash", batch)
236
- .where("client_host", clientHost)
237
- .del();
238
- await db("ensemble")
239
- .whereIn("info_hash", batch)
240
- .where("client_host", clientHost)
241
- .del();
242
- });
243
- await inBatches(newSearchees.map((searchee) => ({
244
- info_hash: searchee.infoHash,
245
- name: searchee.name,
246
- title: searchee.title,
247
- files: JSON.stringify(searchee.files),
248
- length: searchee.length,
249
- client_host: searchee.clientHost,
250
- save_path: searchee.savePath,
251
- category: searchee.category ?? null,
252
- tags: searchee.tags ? JSON.stringify(searchee.tags) : null,
253
- trackers: JSON.stringify(searchee.trackers),
254
- })), async (batch) => {
255
- await db("client_searchee")
256
- .insert(batch)
257
- .onConflict(["client_host", "info_hash"])
258
- .merge();
259
- });
260
- }
261
- export function createSearcheeFromDB(dbTorrent) {
262
- return {
263
- infoHash: dbTorrent.info_hash,
264
- name: dbTorrent.name,
265
- title: dbTorrent.title,
266
- files: JSON.parse(dbTorrent.files),
267
- length: dbTorrent.length,
268
- clientHost: dbTorrent.client_host,
269
- savePath: dbTorrent.save_path,
270
- category: dbTorrent.category ?? undefined,
271
- tags: dbTorrent.tags ? JSON.parse(dbTorrent.tags) : undefined,
272
- trackers: JSON.parse(dbTorrent.trackers),
273
- };
274
- }
275
- export function createSearcheeFromMetafile(meta) {
276
- const title = parseTitle(meta.name, meta.files);
277
- if (title) {
278
- return resultOf({
279
- files: meta.files,
280
- infoHash: meta.infoHash,
281
- name: meta.name,
282
- title,
283
- length: meta.length,
284
- category: meta.category,
285
- tags: meta.tags,
286
- trackers: meta.trackers,
287
- });
288
- }
289
- const msg = `Could not find title for ${getLogString(meta)} from child files`;
290
- logger.verbose({
291
- label: Label.PREFILTER,
292
- message: msg,
293
- });
294
- return resultOfErr(new Error(msg));
295
- }
296
- export async function createSearcheeFromTorrentFile(filePath, torrentInfos) {
297
- try {
298
- const meta = await parseTorrentWithMetadata(filePath, torrentInfos);
299
- return createSearcheeFromMetafile(meta);
300
- }
301
- catch (e) {
302
- logger.error(`Failed to parse ${basename(filePath)}: ${e.message}`);
303
- logger.debug(e);
304
- return resultOfErr(e);
305
- }
306
- }
307
- export async function createSearcheeFromPath(root, memoizedPaths = new Map(), memoizedLengths = new Map()) {
308
- const files = await getFilesFromDataRoot(root, memoizedPaths, memoizedLengths);
309
- if (files.length === 0) {
310
- const msg = `Failed to retrieve files in ${root}`;
311
- logger.verbose({
312
- label: Label.PREFILTER,
313
- message: msg,
314
- });
315
- return resultOfErr(new Error(msg));
316
- }
317
- const totalLength = files.reduce((runningTotal, file) => runningTotal + file.length, 0);
318
- const name = basename(root);
319
- const title = parseTitle(name, files, root);
320
- if (title) {
321
- const searchee = {
322
- infoHash: undefined,
323
- path: root,
324
- files: files,
325
- name,
326
- title,
327
- length: totalLength,
328
- };
329
- searchee.mtimeMs = await getSearcheeNewestFileAge(searchee);
330
- return resultOf(searchee);
331
- }
332
- const msg = `Could not find title for ${root} in parent directory or child files`;
333
- logger.verbose({
334
- label: Label.PREFILTER,
335
- message: msg,
336
- });
337
- return resultOfErr(new Error(msg));
338
- }
339
- export function getAllTitles(titles) {
340
- const allTitles = titles.slice();
341
- for (const title of titles) {
342
- if (AKA_REGEX.test(title) && title.trim().toLowerCase() !== "aka") {
343
- allTitles.push(...title.split(AKA_REGEX));
344
- }
345
- }
346
- return allTitles;
347
- }
348
- export function getMovieKeys(stem) {
349
- const match = stem.match(MOVIE_REGEX);
350
- if (!match)
351
- return null;
352
- const titles = getAllTitles([match.groups.title]);
353
- const year = extractInt(match.groups.year);
354
- const keyTitles = [];
355
- const ensembleTitles = [];
356
- for (const title of titles) {
357
- const keyTitle = createKeyTitle(title);
358
- if (!keyTitle)
359
- continue;
360
- keyTitles.push(keyTitle);
361
- ensembleTitles.push(`${title}.${year}`);
362
- }
363
- if (!keyTitles.length)
364
- return null;
365
- return { ensembleTitles, keyTitles, year };
366
- }
367
- export function getSeasonKeys(stem) {
368
- const match = stem.match(SEASON_REGEX);
369
- if (!match)
370
- return null;
371
- const titles = getAllTitles([match.groups.title]);
372
- const season = `S${extractInt(match.groups.season)}`;
373
- const keyTitles = [];
374
- const ensembleTitles = [];
375
- for (const title of titles) {
376
- const keyTitle = createKeyTitle(title);
377
- if (!keyTitle)
378
- continue;
379
- keyTitles.push(keyTitle);
380
- ensembleTitles.push(`${title}.${season}`);
381
- }
382
- if (!keyTitles.length)
383
- return null;
384
- return { ensembleTitles, keyTitles, season };
385
- }
386
- export function getEpisodeKeys(stem) {
387
- const match = stem.match(EP_REGEX);
388
- if (!match)
389
- return null;
390
- const titles = getAllTitles([match.groups.title]);
391
- const season = match.groups.season
392
- ? `S${extractInt(match.groups.season)}`
393
- : match.groups.year
394
- ? `S${match.groups.year}`
395
- : undefined;
396
- const keyTitles = [];
397
- const ensembleTitles = [];
398
- for (const title of titles) {
399
- const keyTitle = createKeyTitle(title);
400
- if (!keyTitle)
401
- continue;
402
- keyTitles.push(keyTitle);
403
- ensembleTitles.push(`${title}${season ? `.${season}` : ""}`);
404
- }
405
- if (!keyTitles.length)
406
- return null;
407
- const episode = match.groups.episode
408
- ? extractInt(match.groups.episode)
409
- : `${match.groups.month}.${match.groups.day}`;
410
- return { ensembleTitles, keyTitles, season, episode };
411
- }
412
- export function getAnimeKeys(stem) {
413
- const match = stem.match(ANIME_REGEX);
414
- if (!match)
415
- return null;
416
- const titles = getAllTitles([match.groups.title, match.groups.altTitle]);
417
- const keyTitles = [];
418
- const ensembleTitles = [];
419
- for (const title of titles) {
420
- if (!title)
421
- continue;
422
- if (isBadTitle(title))
423
- continue;
424
- const keyTitle = createKeyTitle(title);
425
- if (!keyTitle)
426
- continue;
427
- keyTitles.push(keyTitle);
428
- ensembleTitles.push(title);
429
- }
430
- if (!keyTitles.length)
431
- return null;
432
- const release = extractInt(match.groups.release);
433
- return { ensembleTitles, keyTitles, release };
434
- }
435
- export function getReleaseGroup(stem) {
436
- const predictedGroupMatch = stem.match(RELEASE_GROUP_REGEX);
437
- if (!predictedGroupMatch) {
438
- return null;
439
- }
440
- const parsedGroupMatchString = predictedGroupMatch.groups.group.trim();
441
- if (BAD_GROUP_PARSE_REGEX.test(parsedGroupMatchString))
442
- return null;
443
- const match = stem.match(EP_REGEX) ??
444
- stem.match(SEASON_REGEX) ??
445
- stem.match(MOVIE_REGEX) ??
446
- stem.match(ANIME_REGEX);
447
- const titles = getAllTitles([match?.groups?.title, match?.groups?.altTitle].filter(isTruthy));
448
- for (const title of titles) {
449
- const group = title.match(RELEASE_GROUP_REGEX)?.groups.group.trim();
450
- if (group && parsedGroupMatchString.includes(group))
451
- return null;
452
- }
453
- return parsedGroupMatchString;
454
- }
455
- export function getKeyMetaInfo(stem) {
456
- const resM = stem.match(RES_STRICT_REGEX)?.groups?.res;
457
- const res = resM ? `.${resM}` : "";
458
- const sourceM = parseSource(stem);
459
- const source = sourceM ? `.${sourceM}` : "";
460
- const groupM = getReleaseGroup(stem);
461
- if (groupM) {
462
- return `${res}${source}-${groupM}`.toLowerCase();
463
- }
464
- const groupAnimeM = stem.match(ANIME_GROUP_REGEX)?.groups?.group;
465
- if (groupAnimeM) {
466
- return `${res}${source}-${groupAnimeM}`.toLowerCase();
467
- }
468
- return `${res}${source}`.toLowerCase();
469
- }
470
- const logEnsemble = (reason, searcheeLabel, options) => {
471
- if (!options.useFilters)
472
- return;
473
- logger.verbose({
474
- label: `${searcheeLabel}/${Label.PREFILTER}`,
475
- message: reason,
476
- });
477
- };
478
- function parseEnsembleKeys(searchee, keys, ensembleTitles, episode, existingSeasonMap, keyMap, ensembleTitleMap) {
479
- for (let i = 0; i < keys.length; i++) {
480
- const key = keys[i];
481
- if (existingSeasonMap.has(key))
482
- continue;
483
- if (!ensembleTitleMap.has(key)) {
484
- ensembleTitleMap.set(key, ensembleTitles[i]);
485
- }
486
- const episodesMap = keyMap.get(key);
487
- if (!episodesMap) {
488
- keyMap.set(key, new Map([[episode, [searchee]]]));
489
- continue;
490
- }
491
- const episodeSearchees = episodesMap.get(episode);
492
- if (!episodeSearchees) {
493
- episodesMap.set(episode, [searchee]);
494
- continue;
495
- }
496
- episodeSearchees.push(searchee);
497
- }
498
- }
499
- /**
500
- * Organize episodes by {key: {episode: [searchee]}}
501
- */
502
- function organizeEnsembleKeys(allSearchees, options) {
503
- const existingSeasonMap = new Map();
504
- if (options.useFilters) {
505
- for (const searchee of allSearchees) {
506
- const stem = stripExtension(searchee.title);
507
- const seasonKeys = getSeasonKeys(stem);
508
- if (!seasonKeys)
509
- continue;
510
- const info = getKeyMetaInfo(stem);
511
- const keys = seasonKeys.keyTitles.map((k) => `${k}.${seasonKeys.season}${info}`);
512
- for (const key of keys) {
513
- if (!existingSeasonMap.has(key))
514
- existingSeasonMap.set(key, []);
515
- existingSeasonMap.get(key).push(searchee);
516
- }
517
- }
518
- }
519
- const keyMap = new Map();
520
- const ensembleTitleMap = new Map();
521
- for (const searchee of allSearchees) {
522
- const stem = stripExtension(searchee.title);
523
- const episodeKeys = getEpisodeKeys(stem);
524
- if (episodeKeys) {
525
- const info = getKeyMetaInfo(stem);
526
- const keys = episodeKeys.keyTitles.map((k) => `${k}${episodeKeys.season ? `.${episodeKeys.season}` : ""}${info}`);
527
- const ensembleTitles = episodeKeys.ensembleTitles.map((t) => `${t}${info}`);
528
- parseEnsembleKeys(searchee, keys, ensembleTitles, episodeKeys.episode, existingSeasonMap, keyMap, ensembleTitleMap);
529
- if (options.useFilters)
530
- continue;
531
- }
532
- if (options.useFilters && SEASON_REGEX.test(stem))
533
- continue;
534
- const animeKeys = getAnimeKeys(stem);
535
- if (animeKeys) {
536
- const info = getKeyMetaInfo(stem);
537
- const keys = animeKeys.keyTitles.map((k) => `${k}${info}`);
538
- const ensembleTitles = animeKeys.ensembleTitles.map((t) => `${t}${info}`);
539
- parseEnsembleKeys(searchee, keys, ensembleTitles, animeKeys.release, existingSeasonMap, keyMap, ensembleTitleMap);
540
- if (options.useFilters)
541
- continue;
542
- }
543
- }
544
- return { keyMap, ensembleTitleMap };
545
- }
546
- async function pushEnsembleEpisode(searchee, episodeFiles, hosts, torrentSavePaths) {
547
- const savePath = searchee.path
548
- ? dirname(searchee.path)
549
- : searchee.savePath ?? torrentSavePaths.get(searchee.infoHash);
550
- if (!savePath)
551
- return;
552
- const largestFile = getLargestFile(searchee.files);
553
- if (largestFile.length / searchee.length < 0.5)
554
- return;
555
- const absoluteFile = {
556
- length: largestFile.length,
557
- name: largestFile.name,
558
- path: join(savePath, largestFile.path),
559
- };
560
- if (await notExists(absoluteFile.path))
561
- return;
562
- // Use the oldest file for episode if dupe (cross seeds)
563
- const duplicateFile = episodeFiles.find((file) => file.length === absoluteFile.length);
564
- if (duplicateFile) {
565
- const dupeFileAge = (await stat(duplicateFile.path)).mtimeMs;
566
- const newFileAge = (await stat(absoluteFile.path)).mtimeMs;
567
- if (dupeFileAge <= newFileAge)
568
- return;
569
- episodeFiles.splice(episodeFiles.indexOf(duplicateFile), 1);
570
- }
571
- episodeFiles.push(absoluteFile);
572
- const clientHost = searchee.clientHost;
573
- if (clientHost)
574
- hosts.set(clientHost, (hosts.get(clientHost) ?? 0) + 1);
575
- }
576
- async function createVirtualSeasonSearchee(key, episodeSearchees, ensembleTitleMap, torrentSavePaths, searcheeLabel, options) {
577
- const seasonFromEpisodes = getRuntimeConfig().seasonFromEpisodes;
578
- const minEpisodes = 3;
579
- if (options.useFilters && episodeSearchees.size < minEpisodes) {
580
- return null;
581
- }
582
- const ensembleTitle = ensembleTitleMap.get(key);
583
- const episodes = Array.from(episodeSearchees.keys());
584
- if (typeof episodes[0] === "number") {
585
- const highestEpisode = Math.max(...episodes);
586
- const availPct = episodes.length / highestEpisode;
587
- if (options.useFilters && availPct < seasonFromEpisodes) {
588
- logEnsemble(`Skipping virtual searchee for ${ensembleTitle} episodes as there's only ${episodes.length}/${highestEpisode} (${availPct.toFixed(2)} < ${seasonFromEpisodes.toFixed(2)})`, searcheeLabel, options);
589
- return null;
590
- }
591
- }
592
- const seasonSearchee = {
593
- name: ensembleTitle,
594
- title: ensembleTitle,
595
- files: [], // Can have multiple files per episode
596
- length: 0, // Total length of episodes (uses average for multi-file episodes)
597
- label: searcheeLabel,
598
- };
599
- let newestFileAge = 0;
600
- const hosts = new Map();
601
- for (const [, searchees] of episodeSearchees) {
602
- const episodeFiles = [];
603
- for (const searchee of searchees) {
604
- await pushEnsembleEpisode(searchee, episodeFiles, hosts, torrentSavePaths);
605
- }
606
- if (episodeFiles.length === 0)
607
- continue;
608
- const total = episodeFiles.reduce((a, b) => a + b.length, 0);
609
- seasonSearchee.length += Math.round(total / episodeFiles.length);
610
- seasonSearchee.files.push(...episodeFiles);
611
- const fileAges = await mapAsync(episodeFiles, async (f) => (await stat(f.path)).mtimeMs);
612
- newestFileAge = Math.max(newestFileAge, ...fileAges);
613
- }
614
- seasonSearchee.mtimeMs = newestFileAge;
615
- seasonSearchee.clientHost = [...hosts].sort(comparing((host) => -host[1], (host) => byClientHostPriority(host[0])))[0]?.[0];
616
- if (seasonSearchee.files.length < minEpisodes) {
617
- logEnsemble(`Skipping virtual searchee for ${ensembleTitle} episodes as only ${seasonSearchee.files.length} episode files were found (min: ${minEpisodes})`, searcheeLabel, options);
618
- return null;
619
- }
620
- if (options.useFilters && Date.now() - newestFileAge < ms("8 days")) {
621
- logEnsemble(`Skipping virtual searchee for ${ensembleTitle} episodes as some are below the minimum age of 8 days: ${humanReadableDate(newestFileAge)}`, searcheeLabel, options);
622
- return null;
623
- }
624
- logEnsemble(`Created virtual searchee for ${ensembleTitle}: ${episodeSearchees.size} episodes - ${seasonSearchee.files.length} files - ${humanReadableSize(seasonSearchee.length)}`, searcheeLabel, options);
625
- return seasonSearchee;
626
- }
627
- export async function createEnsembleSearchees(allSearchees, options) {
628
- return withMutex(Mutex.CREATE_ALL_SEARCHEES, { useQueue: true }, async () => {
629
- const { seasonFromEpisodes, useClientTorrents } = getRuntimeConfig();
630
- if (!allSearchees.length)
631
- return [];
632
- if (!seasonFromEpisodes)
633
- return [];
634
- const searcheeLabel = allSearchees[0].label;
635
- if (options.useFilters) {
636
- logger.info({
637
- label: searcheeLabel,
638
- message: `Creating virtual seasons from episode searchees...`,
639
- });
640
- }
641
- const { keyMap, ensembleTitleMap } = organizeEnsembleKeys(allSearchees, options);
642
- const torrentSavePaths = useClientTorrents
643
- ? new Map()
644
- : (await getClients()[0]?.getAllDownloadDirs({
645
- metas: allSearchees.filter(hasInfoHash),
646
- onlyCompleted: false,
647
- })) ?? new Map();
648
- const seasonSearchees = [];
649
- for (const [key, episodeSearchees] of keyMap) {
650
- const seasonSearchee = await createVirtualSeasonSearchee(key, episodeSearchees, ensembleTitleMap, torrentSavePaths, searcheeLabel, options);
651
- if (seasonSearchee)
652
- seasonSearchees.push(seasonSearchee);
653
- }
654
- logEnsemble(`Created ${seasonSearchees.length} virtual season searchees...`, searcheeLabel, options);
655
- return seasonSearchees;
656
- });
657
- }
658
- //# sourceMappingURL=searchee.js.map