@muhammedaksam/easiarr 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/index.js +29837 -0
  2. package/package.json +15 -15
  3. package/src/VersionInfo.ts +0 -12
  4. package/src/api/arr-api.ts +0 -521
  5. package/src/api/auto-setup-types.ts +0 -64
  6. package/src/api/base-api.ts +0 -216
  7. package/src/api/bazarr-api.ts +0 -297
  8. package/src/api/cloudflare-api.ts +0 -493
  9. package/src/api/custom-format-api.ts +0 -224
  10. package/src/api/grafana-api.ts +0 -230
  11. package/src/api/heimdall-api.ts +0 -141
  12. package/src/api/homarr-api.ts +0 -209
  13. package/src/api/huntarr-api.ts +0 -457
  14. package/src/api/index.ts +0 -9
  15. package/src/api/jellyfin-api.ts +0 -389
  16. package/src/api/jellyseerr-api.ts +0 -559
  17. package/src/api/maintainerr-api.ts +0 -461
  18. package/src/api/naming-config.ts +0 -101
  19. package/src/api/overseerr-api.ts +0 -392
  20. package/src/api/plex-api.ts +0 -308
  21. package/src/api/portainer-api.ts +0 -374
  22. package/src/api/profilarr-api.ts +0 -391
  23. package/src/api/prowlarr-api.ts +0 -488
  24. package/src/api/qbittorrent-api.ts +0 -300
  25. package/src/api/quality-profile-api.ts +0 -272
  26. package/src/api/tautulli-api.ts +0 -244
  27. package/src/api/uptime-kuma-api.ts +0 -343
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1275
  31. package/src/compose/caddy-config.ts +0 -130
  32. package/src/compose/generator.ts +0 -287
  33. package/src/compose/index.ts +0 -7
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -187
  36. package/src/config/bookmarks-generator.ts +0 -160
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -381
  39. package/src/config/index.ts +0 -6
  40. package/src/config/manager.ts +0 -150
  41. package/src/config/recyclarr-config.ts +0 -181
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -86
  44. package/src/config/soularr-config.ts +0 -105
  45. package/src/config/trash-quality-definitions.ts +0 -213
  46. package/src/data/lidarr-custom-formats.ts +0 -127
  47. package/src/data/trash-profiles.ts +0 -333
  48. package/src/docker/client.ts +0 -312
  49. package/src/docker/index.ts +0 -18
  50. package/src/index.ts +0 -77
  51. package/src/setup/actions/arr-common.ts +0 -307
  52. package/src/setup/actions/dashboards.ts +0 -79
  53. package/src/setup/actions/download-clients.ts +0 -54
  54. package/src/setup/actions/jellyseerr.ts +0 -331
  55. package/src/setup/actions/media-servers.ts +0 -68
  56. package/src/setup/actions/monitoring.ts +0 -120
  57. package/src/setup/actions/prowlarr.ts +0 -137
  58. package/src/setup/actions/utilities.ts +0 -490
  59. package/src/setup/index.ts +0 -78
  60. package/src/setup/types.ts +0 -144
  61. package/src/structure/manager.ts +0 -129
  62. package/src/ui/App.ts +0 -111
  63. package/src/ui/components/ApplicationSelector.ts +0 -275
  64. package/src/ui/components/CredentialsForm.ts +0 -180
  65. package/src/ui/components/DownloadClientForm.ts +0 -114
  66. package/src/ui/components/FileEditor.ts +0 -91
  67. package/src/ui/components/FooterHint.ts +0 -119
  68. package/src/ui/components/PageLayout.ts +0 -160
  69. package/src/ui/components/UpdateNotification.ts +0 -102
  70. package/src/ui/index.ts +0 -7
  71. package/src/ui/screens/AdvancedSettings.ts +0 -188
  72. package/src/ui/screens/ApiKeyViewer.ts +0 -511
  73. package/src/ui/screens/AppConfigurator.ts +0 -543
  74. package/src/ui/screens/AppManager.ts +0 -284
  75. package/src/ui/screens/ContainerControl.ts +0 -581
  76. package/src/ui/screens/LogsViewer.ts +0 -478
  77. package/src/ui/screens/MainMenu.ts +0 -299
  78. package/src/ui/screens/MonitorDashboard.ts +0 -881
  79. package/src/ui/screens/SecretsEditor.ts +0 -216
  80. package/src/ui/screens/SettingsScreen.ts +0 -633
  81. package/src/ui/screens/index.ts +0 -22
  82. package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
  83. package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
  84. package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
  85. package/src/ui/screens/setup/HomepageSetup.ts +0 -165
  86. package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
  87. package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
  88. package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
  89. package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
  90. package/src/ui/screens/setup/QuickSetup.ts +0 -1169
  91. package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
  92. package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
  93. package/src/utils/arch.ts +0 -78
  94. package/src/utils/browser.ts +0 -27
  95. package/src/utils/categories.ts +0 -61
  96. package/src/utils/debug.ts +0 -84
  97. package/src/utils/env.ts +0 -138
  98. package/src/utils/index.ts +0 -8
  99. package/src/utils/logs.ts +0 -119
  100. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
  101. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
  102. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
  103. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
  104. package/src/utils/migrations.ts +0 -165
  105. package/src/utils/password.ts +0 -42
  106. package/src/utils/paths.ts +0 -33
  107. package/src/utils/unraid.ts +0 -103
  108. package/src/utils/update-checker.ts +0 -210
  109. package/src/utils/url-utils.ts +0 -38
