v1.5.28: Visuelle Fortschrittsanzeigen (JDownloader 2 Style)
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
0d6053fa76
commit
e35fc1f31a
126
.claude/memory-bank.md
Normal file
126
.claude/memory-bank.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Memory Bank - Multi Debrid Downloader
|
||||||
|
|
||||||
|
## Projekt-Überblick
|
||||||
|
|
||||||
|
**Name:** Multi Debrid Downloader (MDD)
|
||||||
|
**Typ:** Electron Desktop App für Windows 10/11
|
||||||
|
**Repository:**
|
||||||
|
- Codeberg: https://codeberg.org/Sucukdeluxe/real-debrid-downloader.git
|
||||||
|
- GitHub: https://github.com/Sucukdeluxe/real-debrid-downloader.git
|
||||||
|
|
||||||
|
## Technologie-Stack
|
||||||
|
|
||||||
|
- **Runtime:** Electron 31.x
|
||||||
|
- **Frontend:** React 18.x + TypeScript 5.x
|
||||||
|
- **Build:** Vite (Renderer) + tsup (Main/Preload)
|
||||||
|
- **Tests:** Vitest (262+ Tests)
|
||||||
|
- **Installer:** NSIS via electron-builder
|
||||||
|
|
||||||
|
## Unterstützte Debrid-Provider
|
||||||
|
|
||||||
|
| Provider | Auth | Priorität |
|
||||||
|
|----------|------|-----------|
|
||||||
|
| Real-Debrid | API Token | Primär |
|
||||||
|
| Mega-Debrid | Login + Passwort | Fallback 1 |
|
||||||
|
| BestDebrid | API Token | Fallback 2 |
|
||||||
|
| AllDebrid | API Key | Fallback 3 |
|
||||||
|
|
||||||
|
## Kernfeatures
|
||||||
|
|
||||||
|
- **Queue-Management:** Package-basierte Organisation mit Drag & Drop
|
||||||
|
- **Auto-Extract:** RAR, ZIP, 7z mit Passwortliste
|
||||||
|
- **Auto-Rename:** Scene-Release Muster (4sf/4sj) → saubere Namen
|
||||||
|
- **Integritätsprüfung:** CRC32, MD5, SHA1 via SFV-Dateien
|
||||||
|
- **Provider-Fallback:** Automatischer Wechsel bei Fehlern/Fair-Use
|
||||||
|
- **Session-Persistenz:** Queue überlebt App-Neustart
|
||||||
|
- **Clipboard-Watcher:** Automatische Link-Erkennung
|
||||||
|
- **System-Tray:** Minimize to Tray
|
||||||
|
- **Speed-Limit:** Global oder per Download + Bandwidth-Schedules
|
||||||
|
- **MKV-Sammelordner:** Automatisches Verschieben nach Paketabschluss
|
||||||
|
- **Update-System:** Automatische Updates via Codeberg Releases
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/ # Electron Main Process
|
||||||
|
│ ├── main.ts # Entry Point, IPC Handler, Window Management
|
||||||
|
│ ├── app-controller.ts # Koordiniert DownloadManager + Settings
|
||||||
|
│ ├── download-manager.ts # Core: Queue, Downloads, Retry-Logic
|
||||||
|
│ ├── debrid.ts # Debrid-Service Abstraktion
|
||||||
|
│ ├── realdebrid.ts # Real-Debrid API Client
|
||||||
|
│ ├── extractor.ts # Archiv-Entpackung
|
||||||
|
│ ├── integrity.ts # CRC32/Hash-Validierung
|
||||||
|
│ ├── storage.ts # Session/Settings Persistenz
|
||||||
|
│ ├── update.ts # Update-Check & Installation
|
||||||
|
│ └── ...
|
||||||
|
├── renderer/ # React UI
|
||||||
|
│ ├── App.tsx # Hauptkomponente mit allen Tabs
|
||||||
|
│ └── styles.css # Styling
|
||||||
|
├── preload/ # Preload Script (IPC Bridge)
|
||||||
|
│ └── preload.ts
|
||||||
|
└── shared/ # Geteilte Types
|
||||||
|
├── types.ts # Alle TypeScript Interfaces
|
||||||
|
├── ipc.ts # IPC Channel Konstanten
|
||||||
|
└── preload-api.ts # window.rd API Definition
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Types (src/shared/types.ts)
|
||||||
|
|
||||||
|
- `DownloadItem`: Einzelner Download mit Status, Progress, Speed
|
||||||
|
- `PackageEntry`: Gruppe von Downloads mit OutputDir, ExtractDir
|
||||||
|
- `SessionState`: Gesamter Queue-Zustand (persistiert)
|
||||||
|
- `AppSettings`: Alle Einstellungen
|
||||||
|
- `UiSnapshot`: Kompletter UI-State für Renderer
|
||||||
|
|
||||||
|
## IPC Channels (src/shared/ipc.ts)
|
||||||
|
|
||||||
|
Hauptchannels für Renderer ↔ Main Kommunikation:
|
||||||
|
- `GET_SNAPSHOT`, `STATE_UPDATE`: State-Sync
|
||||||
|
- `ADD_LINKS`, `ADD_CONTAINERS`: Queue befüllen
|
||||||
|
- `START`, `STOP`, `TOGGLE_PAUSE`: Download-Kontrolle
|
||||||
|
- `UPDATE_SETTINGS`: Einstellungen ändern
|
||||||
|
|
||||||
|
## Aktuelle Version
|
||||||
|
|
||||||
|
**Version:** 1.5.27
|
||||||
|
**Letztes Release:** 1.4.68 (2026-03-01)
|
||||||
|
|
||||||
|
### Letzte Änderungen (CHANGELOG)
|
||||||
|
- Session-Backup für Queue-Zustand
|
||||||
|
- Start-Konflikt-Behandlung verbessert
|
||||||
|
- Mega-Web Unrestrict abort-fähig
|
||||||
|
- DLC-Import gehärtet
|
||||||
|
- Auto-Renamer erweitert
|
||||||
|
|
||||||
|
## Offene Pläne
|
||||||
|
|
||||||
|
1. **Native Menüleiste** (`.claude/plans/agile-watching-lampson.md`)
|
||||||
|
- JDownloader 2 Style Menü
|
||||||
|
- Electron Menu API nutzen
|
||||||
|
- Bestehende React Menu-Bar ersetzen
|
||||||
|
|
||||||
|
## Coding-Conventions
|
||||||
|
|
||||||
|
- TypeScript strict mode
|
||||||
|
- Async/Await über Promises
|
||||||
|
- Deutsche UI-Texte
|
||||||
|
- Ausführliche Error-Logs via `logger`
|
||||||
|
- Retry-Logic mit exponential backoff
|
||||||
|
- AbortController für abbrechbare Operationen
|
||||||
|
|
||||||
|
## Build & Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build # TypeScript + Vite Build
|
||||||
|
npm run dist # electron-builder (NSIS + Portable)
|
||||||
|
npm test # Vitest Tests
|
||||||
|
npm run self-check # Vollständiger Check (Typecheck + Tests)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Dateien
|
||||||
|
|
||||||
|
- `CHANGELOG.md` - Detaillierte Versionshistorie
|
||||||
|
- `.claude/plans/` - Feature-Pläne
|
||||||
|
- `tests/` - Umfangreiche Test-Suite
|
||||||
|
- `installer/RealDebridDownloader.iss` - Inno Setup Script
|
||||||
66
.claude/plans/agile-watching-lampson.md
Normal file
66
.claude/plans/agile-watching-lampson.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Native Menüleiste (JDownloader 2 Style)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Die App hat aktuell keine native Menüleiste (nur ein Tray-Kontextmenü). Der User möchte eine Menüleiste oben links wie bei JDownloader 2 mit Datei-Menü, Shortcuts und Sicherungs-Funktion.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### "Datei"-Menü (oben links)
|
||||||
|
| Menüpunkt | Shortcut | Aktion |
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| Text mit Links analysieren | Ctrl+L | Wechselt zum Linksammler-Tab |
|
||||||
|
| Linkcontainer laden | Ctrl+O | Öffnet DLC-Dateiauswahl (existiert bereits) |
|
||||||
|
| --- Separator --- | | |
|
||||||
|
| Sicherung → Backup erstellen | | Exportiert Queue als JSON (existiert: `exportQueue`) |
|
||||||
|
| Sicherung → Backup laden | | Importiert Queue-JSON (existiert: `importQueue`) |
|
||||||
|
| --- Separator --- | | |
|
||||||
|
| Neustart | Ctrl+Shift+R | `app.relaunch()` + `app.quit()` |
|
||||||
|
| Beenden | Ctrl+Q | `app.quit()` |
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Step 1: Neue IPC-Channels
|
||||||
|
**Datei:** `src/shared/ipc.ts`
|
||||||
|
- `NAVIGATE_TAB: "app:navigate-tab"` — Renderer wechselt Tab
|
||||||
|
- `RESTART: "app:restart"` — App neustarten
|
||||||
|
- `SAVE_BACKUP: "dialog:save-backup"` — Save-Dialog + Export
|
||||||
|
- `LOAD_BACKUP: "dialog:load-backup"` — Open-Dialog + Import
|
||||||
|
|
||||||
|
### Step 2: Preload-API erweitern
|
||||||
|
**Datei:** `src/shared/preload-api.ts` + `src/preload/preload.ts`
|
||||||
|
- `onNavigateTab(callback)` — Event-Listener für Tab-Wechsel
|
||||||
|
- `saveBackup()` — Backup über nativen Save-Dialog speichern
|
||||||
|
- `loadBackup()` — Backup über nativen Open-Dialog laden
|
||||||
|
|
||||||
|
### Step 3: Menüleiste erstellen
|
||||||
|
**Datei:** `src/main/main.ts`
|
||||||
|
|
||||||
|
Neue Funktion `createApplicationMenu()` nach `createTray()`:
|
||||||
|
- Nutzt `Menu.buildFromTemplate()` + `Menu.setApplicationMenu()`
|
||||||
|
- "Datei"-Menü mit allen Punkten aus der Tabelle
|
||||||
|
- Accelerators für Shortcuts (Electron handelt die automatisch)
|
||||||
|
- Menü-Clicks senden IPC-Events an den Renderer oder rufen direkt Main-Process-Funktionen auf
|
||||||
|
|
||||||
|
**Backup erstellen:** `dialog.showSaveDialog()` → `controller.exportQueue()` → `fs.writeFile()`
|
||||||
|
**Backup laden:** `dialog.showOpenDialog()` → `fs.readFile()` → `controller.importQueue()`
|
||||||
|
**Neustart:** `app.relaunch()` → `app.quit()`
|
||||||
|
**Beenden:** `app.quit()`
|
||||||
|
**Linksammler/DLC:** IPC-Event an Renderer senden
|
||||||
|
|
||||||
|
### Step 4: Renderer reagiert auf Menü-Events
|
||||||
|
**Datei:** `src/renderer/App.tsx`
|
||||||
|
- `onNavigateTab` Listener registrieren im `useEffect`
|
||||||
|
- Bei `"collector"` → `setTab("collector")`
|
||||||
|
- DLC-Import: `pickContainers` + `addContainers` (bestehendes Pattern)
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
- `src/shared/ipc.ts` — Neue Channels
|
||||||
|
- `src/shared/preload-api.ts` — Neue API-Methoden
|
||||||
|
- `src/preload/preload.ts` — IPC-Bridge
|
||||||
|
- `src/main/main.ts` — Menüleiste + IPC-Handler + Backup-Logik
|
||||||
|
- `src/renderer/App.tsx` — Tab-Navigation Listener
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
1. `npm run build`
|
||||||
|
2. `npx vitest run` (schnelle Tests)
|
||||||
|
3. Manuell: App starten, Datei-Menü prüfen, Shortcuts testen
|
||||||
6
.claude/settings.json
Normal file
6
.claude/settings.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"frontend-design@claude-plugins-official": true,
|
||||||
|
"code-review@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebFetch(domain:github.com)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.claude/worktrees/nifty-vaughan
Submodule
1
.claude/worktrees/nifty-vaughan
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit f1e132b2ed4717667fd7318ecab22e5ef52da0cc
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
||||||
|
|
||||||
|
## 1.5.28 - 2026-03-02
|
||||||
|
|
||||||
|
UI-Verbesserung: Visuelle Fortschrittsanzeigen in der Download-Liste (JDownloader 2 Style).
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Visuelle Fortschrittsbalken:**
|
||||||
|
- Paket-Fortschritt wird jetzt als grafische Progress-Bar in der Spalte "Fortschritt" angezeigt.
|
||||||
|
- Einzelne Items haben ebenfalls eine kleinere Progress-Bar.
|
||||||
|
- Grüner Gradient (#22c55e → #4ade80) für bessere Text-Lesbarkeit.
|
||||||
|
- Prozentanzeige als Overlay-Text auf der Bar.
|
||||||
|
|
||||||
## 1.4.68 - 2026-03-01
|
## 1.4.68 - 2026-03-01
|
||||||
|
|
||||||
Stabilitaets-Hotfix fuer Session-Verlust nach Update/Neustart: Session-Dateien haben jetzt ein robustes Backup-/Restore-Fallback.
|
Stabilitaets-Hotfix fuer Session-Verlust nach Update/Neustart: Session-Dateien haben jetzt ein robustes Backup-/Restore-Fallback.
|
||||||
|
|||||||
75
_upload_release.mjs
Normal file
75
_upload_release.mjs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
|
||||||
|
const credResult = spawnSync("git", ["credential", "fill"], {
|
||||||
|
input: "protocol=https\nhost=codeberg.org\n\n",
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["pipe", "pipe", "pipe"]
|
||||||
|
});
|
||||||
|
const creds = new Map();
|
||||||
|
for (const line of credResult.stdout.split(/\r?\n/)) {
|
||||||
|
if (line.includes("=")) {
|
||||||
|
const [k, v] = line.split("=", 2);
|
||||||
|
creds.set(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auth = "Basic " + Buffer.from(creds.get("username") + ":" + creds.get("password")).toString("base64");
|
||||||
|
const owner = "Sucukdeluxe";
|
||||||
|
const repo = "real-debrid-downloader";
|
||||||
|
const tag = "v1.5.27";
|
||||||
|
const baseApi = `https://codeberg.org/api/v1/repos/${owner}/${repo}`;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await fetch(baseApi, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { Authorization: auth, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ has_releases: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRes = await fetch(`${baseApi}/releases`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: auth, "Content-Type": "application/json", Accept: "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
tag_name: tag,
|
||||||
|
target_commitish: "main",
|
||||||
|
name: tag,
|
||||||
|
body: "- Increase column spacing for Fortschritt/Größe/Geladen",
|
||||||
|
draft: false,
|
||||||
|
prerelease: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const release = await createRes.json();
|
||||||
|
if (!createRes.ok) {
|
||||||
|
console.error("Create failed:", JSON.stringify(release));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("Release created:", release.id);
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
"Real-Debrid-Downloader Setup 1.5.27.exe",
|
||||||
|
"Real-Debrid-Downloader 1.5.27.exe",
|
||||||
|
"latest.yml",
|
||||||
|
"Real-Debrid-Downloader Setup 1.5.27.exe.blockmap"
|
||||||
|
];
|
||||||
|
for (const f of files) {
|
||||||
|
const filePath = path.join("release", f);
|
||||||
|
const data = fs.readFileSync(filePath);
|
||||||
|
const uploadUrl = `${baseApi}/releases/${release.id}/assets?name=${encodeURIComponent(f)}`;
|
||||||
|
const res = await fetch(uploadUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: auth, "Content-Type": "application/octet-stream" },
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("Uploaded:", f);
|
||||||
|
} else if (res.status === 409 || res.status === 422) {
|
||||||
|
console.log("Skipped existing:", f);
|
||||||
|
} else {
|
||||||
|
console.error("Upload failed for", f, ":", res.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Done! https://codeberg.org/${owner}/${repo}/releases/tag/${tag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => { console.error(e.message); process.exit(1); });
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.5.27",
|
"version": "1.5.28",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -2718,7 +2718,12 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
<h4 onClick={(e) => { e.stopPropagation(); onStartEdit(pkg.id, pkg.name); }} title="Klicken zum Umbenennen">{pkg.name}</h4>
|
<h4 onClick={(e) => { e.stopPropagation(); onStartEdit(pkg.id, pkg.name); }} title="Klicken zum Umbenennen">{pkg.name}</h4>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="pkg-col pkg-col-progress">{dlProgress}%</span>
|
<span className="pkg-col pkg-col-progress">
|
||||||
|
<span className="progress-inline">
|
||||||
|
<span className="progress-inline-bar" style={{ width: `${dlProgress}%` }} />
|
||||||
|
<span className="progress-inline-text">{dlProgress}%</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
<span className="pkg-col pkg-col-size">{humanSize(items.reduce((sum, item) => sum + (item.totalBytes || item.downloadedBytes || 0), 0))}</span>
|
<span className="pkg-col pkg-col-size">{humanSize(items.reduce((sum, item) => sum + (item.totalBytes || item.downloadedBytes || 0), 0))}</span>
|
||||||
<span className="pkg-col pkg-col-downloaded">{humanSize(items.reduce((sum, item) => sum + (item.downloadedBytes || 0), 0))}</span>
|
<span className="pkg-col pkg-col-downloaded">{humanSize(items.reduce((sum, item) => sum + (item.downloadedBytes || 0), 0))}</span>
|
||||||
<span className="pkg-col pkg-col-hoster" title={(() => {
|
<span className="pkg-col pkg-col-hoster" title={(() => {
|
||||||
@ -2742,7 +2747,14 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
{!collapsed && items.map((item) => (
|
{!collapsed && items.map((item) => (
|
||||||
<div key={item.id} className={`item-row${selectedIds.has(item.id) ? " item-selected" : ""}`} onClick={(e) => { e.stopPropagation(); onSelect(item.id, e.ctrlKey); }} onMouseDown={(e) => { e.stopPropagation(); onSelectMouseDown(item.id, e); }} onMouseEnter={() => onSelectMouseEnter(item.id)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, item.id, e.clientX, e.clientY); }}>
|
<div key={item.id} className={`item-row${selectedIds.has(item.id) ? " item-selected" : ""}`} onClick={(e) => { e.stopPropagation(); onSelect(item.id, e.ctrlKey); }} onMouseDown={(e) => { e.stopPropagation(); onSelectMouseDown(item.id, e); }} onMouseEnter={() => onSelectMouseEnter(item.id)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, item.id, e.clientX, e.clientY); }}>
|
||||||
<span className="pkg-col pkg-col-name item-indent" title={item.fileName}>{item.fileName}</span>
|
<span className="pkg-col pkg-col-name item-indent" title={item.fileName}>{item.fileName}</span>
|
||||||
<span className="pkg-col pkg-col-progress">{item.totalBytes > 0 ? `${item.progressPercent}%` : "-"}</span>
|
<span className="pkg-col pkg-col-progress">
|
||||||
|
{item.totalBytes > 0 ? (
|
||||||
|
<span className="progress-inline progress-inline-small">
|
||||||
|
<span className="progress-inline-bar" style={{ width: `${item.progressPercent}%` }} />
|
||||||
|
<span className="progress-inline-text">{item.progressPercent}%</span>
|
||||||
|
</span>
|
||||||
|
) : "-"}
|
||||||
|
</span>
|
||||||
<span className="pkg-col pkg-col-size">{(item.totalBytes || item.downloadedBytes) ? humanSize(item.totalBytes || item.downloadedBytes || 0) : "-"}</span>
|
<span className="pkg-col pkg-col-size">{(item.totalBytes || item.downloadedBytes) ? humanSize(item.totalBytes || item.downloadedBytes || 0) : "-"}</span>
|
||||||
<span className="pkg-col pkg-col-downloaded">{item.downloadedBytes > 0 ? humanSize(item.downloadedBytes) : "-"}</span>
|
<span className="pkg-col pkg-col-downloaded">{item.downloadedBytes > 0 ? humanSize(item.downloadedBytes) : "-"}</span>
|
||||||
<span className="pkg-col pkg-col-hoster" title={formatHoster(item)}>{formatHoster(item)}</span>
|
<span className="pkg-col pkg-col-hoster" title={formatHoster(item)}>{formatHoster(item)}</span>
|
||||||
|
|||||||
@ -641,6 +641,49 @@ body,
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-inline {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--progress-track);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-inline-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #22c55e, #4ade80);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-inline-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-inline-small {
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-inline-small .progress-inline-text {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: min(360px, 100%);
|
width: min(360px, 100%);
|
||||||
background: var(--field);
|
background: var(--field);
|
||||||
|
|||||||
58
verify_remote.mjs
Normal file
58
verify_remote.mjs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
import { pipeline } from "node:stream/promises";
|
||||||
|
|
||||||
|
const localPath = "release/Real-Debrid-Downloader Setup 1.4.61.exe";
|
||||||
|
const remoteUrl = "https://codeberg.org/Sucukdeluxe/real-debrid-downloader/releases/download/v1.4.61/Real-Debrid-Downloader%20Setup%201.4.61.exe";
|
||||||
|
const tmpPath = path.join(os.tmpdir(), "rd-verify-1.4.61.exe");
|
||||||
|
|
||||||
|
// Local file info
|
||||||
|
const localSize = fs.statSync(localPath).size;
|
||||||
|
const localHash = crypto.createHash("sha512");
|
||||||
|
localHash.update(fs.readFileSync(localPath));
|
||||||
|
const localSha = localHash.digest("hex");
|
||||||
|
console.log("Local file size:", localSize);
|
||||||
|
console.log("Local SHA512:", localSha.substring(0, 40) + "...");
|
||||||
|
|
||||||
|
// Download from Codeberg
|
||||||
|
console.log("\nDownloading from Codeberg...");
|
||||||
|
const resp = await fetch(remoteUrl, { redirect: "follow" });
|
||||||
|
console.log("Status:", resp.status);
|
||||||
|
console.log("Content-Length:", resp.headers.get("content-length"));
|
||||||
|
|
||||||
|
const source = Readable.fromWeb(resp.body);
|
||||||
|
const target = fs.createWriteStream(tmpPath);
|
||||||
|
await pipeline(source, target);
|
||||||
|
|
||||||
|
const remoteSize = fs.statSync(tmpPath).size;
|
||||||
|
const remoteHash = crypto.createHash("sha512");
|
||||||
|
remoteHash.update(fs.readFileSync(tmpPath));
|
||||||
|
const remoteSha = remoteHash.digest("hex");
|
||||||
|
console.log("\nRemote file size:", remoteSize);
|
||||||
|
console.log("Remote SHA512:", remoteSha.substring(0, 40) + "...");
|
||||||
|
|
||||||
|
console.log("\nSize match:", localSize === remoteSize);
|
||||||
|
console.log("SHA512 match:", localSha === remoteSha);
|
||||||
|
|
||||||
|
if (localSha !== remoteSha) {
|
||||||
|
console.log("\n!!! FILE ON CODEBERG IS CORRUPTED !!!");
|
||||||
|
console.log("The upload to Codeberg damaged the file.");
|
||||||
|
|
||||||
|
// Find first difference
|
||||||
|
const localBuf = fs.readFileSync(localPath);
|
||||||
|
const remoteBuf = fs.readFileSync(tmpPath);
|
||||||
|
for (let i = 0; i < Math.min(localBuf.length, remoteBuf.length); i++) {
|
||||||
|
if (localBuf[i] !== remoteBuf[i]) {
|
||||||
|
console.log(`First byte difference at offset ${i}: local=0x${localBuf[i].toString(16)} remote=0x${remoteBuf[i].toString(16)}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("\n>>> File on Codeberg is identical to local file <<<");
|
||||||
|
console.log("The problem is on the user's server (network/proxy issue).");
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
Loading…
Reference in New Issue
Block a user