diff --git a/package-lock.json b/package-lock.json index 1474056..94ebda7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.1.14", + "version": "1.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.14", + "version": "1.1.15", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 38bca74..4d39e46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.14", + "version": "1.1.15", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 0f1031a..364e7b3 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -1,12 +1,13 @@ import path from "node:path"; import { app } from "electron"; -import { AddLinksPayload, AppSettings, ParsedPackageInput, UiSnapshot } from "../shared/types"; +import { AddLinksPayload, AppSettings, ParsedPackageInput, UiSnapshot, UpdateCheckResult } from "../shared/types"; import { importDlcContainers } from "./container"; import { APP_VERSION, defaultSettings } from "./constants"; import { DownloadManager } from "./download-manager"; import { parseCollectorInput } from "./link-parser"; import { configureLogger, logger } from "./logger"; import { createStoragePaths, emptySession, loadSession, loadSettings, saveSettings } from "./storage"; +import { checkGitHubUpdate } from "./update"; export class AppController { private settings: AppSettings; @@ -64,6 +65,10 @@ export class AppController { return this.settings; } + public async checkUpdates(): Promise { + return checkGitHubUpdate(this.settings.updateRepo); + } + public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } { const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName); if (parsed.length === 0) { diff --git a/src/main/constants.ts b/src/main/constants.ts index 898aa2d..3583a77 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -3,7 +3,7 @@ import os from "node:os"; import { AppSettings } from "../shared/types"; export const APP_NAME = "Debrid Download Manager"; -export const APP_VERSION = "1.1.14"; +export const APP_VERSION = "1.1.15"; export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; diff --git a/src/main/main.ts b/src/main/main.ts index bc5abc8..b06fd74 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -40,6 +40,7 @@ function createWindow(): BrowserWindow { function registerIpcHandlers(): void { ipcMain.handle(IPC_CHANNELS.GET_SNAPSHOT, () => controller.getSnapshot()); ipcMain.handle(IPC_CHANNELS.GET_VERSION, () => controller.getVersion()); + ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => controller.checkUpdates()); ipcMain.handle(IPC_CHANNELS.UPDATE_SETTINGS, (_event: IpcMainInvokeEvent, partial: Partial) => controller.updateSettings(partial ?? {})); ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => controller.addLinks(payload)); ipcMain.handle(IPC_CHANNELS.ADD_CONTAINERS, async (_event: IpcMainInvokeEvent, filePaths: string[]) => controller.addContainers(filePaths ?? [])); diff --git a/src/main/update.ts b/src/main/update.ts new file mode 100644 index 0000000..9f0cff9 --- /dev/null +++ b/src/main/update.ts @@ -0,0 +1,67 @@ +import { APP_VERSION, DEFAULT_UPDATE_REPO } from "./constants"; +import { UpdateCheckResult } from "../shared/types"; +import { compactErrorText } from "./utils"; + +function parseVersionParts(version: string): number[] { + const cleaned = version.replace(/^v/i, "").trim(); + return cleaned.split(".").map((part) => Number(part.replace(/[^0-9].*$/, "") || "0")); +} + +function isRemoteNewer(currentVersion: string, latestVersion: string): boolean { + const current = parseVersionParts(currentVersion); + const latest = parseVersionParts(latestVersion); + const maxLen = Math.max(current.length, latest.length); + for (let i = 0; i < maxLen; i += 1) { + const a = current[i] ?? 0; + const b = latest[i] ?? 0; + if (b > a) { + return true; + } + if (b < a) { + return false; + } + } + return false; +} + +export async function checkGitHubUpdate(repo: string): Promise { + const safeRepo = (repo || DEFAULT_UPDATE_REPO).trim() || DEFAULT_UPDATE_REPO; + const fallback: UpdateCheckResult = { + updateAvailable: false, + currentVersion: APP_VERSION, + latestVersion: APP_VERSION, + latestTag: `v${APP_VERSION}`, + releaseUrl: `https://github.com/${safeRepo}/releases/latest` + }; + + try { + const response = await fetch(`https://api.github.com/repos/${safeRepo}/releases/latest`, { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "RD-Node-Downloader/1.1.14" + } + }); + const payload = await response.json().catch(() => null) as Record | null; + if (!response.ok || !payload) { + const reason = String((payload?.message as string) || `HTTP ${response.status}`); + return { ...fallback, error: reason }; + } + + const latestTag = String(payload.tag_name || `v${APP_VERSION}`).trim(); + const latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION; + const releaseUrl = String(payload.html_url || fallback.releaseUrl); + + return { + updateAvailable: isRemoteNewer(APP_VERSION, latestVersion), + currentVersion: APP_VERSION, + latestVersion, + latestTag, + releaseUrl + }; + } catch (error) { + return { + ...fallback, + error: compactErrorText(error) + }; + } +} diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 6aaad35..6d3e765 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -1,11 +1,12 @@ import { contextBridge, ipcRenderer } from "electron"; -import { AddLinksPayload, AppSettings, UiSnapshot } from "../shared/types"; +import { AddLinksPayload, AppSettings, UiSnapshot, UpdateCheckResult } from "../shared/types"; import { IPC_CHANNELS } from "../shared/ipc"; import { ElectronApi } from "../shared/preload-api"; const api: ElectronApi = { getSnapshot: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_SNAPSHOT), getVersion: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_VERSION), + checkUpdates: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.CHECK_UPDATES), updateSettings: (settings: Partial): Promise => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings), addLinks: (payload: AddLinksPayload): Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }> => ipcRenderer.invoke(IPC_CHANNELS.ADD_LINKS, payload), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 94cf791..5a4b26c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -84,6 +84,14 @@ export function App(): ReactElement { void window.rd.getSnapshot().then((state) => { setSnapshot(state); setSettingsDraft(state.settings); + if (state.settings.autoUpdateCheck) { + void window.rd.checkUpdates().then((result) => { + if (result.updateAvailable) { + setStatusToast(`Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})`); + setTimeout(() => setStatusToast(""), 3800); + } + }); + } }); unsubscribe = window.rd.onStateUpdate((state) => { setSnapshot(state); @@ -105,6 +113,22 @@ export function App(): ReactElement { setTimeout(() => setStatusToast(""), 1800); }; + const onCheckUpdates = async (): Promise => { + const result = await window.rd.checkUpdates(); + if (result.error) { + setStatusToast(`Update-Check fehlgeschlagen: ${result.error}`); + setTimeout(() => setStatusToast(""), 2800); + return; + } + if (result.updateAvailable) { + setStatusToast(`Update verfügbar: ${result.latestTag} (aktuell v${result.currentVersion})`); + setTimeout(() => setStatusToast(""), 3200); + return; + } + setStatusToast(`Kein Update verfügbar (v${result.currentVersion})`); + setTimeout(() => setStatusToast(""), 2000); + }; + const onAddLinks = async (): Promise => { await window.rd.updateSettings(settingsDraft); const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName }); @@ -198,6 +222,7 @@ export function App(): ReactElement { + diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index d257032..c29cc32 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -1,6 +1,7 @@ export const IPC_CHANNELS = { GET_SNAPSHOT: "app:get-snapshot", GET_VERSION: "app:get-version", + CHECK_UPDATES: "app:check-updates", UPDATE_SETTINGS: "app:update-settings", ADD_LINKS: "queue:add-links", ADD_CONTAINERS: "queue:add-containers", diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index 18d1896..a64ccdd 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -1,8 +1,9 @@ -import type { AddLinksPayload, AppSettings, UiSnapshot } from "./types"; +import type { AddLinksPayload, AppSettings, UiSnapshot, UpdateCheckResult } from "./types"; export interface ElectronApi { getSnapshot: () => Promise; getVersion: () => Promise; + checkUpdates: () => Promise; updateSettings: (settings: Partial) => Promise; addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>; addContainers: (filePaths: string[]) => Promise<{ addedPackages: number; addedLinks: number }>; diff --git a/src/shared/types.ts b/src/shared/types.ts index 556a81d..a2bf5e1 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -137,6 +137,15 @@ export interface AddContainerPayload { filePaths: string[]; } +export interface UpdateCheckResult { + updateAvailable: boolean; + currentVersion: string; + latestVersion: string; + latestTag: string; + releaseUrl: string; + error?: string; +} + export interface ParsedHashEntry { fileName: string; algorithm: "crc32" | "md5" | "sha1";