@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.
- package/README.md +8 -5
- package/dist/index.js +29837 -0
- package/package.json +18 -16
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -508
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/bazarr-api.ts +0 -356
- package/src/api/cloudflare-api.ts +0 -577
- package/src/api/custom-format-api.ts +0 -212
- package/src/api/grafana-api.ts +0 -314
- package/src/api/heimdall-api.ts +0 -209
- package/src/api/homarr-api.ts +0 -296
- package/src/api/huntarr-api.ts +0 -622
- package/src/api/index.ts +0 -1
- package/src/api/jellyfin-api.ts +0 -473
- package/src/api/jellyseerr-api.ts +0 -660
- package/src/api/maintainerr-api.ts +0 -731
- package/src/api/naming-config.ts +0 -93
- package/src/api/overseerr-api.ts +0 -513
- package/src/api/plex-api.ts +0 -358
- package/src/api/portainer-api.ts +0 -467
- package/src/api/profilarr-api.ts +0 -380
- package/src/api/prowlarr-api.ts +0 -859
- package/src/api/qbittorrent-api.ts +0 -339
- package/src/api/quality-profile-api.ts +0 -269
- package/src/api/tautulli-api.ts +0 -277
- package/src/api/uptime-kuma-api.ts +0 -342
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1255
- package/src/compose/caddy-config.ts +0 -129
- package/src/compose/generator.ts +0 -262
- package/src/compose/index.ts +0 -3
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -185
- package/src/config/bookmarks-generator.ts +0 -149
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -376
- package/src/config/index.ts +0 -3
- package/src/config/manager.ts +0 -149
- package/src/config/recyclarr-config.ts +0 -179
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -85
- package/src/config/soularr-config.ts +0 -105
- package/src/config/trash-quality-definitions.ts +0 -213
- package/src/data/lidarr-custom-formats.ts +0 -127
- package/src/data/trash-profiles.ts +0 -326
- package/src/docker/client.ts +0 -291
- package/src/docker/index.ts +0 -1
- package/src/index.ts +0 -77
- package/src/structure/manager.ts +0 -128
- package/src/ui/App.ts +0 -109
- package/src/ui/components/ApplicationSelector.ts +0 -272
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -152
- package/src/ui/components/UpdateNotification.ts +0 -101
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/AdvancedSettings.ts +0 -181
- package/src/ui/screens/ApiKeyViewer.ts +0 -500
- package/src/ui/screens/AppConfigurator.ts +0 -731
- package/src/ui/screens/AppManager.ts +0 -276
- package/src/ui/screens/CloudflaredSetup.ts +0 -974
- package/src/ui/screens/ContainerControl.ts +0 -579
- package/src/ui/screens/FullAutoSetup.ts +0 -1719
- package/src/ui/screens/HomepageSetup.ts +0 -306
- package/src/ui/screens/JellyfinSetup.ts +0 -471
- package/src/ui/screens/JellyseerrSetup.ts +0 -612
- package/src/ui/screens/LogsViewer.ts +0 -468
- package/src/ui/screens/MainMenu.ts +0 -288
- package/src/ui/screens/MonitorDashboard.ts +0 -866
- package/src/ui/screens/ProwlarrSetup.ts +0 -623
- package/src/ui/screens/QBittorrentSetup.ts +0 -268
- package/src/ui/screens/QuickSetup.ts +0 -1161
- package/src/ui/screens/RecyclarrSetup.ts +0 -418
- package/src/ui/screens/SecretsEditor.ts +0 -214
- package/src/ui/screens/SettingsScreen.ts +0 -605
- package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
- package/src/ui/screens/index.ts +0 -4
- package/src/util/arch.ts +0 -78
- package/src/utils/browser.ts +0 -26
- package/src/utils/categories.ts +0 -59
- package/src/utils/debug.ts +0 -91
- package/src/utils/env.ts +0 -137
- package/src/utils/logs.ts +0 -118
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
- package/src/utils/migrations.ts +0 -164
- package/src/utils/password.ts +0 -42
- package/src/utils/unraid.ts +0 -101
- package/src/utils/update-checker.ts +0 -210
- package/src/utils/url-utils.ts +0 -38
package/src/utils/migrations.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/password.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/unraid.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/url-utils.ts
DELETED
|
@@ -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
|
-
}
|