cross-seed 7.0.0-1 → 7.0.0-2

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 (229) hide show
  1. package/dist/Result.d.ts +27 -0
  2. package/dist/Result.js +64 -0
  3. package/dist/Result.js.map +1 -0
  4. package/dist/action.d.ts +34 -0
  5. package/dist/action.js +694 -0
  6. package/dist/action.js.map +1 -0
  7. package/dist/arr.d.ts +31 -0
  8. package/dist/arr.js +267 -0
  9. package/dist/arr.js.map +1 -0
  10. package/dist/auth.d.ts +3 -0
  11. package/dist/auth.js +28 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/clients/Deluge.d.ts +153 -0
  14. package/dist/clients/Deluge.js +698 -0
  15. package/dist/clients/Deluge.js.map +1 -0
  16. package/dist/clients/QBittorrent.d.ts +218 -0
  17. package/dist/clients/QBittorrent.js +785 -0
  18. package/dist/clients/QBittorrent.js.map +1 -0
  19. package/dist/clients/RTorrent.d.ts +43 -0
  20. package/dist/clients/RTorrent.js +657 -0
  21. package/dist/clients/RTorrent.js.map +1 -0
  22. package/dist/clients/TorrentClient.d.ts +108 -0
  23. package/dist/clients/TorrentClient.js +341 -0
  24. package/dist/clients/TorrentClient.js.map +1 -0
  25. package/dist/clients/Transmission.d.ts +43 -0
  26. package/dist/clients/Transmission.js +404 -0
  27. package/dist/clients/Transmission.js.map +1 -0
  28. package/dist/cmd.d.ts +2 -0
  29. package/dist/cmd.js +128 -0
  30. package/dist/cmd.js.map +1 -0
  31. package/dist/configSchema.d.ts +1 -0
  32. package/dist/configSchema.js +2 -0
  33. package/dist/configSchema.js.map +1 -0
  34. package/dist/configuration.d.ts +63 -0
  35. package/dist/configuration.js +321 -0
  36. package/dist/configuration.js.map +1 -0
  37. package/dist/constants.d.ts +108 -0
  38. package/dist/constants.js +251 -0
  39. package/dist/constants.js.map +1 -0
  40. package/dist/dataFiles.d.ts +8 -0
  41. package/dist/dataFiles.js +223 -0
  42. package/dist/dataFiles.js.map +1 -0
  43. package/dist/db.d.ts +3 -0
  44. package/dist/db.js +216 -0
  45. package/dist/db.js.map +1 -0
  46. package/dist/dbConfig.d.ts +4 -0
  47. package/dist/dbConfig.js +67 -0
  48. package/dist/dbConfig.js.map +1 -0
  49. package/dist/decide.d.ts +25 -0
  50. package/dist/decide.js +553 -0
  51. package/dist/decide.js.map +1 -0
  52. package/dist/diff.d.ts +1 -0
  53. package/dist/diff.js +24 -0
  54. package/dist/diff.js.map +1 -0
  55. package/dist/errors.d.ts +3 -0
  56. package/dist/errors.js +7 -0
  57. package/dist/errors.js.map +1 -0
  58. package/dist/indexers.d.ts +105 -0
  59. package/dist/indexers.js +248 -0
  60. package/dist/indexers.js.map +1 -0
  61. package/dist/inject.d.ts +2 -0
  62. package/dist/inject.js +594 -0
  63. package/dist/inject.js.map +1 -0
  64. package/dist/jobs.d.ts +29 -0
  65. package/dist/jobs.js +151 -0
  66. package/dist/jobs.js.map +1 -0
  67. package/dist/logger.d.ts +29 -0
  68. package/dist/logger.js +157 -0
  69. package/dist/logger.js.map +1 -0
  70. package/dist/migrations/00-initialSchema.d.ts +9 -0
  71. package/dist/migrations/00-initialSchema.js +30 -0
  72. package/dist/migrations/00-initialSchema.js.map +1 -0
  73. package/dist/migrations/01-jobs.d.ts +9 -0
  74. package/dist/migrations/01-jobs.js +12 -0
  75. package/dist/migrations/01-jobs.js.map +1 -0
  76. package/dist/migrations/02-timestamps.d.ts +9 -0
  77. package/dist/migrations/02-timestamps.js +21 -0
  78. package/dist/migrations/02-timestamps.js.map +1 -0
  79. package/dist/migrations/03-rateLimits.d.ts +9 -0
  80. package/dist/migrations/03-rateLimits.js +14 -0
  81. package/dist/migrations/03-rateLimits.js.map +1 -0
  82. package/dist/migrations/04-auth.d.ts +9 -0
  83. package/dist/migrations/04-auth.js +13 -0
  84. package/dist/migrations/04-auth.js.map +1 -0
  85. package/dist/migrations/05-caps.d.ts +9 -0
  86. package/dist/migrations/05-caps.js +16 -0
  87. package/dist/migrations/05-caps.js.map +1 -0
  88. package/dist/migrations/06-uniqueDecisions.d.ts +9 -0
  89. package/dist/migrations/06-uniqueDecisions.js +29 -0
  90. package/dist/migrations/06-uniqueDecisions.js.map +1 -0
  91. package/dist/migrations/07-limits.d.ts +9 -0
  92. package/dist/migrations/07-limits.js +12 -0
  93. package/dist/migrations/07-limits.js.map +1 -0
  94. package/dist/migrations/08-rss.d.ts +9 -0
  95. package/dist/migrations/08-rss.js +15 -0
  96. package/dist/migrations/08-rss.js.map +1 -0
  97. package/dist/migrations/09-clientAndDataSearchees.d.ts +9 -0
  98. package/dist/migrations/09-clientAndDataSearchees.js +34 -0
  99. package/dist/migrations/09-clientAndDataSearchees.js.map +1 -0
  100. package/dist/migrations/10-indexerNameAudioBookCaps.d.ts +9 -0
  101. package/dist/migrations/10-indexerNameAudioBookCaps.js +18 -0
  102. package/dist/migrations/10-indexerNameAudioBookCaps.js.map +1 -0
  103. package/dist/migrations/11-trackers.d.ts +9 -0
  104. package/dist/migrations/11-trackers.js +38 -0
  105. package/dist/migrations/11-trackers.js.map +1 -0
  106. package/dist/migrations/12-user-auth.d.ts +9 -0
  107. package/dist/migrations/12-user-auth.js +22 -0
  108. package/dist/migrations/12-user-auth.js.map +1 -0
  109. package/dist/migrations/13-settings.d.ts +9 -0
  110. package/dist/migrations/13-settings.js +23 -0
  111. package/dist/migrations/13-settings.js.map +1 -0
  112. package/dist/migrations/14-indexer-enabled-flag.d.ts +9 -0
  113. package/dist/migrations/14-indexer-enabled-flag.js +12 -0
  114. package/dist/migrations/14-indexer-enabled-flag.js.map +1 -0
  115. package/dist/migrations/15-remove-url-unique-constraint.d.ts +9 -0
  116. package/dist/migrations/15-remove-url-unique-constraint.js +14 -0
  117. package/dist/migrations/15-remove-url-unique-constraint.js.map +1 -0
  118. package/dist/migrations/16-prune-inactive-indexers.d.ts +9 -0
  119. package/dist/migrations/16-prune-inactive-indexers.js +17 -0
  120. package/dist/migrations/16-prune-inactive-indexers.js.map +1 -0
  121. package/dist/migrations/migrations.d.ts +13 -0
  122. package/dist/migrations/migrations.js +41 -0
  123. package/dist/migrations/migrations.js.map +1 -0
  124. package/dist/parseTorrent.d.ts +53 -0
  125. package/dist/parseTorrent.js +128 -0
  126. package/dist/parseTorrent.js.map +1 -0
  127. package/dist/pipeline.d.ts +41 -0
  128. package/dist/pipeline.js +574 -0
  129. package/dist/pipeline.js.map +1 -0
  130. package/dist/preFilter.d.ts +25 -0
  131. package/dist/preFilter.js +250 -0
  132. package/dist/preFilter.js.map +1 -0
  133. package/dist/problems/linking.d.ts +2 -0
  134. package/dist/problems/linking.js +80 -0
  135. package/dist/problems/linking.js.map +1 -0
  136. package/dist/problems/path.d.ts +22 -0
  137. package/dist/problems/path.js +96 -0
  138. package/dist/problems/path.js.map +1 -0
  139. package/dist/problems.d.ts +13 -0
  140. package/dist/problems.js +48 -0
  141. package/dist/problems.js.map +1 -0
  142. package/dist/pushNotifier.d.ts +19 -0
  143. package/dist/pushNotifier.js +137 -0
  144. package/dist/pushNotifier.js.map +1 -0
  145. package/dist/routes/baseApi.d.ts +2 -0
  146. package/dist/routes/baseApi.js +354 -0
  147. package/dist/routes/baseApi.js.map +1 -0
  148. package/dist/routes/indexerApi.d.ts +6 -0
  149. package/dist/routes/indexerApi.js +165 -0
  150. package/dist/routes/indexerApi.js.map +1 -0
  151. package/dist/routes/staticFrontendPlugin.d.ts +4 -0
  152. package/dist/routes/staticFrontendPlugin.js +61 -0
  153. package/dist/routes/staticFrontendPlugin.js.map +1 -0
  154. package/dist/runtimeConfig.d.ts +6 -0
  155. package/dist/runtimeConfig.js +27 -0
  156. package/dist/runtimeConfig.js.map +1 -0
  157. package/dist/searchee.d.ts +108 -0
  158. package/dist/searchee.js +689 -0
  159. package/dist/searchee.js.map +1 -0
  160. package/dist/server.d.ts +4 -0
  161. package/dist/server.js +65 -0
  162. package/dist/server.js.map +1 -0
  163. package/dist/services/indexerService.d.ts +96 -0
  164. package/dist/services/indexerService.js +287 -0
  165. package/dist/services/indexerService.js.map +1 -0
  166. package/dist/sessionCookies.d.ts +5 -0
  167. package/dist/sessionCookies.js +27 -0
  168. package/dist/sessionCookies.js.map +1 -0
  169. package/dist/startup.d.ts +25 -0
  170. package/dist/startup.js +157 -0
  171. package/dist/startup.js.map +1 -0
  172. package/dist/torrent.d.ts +69 -0
  173. package/dist/torrent.js +641 -0
  174. package/dist/torrent.js.map +1 -0
  175. package/dist/torznab.d.ts +60 -0
  176. package/dist/torznab.js +711 -0
  177. package/dist/torznab.js.map +1 -0
  178. package/dist/trpc/fastifyAdapter.d.ts +2 -0
  179. package/dist/trpc/fastifyAdapter.js +9 -0
  180. package/dist/trpc/fastifyAdapter.js.map +1 -0
  181. package/dist/trpc/index.d.ts +49 -0
  182. package/dist/trpc/index.js +53 -0
  183. package/dist/trpc/index.js.map +1 -0
  184. package/dist/trpc/routers/auth.d.ts +43 -0
  185. package/dist/trpc/routers/auth.js +116 -0
  186. package/dist/trpc/routers/auth.js.map +1 -0
  187. package/dist/trpc/routers/clients.d.ts +21 -0
  188. package/dist/trpc/routers/clients.js +65 -0
  189. package/dist/trpc/routers/clients.js.map +1 -0
  190. package/dist/trpc/routers/health.d.ts +14 -0
  191. package/dist/trpc/routers/health.js +20 -0
  192. package/dist/trpc/routers/health.js.map +1 -0
  193. package/dist/trpc/routers/index.d.ts +391 -0
  194. package/dist/trpc/routers/index.js +23 -0
  195. package/dist/trpc/routers/index.js.map +1 -0
  196. package/dist/trpc/routers/indexers.d.ts +75 -0
  197. package/dist/trpc/routers/indexers.js +79 -0
  198. package/dist/trpc/routers/indexers.js.map +1 -0
  199. package/dist/trpc/routers/jobs.d.ts +33 -0
  200. package/dist/trpc/routers/jobs.js +84 -0
  201. package/dist/trpc/routers/jobs.js.map +1 -0
  202. package/dist/trpc/routers/logs.d.ts +27 -0
  203. package/dist/trpc/routers/logs.js +91 -0
  204. package/dist/trpc/routers/logs.js.map +1 -0
  205. package/dist/trpc/routers/searchees.d.ts +51 -0
  206. package/dist/trpc/routers/searchees.js +156 -0
  207. package/dist/trpc/routers/searchees.js.map +1 -0
  208. package/dist/trpc/routers/settings.d.ts +83 -0
  209. package/dist/trpc/routers/settings.js +92 -0
  210. package/dist/trpc/routers/settings.js.map +1 -0
  211. package/dist/trpc/routers/stats.d.ts +42 -0
  212. package/dist/trpc/routers/stats.js +102 -0
  213. package/dist/trpc/routers/stats.js.map +1 -0
  214. package/dist/userAuth.d.ts +21 -0
  215. package/dist/userAuth.js +86 -0
  216. package/dist/userAuth.js.map +1 -0
  217. package/dist/utils/authUtils.d.ts +10 -0
  218. package/dist/utils/authUtils.js +24 -0
  219. package/dist/utils/authUtils.js.map +1 -0
  220. package/dist/utils/logWatcher.d.ts +28 -0
  221. package/dist/utils/logWatcher.js +218 -0
  222. package/dist/utils/logWatcher.js.map +1 -0
  223. package/dist/utils/object.d.ts +1 -0
  224. package/dist/utils/object.js +4 -0
  225. package/dist/utils/object.js.map +1 -0
  226. package/dist/utils.d.ts +175 -0
  227. package/dist/utils.js +660 -0
  228. package/dist/utils.js.map +1 -0
  229. package/package.json +2 -2
