cross-seed 7.0.0-1 → 7.0.0-11

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