@@ -1,543 +0,0 @@
1
- /**
2
- * App Configurator Screen
3
- * Configures *arr apps via API - sets root folders and download clients
4
- */
5
-
6
- import { BoxRenderable, CliRenderer, KeyEvent, TextRenderable } from "@opentui/core"
7
-
8
- import { ArrApiClient, createQBittorrentConfig, createSABnzbdConfig } from "~/api/arr-api"
9
- import { QBittorrentClient } from "~/api/qbittorrent-api"
10
- import { getApp } from "~/apps/registry"
11
- import { AppId, EasiarrConfig } from "~/config/schema"
12
- import { CredentialsForm, CredentialsFormResult } from "~/ui/components/CredentialsForm"
13
- import { DownloadClientForm, DownloadClientFormResult } from "~/ui/components/DownloadClientForm"
14
- import { createPageLayout } from "~/ui/components/PageLayout"
15
- import { getCategoriesForApps } from "~/utils/categories"
16
- import { readEnvSync, updateEnv } from "~/utils/env"
17
-
18
- interface ConfigResult {
19
- appId: AppId
20
- appName: string
21
- status: "pending" | "configuring" | "success" | "error" | "skipped"
22
- message?: string
23
- }
24
-
25
- type Step = "credentials" | "configure" | "qbittorrent" | "sabnzbd" | "done"
26
-
27
- export class AppConfigurator extends BoxRenderable {
28
- private config: EasiarrConfig
29
- private cliRenderer: CliRenderer
30
- private keyHandler!: (key: KeyEvent) => void
31
- private results: ConfigResult[] = []
32
- private currentStep: Step = "credentials"
33
- private contentBox!: BoxRenderable
34
- private pageContainer!: BoxRenderable
35
-
36
- // Global *arr credentials
37
- private globalUsername = "admin"
38
- private globalPassword = ""
39
- private globalEmail = ""
40
- private overrideExisting = false
41
-
42
- // Download client credentials
43
- private qbHost = "qbittorrent"
44
- private qbPort = 8080
45
- private qbUser = "admin"
46
- private qbPass = ""
47
- private sabHost = "sabnzbd"
48
- private sabPort = 8080
49
- private sabApiKey = ""
50
-
51
- // Check which download clients are enabled
52
- private hasQBittorrent = false
53
- private hasSABnzbd = false
54
-
55
- constructor(
56
- renderer: CliRenderer,
57
- config: EasiarrConfig,
58
- private onBack: () => void
59
- ) {
60
- super(renderer, {
61
- id: "app-configurator",
62
- width: "100%",
63
- height: "100%",
64
- backgroundColor: "#111111",
65
- zIndex: 200,
66
- })
67
- this.cliRenderer = renderer
68
- this.config = config
69
-
70
- // Check enabled download clients
71
- this.hasQBittorrent = config.apps.some((a) => a.id === "qbittorrent" && a.enabled)
72
- this.hasSABnzbd = config.apps.some((a) => a.id === "sabnzbd" && a.enabled)
73
-
74
- // Load saved credentials from .env
75
- this.loadSavedCredentials()
76
-
77
- // Start with credentials prompt
78
- this.renderCredentialsPrompt()
79
- }
80
-
81
- private loadSavedCredentials() {
82
- const env = readEnvSync()
83
- if (env.USERNAME_GLOBAL) this.globalUsername = env.USERNAME_GLOBAL
84
- this.globalPassword = env.PASSWORD_GLOBAL || "Ch4ng3m3!1234securityReasons"
85
- if (env.EMAIL_GLOBAL) this.globalEmail = env.EMAIL_GLOBAL
86
- if (env.PASSWORD_QBITTORRENT) this.qbPass = env.PASSWORD_QBITTORRENT
87
- if (env.API_KEY_SABNZBD) this.sabApiKey = env.API_KEY_SABNZBD
88
- }
89
-
90
- private renderCredentialsPrompt() {
91
- this.clear()
92
-
93
- const { container, content } = createPageLayout(this.cliRenderer, {
94
- title: "Configure Apps",
95
- stepInfo: "Global Credentials",
96
- footerHint: [
97
- { type: "key", key: "Tab", value: "Cycle Fields/Shortcuts" },
98
- { type: "key", key: "O", value: "Override" },
99
- { type: "key", key: "Enter", value: "Continue" },
100
- { type: "key", key: "Esc", value: "Skip" },
101
- ],
102
- })
103
- this.pageContainer = container
104
- this.add(container)
105
-
106
- const form = new CredentialsForm(
107
- this.cliRenderer,
108
- {
109
- title: "Set a global username/password for all *arr applications:",
110
- initialUsername: this.globalUsername,
111
- initialPassword: this.globalPassword,
112
- initialEmail: this.globalEmail,
113
- showEmail: true,
114
- showOverride: true,
115
- initialOverride: this.overrideExisting,
116
- },
117
- (result: CredentialsFormResult) => {
118
- this.globalUsername = result.username
119
- this.globalPassword = result.password
120
- this.globalEmail = result.email
121
- this.overrideExisting = result.override
122
- this.saveGlobalCredentialsToEnv()
123
- this.currentStep = "configure"
124
- this.runConfiguration()
125
- },
126
- () => {
127
- // Cancelled - skip credentials
128
- this.currentStep = "configure"
129
- this.runConfiguration()
130
- }
131
- )
132
- content.add(form)
133
- }
134
-
135
- private async saveGlobalCredentialsToEnv() {
136
- try {
137
- const updates: Record<string, string> = {}
138
- if (this.globalUsername) updates.USERNAME_GLOBAL = this.globalUsername
139
- if (this.globalPassword) updates.PASSWORD_GLOBAL = this.globalPassword
140
- if (this.globalEmail) updates.EMAIL_GLOBAL = this.globalEmail
141
- await updateEnv(updates)
142
- } catch {
143
- // Ignore errors - not critical
144
- }
145
- }
146
-
147
- private async runConfiguration() {
148
- // Initialize results for apps that have rootFolder
149
- for (const appConfig of this.config.apps) {
150
- if (!appConfig.enabled) continue
151
- const appDef = getApp(appConfig.id)
152
- if (!appDef?.rootFolder) continue
153
-
154
- this.results.push({
155
- appId: appConfig.id,
156
- appName: appDef.name,
157
- status: "pending",
158
- })
159
- }
160
-
161
- this.renderConfigProgress()
162
-
163
- // Configure each app
164
- for (let i = 0; i < this.results.length; i++) {
165
- const result = this.results[i]
166
- result.status = "configuring"
167
- this.updateDisplay()
168
- await this.configureApp(result.appId)
169
- }
170
-
171
- // After all apps configured, handle download clients
172
- if (this.hasQBittorrent) {
173
- this.currentStep = "qbittorrent"
174
- this.renderQBittorrentPrompt()
175
- } else if (this.hasSABnzbd) {
176
- this.currentStep = "sabnzbd"
177
- this.renderSABnzbdPrompt()
178
- } else {
179
- this.currentStep = "done"
180
- this.renderDone()
181
- }
182
- }
183
-
184
- private async configureApp(appId: AppId) {
185
- const result = this.results.find((r) => r.appId === appId)
186
- if (!result) return
187
-
188
- const appDef = getApp(appId)
189
- if (!appDef) {
190
- result.status = "error"
191
- result.message = "Unknown app"
192
- return
193
- }
194
-
195
- // Get API key from env
196
- const apiKey = this.extractApiKey(appId)
197
- if (!apiKey) {
198
- result.status = "skipped"
199
- result.message = "No API key"
200
- return
201
- }
202
-
203
- try {
204
- const appConfig = this.config.apps.find((a) => a.id === appId)
205
- const port = appConfig?.port ?? appDef.defaultPort
206
- const client = new ArrApiClient(appId, port, apiKey)
207
-
208
- // Check current root folder
209
- const rootFolders = await client.getRootFolders()
210
- const targetPath = appDef.rootFolder?.path
211
-
212
- // Check if already configured properly
213
- const hasCorrectRoot = rootFolders.some((rf) => rf.path === targetPath)
214
- if (hasCorrectRoot && !this.overrideExisting) {
215
- result.status = "success"
216
- result.message = "Already configured"
217
- return
218
- }
219
-
220
- // Add root folder if missing
221
- if (!hasCorrectRoot && targetPath) {
222
- await client.addRootFolder(targetPath)
223
- }
224
-
225
- result.status = "success"
226
- result.message = "Root folder set"
227
- } catch (error) {
228
- result.status = "error"
229
- result.message = String(error).substring(0, 50)
230
- }
231
- }
232
-
233
- private extractApiKey(appId: AppId): string | null {
234
- const envKey = `API_KEY_${appId.toUpperCase()}`
235
- return readEnvSync()[envKey] ?? null
236
- }
237
-
238
- private getEnabledCategories(): { name: string; savePath: string }[] {
239
- const enabledAppIds = this.config.apps.filter((a) => a.enabled).map((a) => a.id)
240
- return getCategoriesForApps(enabledAppIds)
241
- }
242
-
243
- private renderConfigProgress() {
244
- this.clear()
245
-
246
- const { container, content } = createPageLayout(this.cliRenderer, {
247
- title: "Configure Apps",
248
- stepInfo: "Setting up root folders",
249
- footerHint: [{ type: "text", value: "Please wait..." }],
250
- })
251
- this.pageContainer = container
252
- this.contentBox = content
253
- this.add(container)
254
-
255
- this.updateDisplay()
256
- }
257
-
258
- private updateDisplay() {
259
- // Clear content and rebuild
260
- const contentChildren = [...this.contentBox.getChildren()]
261
- for (const child of contentChildren) {
262
- if (child.id) {
263
- try {
264
- this.contentBox.remove(child.id)
265
- } catch {
266
- /* ignore */
267
- }
268
- }
269
- }
270
-
271
- // Header
272
- this.contentBox.add(
273
- new TextRenderable(this.cliRenderer, {
274
- content: "Configuring *arr applications...\n",
275
- fg: "#4a9eff",
276
- })
277
- )
278
-
279
- // Results
280
- for (const result of this.results) {
281
- let icon = "○"
282
- let color = "#6272a4"
283
-
284
- switch (result.status) {
285
- case "configuring":
286
- icon = "◉"
287
- color = "#f1fa8c"
288
- break
289
- case "success":
290
- icon = "✓"
291
- color = "#50fa7b"
292
- break
293
- case "error":
294
- icon = "✗"
295
- color = "#ff5555"
296
- break
297
- case "skipped":
298
- icon = "⏭"
299
- color = "#6272a4"
300
- break
301
- }
302
-
303
- const message = result.message ? ` - ${result.message}` : ""
304
- this.contentBox.add(
305
- new TextRenderable(this.cliRenderer, {
306
- content: `${icon} ${result.appName}${message}`,
307
- fg: color,
308
- })
309
- )
310
- }
311
- }
312
-
313
- private renderQBittorrentPrompt() {
314
- this.clear()
315
-
316
- const { container, content } = createPageLayout(this.cliRenderer, {
317
- title: "Configure Apps",
318
- stepInfo: "qBittorrent Credentials",
319
- footerHint: [
320
- { type: "text", value: "Enter credentials from qBittorrent WebUI" },
321
- { type: "key", key: "Esc", value: "Skip" },
322
- ],
323
- })
324
- this.pageContainer = container
325
- this.add(container)
326
-
327
- const form = new DownloadClientForm(
328
- this.cliRenderer,
329
- { type: "qbittorrent", initialValue: this.qbPass },
330
- (result: DownloadClientFormResult) => {
331
- this.qbPass = result.value
332
- this.addDownloadClients("qbittorrent")
333
- },
334
- () => {
335
- // Cancelled
336
- if (this.hasSABnzbd) {
337
- this.currentStep = "sabnzbd"
338
- this.renderSABnzbdPrompt()
339
- } else {
340
- this.currentStep = "done"
341
- this.renderDone()
342
- }
343
- }
344
- )
345
- content.add(form)
346
- }
347
-
348
- private renderSABnzbdPrompt() {
349
- this.clear()
350
-
351
- const { container, content } = createPageLayout(this.cliRenderer, {
352
- title: "Configure Apps",
353
- stepInfo: "SABnzbd Credentials",
354
- footerHint: [
355
- { type: "text", value: "Enter API key from SABnzbd Config → General" },
356
- { type: "key", key: "Esc", value: "Skip" },
357
- ],
358
- })
359
- this.pageContainer = container
360
- this.add(container)
361
-
362
- const form = new DownloadClientForm(
363
- this.cliRenderer,
364
- { type: "sabnzbd", initialValue: this.sabApiKey },
365
- (result: DownloadClientFormResult) => {
366
- this.sabApiKey = result.value
367
- this.addDownloadClients("sabnzbd")
368
- },
369
- () => {
370
- // Cancelled
371
- this.currentStep = "done"
372
- this.renderDone()
373
- }
374
- )
375
- content.add(form)
376
- }
377
-
378
- private async addDownloadClients(type: "qbittorrent" | "sabnzbd") {
379
- // Configure qBittorrent settings via its API first
380
- if (type === "qbittorrent") {
381
- try {
382
- const qbClient = new QBittorrentClient(this.qbHost, this.qbPort, this.qbUser, this.qbPass)
383
- await qbClient.login()
384
-
385
- // Create categories for enabled *arr apps
386
- const categories = this.getEnabledCategories()
387
- for (const cat of categories) {
388
- await qbClient.createCategory(cat.name, cat.savePath)
389
- }
390
- } catch {
391
- // QB may not be ready or credentials wrong - continue anyway
392
- }
393
- }
394
-
395
- // Save credentials to .env
396
- await this.saveCredentialsToEnv(type)
397
-
398
- // Add download clients to *arr apps
399
- for (const result of this.results) {
400
- if (result.status !== "success") continue
401
-
402
- const apiKey = this.extractApiKey(result.appId)
403
- if (!apiKey) continue
404
-
405
- const appDef = getApp(result.appId)
406
- const appConfig = this.config.apps.find((a) => a.id === result.appId)
407
- const port = appConfig?.port ?? appDef?.defaultPort ?? 8989
408
-
409
- try {
410
- const client = new ArrApiClient(result.appId, port, apiKey)
411
-
412
- if (type === "qbittorrent") {
413
- const qbConfig = createQBittorrentConfig(
414
- this.qbHost,
415
- this.qbPort,
416
- this.qbUser,
417
- this.qbPass,
418
- result.appId
419
- )
420
- await client.addDownloadClient(qbConfig)
421
- } else {
422
- const sabConfig = createSABnzbdConfig(this.sabHost, this.sabPort, this.sabApiKey)
423
- await client.addDownloadClient(sabConfig)
424
- }
425
- } catch {
426
- // Continue - download client may fail but app is still configured
427
- }
428
- }
429
-
430
- // Move to next step
431
- if (type === "qbittorrent" && this.hasSABnzbd) {
432
- this.currentStep = "sabnzbd"
433
- this.renderSABnzbdPrompt()
434
- } else {
435
- this.currentStep = "done"
436
- this.renderDone()
437
- }
438
- }
439
-
440
- private async saveCredentialsToEnv(type: "qbittorrent" | "sabnzbd") {
441
- try {
442
- const updates: Record<string, string> = {}
443
- if (type === "qbittorrent" && this.qbPass) {
444
- updates.PASSWORD_QBITTORRENT = this.qbPass
445
- } else if (type === "sabnzbd" && this.sabApiKey) {
446
- updates.API_KEY_SABNZBD = this.sabApiKey
447
- }
448
- await updateEnv(updates)
449
- } catch {
450
- // Ignore errors - not critical
451
- }
452
- }
453
-
454
- private renderDone() {
455
- this.clear()
456
-
457
- const { container, content } = createPageLayout(this.cliRenderer, {
458
- title: "Configure Apps",
459
- stepInfo: "Complete",
460
- footerHint: [{ type: "text", value: "Press any key to return" }],
461
- })
462
- this.add(container)
463
-
464
- const successCount = this.results.filter((r) => r.status === "success").length
465
- const errorCount = this.results.filter((r) => r.status === "error").length
466
-
467
- content.add(
468
- new TextRenderable(this.cliRenderer, {
469
- content: "Configuration complete!\n",
470
- fg: "#50fa7b",
471
- })
472
- )
473
-
474
- content.add(
475
- new TextRenderable(this.cliRenderer, {
476
- content: `✓ ${successCount} app(s) configured`,
477
- fg: "#50fa7b",
478
- })
479
- )
480
-
481
- if (errorCount > 0) {
482
- content.add(
483
- new TextRenderable(this.cliRenderer, {
484
- content: `✗ ${errorCount} app(s) had errors (see above)`,
485
- fg: "#ff5555",
486
- })
487
- )
488
- }
489
-
490
- content.add(new BoxRenderable(this.cliRenderer, { width: 1, height: 1 }))
491
-
492
- // Show results summary
493
- for (const result of this.results) {
494
- const icon = result.status === "success" ? "✓" : result.status === "skipped" ? "⏭" : "✗"
495
- const color =
496
- result.status === "success"
497
- ? "#50fa7b"
498
- : result.status === "skipped"
499
- ? "#6272a4"
500
- : "#ff5555"
501
- const message = result.message ? ` - ${result.message}` : ""
502
-
503
- content.add(
504
- new TextRenderable(this.cliRenderer, {
505
- content: `${icon} ${result.appName}${message}`,
506
- fg: color,
507
- })
508
- )
509
- }
510
-
511
- this.keyHandler = () => {
512
- this.destroy()
513
- this.onBack()
514
- }
515
- this.cliRenderer.keyInput.on("keypress", this.keyHandler)
516
- }
517
-
518
- private clear() {
519
- const children = [...this.getChildren()]
520
- for (const child of children) {
521
- if (child.id) {
522
- try {
523
- this.remove(child.id)
524
- } catch {
525
- /* ignore */
526
- }
527
- }
528
- }
529
- }
530
-
531
- public destroy() {
532
- if (this.keyHandler) {
533
- this.cliRenderer.keyInput.off("keypress", this.keyHandler)
534
- }
535
- if (this.parent && this.id) {
536
- try {
537
- this.parent.remove(this.id)
538
- } catch {
539
- /* ignore */
540
- }
541
- }
542
- }
543
- }