package/dist/utils.js ADDED
@@ -0,0 +1,660 @@
1
+ import chalk from "chalk";
2
+ import { distance } from "fastest-levenshtein";
3
+ import { access, constants, readdir, stat, unlink, writeFile, } from "fs/promises";
4
+ import path from "path";
5
+ import { ALL_EXTENSIONS, ALL_PARENTHESES_REGEX, ALL_SPACES_REGEX, ALL_SQUARE_BRACKETS_REGEX, ANIME_REGEX, EP_REGEX, JSON_VALUES_REGEX, LEVENSHTEIN_DIVISOR, MIN_VIDEO_QUERY_LENGTH, MOVIE_REGEX, NON_UNICODE_ALPHANUM_REGEX, RELEASE_GROUP_REGEX, REPACK_PROPER_REGEX, RESOLUTION_REGEX, SCENE_TITLE_REGEX, SEASON_REGEX, sourceRegexRemove, YEARS_REGEX, } from "./constants.js";
6
+ import { logger } from "./logger.js";
7
+ import { resultOf, resultOfErr } from "./Result.js";
8
+ import { getAllTitles } from "./searchee.js";
9
+ export function isTruthy(value) {
10
+ return Boolean(value);
11
+ }
12
+ // ==================================== OS ====================================
13
+ export async function exists(srcPath) {
14
+ try {
15
+ await access(srcPath);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export async function notExists(srcPath) {
23
+ try {
24
+ await access(srcPath);
25
+ return false;
26
+ }
27
+ catch {
28
+ return true;
29
+ }
30
+ }
31
+ export async function verifyDir(srcDir, testSrcName, permissions) {
32
+ const logMissing = (message) => {
33
+ logger.error(`\tYour ${testSrcName} "${srcDir}" is not a valid directory on the filesystem: ${message}.`);
34
+ if (path.sep === "\\" &&
35
+ !srcDir.includes("\\") &&
36
+ !srcDir.includes("/")) {
37
+ logger.error("\tIt may not be formatted properly for Windows.\n" +
38
+ '\t\t\t\tMake sure to use "\\\\" or "/" for directory separators.');
39
+ }
40
+ };
41
+ const logPermissions = (message) => {
42
+ logger.error(`\tYour ${testSrcName} "${srcDir}" has invalid permissions: ${message}.`);
43
+ };
44
+ if (await notExists(srcDir)) {
45
+ logMissing("does not exist.");
46
+ return { ok: false, reason: "missing" };
47
+ }
48
+ let stats;
49
+ try {
50
+ stats = await stat(srcDir);
51
+ }
52
+ catch (error) {
53
+ const err = error;
54
+ if (err?.code === "ENOENT") {
55
+ logMissing(err.message ?? "does not exist.");
56
+ return { ok: false, reason: "missing", error };
57
+ }
58
+ logPermissions(err?.message ?? "stat failed.");
59
+ return { ok: false, reason: "unreadable", error };
60
+ }
61
+ if (!stats.isDirectory()) {
62
+ logger.error(`\tYour ${testSrcName} "${srcDir}" is not a directory on the filesystem.`);
63
+ return { ok: false, reason: "not-directory" };
64
+ }
65
+ if (permissions & constants.R_OK) {
66
+ try {
67
+ await readdir(srcDir);
68
+ }
69
+ catch (error) {
70
+ logger.debug(error);
71
+ logPermissions("no read permissions.");
72
+ return { ok: false, reason: "unreadable", error };
73
+ }
74
+ }
75
+ if (permissions & constants.W_OK) {
76
+ const tempFile = path.join(srcDir, testSrcName);
77
+ try {
78
+ await writeFile(tempFile, testSrcName);
79
+ if (await notExists(tempFile)) {
80
+ logPermissions("no write permissions - could not verify test file.");
81
+ return { ok: false, reason: "unwritable" };
82
+ }
83
+ await unlink(tempFile);
84
+ }
85
+ catch (error) {
86
+ logger.debug(error);
87
+ logPermissions("no write permissions.");
88
+ return { ok: false, reason: "unwritable", error };
89
+ }
90
+ }
91
+ return { ok: true, stats };
92
+ }
93
+ export function isChildPath(childPath, parentDirs) {
94
+ return parentDirs.some((parentDir) => {
95
+ const resolvedParent = path.resolve(parentDir);
96
+ const resolvedChild = path.resolve(childPath);
97
+ const relativePath = path.relative(resolvedParent, resolvedChild);
98
+ return (relativePath.length > 0 &&
99
+ !relativePath.startsWith("..") &&
100
+ !path.isAbsolute(relativePath));
101
+ });
102
+ }
103
+ export async function countDirEntriesRec(dirs, maxDataDepth) {
104
+ if (maxDataDepth === 0)
105
+ return 0;
106
+ let count = 0;
107
+ for (const dir of dirs) {
108
+ const newDirs = [];
109
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
110
+ count++;
111
+ if (entry.isDirectory())
112
+ newDirs.push(path.join(dir, entry.name));
113
+ }
114
+ count += await countDirEntriesRec(newDirs, maxDataDepth - 1);
115
+ }
116
+ return count;
117
+ }
118
+ // ================================ EXTENSIONS ================================
119
+ export function hasExt(files, exts) {
120
+ return files.some((f) => exts.includes(path.extname(f.name.toLowerCase())));
121
+ }
122
+ export function stripExtension(filename) {
123
+ for (const ext of ALL_EXTENSIONS) {
124
+ if (filename.endsWith(ext))
125
+ return path.basename(filename, ext);
126
+ }
127
+ return filename;
128
+ }
129
+ export function filesWithExt(files, exts) {
130
+ return files.filter((f) => exts.includes(path.extname(f.name.toLowerCase())));
131
+ }
132
+ export async function findAFileWithExt(dir, exts) {
133
+ try {
134
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
135
+ const fullPath = path.join(dir, entry.name);
136
+ if (entry.isFile() && exts.includes(path.extname(fullPath))) {
137
+ return fullPath;
138
+ }
139
+ if (entry.isDirectory()) {
140
+ const file = await findAFileWithExt(fullPath, exts);
141
+ if (file)
142
+ return file;
143
+ }
144
+ }
145
+ }
146
+ catch (e) {
147
+ logger.debug(e);
148
+ }
149
+ return null;
150
+ }
151
+ // =================================== TIME ===================================
152
+ export function nMsAgo(n) {
153
+ return Date.now() - n;
154
+ }
155
+ export function wait(n) {
156
+ return new Promise((resolve) => setTimeout(resolve, n));
157
+ }
158
+ /**
159
+ * Yield control to the event loop allowing all pending tasks to be processed
160
+ * regardless of the current phase.
161
+ * @param n Optional number of milliseconds to wait before yielding.
162
+ */
163
+ export async function yieldToEventLoop(n = 0) {
164
+ await wait(n);
165
+ return new Promise((resolve) => setImmediate(resolve));
166
+ }
167
+ export async function time(cb, times) {
168
+ const before = performance.now();
169
+ try {
170
+ return await cb();
171
+ }
172
+ finally {
173
+ times.push(performance.now() - before);
174
+ }
175
+ }
176
+ // ================================= LOGGING =================================
177
+ export function humanReadableDate(timestamp) {
178
+ return new Date(timestamp).toLocaleString("sv");
179
+ }
180
+ export function humanReadableSize(bytes, options) {
181
+ if (bytes === 0)
182
+ return "0 B";
183
+ const k = options?.binary ? 1024 : 1000;
184
+ const sizes = options?.binary
185
+ ? ["B", "KiB", "MiB", "GiB", "TiB"]
186
+ : ["B", "kB", "MB", "GB", "TB"];
187
+ // engineering notation: (coefficient) * 1000 ^ (exponent)
188
+ const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
189
+ const coefficient = bytes / Math.pow(k, exponent);
190
+ return `${parseFloat(coefficient.toFixed(2))} ${sizes[exponent]}`;
191
+ }
192
+ export function getLogString(searchee, color = chalk.reset) {
193
+ if (searchee.title === searchee.name) {
194
+ return searchee.infoHash || searchee.clientHost
195
+ ? `${color(searchee.title)} ${chalk.dim(`[${searchee.infoHash ? sanitizeInfoHash(searchee.infoHash) : ""}${searchee.clientHost ? `@${searchee.clientHost}` : ""}]`)}`
196
+ : searchee.path
197
+ ? color(searchee.path)
198
+ : color(searchee.title);
199
+ }
200
+ return searchee.infoHash || searchee.clientHost
201
+ ? `${color(searchee.title)} ${chalk.dim(`[${searchee.name} [${searchee.infoHash ? sanitizeInfoHash(searchee.infoHash) : ""}${searchee.clientHost ? `@${searchee.clientHost}` : ""}]]`)}`
202
+ : searchee.path
203
+ ? `${color(searchee.title)} ${chalk.dim(`[${searchee.path}]`)}`
204
+ : `${color(searchee.title)} ${chalk.dim(`[${searchee.name}]`)}`;
205
+ }
206
+ export function formatAsList(strings, options) {
207
+ if (options.sort)
208
+ strings.sort((a, b) => a.localeCompare(b));
209
+ return new Intl.ListFormat("en", {
210
+ style: options.style ?? "long",
211
+ type: options.type ?? "conjunction",
212
+ }).format(strings);
213
+ }
214
+ /**
215
+ * This cannot be done at the log level because of too many false positives.
216
+ * The caller will need to extract the infoHash from their specific syntax.
217
+ * @param infoHash The infoHash to sanitize
218
+ */
219
+ export function sanitizeInfoHash(infoHash) {
220
+ return `${infoHash.slice(0, 8)}...`;
221
+ }
222
+ // ================================== TITLES ==================================
223
+ export function areMediaTitlesSimilar(a, b) {
224
+ const matchA = a.match(EP_REGEX) ??
225
+ a.match(SEASON_REGEX) ??
226
+ a.match(MOVIE_REGEX) ??
227
+ a.match(ANIME_REGEX);
228
+ const matchB = b.match(EP_REGEX) ??
229
+ b.match(SEASON_REGEX) ??
230
+ b.match(MOVIE_REGEX) ??
231
+ b.match(ANIME_REGEX);
232
+ const titlesA = getAllTitles(matchA
233
+ ? [matchA.groups?.title, matchA.groups?.altTitle].filter(isTruthy)
234
+ : [a])
235
+ .map((title) => createKeyTitle(stripMetaFromName(title)))
236
+ .filter(isTruthy);
237
+ const titlesB = getAllTitles(matchB
238
+ ? [matchB.groups?.title, matchB.groups?.altTitle].filter(isTruthy)
239
+ : [b])
240
+ .map((title) => createKeyTitle(stripMetaFromName(title)))
241
+ .filter(isTruthy);
242
+ const maxDistanceA = Math.floor(titlesA.reduce((sum, title) => sum + title.length, 0) /
243
+ titlesA.length /
244
+ LEVENSHTEIN_DIVISOR);
245
+ const maxDistanceB = Math.floor(titlesB.reduce((sum, title) => sum + title.length, 0) /
246
+ titlesB.length /
247
+ LEVENSHTEIN_DIVISOR);
248
+ const maxDistance = Math.max(maxDistanceA, maxDistanceB);
249
+ return titlesA.some((titleA) => titlesB.some((titleB) => distance(titleA, titleB) <= maxDistance ||
250
+ titleA.includes(titleB) ||
251
+ titleB.includes(titleA)));
252
+ }
253
+ export function cleanseSeparators(str) {
254
+ return str
255
+ .replace(ALL_SQUARE_BRACKETS_REGEX, "") // bracketed text
256
+ .replace(/[._()[\]]/g, " ") // release delimiters (except '-')
257
+ .replace(ALL_SPACES_REGEX, " ") // normalize spaces
258
+ .replace(/^\s*-+|-+\s*$/g, "") // "trim()" hyphens
259
+ .trim();
260
+ }
261
+ export function cleanTitle(title) {
262
+ return cleanseSeparators(title).match(SCENE_TITLE_REGEX).groups.title;
263
+ }
264
+ export function reformatTitleForSearching(name) {
265
+ const seriesTitle = name.match(EP_REGEX)?.groups?.title ??
266
+ name.match(SEASON_REGEX)?.groups?.title;
267
+ if (seriesTitle) {
268
+ const title = cleanTitle(seriesTitle);
269
+ return title.length > 4
270
+ ? replaceLastOccurrence(title, YEARS_REGEX, "")
271
+ .replace(ALL_SPACES_REGEX, " ")
272
+ .trim()
273
+ : title;
274
+ }
275
+ return cleanTitle(name.match(MOVIE_REGEX)?.[0] ?? name);
276
+ }
277
+ export function createKeyTitle(title) {
278
+ const key = cleanTitle(title)
279
+ .replace(NON_UNICODE_ALPHANUM_REGEX, "")
280
+ .toLowerCase();
281
+ return key.length > 4
282
+ ? replaceLastOccurrence(key, YEARS_REGEX, "")
283
+ : key.length
284
+ ? key
285
+ : null;
286
+ }
287
+ export function isBadTitle(title) {
288
+ return ["season", "ep"].includes(title.toLowerCase());
289
+ }
290
+ /**
291
+ * Generates possible anime search queries from a given name.
292
+ * Only use if getMediaType returns anime as it's conditional on a few factors.
293
+ * @param stem The name without extension to generate queries from.
294
+ * @returns An array of possible search queries.
295
+ */
296
+ export function getAnimeQueries(stem) {
297
+ const animeQueries = [];
298
+ const { title, altTitle, release } = stem.match(ANIME_REGEX)?.groups ?? {};
299
+ if (title) {
300
+ const strippedTitle = cleanTitle(title);
301
+ animeQueries.push(`${strippedTitle.length ? strippedTitle : title} ${release}`);
302
+ }
303
+ if (altTitle) {
304
+ if (isBadTitle(altTitle))
305
+ return animeQueries;
306
+ const strippedAltTitle = cleanTitle(altTitle);
307
+ animeQueries.push(`${strippedAltTitle.length ? strippedAltTitle : altTitle} ${release}`);
308
+ }
309
+ return animeQueries;
310
+ }
311
+ /**
312
+ * Generates possible video search queries from a given name.
313
+ * Only use if getMediaType returns video as it's conditional on a few factors.
314
+ * @param stem The name without extension to generate queries from.
315
+ * @returns An array of possible search queries.
316
+ */
317
+ export function getVideoQueries(stem) {
318
+ // Anime that fails MediaType.ANIME often has `[group] Title (Extra Info)`
319
+ const noParentheses = cleanTitle(stripMetaFromName(stem
320
+ .replace(ALL_PARENTHESES_REGEX, "")
321
+ .replace(ALL_SPACES_REGEX, " ")
322
+ .trim()));
323
+ if (noParentheses.length >= MIN_VIDEO_QUERY_LENGTH)
324
+ return [noParentheses];
325
+ let videoQuery = cleanTitle(stripMetaFromName(stem));
326
+ if (videoQuery.length)
327
+ return [videoQuery];
328
+ videoQuery = stripMetaFromName(stem);
329
+ if (videoQuery.length)
330
+ return [videoQuery];
331
+ return [stem];
332
+ }
333
+ export function stripMetaFromName(name) {
334
+ return sourceRegexRemove(stripExtension(name)
335
+ .match(SCENE_TITLE_REGEX)
336
+ .groups.title.replace(RELEASE_GROUP_REGEX, "")
337
+ .replace(/\s*-\s*$/, "")
338
+ .replace(RESOLUTION_REGEX, "")
339
+ .replace(REPACK_PROPER_REGEX, ""));
340
+ }
341
+ // =================================== URLS ===================================
342
+ export function sanitizeUrl(url) {
343
+ if (typeof url === "string") {
344
+ url = new URL(url);
345
+ }
346
+ return url.origin + url.pathname;
347
+ }
348
+ export function getApikey(url) {
349
+ return new URL(url).searchParams.get("apikey");
350
+ }
351
+ export function extractCredentialsFromUrl(url, basePath) {
352
+ try {
353
+ const { origin, pathname, username, password } = new URL(url);
354
+ return resultOf({
355
+ username: decodeURIComponent(username),
356
+ password: decodeURIComponent(password),
357
+ href: basePath
358
+ ? origin + path.posix.join(pathname, basePath)
359
+ : pathname === "/"
360
+ ? origin
361
+ : origin + pathname,
362
+ });
363
+ }
364
+ catch (e) {
365
+ return resultOfErr("invalid URL");
366
+ }
367
+ }
368
+ // ================================ FUNCTIONAL ================================
369
+ export const tap = (fn) => (value) => {
370
+ fn(value);
371
+ return value;
372
+ };
373
+ export function fallback(...args) {
374
+ for (const arg of args) {
375
+ if (arg !== undefined)
376
+ return arg;
377
+ }
378
+ return undefined;
379
+ }
380
+ export function findFallback(arr, items, cb) {
381
+ for (const item of items) {
382
+ const found = arr.find((e) => cb(e, item));
383
+ if (found)
384
+ return found;
385
+ }
386
+ return undefined;
387
+ }
388
+ export async function inBatches(items, cb, options = { batchSize: 100 }) {
389
+ for (let i = 0; i < items.length; i += options.batchSize) {
390
+ await cb(items.slice(i, i + options.batchSize));
391
+ }
392
+ }
393
+ export async function fromBatches(items, cb, options = { batchSize: 100 }) {
394
+ const results = [];
395
+ for (let i = 0; i < items.length; i += options.batchSize) {
396
+ results.push(...(await cb(items.slice(i, i + options.batchSize))));
397
+ }
398
+ return results;
399
+ }
400
+ /**
401
+ * Makes comparators for `Array.prototype.sort`.
402
+ * Second getter will be used if the first is a tie, etc.
403
+ * Booleans are treated as 0 and 1,
404
+ * Ascending by default, use - or ! for descending.
405
+ * @param getters
406
+ */
407
+ export function comparing(...getters) {
408
+ return function compare(a, b) {
409
+ for (const getter of getters) {
410
+ const x = getter(a);
411
+ const y = getter(b);
412
+ if (x < y) {
413
+ return -1;
414
+ }
415
+ else if (x > y) {
416
+ return 1;
417
+ }
418
+ }
419
+ return 0;
420
+ };
421
+ }
422
+ // ================================== ASYNC ==================================
423
+ export async function filterAsync(arr, predicate) {
424
+ const results = await mapAsync(arr, predicate);
425
+ return arr.filter((_, index) => results[index]);
426
+ }
427
+ /**
428
+ * Filters an array asynchronously in batches, yielding to the event loop
429
+ * between batches to avoid blocking the event loop for too long.
430
+ * @param arr The array to filter.
431
+ * @param predicate The asynchronous predicate function to test each element.
432
+ * @param options.batchSize The size of each batch to process.
433
+ * @returns A promise that resolves to an array of elements that satisfy the predicate.
434
+ */
435
+ export async function filterAsyncYield(arr, predicate, options = { batchSize: 1000 }) {
436
+ const results = await fromBatches(arr, async (batch) => {
437
+ await yieldToEventLoop();
438
+ return Promise.all(batch.map(predicate));
439
+ }, options);
440
+ return arr.filter((_, index) => results[index]);
441
+ }
442
+ export async function mapAsync(arr, cb) {
443
+ return fromBatches(arr, async (batch) => Promise.all(batch.map(cb)), {
444
+ batchSize: 10000,
445
+ });
446
+ }
447
+ export async function flatMapAsync(arr, cb) {
448
+ return (await mapAsync(arr, cb)).flat();
449
+ }
450
+ export async function reduceAsync(arr, cb, initialValue) {
451
+ let accumulator = initialValue;
452
+ for (let index = 0; index < arr.length; index++) {
453
+ accumulator = await cb(accumulator, arr[index], index, arr);
454
+ }
455
+ return accumulator;
456
+ }
457
+ export async function findAsync(it, cb) {
458
+ for (const item of it) {
459
+ if (await cb(item))
460
+ return item;
461
+ }
462
+ return undefined;
463
+ }
464
+ export async function someAsync(it, cb) {
465
+ for (const item of it) {
466
+ if (await cb(item))
467
+ return true;
468
+ }
469
+ return false;
470
+ }
471
+ export async function everyAsync(it, cb) {
472
+ for (const item of it) {
473
+ if (!(await cb(item)))
474
+ return false;
475
+ }
476
+ return true;
477
+ }
478
+ /**
479
+ * Given multiple async iterables, this function will merge/interleave
480
+ * them all into one iterable, yielding on a first-come, first-serve basis.
481
+ * https://stackoverflow.com/questions/50585456/how-can-i-interleave-merge-async-iterables
482
+ */
483
+ export async function* combineAsyncIterables(asyncIterables) {
484
+ const asyncIterators = Array.from(asyncIterables, (o) => o[Symbol.asyncIterator]());
485
+ let unfinishedIterators = asyncIterators.length;
486
+ const alwaysPending = new Promise(() => { });
487
+ const getNext = (asyncIterator, index) => asyncIterator.next().then((result) => ({ index, result }));
488
+ const nextPromises = asyncIterators.map(getNext);
489
+ try {
490
+ while (unfinishedIterators) {
491
+ const { index, result } = await Promise.race(nextPromises);
492
+ if (result.done) {
493
+ nextPromises[index] = alwaysPending;
494
+ unfinishedIterators--;
495
+ }
496
+ else {
497
+ nextPromises[index] = getNext(asyncIterators[index], index);
498
+ yield result.value;
499
+ }
500
+ }
501
+ }
502
+ finally {
503
+ // cancel unfinished iterators if one throws
504
+ for (const [index, iterator] of asyncIterators.entries()) {
505
+ if (nextPromises[index] !== alwaysPending &&
506
+ iterator.return != null) {
507
+ // no await here - see https://github.com/tc39/proposal-async-iteration/issues/126
508
+ void iterator.return();
509
+ }
510
+ }
511
+ }
512
+ return;
513
+ }
514
+ // ================================= STRINGS =================================
515
+ export function capitalizeFirstLetter(string) {
516
+ return string.charAt(0).toUpperCase() + string.slice(1);
517
+ }
518
+ /**
519
+ * Replaces the last occurrence of a GLOBAL regex match in a string
520
+ * @param str The string to replace the last occurrence in
521
+ * @param globalRegExp The regex to match (must be global)
522
+ * @param newStr The string to replace the last occurrence with
523
+ */
524
+ export function replaceLastOccurrence(str, globalRegExp, newStr) {
525
+ const matches = Array.from(str.matchAll(globalRegExp));
526
+ if (matches.length === 0)
527
+ return str;
528
+ const lastMatch = matches[matches.length - 1];
529
+ const lastMatchIndex = lastMatch.index;
530
+ const lastMatchStr = lastMatch[0];
531
+ return (str.slice(0, lastMatchIndex) +
532
+ newStr +
533
+ str.slice(lastMatchIndex + lastMatchStr.length));
534
+ }
535
+ export function escapeUnescapedQuotesInJsonValues(jsonStr) {
536
+ return jsonStr.replace(JSON_VALUES_REGEX, (match, _p1, _offset, _str, groups) => {
537
+ const escapedValue = groups.value.replace(/(?<!\\)"/g, '\\"');
538
+ return match.replace(groups.value, escapedValue);
539
+ });
540
+ }
541
+ export function extractInt(str) {
542
+ return parseInt(str.match(/\d+/)[0]);
543
+ }
544
+ export function getPathParts(pathStr, dirnameFunc = path.dirname) {
545
+ const parts = [];
546
+ let parent = pathStr;
547
+ while (parent !== ".") {
548
+ parts.unshift(path.basename(parent));
549
+ const newParent = dirnameFunc(parent);
550
+ if (newParent === parent) {
551
+ parts.shift();
552
+ break;
553
+ }
554
+ parent = newParent;
555
+ }
556
+ return parts;
557
+ }
558
+ // ================================== MUTEX ==================================
559
+ export var Mutex;
560
+ (function (Mutex) {
561
+ Mutex["INDEX_TORRENTS_AND_DATA_DIRS"] = "INDEX_TORRENTS_AND_DATA_DIRS";
562
+ Mutex["CHECK_JOBS"] = "CHECK_JOBS";
563
+ Mutex["CREATE_ALL_SEARCHEES"] = "CREATE_ALL_SEARCHEES";
564
+ Mutex["GUID_INFO_HASH_MAP"] = "GUID_INFO_HASH_MAP";
565
+ Mutex["CLIENT_INJECTION"] = "CLIENT_INJECTION";
566
+ })(Mutex || (Mutex = {}));
567
+ const mutexes = new Map();
568
+ /**
569
+ * Executes a callback function within a mutex for the given name.
570
+ * @param name The name of the mutex to create/use.
571
+ * @param options.useQueue If false, concurrent calls will share the pending result.
572
+ * @param cb The callback to execute.
573
+ * @returns The result of the callback.
574
+ */
575
+ export async function withMutex(name, options, cb) {
576
+ const existingMutex = mutexes.get(name);
577
+ if (existingMutex) {
578
+ if (options.useQueue) {
579
+ while (mutexes.has(name))
580
+ await mutexes.get(name);
581
+ }
582
+ else {
583
+ return existingMutex;
584
+ }
585
+ }
586
+ const mutex = (async () => {
587
+ try {
588
+ return await cb();
589
+ }
590
+ finally {
591
+ mutexes.delete(name);
592
+ }
593
+ })();
594
+ mutexes.set(name, mutex);
595
+ return mutex;
596
+ }
597
+ /**
598
+ * An async safe semaphore implementation that preserves FIFO order.
599
+ * It uses an id for release to allow multiple releases (e.g try/finally with early releases).
600
+ * @param options.permits The number of concurrent permits.
601
+ * @param options.lifetimeMs Maximum lifetime of an acquire before automatic release.
602
+ */
603
+ export class AsyncSemaphore {
604
+ permits;
605
+ lifetimeMs;
606
+ acquired = new Set();
607
+ timers = new Map();
608
+ waiting = [];
609
+ counter = 0;
610
+ getNextId = () => {
611
+ if (this.counter === Number.MAX_SAFE_INTEGER)
612
+ this.counter = 0;
613
+ return ++this.counter;
614
+ };
615
+ constructor(options) {
616
+ this.permits = Math.floor(options.permits);
617
+ if (this.permits <= 0) {
618
+ throw new Error("Permits count must be positive");
619
+ }
620
+ this.lifetimeMs = options.lifetimeMs;
621
+ if (this.lifetimeMs && this.lifetimeMs <= 0) {
622
+ throw new Error("Lifetime must be positive");
623
+ }
624
+ }
625
+ acquire() {
626
+ return new Promise((resolve) => {
627
+ if (this.permits > 0) {
628
+ this.permits--;
629
+ const id = this.getNextId();
630
+ this.acquired.add(id);
631
+ if (this.lifetimeMs) {
632
+ this.timers.set(id, setTimeout(() => this.release(id), this.lifetimeMs));
633
+ }
634
+ resolve(id);
635
+ }
636
+ else {
637
+ this.waiting.push(resolve);
638
+ }
639
+ });
640
+ }
641
+ release(id) {
642
+ if (!this.acquired.has(id))
643
+ return;
644
+ this.acquired.delete(id);
645
+ clearTimeout(this.timers.get(id));
646
+ this.timers.delete(id);
647
+ if (this.waiting.length > 0) {
648
+ const newId = this.getNextId();
649
+ this.acquired.add(newId);
650
+ if (this.lifetimeMs) {
651
+ this.timers.set(newId, setTimeout(() => this.release(newId), this.lifetimeMs));
652
+ }
653
+ this.waiting.shift()(newId);
654
+ }
655
+ else {
656
+ this.permits++;
657
+ }
658
+ }
659
+ }
660
+ //# sourceMappingURL=utils.js.map