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,665 @@
1
+ import { distance } from "fastest-levenshtein";
2
+ import bencode from "bencode";
3
+ import { readdir, readFile, stat } from "fs/promises";
4
+ import { extname, join, resolve } from "path";
5
+ import { inspect } from "util";
6
+ import { getClients, } from "./clients/TorrentClient.js";
7
+ import { isChildPath } from "./utils.js";
8
+ import { ALL_PARENTHESES_REGEX, ALL_SPACES_REGEX, ALL_SQUARE_BRACKETS_REGEX, LEVENSHTEIN_DIVISOR, MAX_PATH_BYTES, MediaType, MIN_VIDEO_QUERY_LENGTH, SAVED_TORRENTS_INFO_REGEX, USER_AGENT, } from "./constants.js";
9
+ import { getDataByFuzzyName, indexDataDirs, shouldIgnorePathHeuristically, } from "./dataFiles.js";
10
+ import { db } from "./db.js";
11
+ import { Label, logger, logOnce } from "./logger.js";
12
+ import { Metafile, updateMetafileMetadata } from "./parseTorrent.js";
13
+ import { isOk, resultOf, resultOfErr } from "./Result.js";
14
+ import { getRuntimeConfig } from "./runtimeConfig.js";
15
+ import { createSearcheeFromDB, createSearcheeFromPath, createSearcheeFromTorrentFile, getAnimeKeys, getEpisodeKeys, getLargestFile, getMovieKeys, getSeasonKeys, } from "./searchee.js";
16
+ import { createKeyTitle, exists, filterAsync, filterAsyncYield, flatMapAsync, getLogString, inBatches, mapAsync, Mutex, notExists, stripExtension, wait, withMutex, } from "./utils.js";
17
+ export var SnatchError;
18
+ (function (SnatchError) {
19
+ SnatchError["ABORTED"] = "ABORTED";
20
+ SnatchError["RATE_LIMITED"] = "RATE_LIMITED";
21
+ SnatchError["MAGNET_LINK"] = "MAGNET_LINK";
22
+ SnatchError["INVALID_CONTENTS"] = "INVALID_CONTENTS";
23
+ SnatchError["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
24
+ })(SnatchError || (SnatchError = {}));
25
+ export async function parseTorrentFromPath(filePath) {
26
+ return Metafile.decode(await readFile(filePath));
27
+ }
28
+ export async function parseTorrentWithMetadata(filePath, torrentInfos) {
29
+ const meta = await parseTorrentFromPath(filePath);
30
+ const clients = getClients();
31
+ if (clients[0]?.clientType === Label.QBITTORRENT) {
32
+ const fastResumePath = filePath.replace(extname(filePath), ".fastresume");
33
+ if (await exists(fastResumePath)) {
34
+ updateMetafileMetadata(meta, bencode.decode(await readFile(fastResumePath)));
35
+ return meta;
36
+ }
37
+ else {
38
+ logger.verbose(`No .fastresume found at ${fastResumePath} from ${filePath}, using client to get metadata without trackers`);
39
+ }
40
+ }
41
+ // All other clients keep trackers in their .torrent files
42
+ // Also fallback for qBittorrent if .fastresume is missing e.g cross-seed search --torrents ...
43
+ // Getting all trackers for qbit require individual torrent requests but we do have the first active one
44
+ const torrentInfo = torrentInfos.find((t) => t.infoHash === meta.infoHash);
45
+ if (torrentInfo) {
46
+ meta.category = torrentInfo.category;
47
+ meta.tags = torrentInfo.tags;
48
+ if (!meta.trackers.length && torrentInfo.trackers) {
49
+ meta.trackers = torrentInfo.trackers;
50
+ }
51
+ }
52
+ else if (clients.length) {
53
+ logger.verbose(`No torrent info found for ${meta.infoHash} from ${filePath}`);
54
+ }
55
+ return meta;
56
+ }
57
+ function isMagnetRedirectError(error) {
58
+ return (
59
+ // node-fetch
60
+ error.message.includes('URL scheme "magnet" is not supported.') ||
61
+ // undici
62
+ Boolean(error.cause?.message.includes("URL scheme must be a HTTP(S) scheme")));
63
+ }
64
+ async function snatchOnce(candidate) {
65
+ const { snatchTimeout } = getRuntimeConfig();
66
+ const url = candidate.link;
67
+ let response;
68
+ try {
69
+ response = await fetch(url, {
70
+ headers: {
71
+ ...(candidate.cookie ? { Cookie: candidate.cookie } : {}),
72
+ "User-Agent": USER_AGENT,
73
+ },
74
+ signal: typeof snatchTimeout === "number"
75
+ ? AbortSignal.timeout(snatchTimeout)
76
+ : undefined,
77
+ });
78
+ }
79
+ catch (e) {
80
+ if (e.name === "AbortError" || e.name === "TimeoutError") {
81
+ logger.debug(`${candidate.name}: ${url}`);
82
+ return {
83
+ snatchError: SnatchError.ABORTED,
84
+ extra: `snatch timed out`,
85
+ };
86
+ }
87
+ else if (isMagnetRedirectError(e)) {
88
+ logger.debug(`${candidate.name}: ${url}`);
89
+ return { snatchError: SnatchError.MAGNET_LINK };
90
+ }
91
+ logger.debug(`${candidate.name}: ${url}`);
92
+ logger.debug(e);
93
+ return {
94
+ snatchError: SnatchError.UNKNOWN_ERROR,
95
+ extra: `failed to access`,
96
+ };
97
+ }
98
+ const retryAfterSeconds = Number(response.headers.get("Retry-After"));
99
+ const retryAfterMs = !Number.isNaN(retryAfterSeconds)
100
+ ? retryAfterSeconds * 1000
101
+ : undefined;
102
+ if (response.status === 429) {
103
+ return { snatchError: SnatchError.RATE_LIMITED, retryAfterMs };
104
+ }
105
+ else if (!response.ok) {
106
+ const responseText = await response.clone().text();
107
+ logger.debug(`${candidate.name}: ${url} - Response: "${responseText.slice(0, 100)}${responseText.length > 100 ? "..." : ""}"`);
108
+ return {
109
+ snatchError: SnatchError.UNKNOWN_ERROR,
110
+ retryAfterMs,
111
+ extra: `error downloading torrent - ${response.status} ${response.statusText}`,
112
+ };
113
+ }
114
+ else if (response.headers.get("Content-Type") === "application/rss+xml") {
115
+ const responseText = await response.clone().text();
116
+ logger.debug(`${candidate.name}: ${url} - Contents: "${responseText.slice(0, 100)}${responseText.length > 100 ? "..." : ""}"`);
117
+ return { snatchError: SnatchError.INVALID_CONTENTS, retryAfterMs };
118
+ }
119
+ try {
120
+ return Metafile.decode(Buffer.from(new Uint8Array(await response.arrayBuffer())));
121
+ }
122
+ catch (e) {
123
+ const contentType = response.headers.get("Content-Type");
124
+ const contentLength = response.headers.get("Content-Length");
125
+ logger.debug(`${candidate.name}: ${url} - Content-Type: ${contentType} - Content-Length: ${contentLength}`);
126
+ logger.debug(e);
127
+ return { snatchError: SnatchError.INVALID_CONTENTS, retryAfterMs };
128
+ }
129
+ }
130
+ export const snatchHistory = new Map();
131
+ export async function snatch(candidate, label, options) {
132
+ const retries = Math.max(options.retries, 0);
133
+ const retryAfterEndTime = Date.now() + retries * options.delayMs;
134
+ let snatchError;
135
+ for (let i = 0; i <= retries; i++) {
136
+ const progress = `${i + 1}/${retries + 1}`;
137
+ const snatchResult = await snatchOnce(candidate);
138
+ if (snatchResult instanceof Metafile) {
139
+ logger.verbose({
140
+ label,
141
+ message: `Snatched ${candidate.name} from ${candidate.tracker}${i > 0 ? ` on attempt ${progress}` : ""}`,
142
+ });
143
+ snatchHistory.delete(candidate.link);
144
+ return resultOf(snatchResult);
145
+ }
146
+ snatchError = snatchResult.snatchError;
147
+ if (snatchError === SnatchError.RATE_LIMITED ||
148
+ snatchError === SnatchError.MAGNET_LINK) {
149
+ snatchHistory.delete(candidate.link);
150
+ return resultOfErr(snatchError);
151
+ }
152
+ const { extra, retryAfterMs } = snatchResult;
153
+ let linkHistory = snatchHistory.get(candidate.link);
154
+ if (linkHistory) {
155
+ ++linkHistory.numFailures;
156
+ }
157
+ else {
158
+ linkHistory = { initialFailureAt: Date.now(), numFailures: 1 };
159
+ snatchHistory.set(candidate.link, linkHistory);
160
+ }
161
+ let trackerHistory = snatchHistory.get(candidate.tracker);
162
+ if (trackerHistory) {
163
+ ++trackerHistory.numFailures;
164
+ }
165
+ else {
166
+ trackerHistory = { initialFailureAt: Date.now(), numFailures: 1 };
167
+ snatchHistory.set(candidate.tracker, trackerHistory);
168
+ }
169
+ if (linkHistory.numFailures > retries + 1) {
170
+ logger.warn({
171
+ label,
172
+ message: `Snatching ${candidate.name} from ${candidate.tracker} stopped after attempt ${progress}, this snatch has failed too many times recently: ${snatchError}${extra ? ` - ${extra}` : ""}`,
173
+ });
174
+ return resultOfErr(snatchError);
175
+ }
176
+ if (trackerHistory.numFailures > retries * 2 + 1) {
177
+ logger.warn({
178
+ label,
179
+ message: `Snatching ${candidate.name} from ${candidate.tracker} stopped after attempt ${progress}, this tracker has failed too many times recently: ${snatchError}${extra ? ` - ${extra}` : ""}`,
180
+ });
181
+ return resultOfErr(snatchError);
182
+ }
183
+ if (retryAfterMs && Date.now() + retryAfterMs >= retryAfterEndTime) {
184
+ logger.warn({
185
+ label,
186
+ message: `Snatching ${candidate.name} from ${candidate.tracker} stopped after attempt ${progress}, Retry-After of ${retryAfterMs / 1000}s exceeds timeout: ${snatchError}${extra ? ` - ${extra}` : ""}`,
187
+ });
188
+ return resultOfErr(snatchError);
189
+ }
190
+ const delayMs = Math.max(options.delayMs, retryAfterMs ?? 0);
191
+ logger.error({
192
+ label,
193
+ message: `Snatch attempt ${progress} from ${candidate.tracker} for ${candidate.name} failed${i < retries ? `, retrying in ${delayMs / 1000}s` : ""}: ${snatchError}${extra ? ` - ${extra}` : ""}`,
194
+ });
195
+ if (i >= retries)
196
+ break;
197
+ await wait(delayMs);
198
+ }
199
+ return resultOfErr(snatchError);
200
+ }
201
+ function buildTorrentSaveName(mediaType, tracker, name, infoHash, ext) {
202
+ return `[${mediaType}][${tracker}]${name}[${infoHash}]${ext}`;
203
+ }
204
+ /**
205
+ * Be sure to update parseMetadataFromFilename if changing the format
206
+ */
207
+ export function getTorrentSavePath(meta, mediaType, tracker, dir, options) {
208
+ const fullName = stripExtension(meta.getFileSystemSafeName());
209
+ const ext = options.cached ? ".cached.torrent" : ".torrent";
210
+ const fullPath = join(dir, buildTorrentSaveName(mediaType, tracker, fullName, meta.infoHash, ext));
211
+ if (Buffer.byteLength(fullPath, "utf8") <= MAX_PATH_BYTES)
212
+ return fullPath;
213
+ const codePoints = Array.from(fullName);
214
+ let currBytes = Buffer.byteLength(`${fullPath}...`, "utf8");
215
+ let codePointsToRemove = 0;
216
+ for (let i = codePoints.length - 1; i >= 0; i--) {
217
+ codePointsToRemove++;
218
+ currBytes -= Buffer.byteLength(codePoints[i], "utf8");
219
+ if (currBytes <= MAX_PATH_BYTES)
220
+ break;
221
+ }
222
+ const safeName = `${codePoints.slice(0, -codePointsToRemove).join("")}...`;
223
+ const safePath = join(dir, buildTorrentSaveName(mediaType, tracker, safeName, meta.infoHash, ext));
224
+ return safePath; // Handle the error on save if it exists
225
+ }
226
+ /**
227
+ * Depends on getTorrentSavePath format
228
+ */
229
+ export function parseMetadataFromFilename(filename) {
230
+ const match = filename.match(SAVED_TORRENTS_INFO_REGEX);
231
+ if (!match) {
232
+ return {};
233
+ }
234
+ const mediaType = match.groups.mediaType;
235
+ if (!Object.values(MediaType).includes(mediaType)) {
236
+ return {};
237
+ }
238
+ const { tracker, name, infoHash, cached } = match.groups;
239
+ return { mediaType, tracker, name, infoHash, cached: !!cached };
240
+ }
241
+ export async function findAllTorrentFilesInDir(dir) {
242
+ return (await readdir(dir))
243
+ .filter((fn) => extname(fn) === ".torrent")
244
+ .sort()
245
+ .map((fn) => resolve(join(dir, fn)));
246
+ }
247
+ export function createEnsemblePieces(title, files) {
248
+ const episodeKeys = getEpisodeKeys(stripExtension(title));
249
+ if (!episodeKeys)
250
+ return null;
251
+ const element = episodeKeys.episode;
252
+ const largestFile = getLargestFile(files);
253
+ return episodeKeys.keyTitles.map((keyTitle) => {
254
+ const key = `${keyTitle}${episodeKeys.season ? `.${episodeKeys.season}` : ""}`;
255
+ return { key, element, largestFile };
256
+ });
257
+ }
258
+ export async function cacheEnsembleTorrentEntry(searchee, torrentSavePaths) {
259
+ const ensemblePieces = createEnsemblePieces(searchee.title, searchee.files);
260
+ if (!ensemblePieces || !ensemblePieces.length)
261
+ return null;
262
+ let savePath;
263
+ if (searchee.savePath) {
264
+ savePath = searchee.savePath;
265
+ }
266
+ else if (torrentSavePaths) {
267
+ savePath = torrentSavePaths.get(searchee.infoHash);
268
+ }
269
+ else {
270
+ const downloadDirResult = await getClients()[0].getDownloadDir(searchee, {
271
+ onlyCompleted: false,
272
+ });
273
+ if (downloadDirResult.isErr()) {
274
+ logger.error(`Failed to get download dir for ${getLogString(searchee)}: ${downloadDirResult.unwrapErr()}`);
275
+ return null;
276
+ }
277
+ savePath = downloadDirResult.unwrap();
278
+ }
279
+ if (!savePath) {
280
+ logger.error(`Failed to get save path for ${getLogString(searchee)}`);
281
+ return null;
282
+ }
283
+ return ensemblePieces.map((ensemblePiece) => ({
284
+ client_host: searchee.clientHost ?? null,
285
+ path: join(savePath, ensemblePiece.largestFile.path),
286
+ info_hash: searchee.infoHash,
287
+ ensemble: ensemblePiece.key,
288
+ element: ensemblePiece.element,
289
+ }));
290
+ }
291
+ async function indexTorrents(options) {
292
+ const { seasonFromEpisodes, torrentDir, useClientTorrents } = getRuntimeConfig();
293
+ if (!useClientTorrents && !torrentDir)
294
+ return;
295
+ const clients = getClients();
296
+ let searchees;
297
+ let infoHashPathMap;
298
+ if (options.startup) {
299
+ if (torrentDir) {
300
+ logger.info({
301
+ label: Label.INDEX,
302
+ message: "Indexing torrentDir for reverse lookup...",
303
+ });
304
+ searchees = await loadTorrentDirLight(torrentDir);
305
+ if (clients.length) {
306
+ infoHashPathMap = await clients[0].getAllDownloadDirs({
307
+ metas: searchees,
308
+ onlyCompleted: false,
309
+ v1HashOnly: true,
310
+ });
311
+ }
312
+ }
313
+ else {
314
+ logger.info("Indexing client torrents for reverse lookup...");
315
+ searchees = await flatMapAsync(clients, async (client) => {
316
+ try {
317
+ const { searchees } = await client.getClientSearchees({
318
+ includeFiles: true,
319
+ includeTrackers: true,
320
+ });
321
+ return searchees;
322
+ }
323
+ catch (e) {
324
+ const message = e instanceof Error ? e.message : String(e ?? "");
325
+ logger.warn({
326
+ label: client.label,
327
+ message: `Indexing client torrents failed; disabling client until next retry: ${message}`,
328
+ });
329
+ logger.debug(e);
330
+ return [];
331
+ }
332
+ });
333
+ }
334
+ }
335
+ else {
336
+ if (torrentDir) {
337
+ searchees = await indexTorrentDir(torrentDir);
338
+ }
339
+ else {
340
+ searchees = await flatMapAsync(clients, async (client) => {
341
+ try {
342
+ return (await client.getClientSearchees({
343
+ newSearcheesOnly: true,
344
+ includeFiles: true,
345
+ includeTrackers: true,
346
+ })).newSearchees;
347
+ }
348
+ catch (e) {
349
+ const message = e instanceof Error ? e.message : String(e ?? "");
350
+ logger.warn({
351
+ label: client.label,
352
+ message: `Indexing client torrents failed; disabling client until next retry: ${message}`,
353
+ });
354
+ logger.debug(e);
355
+ return [];
356
+ }
357
+ });
358
+ }
359
+ }
360
+ if (!seasonFromEpisodes)
361
+ return;
362
+ const ensembleRows = await flatMapAsync(searchees, async (searchee) => (await cacheEnsembleTorrentEntry(searchee, infoHashPathMap)) ?? []);
363
+ await inBatches(ensembleRows, async (batch) => {
364
+ await db("ensemble")
365
+ .insert(batch)
366
+ .onConflict(["client_host", "path"])
367
+ .merge();
368
+ });
369
+ }
370
+ async function indexTorrentDir(dir) {
371
+ const dirContents = new Set(await findAllTorrentFilesInDir(dir));
372
+ const newSearchees = [];
373
+ for (const filePath of dirContents) {
374
+ const doesAlreadyExist = await db("torrent")
375
+ .select("id")
376
+ .where({ file_path: filePath })
377
+ .first();
378
+ if (doesAlreadyExist)
379
+ continue;
380
+ let meta;
381
+ try {
382
+ meta = await parseTorrentFromPath(filePath);
383
+ }
384
+ catch (e) {
385
+ logOnce(`Failed to parse ${filePath}`, () => {
386
+ logger.error({
387
+ label: Label.INDEX,
388
+ message: `Failed to parse ${filePath}`,
389
+ });
390
+ logger.debug(e);
391
+ });
392
+ continue;
393
+ }
394
+ await db("torrent")
395
+ .insert({
396
+ file_path: filePath,
397
+ info_hash: meta.infoHash,
398
+ name: meta.name,
399
+ })
400
+ .onConflict("file_path")
401
+ .ignore();
402
+ const res = await createSearcheeFromTorrentFile(filePath, []);
403
+ if (res.isOk())
404
+ newSearchees.push(res.unwrap());
405
+ }
406
+ const rows = await db("torrent").select("file_path", "info_hash");
407
+ const toDelete = rows.filter((row) => !dirContents.has(row.file_path));
408
+ await inBatches(toDelete, async (batch) => {
409
+ await db("torrent")
410
+ .whereIn("file_path", batch.map((row) => row.file_path))
411
+ .del();
412
+ await db("ensemble")
413
+ .whereIn("info_hash", batch.map((row) => row.info_hash))
414
+ .del();
415
+ });
416
+ return newSearchees;
417
+ }
418
+ export async function indexTorrentsAndDataDirs(options = { startup: false }) {
419
+ if (options.startup) {
420
+ const { dataDirs, seasonFromEpisodes, torrentDir, useClientTorrents } = getRuntimeConfig();
421
+ if (!useClientTorrents) {
422
+ const hashes = (await db("client_searchee").select("info_hash")).map((r) => r.info_hash);
423
+ await inBatches(hashes, async (batch) => {
424
+ await db("ensemble").whereIn("info_hash", batch).del();
425
+ });
426
+ await db("client_searchee").del();
427
+ }
428
+ else {
429
+ const clientHosts = getClients().map((client) => client.clientHost);
430
+ await db("client_searchee")
431
+ .whereNotIn("client_host", clientHosts)
432
+ .del();
433
+ await db("ensemble")
434
+ .whereNotNull("client_host")
435
+ .whereNotIn("client_host", clientHosts)
436
+ .del();
437
+ }
438
+ if (!torrentDir) {
439
+ const hashes = (await db("torrent").select("info_hash")).map((r) => r.info_hash);
440
+ await inBatches(hashes, async (batch) => {
441
+ await db("ensemble").whereIn("info_hash", batch).del();
442
+ });
443
+ await db("torrent").del();
444
+ }
445
+ if (!dataDirs.length) {
446
+ const paths = (await db("data").select("path")).map((r) => r.path);
447
+ await inBatches(paths, async (batch) => {
448
+ await db("ensemble")
449
+ .whereIn("path", batch)
450
+ .whereNull("client_host")
451
+ .del();
452
+ });
453
+ await db("data").del();
454
+ }
455
+ else {
456
+ const paths = (await db("data").select("path")).map((r) => r.path);
457
+ const toDelete = paths.filter((p) => !isChildPath(p, dataDirs));
458
+ await inBatches(toDelete, async (batch) => {
459
+ await db("data").whereIn("path", batch).del();
460
+ await db("ensemble")
461
+ .whereIn("path", batch)
462
+ .whereNull("client_host")
463
+ .del();
464
+ });
465
+ }
466
+ if (!seasonFromEpisodes)
467
+ await db("ensemble").del();
468
+ }
469
+ return withMutex(Mutex.INDEX_TORRENTS_AND_DATA_DIRS, { useQueue: false }, async () => {
470
+ const maxRetries = 3;
471
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
472
+ try {
473
+ await indexDataDirs(options); // Running together may increase failures
474
+ await indexTorrents(options); // Run second so this data is more fresh
475
+ break;
476
+ }
477
+ catch (e) {
478
+ const log = {
479
+ label: Label.INDEX,
480
+ message: `Indexing failed (${maxRetries - attempt}): ${e.message}`,
481
+ };
482
+ logger.debug(e);
483
+ if (attempt < maxRetries) {
484
+ logger.verbose(log);
485
+ }
486
+ else {
487
+ logger.error(log);
488
+ if (options.startup)
489
+ throw e;
490
+ }
491
+ }
492
+ }
493
+ });
494
+ }
495
+ export async function getInfoHashesToExclude() {
496
+ const { useClientTorrents } = getRuntimeConfig();
497
+ const database = useClientTorrents ? db("client_searchee") : db("torrent");
498
+ return new Set((await database.select("*")).map((e) => e.info_hash));
499
+ }
500
+ export async function loadTorrentDirLight(torrentDir) {
501
+ const torrentFilePaths = await findAllTorrentFilesInDir(torrentDir);
502
+ const searchees = [];
503
+ const client = getClients()[0];
504
+ const torrentInfos = client && client.clientType !== Label.QBITTORRENT
505
+ ? await client.getAllTorrents()
506
+ : [];
507
+ for (const torrentFilePath of torrentFilePaths) {
508
+ const searcheeResult = await createSearcheeFromTorrentFile(torrentFilePath, torrentInfos);
509
+ if (searcheeResult.isOk()) {
510
+ searchees.push(searcheeResult.unwrap());
511
+ }
512
+ }
513
+ return searchees;
514
+ }
515
+ function getKeysFromName(stem) {
516
+ const episodeKeys = getEpisodeKeys(stem);
517
+ if (episodeKeys) {
518
+ const keyTitles = episodeKeys.keyTitles;
519
+ const element = `${episodeKeys.season ? `${episodeKeys.season}.` : ""}${episodeKeys.episode}`;
520
+ return { keyTitles, element, useFallback: false };
521
+ }
522
+ const seasonKeys = getSeasonKeys(stem);
523
+ if (seasonKeys) {
524
+ const keyTitles = seasonKeys.keyTitles;
525
+ const element = seasonKeys.season;
526
+ return { keyTitles, element, useFallback: false };
527
+ }
528
+ const movieKeys = getMovieKeys(stem);
529
+ if (movieKeys) {
530
+ const keyTitles = movieKeys.keyTitles;
531
+ return { keyTitles, useFallback: false };
532
+ }
533
+ const animeKeys = getAnimeKeys(stem);
534
+ if (animeKeys) {
535
+ const keyTitles = animeKeys.keyTitles;
536
+ const element = animeKeys.release;
537
+ return { keyTitles, element, useFallback: false };
538
+ }
539
+ return { keyTitles: [], useFallback: true };
540
+ }
541
+ export async function getSimilarByName(name) {
542
+ const { torrentDir, useClientTorrents } = getRuntimeConfig();
543
+ const stem = stripExtension(name);
544
+ const { keyTitles, element, useFallback } = getKeysFromName(stem);
545
+ const clientSearchees = useFallback
546
+ ? await getTorrentByFuzzyName(stem)
547
+ : [];
548
+ const dataSearchees = useFallback ? await getDataByFuzzyName(stem) : [];
549
+ if (!keyTitles.length) {
550
+ const noParentheses = stem
551
+ .replace(ALL_SQUARE_BRACKETS_REGEX, "")
552
+ .replace(ALL_PARENTHESES_REGEX, "")
553
+ .replace(ALL_SPACES_REGEX, " ")
554
+ .trim(); // Anime that fails MediaType.ANIME often has `[group] Title (Extra Info)`
555
+ if (noParentheses.length >= MIN_VIDEO_QUERY_LENGTH &&
556
+ noParentheses !== stem) {
557
+ clientSearchees.push(...(await getTorrentByFuzzyName(noParentheses)));
558
+ dataSearchees.push(...(await getDataByFuzzyName(noParentheses)));
559
+ }
560
+ return { keys: [], clientSearchees, dataSearchees };
561
+ }
562
+ const candidateMaxDistance = Math.floor(Math.min(...keyTitles.map((t) => t.length)) / LEVENSHTEIN_DIVISOR);
563
+ const filterEntries = async (dbEntries) => {
564
+ return filterAsyncYield(dbEntries, async (dbEntry) => {
565
+ const entry = getKeysFromName(dbEntry.title ?? dbEntry.name);
566
+ if (entry.element !== element)
567
+ return false;
568
+ if (!entry.keyTitles.length)
569
+ return false;
570
+ const maxDistance = Math.min(candidateMaxDistance, Math.floor(Math.min(...entry.keyTitles.map((t) => t.length)) /
571
+ LEVENSHTEIN_DIVISOR));
572
+ return entry.keyTitles.some((dbKeyTitle) => {
573
+ return keyTitles.some((keyTitle) => distance(keyTitle, dbKeyTitle) <= maxDistance);
574
+ });
575
+ });
576
+ };
577
+ if (useClientTorrents) {
578
+ clientSearchees.push(...(await filterEntries(await db("client_searchee"))).map(createSearcheeFromDB));
579
+ }
580
+ else if (torrentDir) {
581
+ const filteredTorrentEntries = (await filterEntries(await db("torrent").select("name", "file_path")));
582
+ if (filteredTorrentEntries.length) {
583
+ const client = getClients()[0];
584
+ const torrentInfos = client && client.clientType !== Label.QBITTORRENT
585
+ ? await client.getAllTorrents()
586
+ : [];
587
+ clientSearchees.push(...(await mapAsync(filteredTorrentEntries, (dbTorrent) => createSearcheeFromTorrentFile(dbTorrent.file_path, torrentInfos)))
588
+ .filter(isOk)
589
+ .map((r) => r.unwrap()));
590
+ }
591
+ }
592
+ const entriesToDelete = [];
593
+ const filteredDataEntries = await filterAsync((await filterEntries(await db("data"))), async ({ path }) => {
594
+ if ((await notExists(path)) ||
595
+ shouldIgnorePathHeuristically(path, (await stat(path)).isDirectory())) {
596
+ entriesToDelete.push(path);
597
+ return false;
598
+ }
599
+ return true;
600
+ });
601
+ await inBatches(entriesToDelete, async (batch) => {
602
+ await db("data").whereIn("path", batch).del();
603
+ await db("ensemble").whereIn("path", batch).del();
604
+ });
605
+ if (filteredDataEntries.length) {
606
+ dataSearchees.push(...(await mapAsync(filteredDataEntries, (dbData) => createSearcheeFromPath(dbData.path)))
607
+ .filter(isOk)
608
+ .map((r) => r.unwrap()));
609
+ }
610
+ const keys = element
611
+ ? keyTitles.map((keyTitle) => `${keyTitle}.${element}`)
612
+ : keyTitles;
613
+ return { keys, clientSearchees, dataSearchees };
614
+ }
615
+ async function getTorrentByFuzzyName(stem) {
616
+ const { useClientTorrents } = getRuntimeConfig();
617
+ const database = useClientTorrents
618
+ ? await db("client_searchee")
619
+ : await db("torrent");
620
+ const fullMatch = createKeyTitle(stem);
621
+ // Attempt to filter torrents in DB to match incoming data before fuzzy check
622
+ let filteredNames = [];
623
+ if (fullMatch) {
624
+ filteredNames = await filterAsyncYield(database, async (dbEntry) => {
625
+ const dbMatch = createKeyTitle(stripExtension(dbEntry.title ?? dbEntry.name));
626
+ return fullMatch === dbMatch;
627
+ });
628
+ }
629
+ // If none match, proceed with fuzzy name check on all names.
630
+ filteredNames = filteredNames.length > 0 ? filteredNames : database;
631
+ const candidateMaxDistance = Math.floor(stem.length / LEVENSHTEIN_DIVISOR);
632
+ const potentialMatches = await filterAsyncYield(filteredNames, async (dbEntry) => {
633
+ const dbTitle = stripExtension(dbEntry.title ?? dbEntry.name);
634
+ const maxDistance = Math.min(candidateMaxDistance, Math.floor(dbTitle.length / LEVENSHTEIN_DIVISOR));
635
+ return distance(stem, dbTitle) <= maxDistance;
636
+ });
637
+ if (useClientTorrents)
638
+ return potentialMatches.map(createSearcheeFromDB);
639
+ const client = getClients()[0];
640
+ const torrentInfos = client && client.clientType !== Label.QBITTORRENT
641
+ ? await client.getAllTorrents()
642
+ : [];
643
+ return (await mapAsync(potentialMatches, (dbTorrent) => createSearcheeFromTorrentFile(dbTorrent.file_path, torrentInfos)))
644
+ .filter(isOk)
645
+ .map((r) => r.unwrap());
646
+ }
647
+ export async function getTorrentByCriteria(criteria) {
648
+ const { useClientTorrents } = getRuntimeConfig();
649
+ const database = useClientTorrents ? db("client_searchee") : db("torrent");
650
+ const dbTorrents = await database.where((b) => b.where({ info_hash: criteria.infoHash }));
651
+ if (!dbTorrents.length) {
652
+ const message = `Torrent client does not have any torrent with criteria ${inspect(criteria)}`;
653
+ throw new Error(message);
654
+ }
655
+ if (useClientTorrents)
656
+ return dbTorrents.map(createSearcheeFromDB);
657
+ const client = getClients()[0];
658
+ const torrentInfos = client && client.clientType !== Label.QBITTORRENT
659
+ ? await client.getAllTorrents()
660
+ : [];
661
+ return [
662
+ (await createSearcheeFromTorrentFile(dbTorrents[0].file_path, torrentInfos)).unwrapOrThrow(new Error(`Failed to create searchee from ${dbTorrents[0].file_path}`)),
663
+ ];
664
+ }
665
+ //# sourceMappingURL=torrent.js.map