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,699 @@
1
+ import { readdir } from "fs/promises";
2
+ import ms from "ms";
3
+ import { basename } from "path";
4
+ import { inspect } from "util";
5
+ import { humanReadableSize } from "@cross-seed/shared/utils";
6
+ import { InjectionResult, TORRENT_CATEGORY_SUFFIX, TORRENT_TAG, USER_AGENT, } from "../constants.js";
7
+ import { db } from "../db.js";
8
+ import { CrossSeedError } from "../errors.js";
9
+ import { Label, logger } from "../logger.js";
10
+ import { resultOf, resultOfErr } from "../Result.js";
11
+ import { getRuntimeConfig } from "../runtimeConfig.js";
12
+ import { createSearcheeFromDB, parseTitle, updateSearcheeClientDB, } from "../searchee.js";
13
+ import { extractCredentialsFromUrl, getLogString, sanitizeInfoHash, wait, } from "../utils.js";
14
+ import { shouldResumeFromNonRelevantFiles, clientSearcheeModified, getMaxRemainingBytes, getResumeStopTime, organizeTrackers, resumeErrSleepTime, resumeSleepTime, shouldRecheck, } from "./TorrentClient.js";
15
+ var DelugeErrorCode;
16
+ (function (DelugeErrorCode) {
17
+ DelugeErrorCode[DelugeErrorCode["NO_AUTH"] = 1] = "NO_AUTH";
18
+ DelugeErrorCode[DelugeErrorCode["BAD_METHOD"] = 2] = "BAD_METHOD";
19
+ DelugeErrorCode[DelugeErrorCode["CALL_ERR"] = 3] = "CALL_ERR";
20
+ DelugeErrorCode[DelugeErrorCode["RPC_FAIL"] = 4] = "RPC_FAIL";
21
+ DelugeErrorCode[DelugeErrorCode["BAD_JSON"] = 5] = "BAD_JSON";
22
+ })(DelugeErrorCode || (DelugeErrorCode = {}));
23
+ export default class Deluge {
24
+ url;
25
+ clientHost;
26
+ clientPriority;
27
+ clientType = Label.DELUGE;
28
+ readonly;
29
+ label;
30
+ delugeCookie = null;
31
+ delugeLabel = TORRENT_TAG;
32
+ delugeLabelSuffix = TORRENT_CATEGORY_SUFFIX;
33
+ isLabelEnabled;
34
+ delugeRequestId = 0;
35
+ constructor(url, clientHost, priority, readonly) {
36
+ this.url = url;
37
+ this.clientHost = clientHost;
38
+ this.clientPriority = priority;
39
+ this.readonly = readonly;
40
+ this.label = `${this.clientType}@${this.clientHost}`;
41
+ }
42
+ /**
43
+ * validates the login and host for deluge webui
44
+ */
45
+ async validateConfig() {
46
+ const { torrentDir } = getRuntimeConfig();
47
+ await this.authenticate();
48
+ this.isLabelEnabled = await this.labelEnabled();
49
+ logger.info({
50
+ label: this.label,
51
+ message: `Logged in successfully${this.readonly ? " (readonly)" : ""}`,
52
+ });
53
+ if (!torrentDir)
54
+ return;
55
+ if (!(await readdir(torrentDir)).some((f) => f.endsWith(".state"))) {
56
+ throw new CrossSeedError(`[${this.label}] Invalid torrentDir, if no torrents are in client set to null for now: https://www.cross-seed.org/docs/basics/options#torrentdir`);
57
+ }
58
+ }
59
+ /**
60
+ * connects and authenticates to the webui
61
+ */
62
+ async authenticate() {
63
+ const { href, password } = extractCredentialsFromUrl(this.url).unwrapOrThrow(new CrossSeedError(`[${this.label}] delugeRpcUrl must be percent-encoded`));
64
+ if (!password) {
65
+ throw new CrossSeedError(`[${this.label}] You need to define a password in the delugeRpcUrl. (e.g. http://:<PASSWORD>@localhost:8112)`);
66
+ }
67
+ try {
68
+ const authResponse = (await this.call("auth.login", [password], 0)).unwrapOrThrow(new Error(`[${this.label}] failed to connect for authentication`));
69
+ if (!authResponse) {
70
+ throw new CrossSeedError(`[${this.label}] Reached Deluge, but failed to authenticate: ${href}`);
71
+ }
72
+ }
73
+ catch (networkError) {
74
+ throw new CrossSeedError(networkError);
75
+ }
76
+ const isConnectedResponse = await this.call("web.connected", [], 0);
77
+ if (isConnectedResponse.isOk() && !isConnectedResponse.unwrap()) {
78
+ logger.warn({
79
+ label: this.label,
80
+ message: "Deluge WebUI disconnected from daemon...attempting to reconnect.",
81
+ });
82
+ const webuiHostList = (await this.call("web.get_hosts", [], 0)).unwrapOrThrow(new Error(`[${this.label}] failed to get host-list for reconnect`));
83
+ const connectResponse = await this.call("web.connect", [webuiHostList[0][0]], 0);
84
+ if (connectResponse.isOk() && connectResponse.unwrap()) {
85
+ logger.info({
86
+ label: this.label,
87
+ message: "Deluge WebUI connected to the daemon.",
88
+ });
89
+ }
90
+ else {
91
+ throw new CrossSeedError(`[${this.label}] Unable to connect WebUI to Deluge daemon. Connect to the WebUI to resolve this.`);
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * ensures authentication and sends JSON-RPC calls to deluge
97
+ * @param method RPC method to send (usually prefaced with module name)
98
+ * @param params parameters for the method (usually in an array)
99
+ * @param retries specify a retry count (optional)
100
+ * @return a promised Result of the specified ResultType or an ErrorType
101
+ */
102
+ async call(method, params, retries = 1) {
103
+ const msg = `Calling method ${method} with params ${inspect(params, { depth: null, compact: true })}`;
104
+ const message = msg.length > 1000 ? `${msg.slice(0, 1000)}...` : msg;
105
+ logger.verbose({ label: this.label, message });
106
+ const { href } = extractCredentialsFromUrl(this.url).unwrapOrThrow(new CrossSeedError(`[${this.label}] delugeRpcUrl must be percent-encoded`));
107
+ const headers = new Headers({
108
+ "Content-Type": "application/json",
109
+ "User-Agent": USER_AGENT,
110
+ });
111
+ if (this.delugeCookie)
112
+ headers.set("Cookie", this.delugeCookie);
113
+ let response, json;
114
+ try {
115
+ response = await fetch(href, {
116
+ body: JSON.stringify({
117
+ method,
118
+ params,
119
+ id: this.delugeRequestId++,
120
+ }),
121
+ method: "POST",
122
+ headers,
123
+ signal: AbortSignal.timeout(ms("10 seconds")),
124
+ });
125
+ }
126
+ catch (networkError) {
127
+ if (networkError.name === "AbortError" ||
128
+ networkError.name === "TimeoutError") {
129
+ throw new Error(`[${this.label}] Deluge method ${method} timed out after 10 seconds`);
130
+ }
131
+ throw new Error(`[${this.label}] Failed to connect to Deluge at ${href}`, {
132
+ cause: networkError,
133
+ });
134
+ }
135
+ try {
136
+ json = (await response.json());
137
+ }
138
+ catch (jsonParseError) {
139
+ throw new Error(`[${this.label}] Deluge method ${method} response was non-JSON ${jsonParseError}`);
140
+ }
141
+ if (json.error?.code === DelugeErrorCode.NO_AUTH && retries > 0) {
142
+ this.delugeCookie = null;
143
+ await this.authenticate();
144
+ if (this.delugeCookie) {
145
+ return this.call(method, params, 0);
146
+ }
147
+ else {
148
+ throw new Error(`[${this.label}] Connection lost with Deluge. Re-authentication failed.`);
149
+ }
150
+ }
151
+ this.handleResponseHeaders(response.headers);
152
+ if (json.error) {
153
+ return resultOfErr(json.error);
154
+ }
155
+ return resultOf(json.result);
156
+ }
157
+ /**
158
+ * parses the set-cookie header and updates stored value
159
+ * @param headers the headers from a request
160
+ */
161
+ handleResponseHeaders(headers) {
162
+ if (headers.has("Set-Cookie")) {
163
+ this.delugeCookie = headers.get("Set-Cookie").split(";")[0];
164
+ }
165
+ }
166
+ /**
167
+ * checks enabled plugins for "Label"
168
+ * @return boolean declaring whether the "Label" plugin is enabled
169
+ */
170
+ async labelEnabled() {
171
+ const enabledPlugins = await this.call("core.get_enabled_plugins", []);
172
+ if (enabledPlugins.isOk()) {
173
+ return enabledPlugins.unwrap().includes("Label");
174
+ }
175
+ else {
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * checks the status of an infohash in the client and resumes if/when criteria is met
181
+ * @param meta MetaFile containing torrent being resumed
182
+ * @param decision decision by which the newTorrent was matched
183
+ * @param options options object for extra flags
184
+ * @param options.checkOnce boolean to only check for resuming once
185
+ * @param options.meta metafile object containing the torrent data
186
+ */
187
+ async resumeInjection(meta, decision, options) {
188
+ const infoHash = meta.infoHash;
189
+ let sleepTime = resumeSleepTime;
190
+ const stopTime = getResumeStopTime();
191
+ let stop = false;
192
+ while (Date.now() < stopTime) {
193
+ if (options.checkOnce) {
194
+ if (stop)
195
+ return;
196
+ stop = true;
197
+ }
198
+ await wait(sleepTime);
199
+ let torrentInfo;
200
+ let torrentLog;
201
+ try {
202
+ torrentInfo = await this.getTorrentInfo(infoHash);
203
+ if (torrentInfo.state === "Checking") {
204
+ continue;
205
+ }
206
+ torrentLog = `${torrentInfo.name} [${sanitizeInfoHash(infoHash)}]`;
207
+ if (torrentInfo.state !== "Paused") {
208
+ logger.warn({
209
+ label: this.label,
210
+ message: `Will not resume ${torrentLog}: state is ${torrentInfo.state}`,
211
+ });
212
+ return;
213
+ }
214
+ const maxRemainingBytes = getMaxRemainingBytes(meta, decision, {
215
+ torrentLog,
216
+ label: this.label,
217
+ });
218
+ if (torrentInfo.total_remaining > maxRemainingBytes) {
219
+ if (!shouldResumeFromNonRelevantFiles(meta, torrentInfo.total_remaining, decision, { torrentLog, label: this.label })) {
220
+ logger.warn({
221
+ label: this.label,
222
+ message: `autoResumeMaxDownload will not resume ${torrentLog}: remainingSize ${humanReadableSize(torrentInfo.total_remaining, { binary: true })} > ${humanReadableSize(maxRemainingBytes, { binary: true })} limit`,
223
+ });
224
+ return;
225
+ }
226
+ }
227
+ }
228
+ catch {
229
+ sleepTime = resumeErrSleepTime; // Dropping connections or restart
230
+ continue;
231
+ }
232
+ logger.info({
233
+ label: this.label,
234
+ message: `Resuming ${torrentLog}: ${humanReadableSize(torrentInfo.total_remaining, { binary: true })} remaining`,
235
+ });
236
+ await this.call("core.resume_torrent", [[infoHash]]);
237
+ return;
238
+ }
239
+ logger.warn({
240
+ label: this.label,
241
+ message: `Will not resume torrent ${infoHash}: timeout`,
242
+ });
243
+ }
244
+ /**
245
+ * generates the label for injection based on searchee and torrentInfo
246
+ * @param searchee Searchee that contains the originating torrent
247
+ * @param torrentInfo TorrentInfo from the searchee
248
+ * @return string with the label for the newTorrent
249
+ */
250
+ calculateLabel(searchee, torrentInfo) {
251
+ const { linkCategory, duplicateCategories } = getRuntimeConfig();
252
+ if (!searchee.infoHash || !torrentInfo.label) {
253
+ return this.delugeLabel;
254
+ }
255
+ const ogLabel = torrentInfo.label;
256
+ if (!duplicateCategories) {
257
+ return ogLabel;
258
+ }
259
+ const shouldSuffixLabel = !ogLabel.endsWith(this.delugeLabelSuffix) && // no .cross-seed
260
+ ogLabel !== linkCategory; // not data
261
+ return !searchee.infoHash
262
+ ? (linkCategory ?? "")
263
+ : shouldSuffixLabel
264
+ ? `${ogLabel}${this.delugeLabelSuffix}`
265
+ : ogLabel;
266
+ }
267
+ /**
268
+ * if Label plugin is loaded, adds (if necessary)
269
+ * and sets the label based on torrent hash.
270
+ * @param newTorrent the searchee of the newTorrent
271
+ * @param label the destination label for the newTorrent/searchee
272
+ */
273
+ async setLabel(newTorrent, label) {
274
+ let setResult;
275
+ if (!this.isLabelEnabled)
276
+ return;
277
+ try {
278
+ const getCurrentLabels = await this.call("label.get_labels", []);
279
+ if (getCurrentLabels.isErr()) {
280
+ this.isLabelEnabled = false;
281
+ throw new Error("Labels have been disabled.");
282
+ }
283
+ if (getCurrentLabels.unwrap().includes(label)) {
284
+ setResult = await this.call("label.set_torrent", [
285
+ newTorrent.infoHash,
286
+ label,
287
+ ]);
288
+ }
289
+ else {
290
+ await this.call("label.add", [label]);
291
+ await wait(300);
292
+ setResult = await this.call("label.set_torrent", [
293
+ newTorrent.infoHash,
294
+ label,
295
+ ]);
296
+ }
297
+ if (setResult.isErr()) {
298
+ throw new Error(setResult.unwrapErr().message);
299
+ }
300
+ }
301
+ catch (e) {
302
+ logger.warn({
303
+ label: this.label,
304
+ message: `Failed to label ${getLogString(newTorrent)} as ${label}: ${e.message}`,
305
+ });
306
+ logger.debug(e);
307
+ }
308
+ }
309
+ /**
310
+ * injects a torrent into deluge client
311
+ * @param newTorrent injected candidate torrent
312
+ * @param searchee originating torrent (searchee)
313
+ * @param decision decision by which the newTorrent was matched
314
+ * @param options.onlyCompleted boolean to only inject completed torrents
315
+ * @param options.destinationDir location of the linked files (optional)
316
+ * @return InjectionResult of the newTorrent's injection
317
+ */
318
+ async inject(newTorrent, searchee, decision, options) {
319
+ try {
320
+ const existsRes = await this.isTorrentInClient(newTorrent.infoHash);
321
+ if (existsRes.isErr())
322
+ return InjectionResult.FAILURE;
323
+ if (existsRes.unwrap())
324
+ return InjectionResult.ALREADY_EXISTS;
325
+ let torrentInfo;
326
+ if (options.onlyCompleted && searchee.infoHash) {
327
+ torrentInfo = await this.getTorrentInfo(searchee.infoHash);
328
+ if (!torrentInfo.complete)
329
+ return InjectionResult.TORRENT_NOT_COMPLETE;
330
+ }
331
+ if (!options.destinationDir &&
332
+ (!searchee.infoHash || !torrentInfo)) {
333
+ logger.debug({
334
+ label: this.label,
335
+ message: `Injection failure: ${getLogString(searchee)} was missing critical data.`,
336
+ });
337
+ return InjectionResult.FAILURE;
338
+ }
339
+ const torrentFileName = `${newTorrent.getFileSystemSafeName()}.cross-seed.torrent`;
340
+ const encodedTorrentData = newTorrent.encode().toString("base64");
341
+ const destinationDir = options.destinationDir
342
+ ? options.destinationDir
343
+ : torrentInfo.save_path;
344
+ const toRecheck = shouldRecheck(newTorrent, decision);
345
+ const params = this.formatData(torrentFileName, encodedTorrentData, destinationDir, toRecheck);
346
+ const addResponse = await this.call("core.add_torrent_file", params);
347
+ if (addResponse.isErr()) {
348
+ const addResponseError = addResponse.unwrapErr();
349
+ if (addResponseError.message.includes("already")) {
350
+ return InjectionResult.ALREADY_EXISTS;
351
+ }
352
+ else if (addResponseError) {
353
+ logger.debug({
354
+ label: this.label,
355
+ message: `Injection failed: ${addResponseError.message}`,
356
+ });
357
+ return InjectionResult.FAILURE;
358
+ }
359
+ else {
360
+ logger.debug({
361
+ label: this.label,
362
+ message: `Unknown injection failure: ${getLogString(newTorrent)}`,
363
+ });
364
+ return InjectionResult.FAILURE;
365
+ }
366
+ }
367
+ if (addResponse.isOk()) {
368
+ await this.setLabel(newTorrent, this.calculateLabel(searchee, torrentInfo));
369
+ if (toRecheck) {
370
+ // when paused, libtorrent doesnt start rechecking
371
+ // leaves torrent ready to download - ~99%
372
+ await wait(1000);
373
+ await this.recheckTorrent(newTorrent.infoHash);
374
+ void this.resumeInjection(newTorrent, decision, {
375
+ checkOnce: false,
376
+ });
377
+ }
378
+ }
379
+ }
380
+ catch (error) {
381
+ logger.error({
382
+ label: this.label,
383
+ message: `Injection failed: ${error}`,
384
+ });
385
+ logger.debug(error);
386
+ return InjectionResult.FAILURE;
387
+ }
388
+ return InjectionResult.SUCCESS;
389
+ }
390
+ async recheckTorrent(infoHash) {
391
+ // Pause first as it may resume after recheck automatically
392
+ await this.call("core.pause_torrent", [[infoHash]]);
393
+ await this.call("core.force_recheck", [[infoHash]]);
394
+ }
395
+ /**
396
+ * formats the json for rpc calls to inject
397
+ * @param filename filename for the injecting torrent file
398
+ * @param filedump string with encoded torrent file
399
+ * @param destinationDir path to the torrent data
400
+ * @param toRecheck boolean to recheck the torrent
401
+ */
402
+ formatData(filename, filedump, destinationDir, toRecheck) {
403
+ return [
404
+ filename,
405
+ filedump,
406
+ {
407
+ add_paused: toRecheck,
408
+ seed_mode: !toRecheck,
409
+ download_location: destinationDir,
410
+ },
411
+ ];
412
+ }
413
+ /**
414
+ * returns directory of an infohash in deluge as a string
415
+ * @param meta SearcheeWithInfoHash or Metafile for torrent to lookup in client
416
+ * @param options options object for extra flags
417
+ * @param options.onlyCompleted boolean to only return a completed torrent
418
+ * @return Result containing either a string with path or reason it was not provided
419
+ */
420
+ async getDownloadDir(meta, options) {
421
+ let response;
422
+ const params = [["save_path", "progress"], { hash: meta.infoHash }];
423
+ try {
424
+ response = await this.call("web.update_ui", params);
425
+ }
426
+ catch {
427
+ return resultOfErr("UNKNOWN_ERROR");
428
+ }
429
+ if (!response.isOk()) {
430
+ return resultOfErr("UNKNOWN_ERROR");
431
+ }
432
+ const torrentResponse = response.unwrap().torrents;
433
+ if (!torrentResponse) {
434
+ return resultOfErr("UNKNOWN_ERROR");
435
+ }
436
+ const torrent = torrentResponse[meta.infoHash];
437
+ if (!torrent) {
438
+ return resultOfErr("NOT_FOUND");
439
+ }
440
+ if (options.onlyCompleted && torrent.progress !== 100) {
441
+ return resultOfErr("TORRENT_NOT_COMPLETE");
442
+ }
443
+ return resultOf(torrent.save_path);
444
+ }
445
+ /**
446
+ * returns map of hashes and download directories for all torrents
447
+ * @param options.metas array of SearcheeWithInfoHash or Metafile for torrents to lookup in client
448
+ * @param options.onlyCompleted boolean to only return completed torrents
449
+ * @return Promise of a Map with hashes and download directories
450
+ */
451
+ async getAllDownloadDirs(options) {
452
+ const dirs = new Map();
453
+ let response;
454
+ const params = [["save_path", "progress"], {}];
455
+ try {
456
+ response = await this.call("web.update_ui", params);
457
+ }
458
+ catch {
459
+ return dirs;
460
+ }
461
+ if (!response.isOk()) {
462
+ return dirs;
463
+ }
464
+ const torrentResponse = response.unwrap().torrents;
465
+ if (!torrentResponse) {
466
+ return dirs;
467
+ }
468
+ for (const [hash, torrent] of Object.entries(torrentResponse)) {
469
+ if (options.onlyCompleted && torrent.progress !== 100)
470
+ continue;
471
+ dirs.set(hash, torrent.save_path);
472
+ }
473
+ return dirs;
474
+ }
475
+ /**
476
+ * checks if a torrent exists in deluge
477
+ * @param inputHash the infoHash of the torrent to check
478
+ * @return Result containing either a boolean or reason it was not provided
479
+ */
480
+ async isTorrentInClient(inputHash) {
481
+ const infoHash = inputHash.toLowerCase();
482
+ try {
483
+ const torrentsRes = await this.call("web.update_ui", [[], {}]);
484
+ if (torrentsRes.isErr()) {
485
+ const err = torrentsRes.unwrapErr();
486
+ throw new Error(`${err.code ? err.code + ": " : ""}${err.message}`);
487
+ }
488
+ const torrents = torrentsRes.unwrap().torrents;
489
+ if (!torrents)
490
+ throw new Error("No torrents found");
491
+ for (const hash of Object.keys(torrents)) {
492
+ if (hash.toLowerCase() === infoHash)
493
+ return resultOf(true);
494
+ }
495
+ return resultOf(false);
496
+ }
497
+ catch (e) {
498
+ return resultOfErr(e);
499
+ }
500
+ }
501
+ /**
502
+ * checks if a torrent is complete in deluge
503
+ * @param infoHash the infoHash of the torrent to check
504
+ * @return Result containing either a boolean or reason it was not provided
505
+ */
506
+ async isTorrentComplete(infoHash) {
507
+ try {
508
+ const torrentInfo = await this.getTorrentInfo(infoHash, {
509
+ useVerbose: true,
510
+ });
511
+ return torrentInfo.complete ? resultOf(true) : resultOf(false);
512
+ }
513
+ catch {
514
+ return resultOfErr("NOT_FOUND");
515
+ }
516
+ }
517
+ /**
518
+ * checks if a torrent is checking in deluge
519
+ * @param infoHash the infoHash of the torrent to check
520
+ * @return Result containing either a boolean or reason it was not provided
521
+ */
522
+ async isTorrentChecking(infoHash) {
523
+ try {
524
+ const torrentInfo = await this.getTorrentInfo(infoHash, {
525
+ useVerbose: true,
526
+ });
527
+ return resultOf(torrentInfo.state === "Checking");
528
+ }
529
+ catch {
530
+ return resultOfErr("NOT_FOUND");
531
+ }
532
+ }
533
+ /**
534
+ * @return All torrents in the client
535
+ */
536
+ async getAllTorrents() {
537
+ const params = [["hash", "label"], {}];
538
+ const response = await this.call("web.update_ui", params);
539
+ if (!response.isOk()) {
540
+ return [];
541
+ }
542
+ const torrents = response.unwrap().torrents;
543
+ if (!torrents) {
544
+ return [];
545
+ }
546
+ return Object.entries(torrents).map(([hash, torrent]) => ({
547
+ infoHash: hash,
548
+ category: torrent.label ?? "",
549
+ }));
550
+ }
551
+ /**
552
+ * Get all searchees from the client and update the db
553
+ * @param options.newSearcheesOnly only return searchees that are not in the db
554
+ * @param options.refresh undefined uses the cache, [] refreshes all searchees, or a list of infoHashes to refresh
555
+ * @return an object containing all searchees and new searchees (refreshed searchees are considered new)
556
+ */
557
+ async getClientSearchees(options) {
558
+ const searchees = [];
559
+ const newSearchees = [];
560
+ const infoHashes = new Set();
561
+ const torrentsRes = await this.call("web.update_ui", [
562
+ ["name", "label", "save_path", "total_size", "files", "trackers"],
563
+ {},
564
+ ]);
565
+ if (torrentsRes.isErr()) {
566
+ logger.error({
567
+ label: this.label,
568
+ message: "Failed to get torrents from client",
569
+ });
570
+ logger.debug(torrentsRes.unwrapErr());
571
+ return { searchees, newSearchees };
572
+ }
573
+ const torrents = torrentsRes.unwrap().torrents;
574
+ if (!torrents || !Object.keys(torrents).length) {
575
+ logger.verbose({
576
+ label: this.label,
577
+ message: "No torrents found in client",
578
+ });
579
+ return { searchees, newSearchees };
580
+ }
581
+ for (const [hash, torrent] of Object.entries(torrents)) {
582
+ const infoHash = hash.toLowerCase();
583
+ infoHashes.add(infoHash);
584
+ const dbTorrent = await db("client_searchee")
585
+ .where("info_hash", infoHash)
586
+ .where("client_host", this.clientHost)
587
+ .first();
588
+ const name = torrent.name;
589
+ const savePath = torrent.save_path;
590
+ const category = torrent.label ?? "";
591
+ const modified = clientSearcheeModified(this.label, dbTorrent, name, savePath, {
592
+ category,
593
+ });
594
+ const refresh = options?.refresh === undefined
595
+ ? false
596
+ : options.refresh.length === 0
597
+ ? true
598
+ : options.refresh.includes(infoHash);
599
+ if (!modified && !refresh) {
600
+ if (!options?.newSearcheesOnly) {
601
+ searchees.push(createSearcheeFromDB(dbTorrent));
602
+ }
603
+ continue;
604
+ }
605
+ const files = torrent.files.map((file) => ({
606
+ name: basename(file.path),
607
+ path: file.path,
608
+ length: file.size,
609
+ }));
610
+ if (!files.length) {
611
+ logger.verbose({
612
+ label: this.label,
613
+ message: `No files found for ${torrent.name} [${sanitizeInfoHash(infoHash)}]: skipping`,
614
+ });
615
+ continue;
616
+ }
617
+ const trackers = organizeTrackers(torrent.trackers);
618
+ const title = parseTitle(name, files) ?? name;
619
+ const length = torrent.total_size;
620
+ const searchee = {
621
+ infoHash,
622
+ name,
623
+ title,
624
+ files,
625
+ length,
626
+ clientHost: this.clientHost,
627
+ savePath,
628
+ category,
629
+ trackers,
630
+ };
631
+ newSearchees.push(searchee);
632
+ searchees.push(searchee);
633
+ }
634
+ await updateSearcheeClientDB(this.clientHost, newSearchees, infoHashes);
635
+ return { searchees, newSearchees };
636
+ }
637
+ /**
638
+ * returns information needed to complete/validate injection
639
+ * @return Promise of TorrentInfo type
640
+ * @param infoHash infohash to query for in the client
641
+ * @param options options object for extra flags
642
+ * @param options.useVerbose use verbose instead of error logging
643
+ * @return Promise of TorrentInfo type
644
+ */
645
+ async getTorrentInfo(infoHash, options) {
646
+ let torrent;
647
+ try {
648
+ const params = [
649
+ [
650
+ "name",
651
+ "state",
652
+ "progress",
653
+ "save_path",
654
+ "label",
655
+ "total_remaining",
656
+ ],
657
+ { hash: infoHash },
658
+ ];
659
+ const response = (await this.call("web.update_ui", params)).unwrapOrThrow(new Error("failed to fetch the torrent list"));
660
+ if (response.torrents) {
661
+ torrent = response.torrents?.[infoHash];
662
+ }
663
+ else {
664
+ throw new Error("Client returned unexpected response (object missing)");
665
+ }
666
+ if (torrent === undefined) {
667
+ throw new Error(`Torrent not found in client (${infoHash})`);
668
+ }
669
+ const completedTorrent = (torrent.state === "Paused" &&
670
+ (torrent.progress === 100 || !torrent.total_remaining)) ||
671
+ torrent.state === "Seeding" ||
672
+ torrent.progress === 100 ||
673
+ !torrent.total_remaining;
674
+ const torrentLabel = this.isLabelEnabled && torrent.label.length != 0
675
+ ? torrent.label
676
+ : undefined;
677
+ return {
678
+ name: torrent.name,
679
+ complete: completedTorrent,
680
+ state: torrent.state,
681
+ save_path: torrent.save_path,
682
+ label: torrentLabel,
683
+ total_remaining: torrent.total_remaining,
684
+ };
685
+ }
686
+ catch (e) {
687
+ const log = options?.useVerbose ? logger.verbose : logger.error;
688
+ log({
689
+ label: this.label,
690
+ message: `Failed to fetch torrent data for ${infoHash}: ${e.message}`,
691
+ });
692
+ logger.debug(e);
693
+ throw new Error(`[${this.label}] web.update_ui: failed to fetch data from client`, {
694
+ cause: e,
695
+ });
696
+ }
697
+ }
698
+ }
699
+ //# sourceMappingURL=Deluge.js.map