@muhammedaksam/easiarr 1.3.5 → 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 (94) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +29837 -0
  3. package/package.json +18 -16
  4. package/src/VersionInfo.ts +0 -12
  5. package/src/api/arr-api.ts +0 -508
  6. package/src/api/auto-setup-types.ts +0 -64
  7. package/src/api/bazarr-api.ts +0 -356
  8. package/src/api/cloudflare-api.ts +0 -577
  9. package/src/api/custom-format-api.ts +0 -212
  10. package/src/api/grafana-api.ts +0 -314
  11. package/src/api/heimdall-api.ts +0 -209
  12. package/src/api/homarr-api.ts +0 -296
  13. package/src/api/huntarr-api.ts +0 -622
  14. package/src/api/index.ts +0 -1
  15. package/src/api/jellyfin-api.ts +0 -473
  16. package/src/api/jellyseerr-api.ts +0 -660
  17. package/src/api/maintainerr-api.ts +0 -731
  18. package/src/api/naming-config.ts +0 -93
  19. package/src/api/overseerr-api.ts +0 -513
  20. package/src/api/plex-api.ts +0 -358
  21. package/src/api/portainer-api.ts +0 -467
  22. package/src/api/profilarr-api.ts +0 -380
  23. package/src/api/prowlarr-api.ts +0 -859
  24. package/src/api/qbittorrent-api.ts +0 -339
  25. package/src/api/quality-profile-api.ts +0 -269
  26. package/src/api/tautulli-api.ts +0 -277
  27. package/src/api/uptime-kuma-api.ts +0 -342
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1255
  31. package/src/compose/caddy-config.ts +0 -129
  32. package/src/compose/generator.ts +0 -262
  33. package/src/compose/index.ts +0 -3
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -185
  36. package/src/config/bookmarks-generator.ts +0 -149
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -376
  39. package/src/config/index.ts +0 -3
  40. package/src/config/manager.ts +0 -149
  41. package/src/config/recyclarr-config.ts +0 -179
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -85
  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 -326
  48. package/src/docker/client.ts +0 -291
  49. package/src/docker/index.ts +0 -1
  50. package/src/index.ts +0 -77
  51. package/src/structure/manager.ts +0 -128
  52. package/src/ui/App.ts +0 -109
  53. package/src/ui/components/ApplicationSelector.ts +0 -272
  54. package/src/ui/components/FileEditor.ts +0 -91
  55. package/src/ui/components/FooterHint.ts +0 -119
  56. package/src/ui/components/PageLayout.ts +0 -152
  57. package/src/ui/components/UpdateNotification.ts +0 -101
  58. package/src/ui/index.ts +0 -1
  59. package/src/ui/screens/AdvancedSettings.ts +0 -181
  60. package/src/ui/screens/ApiKeyViewer.ts +0 -500
  61. package/src/ui/screens/AppConfigurator.ts +0 -731
  62. package/src/ui/screens/AppManager.ts +0 -276
  63. package/src/ui/screens/CloudflaredSetup.ts +0 -974
  64. package/src/ui/screens/ContainerControl.ts +0 -579
  65. package/src/ui/screens/FullAutoSetup.ts +0 -1719
  66. package/src/ui/screens/HomepageSetup.ts +0 -306
  67. package/src/ui/screens/JellyfinSetup.ts +0 -471
  68. package/src/ui/screens/JellyseerrSetup.ts +0 -612
  69. package/src/ui/screens/LogsViewer.ts +0 -468
  70. package/src/ui/screens/MainMenu.ts +0 -288
  71. package/src/ui/screens/MonitorDashboard.ts +0 -866
  72. package/src/ui/screens/ProwlarrSetup.ts +0 -623
  73. package/src/ui/screens/QBittorrentSetup.ts +0 -268
  74. package/src/ui/screens/QuickSetup.ts +0 -1161
  75. package/src/ui/screens/RecyclarrSetup.ts +0 -418
  76. package/src/ui/screens/SecretsEditor.ts +0 -214
  77. package/src/ui/screens/SettingsScreen.ts +0 -605
  78. package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
  79. package/src/ui/screens/index.ts +0 -4
  80. package/src/util/arch.ts +0 -78
  81. package/src/utils/browser.ts +0 -26
  82. package/src/utils/categories.ts +0 -59
  83. package/src/utils/debug.ts +0 -91
  84. package/src/utils/env.ts +0 -137
  85. package/src/utils/logs.ts +0 -118
  86. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
  87. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
  88. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
  89. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
  90. package/src/utils/migrations.ts +0 -164
  91. package/src/utils/password.ts +0 -42
  92. package/src/utils/unraid.ts +0 -101
  93. package/src/utils/update-checker.ts +0 -210
  94. package/src/utils/url-utils.ts +0 -38
