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.
- package/README.md +12 -13
- package/dist/webui/assets/FieldInfo-Bxj_j8SJ.js +1 -0
- package/dist/webui/assets/Page-C3rteCZt.js +1 -0
- package/dist/webui/assets/array-field-DVSC6nHP.js +1 -0
- package/dist/webui/assets/badge-DTZMtS0e.js +1 -0
- package/dist/webui/assets/check-Bu3ldi63.js +1 -0
- package/dist/webui/assets/chevron-down-CRy8M0kJ.js +1 -0
- package/dist/webui/assets/clients-CW8oEZoQ.js +1 -0
- package/dist/webui/assets/connect-YBNsnjWT.js +1 -0
- package/dist/webui/assets/debug-mz8-WYZj.js +1 -0
- package/dist/webui/assets/directories-BSK28RgR.js +1 -0
- package/dist/webui/assets/duration-field-C6xoSlJg.js +1 -0
- package/dist/webui/assets/general-lJJxZhH7.js +1 -0
- package/dist/webui/assets/health-CXbsVrie.js +1 -0
- package/dist/webui/assets/index-Bi48hI2z.js +54 -0
- package/dist/webui/assets/index-C-Ul7GNg.css +1 -0
- package/dist/webui/assets/index-C2cH1Gst.js +1 -0
- package/dist/webui/assets/index-Cc5bDmJr.js +1 -0
- package/dist/webui/assets/jobs-CxmNab9w.js +1 -0
- package/dist/webui/assets/library-vaj2W8sE.js +1 -0
- package/dist/webui/assets/loader-circle-M0gu1gZ-.js +1 -0
- package/dist/webui/assets/logs-Cu9RyKS0.js +1 -0
- package/dist/webui/assets/search-2R5sIdT8.js +1 -0
- package/dist/webui/assets/select-field-BCqNLDrJ.js +1 -0
- package/dist/webui/assets/select-zHgqMzLj.js +1 -0
- package/dist/webui/assets/settings-CMYjpTbZ.js +1 -0
- package/dist/webui/assets/submit-button-BtcnyggQ.js +1 -0
- package/dist/webui/assets/switch-G0W3uJVN.js +1 -0
- package/dist/webui/assets/switch-field-IBd9ORNq.js +1 -0
- package/dist/webui/assets/table-DvgJU7Gh.js +1 -0
- package/dist/webui/assets/test-tube-BIwmoM45.js +1 -0
- package/dist/webui/assets/text-field-DruSbGhy.js +1 -0
- package/dist/webui/assets/time-BSMZjmyW.js +1 -0
- package/dist/webui/assets/trackers-D-OpAe63.js +7 -0
- package/dist/webui/assets/use-form-validation-context-BkAfWAh0.js +1 -0
- package/dist/webui/assets/use-settings-form-submit-CDRh-E9U.js +2 -0
- package/dist/webui/assets/useQuery-A4Hv_4uX.js +1 -0
- package/dist/webui/index.html +13 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.d.ts +261 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.d.ts.map +1 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.js +53 -0
- package/node_modules/@cross-seed/shared/dist/configSchema.js.map +1 -0
- package/node_modules/@cross-seed/shared/dist/constants.d.ts +122 -0
- package/node_modules/@cross-seed/shared/dist/constants.d.ts.map +1 -0
- package/node_modules/@cross-seed/shared/dist/constants.js +127 -0
- package/node_modules/@cross-seed/shared/dist/constants.js.map +1 -0
- package/node_modules/@cross-seed/shared/dist/tsconfig.tsbuildinfo +1 -0
- package/node_modules/@cross-seed/shared/dist/utils.d.ts +6 -0
- package/node_modules/@cross-seed/shared/dist/utils.d.ts.map +1 -0
- package/node_modules/@cross-seed/shared/dist/utils.js +9 -0
- package/node_modules/@cross-seed/shared/dist/utils.js.map +1 -0
- package/node_modules/@cross-seed/shared/package.json +22 -0
- package/package.json +35 -11
- package/dist/Result.js +0 -64
- package/dist/Result.js.map +0 -1
- package/dist/action.js +0 -693
- package/dist/action.js.map +0 -1
- package/dist/arr.js +0 -199
- package/dist/arr.js.map +0 -1
- package/dist/auth.js +0 -25
- package/dist/auth.js.map +0 -1
- package/dist/clients/Deluge.js +0 -698
- package/dist/clients/Deluge.js.map +0 -1
- package/dist/clients/QBittorrent.js +0 -785
- package/dist/clients/QBittorrent.js.map +0 -1
- package/dist/clients/RTorrent.js +0 -654
- package/dist/clients/RTorrent.js.map +0 -1
- package/dist/clients/TorrentClient.js +0 -272
- package/dist/clients/TorrentClient.js.map +0 -1
- package/dist/clients/Transmission.js +0 -404
- package/dist/clients/Transmission.js.map +0 -1
- package/dist/cmd.js +0 -196
- package/dist/cmd.js.map +0 -1
- package/dist/config.template.cjs +0 -353
- package/dist/config.template.cjs.map +0 -1
- package/dist/configSchema.js +0 -667
- package/dist/configSchema.js.map +0 -1
- package/dist/configuration.js +0 -82
- package/dist/configuration.js.map +0 -1
- package/dist/constants.js +0 -281
- package/dist/constants.js.map +0 -1
- package/dist/dataFiles.js +0 -208
- package/dist/dataFiles.js.map +0 -1
- package/dist/db.js +0 -216
- package/dist/db.js.map +0 -1
- package/dist/decide.js +0 -553
- package/dist/decide.js.map +0 -1
- package/dist/diff.js +0 -24
- package/dist/diff.js.map +0 -1
- package/dist/errors.js +0 -16
- package/dist/errors.js.map +0 -1
- package/dist/indexers.js +0 -180
- package/dist/indexers.js.map +0 -1
- package/dist/inject.js +0 -594
- package/dist/inject.js.map +0 -1
- package/dist/jobs.js +0 -146
- package/dist/jobs.js.map +0 -1
- package/dist/logger.js +0 -143
- package/dist/logger.js.map +0 -1
- package/dist/migrations/00-initialSchema.js +0 -30
- package/dist/migrations/00-initialSchema.js.map +0 -1
- package/dist/migrations/01-jobs.js +0 -12
- package/dist/migrations/01-jobs.js.map +0 -1
- package/dist/migrations/02-timestamps.js +0 -21
- package/dist/migrations/02-timestamps.js.map +0 -1
- package/dist/migrations/03-rateLimits.js +0 -14
- package/dist/migrations/03-rateLimits.js.map +0 -1
- package/dist/migrations/04-auth.js +0 -13
- package/dist/migrations/04-auth.js.map +0 -1
- package/dist/migrations/05-caps.js +0 -16
- package/dist/migrations/05-caps.js.map +0 -1
- package/dist/migrations/06-uniqueDecisions.js +0 -29
- package/dist/migrations/06-uniqueDecisions.js.map +0 -1
- package/dist/migrations/07-limits.js +0 -12
- package/dist/migrations/07-limits.js.map +0 -1
- package/dist/migrations/08-rss.js +0 -15
- package/dist/migrations/08-rss.js.map +0 -1
- package/dist/migrations/09-clientAndDataSearchees.js +0 -34
- package/dist/migrations/09-clientAndDataSearchees.js.map +0 -1
- package/dist/migrations/10-indexerNameAudioBookCaps.js +0 -18
- package/dist/migrations/10-indexerNameAudioBookCaps.js.map +0 -1
- package/dist/migrations/11-trackers.js +0 -38
- package/dist/migrations/11-trackers.js.map +0 -1
- package/dist/migrations/migrations.js +0 -31
- package/dist/migrations/migrations.js.map +0 -1
- package/dist/parseTorrent.js +0 -128
- package/dist/parseTorrent.js.map +0 -1
- package/dist/pipeline.js +0 -527
- package/dist/pipeline.js.map +0 -1
- package/dist/preFilter.js +0 -250
- package/dist/preFilter.js.map +0 -1
- package/dist/pushNotifier.js +0 -137
- package/dist/pushNotifier.js.map +0 -1
- package/dist/runtimeConfig.js +0 -11
- package/dist/runtimeConfig.js.map +0 -1
- package/dist/searchee.js +0 -658
- package/dist/searchee.js.map +0 -1
- package/dist/server.js +0 -456
- package/dist/server.js.map +0 -1
- package/dist/startup.js +0 -203
- package/dist/startup.js.map +0 -1
- package/dist/torrent.js +0 -637
- package/dist/torrent.js.map +0 -1
- package/dist/torznab.js +0 -786
- package/dist/torznab.js.map +0 -1
- package/dist/utils.js +0 -637
- 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
|