@@ -1,164 +0,0 @@
1
- /**
2
- * Migration Runner
3
- * Sequelize-style migrations with Unix timestamp naming
4
- *
5
- * Migration files: src/utils/migrations/{timestamp}_{name}.ts
6
- * Each migration exports: name, up()
7
- */
8
-
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
10
- import { join } from "node:path"
11
- import { homedir } from "node:os"
12
- import { debugLog } from "./debug"
13
-
14
- /**
15
- * Get the XDG config directory for easiarr
16
- */
17
- function getXdgConfigDir(): string {
18
- const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
19
- return join(xdgConfigHome, "easiarr")
20
- }
21
-
22
- const EASIARR_DIR = getXdgConfigDir()
23
- const MIGRATIONS_FILE = join(EASIARR_DIR, ".migrations.json")
24
-
25
- interface MigrationState {
26
- applied: string[] // List of applied migration timestamps
27
- lastRun: string
28
- }
29
-
30
- interface Migration {
31
- timestamp: string
32
- name: string
33
- up: () => boolean
34
- down: () => boolean
35
- }
36
-
37
- /**
38
- * Ensure the easiarr directory exists
39
- */
40
- function ensureEasiarrDir(): void {
41
- if (!existsSync(EASIARR_DIR)) {
42
- mkdirSync(EASIARR_DIR, { recursive: true })
43
- }
44
- }
45
-
46
- /**
47
- * Get the current migration state
48
- */
49
- function getMigrationState(): MigrationState {
50
- if (!existsSync(MIGRATIONS_FILE)) {
51
- return { applied: [], lastRun: "" }
52
- }
53
-
54
- try {
55
- const content = readFileSync(MIGRATIONS_FILE, "utf-8")
56
- return JSON.parse(content) as MigrationState
57
- } catch {
58
- return { applied: [], lastRun: "" }
59
- }
60
- }
61
-
62
- /**
63
- * Save the migration state
64
- */
65
- function saveMigrationState(state: MigrationState): void {
66
- ensureEasiarrDir()
67
- state.lastRun = new Date().toISOString()
68
- writeFileSync(MIGRATIONS_FILE, JSON.stringify(state, null, 2), "utf-8")
69
- }
70
-
71
- /**
72
- * Load all migration modules
73
- */
74
- async function loadMigrations(): Promise<Migration[]> {
75
- const migrations: Migration[] = []
76
-
77
- // Import migrations directly - bundled at build time
78
- try {
79
- const m1 = await import("./migrations/1765626338_rename_env_variables")
80
- migrations.push({
81
- timestamp: "1765626338",
82
- name: m1.name,
83
- up: m1.up,
84
- down: m1.down,
85
- })
86
- } catch (e) {
87
- debugLog("Migrations", `Failed to load migration: ${e}`)
88
- }
89
-
90
- try {
91
- const m1765707135 = await import("./migrations/1765707135_rename_easiarr_status")
92
- migrations.push({
93
- timestamp: "1765707135",
94
- name: m1765707135.name,
95
- up: m1765707135.up,
96
- down: m1765707135.down,
97
- })
98
- } catch (e) {
99
- debugLog("Migrations", `Failed to load migration: ${e}`)
100
- }
101
-
102
- try {
103
- const m1765732722 = await import("./migrations/1765732722_remove_cloudflare_dns_api_token")
104
- migrations.push({
105
- timestamp: "1765732722",
106
- name: m1765732722.name,
107
- up: m1765732722.up,
108
- down: m1765732722.down,
109
- })
110
- } catch (e) {
111
- debugLog("Migrations", `Failed to load migration: ${e}`)
112
- }
113
-
114
- try {
115
- const m1769515650 = await import("./migrations/1769515650_migrate_to_xdg")
116
- migrations.push({
117
- timestamp: "1769515650",
118
- name: m1769515650.name,
119
- up: m1769515650.up,
120
- down: m1769515650.down,
121
- })
122
- } catch (e) {
123
- debugLog("Migrations", `Failed to load migration: ${e}`)
124
- }
125
-
126
- // Add future migrations here:
127
- // const m2 = await import("./migrations/1734xxxxxx_xxx")
128
- // migrations.push({ timestamp: "...", name: m2.name, up: m2.up })
129
-
130
- return migrations.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
131
- }
132
-
133
- /**
134
- * Run all pending migrations
135
- */
136
- export async function runMigrations(): Promise<void> {
137
- const state = getMigrationState()
138
- const migrations = await loadMigrations()
139
-
140
- let hasChanges = false
141
-
142
- for (const migration of migrations) {
143
- if (state.applied.includes(migration.timestamp)) {
144
- continue
145
- }
146
-
147
- debugLog("Migrations", `Running migration ${migration.timestamp}_${migration.name}`)
148
-
149
- try {
150
- migration.up()
151
- state.applied.push(migration.timestamp)
152
- hasChanges = true
153
- debugLog("Migrations", `Migration ${migration.timestamp} completed`)
154
- } catch (e) {
155
- debugLog("Migrations", `Migration ${migration.timestamp} failed: ${e}`)
156
- }
157
- }
158
-
159
- if (hasChanges) {
160
- saveMigrationState(state)
161
- } else {
162
- debugLog("Migrations", "No pending migrations")
163
- }
164
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Password Utilities
3
- * Helpers for password validation and transformation
4
- */
5
-
6
- /**
7
- * Ensures a password meets minimum length requirements by repeating it.
8
- * If the password is already long enough, returns it unchanged.
9
- *
10
- * @param password - The original password
11
- * @param minLength - Minimum required length
12
- * @returns Password padded to at least minLength characters
13
- */
14
- export function ensureMinPasswordLength(password: string, minLength: number): string {
15
- if (!password || password.length === 0) {
16
- throw new Error("Password cannot be empty")
17
- }
18
-
19
- if (password.length >= minLength) {
20
- return password
21
- }
22
-
23
- // Repeat the password until it meets minimum length
24
- let extendedPassword = password
25
- while (extendedPassword.length < minLength) {
26
- extendedPassword += password
27
- }
28
-
29
- // Return exactly minLength characters
30
- return extendedPassword.slice(0, minLength)
31
- }
32
-
33
- /**
34
- * Validates if a password meets minimum length requirements.
35
- *
36
- * @param password - The password to validate
37
- * @param minLength - Minimum required length
38
- * @returns true if password meets requirements
39
- */
40
- export function isPasswordValid(password: string, minLength: number): boolean {
41
- return password !== undefined && password !== null && password.length >= minLength
42
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Unraid Detection and Compatibility Utilities
3
- * Provides detection of Unraid environment and path adjustments
4
- */
5
-
6
- import { existsSync } from "node:fs"
7
- import { homedir } from "node:os"
8
- import { join } from "node:path"
9
- import { debugLog } from "./debug"
10
-
11
- // Unraid-specific paths
12
- const UNRAID_IDENTIFIERS = [
13
- "/boot/config/plugins", // Unraid plugin directory
14
- "/etc/unraid-version", // Unraid version file
15
- "/var/local/emhttp", // Unraid emhttp directory
16
- ]
17
-
18
- const UNRAID_APPDATA_PATH = "/mnt/user/appdata"
19
- const COMPOSE_MANAGER_PLUGIN_PATH = "/boot/config/plugins/compose.manager"
20
-
21
- /**
22
- * Detect if running on Unraid OS
23
- * Checks for Unraid-specific filesystem paths
24
- */
25
- export function isUnraid(): boolean {
26
- for (const path of UNRAID_IDENTIFIERS) {
27
- if (existsSync(path)) {
28
- debugLog("Unraid", `Detected Unraid OS (found ${path})`)
29
- return true
30
- }
31
- }
32
- return false
33
- }
34
-
35
- /**
36
- * Get the default appdata path for Unraid
37
- * Returns /mnt/user/appdata/easiarr on Unraid, ~/.easiarr otherwise
38
- */
39
- export function getUnraidAppDataPath(): string {
40
- if (isUnraid()) {
41
- return join(UNRAID_APPDATA_PATH, "easiarr")
42
- }
43
- return join(homedir(), ".easiarr")
44
- }
45
-
46
- /**
47
- * Check if Docker Compose Manager plugin is installed
48
- * This plugin allows managing compose stacks via Unraid's web UI
49
- */
50
- export function hasComposeManager(): boolean {
51
- return existsSync(COMPOSE_MANAGER_PLUGIN_PATH)
52
- }
53
-
54
- /**
55
- * Get the recommended Compose Manager project directory
56
- * Compose Manager expects projects in /boot/config/plugins/compose.manager/projects/
57
- */
58
- export function getComposeManagerProjectPath(projectName: string = "easiarr"): string {
59
- return join(COMPOSE_MANAGER_PLUGIN_PATH, "projects", projectName)
60
- }
61
-
62
- /**
63
- * Get platform-appropriate default root directory
64
- * Adjusts paths for Unraid vs standard Linux/Mac
65
- */
66
- export function getDefaultRootDir(): string {
67
- if (isUnraid()) {
68
- // On Unraid, use /mnt/user/data for media and /mnt/user/appdata for configs
69
- return "/mnt/user/data"
70
- }
71
- // Standard path
72
- return join(homedir(), "easiarr")
73
- }
74
-
75
- /**
76
- * Get platform-appropriate config directory
77
- */
78
- export function getConfigDir(): string {
79
- if (isUnraid()) {
80
- return UNRAID_APPDATA_PATH
81
- }
82
- return join(homedir(), ".easiarr")
83
- }
84
-
85
- /**
86
- * Get Unraid-specific information for display
87
- */
88
- export function getUnraidInfo(): {
89
- isUnraid: boolean
90
- hasComposeManager: boolean
91
- appDataPath: string
92
- composeManagerPath: string | null
93
- } {
94
- const onUnraid = isUnraid()
95
- return {
96
- isUnraid: onUnraid,
97
- hasComposeManager: onUnraid ? hasComposeManager() : false,
98
- appDataPath: onUnraid ? UNRAID_APPDATA_PATH : join(homedir(), ".easiarr"),
99
- composeManagerPath: onUnraid && hasComposeManager() ? getComposeManagerProjectPath() : null,
100
- }
101
- }
@@ -1,210 +0,0 @@
1
- /**
2
- * Update Checker
3
- * Check GitHub releases for new easiarr versions
4
- */
5
-
6
- import { VersionInfo } from "../VersionInfo"
7
- import { existsSync, readFileSync, writeFileSync } from "node:fs"
8
- import { join } from "node:path"
9
- import { homedir } from "node:os"
10
-
11
- const GITHUB_REPO = "muhammedaksam/easiarr"
12
- const CACHE_FILE = join(homedir(), ".easiarr", ".update-cache.json")
13
- const CACHE_DURATION_MS = 24 * 60 * 60 * 1000 // 24 hours
14
-
15
- export interface UpdateInfo {
16
- currentVersion: string
17
- latestVersion: string
18
- updateAvailable: boolean
19
- releaseUrl: string
20
- releaseNotes?: string
21
- publishedAt?: string
22
- }
23
-
24
- interface CachedUpdate {
25
- checkedAt: string
26
- latestVersion: string
27
- releaseUrl: string
28
- releaseNotes?: string
29
- publishedAt?: string
30
- }
31
-
32
- /**
33
- * Get cached update info if still valid
34
- */
35
- function getCachedUpdate(): CachedUpdate | null {
36
- if (!existsSync(CACHE_FILE)) return null
37
-
38
- try {
39
- const content = readFileSync(CACHE_FILE, "utf-8")
40
- const cached = JSON.parse(content) as CachedUpdate
41
-
42
- const checkedAt = new Date(cached.checkedAt).getTime()
43
- const now = Date.now()
44
-
45
- // Cache still valid
46
- if (now - checkedAt < CACHE_DURATION_MS) {
47
- return cached
48
- }
49
- } catch {
50
- // Ignore cache errors
51
- }
52
-
53
- return null
54
- }
55
-
56
- /**
57
- * Save update info to cache
58
- */
59
- function cacheUpdate(info: Omit<CachedUpdate, "checkedAt">): void {
60
- try {
61
- const cached: CachedUpdate = {
62
- ...info,
63
- checkedAt: new Date().toISOString(),
64
- }
65
- writeFileSync(CACHE_FILE, JSON.stringify(cached, null, 2), "utf-8")
66
- } catch {
67
- // Ignore cache write errors
68
- }
69
- }
70
-
71
- /**
72
- * Compare semver versions
73
- * Returns true if v2 is newer than v1
74
- */
75
- function isNewerVersion(v1: string, v2: string): boolean {
76
- const normalize = (v: string) => v.replace(/^v/, "")
77
- const parts1 = normalize(v1).split(".").map(Number)
78
- const parts2 = normalize(v2).split(".").map(Number)
79
-
80
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
81
- const p1 = parts1[i] || 0
82
- const p2 = parts2[i] || 0
83
- if (p2 > p1) return true
84
- if (p2 < p1) return false
85
- }
86
-
87
- return false
88
- }
89
-
90
- /**
91
- * Fetch latest release from GitHub API
92
- */
93
- async function fetchLatestRelease(): Promise<{
94
- version: string
95
- url: string
96
- notes?: string
97
- publishedAt?: string
98
- } | null> {
99
- try {
100
- const response = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, {
101
- headers: {
102
- Accept: "application/vnd.github.v3+json",
103
- "User-Agent": "easiarr-update-checker",
104
- },
105
- })
106
-
107
- if (!response.ok) {
108
- return null
109
- }
110
-
111
- const data = (await response.json()) as {
112
- tag_name: string
113
- html_url: string
114
- body?: string
115
- published_at?: string
116
- }
117
-
118
- return {
119
- version: data.tag_name,
120
- url: data.html_url,
121
- notes: data.body,
122
- publishedAt: data.published_at,
123
- }
124
- } catch {
125
- return null
126
- }
127
- }
128
-
129
- /**
130
- * Check for updates
131
- * Uses cache to avoid repeated API calls
132
- */
133
- export async function checkForUpdates(): Promise<UpdateInfo> {
134
- const currentVersion = VersionInfo.version
135
-
136
- // Check cache first
137
- const cached = getCachedUpdate()
138
- if (cached) {
139
- return {
140
- currentVersion,
141
- latestVersion: cached.latestVersion,
142
- updateAvailable: isNewerVersion(currentVersion, cached.latestVersion),
143
- releaseUrl: cached.releaseUrl,
144
- releaseNotes: cached.releaseNotes,
145
- publishedAt: cached.publishedAt,
146
- }
147
- }
148
-
149
- // Fetch from GitHub
150
- const release = await fetchLatestRelease()
151
-
152
- if (!release) {
153
- return {
154
- currentVersion,
155
- latestVersion: currentVersion,
156
- updateAvailable: false,
157
- releaseUrl: `https://github.com/${GITHUB_REPO}/releases`,
158
- }
159
- }
160
-
161
- // Cache the result
162
- cacheUpdate({
163
- latestVersion: release.version,
164
- releaseUrl: release.url,
165
- releaseNotes: release.notes,
166
- publishedAt: release.publishedAt,
167
- })
168
-
169
- return {
170
- currentVersion,
171
- latestVersion: release.version,
172
- updateAvailable: isNewerVersion(currentVersion, release.version),
173
- releaseUrl: release.url,
174
- releaseNotes: release.notes,
175
- publishedAt: release.publishedAt,
176
- }
177
- }
178
-
179
- /**
180
- * Force refresh update check (ignores cache)
181
- */
182
- export async function forceCheckForUpdates(): Promise<UpdateInfo> {
183
- const currentVersion = VersionInfo.version
184
- const release = await fetchLatestRelease()
185
-
186
- if (!release) {
187
- return {
188
- currentVersion,
189
- latestVersion: currentVersion,
190
- updateAvailable: false,
191
- releaseUrl: `https://github.com/${GITHUB_REPO}/releases`,
192
- }
193
- }
194
-
195
- cacheUpdate({
196
- latestVersion: release.version,
197
- releaseUrl: release.url,
198
- releaseNotes: release.notes,
199
- publishedAt: release.publishedAt,
200
- })
201
-
202
- return {
203
- currentVersion,
204
- latestVersion: release.version,
205
- updateAvailable: isNewerVersion(currentVersion, release.version),
206
- releaseUrl: release.url,
207
- releaseNotes: release.notes,
208
- publishedAt: release.publishedAt,
209
- }
210
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * URL Utilities
3
- * Functions for generating app URLs based on Traefik configuration
4
- */
5
-
6
- import type { EasiarrConfig } from "../config/schema"
7
- import { getLocalIp } from "./env"
8
-
9
- /**
10
- * Get the local URL for an app (http://LOCAL_IP:PORT)
11
- */
12
- export function getLocalAppUrl(port: number): string {
13
- const localIp = getLocalIp()
14
- return `http://${localIp}:${port}`
15
- }
16
-
17
- /**
18
- * Get the external URL for an app (https://APP.DOMAIN)
19
- * Returns null if Traefik is not enabled
20
- */
21
- export function getExternalAppUrl(appId: string, config: EasiarrConfig): string | null {
22
- if (!config.traefik?.enabled || !config.traefik.domain) {
23
- return null
24
- }
25
- return `https://${appId}.${config.traefik.domain}`
26
- }
27
-
28
- /**
29
- * Get the appropriate applicationUrl based on Traefik status
30
- * Returns external URL if Traefik enabled, otherwise local URL
31
- */
32
- export function getApplicationUrl(appId: string, port: number, config: EasiarrConfig): string {
33
- const externalUrl = getExternalAppUrl(appId, config)
34
- if (externalUrl) {
35
- return externalUrl
36
- }
37
- return getLocalAppUrl(port)
38
- }