Comprehensive bugfix release v1.6.45
Fix ~70 issues across the entire codebase including security fixes, error handling improvements, test stabilization, and code quality. - Fix TLS race condition with reference-counted acquire/release - Bind debug server to 127.0.0.1 instead of 0.0.0.0 - Add overall timeout to MegaWebFallback - Stream update installer to disk instead of RAM buffering - Add path traversal protection in JVM extractor - Cache DdownloadClient with credential-based invalidation - Add .catch() to all fire-and-forget IPC calls - Wrap app startup, clipboard, session-log in try/catch - Add timeouts to container.ts fetch calls - Fix variable shadowing, tsconfig path, line endings - Stabilize tests with proper cleanup and timing tolerance - Fix installer privileges, scripts, and afterPack null checks - Delete obsolete _upload_release.mjs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a3c2680fec
commit
a1c8f42435
@ -1,75 +0,0 @@
|
|||||||
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.35";
|
|
||||||
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: "- Fix: Fortschritt zeigt jetzt kombinierten Wert (Download + Entpacken)\n- Fix: Pausieren zeigt nicht mehr 'Warte auf Daten'\n- Pixel-perfekte Dual-Layer Progress-Bar Texte (clip-path)",
|
|
||||||
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.35.exe",
|
|
||||||
"Real-Debrid-Downloader 1.5.35.exe",
|
|
||||||
"latest.yml",
|
|
||||||
"Real-Debrid-Downloader Setup 1.5.35.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); });
|
|
||||||
@ -25,11 +25,11 @@ AppPublisher=Sucukdeluxe
|
|||||||
DefaultDirName={autopf}\{#MyAppName}
|
DefaultDirName={autopf}\{#MyAppName}
|
||||||
DefaultGroupName={#MyAppName}
|
DefaultGroupName={#MyAppName}
|
||||||
OutputDir={#MyOutputDir}
|
OutputDir={#MyOutputDir}
|
||||||
OutputBaseFilename=Real-Debrid-Downloader-Setup-{#MyAppVersion}
|
OutputBaseFilename=Real-Debrid-Downloader Setup {#MyAppVersion}
|
||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
PrivilegesRequired=admin
|
PrivilegesRequired=lowest
|
||||||
ArchitecturesInstallIn64BitMode=x64compatible
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||||
SetupIconFile={#MyIconFile}
|
SetupIconFile={#MyIconFile}
|
||||||
@ -39,8 +39,8 @@ Name: "german"; MessagesFile: "compiler:Languages\German.isl"
|
|||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "{#MySourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "{#MySourceDir}\\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs
|
||||||
Source: "{#MyIconFile}"; DestDir: "{app}"; DestName: "app_icon.ico"; Flags: ignoreversion
|
Source: "{#MyIconFile}"; DestDir: "{app}"; DestName: "app_icon.ico"
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app_icon.ico"
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app_icon.ico"
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -42,6 +43,8 @@ public final class JBindExtractorMain {
|
|||||||
private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$");
|
private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$");
|
||||||
private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$");
|
private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$");
|
||||||
private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$");
|
private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$");
|
||||||
|
private static final Pattern DIGIT_SUFFIX_RE = Pattern.compile("\\d{2,3}");
|
||||||
|
private static final Pattern WINDOWS_SPECIAL_CHARS_RE = Pattern.compile("[:<>*?\"\\|]");
|
||||||
private static volatile boolean sevenZipInitialized = false;
|
private static volatile boolean sevenZipInitialized = false;
|
||||||
|
|
||||||
private JBindExtractorMain() {
|
private JBindExtractorMain() {
|
||||||
@ -152,30 +155,35 @@ public final class JBindExtractorMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureDirectory(output.getParentFile());
|
ensureDirectory(output.getParentFile());
|
||||||
|
rejectSymlink(output);
|
||||||
long[] remaining = new long[] { itemUnits };
|
long[] remaining = new long[] { itemUnits };
|
||||||
|
boolean extractionSuccess = false;
|
||||||
try {
|
try {
|
||||||
InputStream in = zipFile.getInputStream(header);
|
InputStream in = zipFile.getInputStream(header);
|
||||||
OutputStream out = new FileOutputStream(output);
|
|
||||||
try {
|
try {
|
||||||
byte[] buffer = new byte[BUFFER_SIZE];
|
OutputStream out = new FileOutputStream(output);
|
||||||
while (true) {
|
try {
|
||||||
int read = in.read(buffer);
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
if (read < 0) {
|
while (true) {
|
||||||
break;
|
int read = in.read(buffer);
|
||||||
|
if (read < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (read == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.write(buffer, 0, read);
|
||||||
|
long accounted = Math.min(remaining[0], (long) read);
|
||||||
|
remaining[0] -= accounted;
|
||||||
|
progress.advance(accounted);
|
||||||
}
|
}
|
||||||
if (read == 0) {
|
} finally {
|
||||||
continue;
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
out.write(buffer, 0, read);
|
|
||||||
long accounted = Math.min(remaining[0], (long) read);
|
|
||||||
remaining[0] -= accounted;
|
|
||||||
progress.advance(accounted);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
in.close();
|
in.close();
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
@ -188,11 +196,19 @@ public final class JBindExtractorMain {
|
|||||||
if (modified > 0) {
|
if (modified > 0) {
|
||||||
output.setLastModified(modified);
|
output.setLastModified(modified);
|
||||||
}
|
}
|
||||||
|
extractionSuccess = true;
|
||||||
} catch (ZipException error) {
|
} catch (ZipException error) {
|
||||||
if (isWrongPassword(error, encrypted)) {
|
if (isWrongPassword(error, encrypted)) {
|
||||||
throw new WrongPasswordException(error);
|
throw new WrongPasswordException(error);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (!extractionSuccess && output.exists()) {
|
||||||
|
try {
|
||||||
|
output.delete();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +237,9 @@ public final class JBindExtractorMain {
|
|||||||
IInArchive archive = context.archive;
|
IInArchive archive = context.archive;
|
||||||
ISimpleInArchive simple = archive.getSimpleInterface();
|
ISimpleInArchive simple = archive.getSimpleInterface();
|
||||||
ISimpleInArchiveItem[] items = simple.getArchiveItems();
|
ISimpleInArchiveItem[] items = simple.getArchiveItems();
|
||||||
|
if (items == null) {
|
||||||
|
throw new IOException("Archiv enthalt keine Eintrage oder konnte nicht gelesen werden: " + request.archiveFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
long totalUnits = 0;
|
long totalUnits = 0;
|
||||||
boolean encrypted = false;
|
boolean encrypted = false;
|
||||||
@ -260,8 +279,10 @@ public final class JBindExtractorMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensureDirectory(output.getParentFile());
|
ensureDirectory(output.getParentFile());
|
||||||
|
rejectSymlink(output);
|
||||||
final FileOutputStream out = new FileOutputStream(output);
|
final FileOutputStream out = new FileOutputStream(output);
|
||||||
final long[] remaining = new long[] { itemUnits };
|
final long[] remaining = new long[] { itemUnits };
|
||||||
|
boolean extractionSuccess = false;
|
||||||
try {
|
try {
|
||||||
ExtractOperationResult result = item.extractSlow(new ISequentialOutStream() {
|
ExtractOperationResult result = item.extractSlow(new ISequentialOutStream() {
|
||||||
@Override
|
@Override
|
||||||
@ -291,6 +312,7 @@ public final class JBindExtractorMain {
|
|||||||
}
|
}
|
||||||
throw new IOException("7z-Fehler: " + result.name());
|
throw new IOException("7z-Fehler: " + result.name());
|
||||||
}
|
}
|
||||||
|
extractionSuccess = true;
|
||||||
} catch (SevenZipException error) {
|
} catch (SevenZipException error) {
|
||||||
if (looksLikeWrongPassword(error, encrypted)) {
|
if (looksLikeWrongPassword(error, encrypted)) {
|
||||||
throw new WrongPasswordException(error);
|
throw new WrongPasswordException(error);
|
||||||
@ -301,6 +323,12 @@ public final class JBindExtractorMain {
|
|||||||
out.close();
|
out.close();
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
}
|
}
|
||||||
|
if (!extractionSuccess && output.exists()) {
|
||||||
|
try {
|
||||||
|
output.delete();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -328,14 +356,31 @@ public final class JBindExtractorMain {
|
|||||||
|
|
||||||
if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) {
|
if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) {
|
||||||
VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback);
|
VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback);
|
||||||
IInArchive archive = SevenZip.openInArchive(null, volumed, callback);
|
try {
|
||||||
return new SevenZipArchiveContext(archive, null, volumed, callback);
|
IInArchive archive = SevenZip.openInArchive(null, volumed, callback);
|
||||||
|
return new SevenZipArchiveContext(archive, null, volumed, callback);
|
||||||
|
} catch (Exception error) {
|
||||||
|
callback.close();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
|
RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
|
||||||
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
|
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
|
||||||
IInArchive archive = SevenZip.openInArchive(null, stream, callback);
|
try {
|
||||||
return new SevenZipArchiveContext(archive, stream, null, callback);
|
IInArchive archive = SevenZip.openInArchive(null, stream, callback);
|
||||||
|
return new SevenZipArchiveContext(archive, stream, null, callback);
|
||||||
|
} catch (Exception error) {
|
||||||
|
try {
|
||||||
|
stream.close();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
raf.close();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isWrongPassword(ZipException error, boolean encrypted) {
|
private static boolean isWrongPassword(ZipException error, boolean encrypted) {
|
||||||
@ -396,7 +441,7 @@ public final class JBindExtractorMain {
|
|||||||
}
|
}
|
||||||
if (siblingName.startsWith(prefix) && siblingName.length() >= prefix.length() + 2) {
|
if (siblingName.startsWith(prefix) && siblingName.length() >= prefix.length() + 2) {
|
||||||
String suffix = siblingName.substring(prefix.length());
|
String suffix = siblingName.substring(prefix.length());
|
||||||
if (suffix.matches("\\d{2,3}")) {
|
if (DIGIT_SUFFIX_RE.matcher(suffix).matches()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,6 +525,12 @@ public final class JBindExtractorMain {
|
|||||||
}
|
}
|
||||||
if (normalized.matches("^[a-zA-Z]:.*")) {
|
if (normalized.matches("^[a-zA-Z]:.*")) {
|
||||||
normalized = normalized.substring(2);
|
normalized = normalized.substring(2);
|
||||||
|
while (normalized.startsWith("/")) {
|
||||||
|
normalized = normalized.substring(1);
|
||||||
|
}
|
||||||
|
while (normalized.startsWith("\\")) {
|
||||||
|
normalized = normalized.substring(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File targetCanonical = targetDir.getCanonicalFile();
|
File targetCanonical = targetDir.getCanonicalFile();
|
||||||
File output = new File(targetCanonical, normalized);
|
File output = new File(targetCanonical, normalized);
|
||||||
@ -488,7 +539,8 @@ public final class JBindExtractorMain {
|
|||||||
String outputPath = outputCanonical.getPath();
|
String outputPath = outputCanonical.getPath();
|
||||||
String targetPathNorm = isWindows() ? targetPath.toLowerCase(Locale.ROOT) : targetPath;
|
String targetPathNorm = isWindows() ? targetPath.toLowerCase(Locale.ROOT) : targetPath;
|
||||||
String outputPathNorm = isWindows() ? outputPath.toLowerCase(Locale.ROOT) : outputPath;
|
String outputPathNorm = isWindows() ? outputPath.toLowerCase(Locale.ROOT) : outputPath;
|
||||||
if (!outputPathNorm.equals(targetPathNorm) && !outputPathNorm.startsWith(targetPathNorm + File.separator)) {
|
String targetPrefix = targetPathNorm.endsWith(File.separator) ? targetPathNorm : targetPathNorm + File.separator;
|
||||||
|
if (!outputPathNorm.equals(targetPathNorm) && !outputPathNorm.startsWith(targetPrefix)) {
|
||||||
throw new IOException("Path Traversal blockiert: " + entryName);
|
throw new IOException("Path Traversal blockiert: " + entryName);
|
||||||
}
|
}
|
||||||
return outputCanonical;
|
return outputCanonical;
|
||||||
@ -506,20 +558,50 @@ public final class JBindExtractorMain {
|
|||||||
if (entry.length() == 0) {
|
if (entry.length() == 0) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
// Sanitize Windows special characters from each path segment
|
||||||
|
String[] segments = entry.split("/", -1);
|
||||||
|
StringBuilder sanitized = new StringBuilder();
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
sanitized.append('/');
|
||||||
|
}
|
||||||
|
sanitized.append(WINDOWS_SPECIAL_CHARS_RE.matcher(segments[i]).replaceAll("_"));
|
||||||
|
}
|
||||||
|
entry = sanitized.toString();
|
||||||
|
if (entry.length() == 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long safeSize(Long value) {
|
private static long safeSize(Long value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
long size = value.longValue();
|
long size = value.longValue();
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void rejectSymlink(File file) throws IOException {
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Files.isSymbolicLink(file.toPath())) {
|
||||||
|
throw new IOException("Zieldatei ist ein Symlink, Schreiben verweigert: " + file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
// Also check parent directories for symlinks
|
||||||
|
File parent = file.getParentFile();
|
||||||
|
while (parent != null) {
|
||||||
|
if (Files.isSymbolicLink(parent.toPath())) {
|
||||||
|
throw new IOException("Elternverzeichnis ist ein Symlink, Schreiben verweigert: " + parent.getAbsolutePath());
|
||||||
|
}
|
||||||
|
parent = parent.getParentFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ensureDirectory(File dir) throws IOException {
|
private static void ensureDirectory(File dir) throws IOException {
|
||||||
if (dir == null) {
|
if (dir == null) {
|
||||||
return;
|
return;
|
||||||
@ -828,12 +910,11 @@ public final class JBindExtractorMain {
|
|||||||
if (filename == null || filename.trim().length() == 0) {
|
if (filename == null || filename.trim().length() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
File direct = new File(filename);
|
// Always resolve relative to the archive's parent directory.
|
||||||
if (direct.isAbsolute() && direct.exists()) {
|
// Never accept absolute paths to prevent path traversal.
|
||||||
return direct;
|
String baseName = new File(filename).getName();
|
||||||
}
|
|
||||||
if (archiveDir != null) {
|
if (archiveDir != null) {
|
||||||
File relative = new File(archiveDir, filename);
|
File relative = new File(archiveDir, baseName);
|
||||||
if (relative.exists()) {
|
if (relative.exists()) {
|
||||||
return relative;
|
return relative;
|
||||||
}
|
}
|
||||||
@ -843,13 +924,13 @@ public final class JBindExtractorMain {
|
|||||||
if (!sibling.isFile()) {
|
if (!sibling.isFile()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (sibling.getName().equalsIgnoreCase(filename)) {
|
if (sibling.getName().equalsIgnoreCase(baseName)) {
|
||||||
return sibling;
|
return sibling;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return direct.exists() ? direct : null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -2,8 +2,17 @@ const path = require("path");
|
|||||||
const { rcedit } = require("rcedit");
|
const { rcedit } = require("rcedit");
|
||||||
|
|
||||||
module.exports = async function afterPack(context) {
|
module.exports = async function afterPack(context) {
|
||||||
const exePath = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.exe`);
|
const productFilename = context.packager?.appInfo?.productFilename;
|
||||||
|
if (!productFilename) {
|
||||||
|
console.warn(" • rcedit: skipped — productFilename not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exePath = path.join(context.appOutDir, `${productFilename}.exe`);
|
||||||
const iconPath = path.resolve(__dirname, "..", "assets", "app_icon.ico");
|
const iconPath = path.resolve(__dirname, "..", "assets", "app_icon.ico");
|
||||||
console.log(` • rcedit: patching icon → ${exePath}`);
|
console.log(` • rcedit: patching icon → ${exePath}`);
|
||||||
await rcedit(exePath, { icon: iconPath });
|
try {
|
||||||
|
await rcedit(exePath, { icon: iconPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(` • rcedit: failed — ${String(error)}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,18 +31,21 @@ async function main(): Promise<void> {
|
|||||||
login: settings.megaLogin,
|
login: settings.megaLogin,
|
||||||
password: settings.megaPassword
|
password: settings.megaPassword
|
||||||
}));
|
}));
|
||||||
const service = new DebridService(settings, {
|
try {
|
||||||
megaWebUnrestrict: (link) => megaWeb.unrestrict(link)
|
const service = new DebridService(settings, {
|
||||||
});
|
megaWebUnrestrict: (link) => megaWeb.unrestrict(link)
|
||||||
for (const link of links) {
|
});
|
||||||
try {
|
for (const link of links) {
|
||||||
const result = await service.unrestrictLink(link);
|
try {
|
||||||
console.log(`[OK] ${result.providerLabel} -> ${result.fileName}`);
|
const result = await service.unrestrictLink(link);
|
||||||
} catch (error) {
|
console.log(`[OK] ${result.providerLabel} -> ${result.fileName}`);
|
||||||
console.log(`[FAIL] ${String(error)}`);
|
} catch (error) {
|
||||||
|
console.log(`[FAIL] ${String(error)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
megaWeb.dispose();
|
||||||
}
|
}
|
||||||
megaWeb.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void main();
|
main().catch(e => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
@ -16,8 +16,8 @@ function sleep(ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cookieFrom(headers) {
|
function cookieFrom(headers) {
|
||||||
const raw = headers.get("set-cookie") || "";
|
const cookies = headers.getSetCookie();
|
||||||
return raw.split(",").map((x) => x.split(";")[0].trim()).filter(Boolean).join("; ");
|
return cookies.map((x) => x.split(";")[0].trim()).filter(Boolean).join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDebridCodes(html) {
|
function parseDebridCodes(html) {
|
||||||
@ -47,6 +47,9 @@ async function resolveCode(cookie, code) {
|
|||||||
});
|
});
|
||||||
const text = (await res.text()).trim();
|
const text = (await res.text()).trim();
|
||||||
if (text === "reload") {
|
if (text === "reload") {
|
||||||
|
if (attempt % 5 === 0) {
|
||||||
|
console.log(` [retry] code=${code} attempt=${attempt}/50 (waiting for server)`);
|
||||||
|
}
|
||||||
await sleep(800);
|
await sleep(800);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -98,7 +101,13 @@ async function main() {
|
|||||||
redirect: "manual"
|
redirect: "manual"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (loginRes.status >= 400) {
|
||||||
|
throw new Error(`Login failed with HTTP ${loginRes.status}`);
|
||||||
|
}
|
||||||
const cookie = cookieFrom(loginRes.headers);
|
const cookie = cookieFrom(loginRes.headers);
|
||||||
|
if (!cookie) {
|
||||||
|
throw new Error("Login returned no session cookie");
|
||||||
|
}
|
||||||
console.log("login", loginRes.status, loginRes.headers.get("location") || "");
|
console.log("login", loginRes.status, loginRes.headers.get("location") || "");
|
||||||
|
|
||||||
const debridRes = await fetch("https://www.mega-debrid.eu/index.php?form=debrid", {
|
const debridRes = await fetch("https://www.mega-debrid.eu/index.php?form=debrid", {
|
||||||
@ -136,4 +145,4 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main().catch((e) => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
@ -66,6 +66,8 @@ async function callRealDebrid(link) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// megaCookie is intentionally cached at module scope so that multiple
|
||||||
|
// callMegaDebrid() invocations reuse the same session cookie.
|
||||||
async function callMegaDebrid(link) {
|
async function callMegaDebrid(link) {
|
||||||
if (!megaCookie) {
|
if (!megaCookie) {
|
||||||
const loginRes = await fetch("https://www.mega-debrid.eu/index.php?form=login", {
|
const loginRes = await fetch("https://www.mega-debrid.eu/index.php?form=login", {
|
||||||
@ -77,13 +79,15 @@ async function callMegaDebrid(link) {
|
|||||||
body: new URLSearchParams({ login: megaLogin, password: megaPassword, remember: "on" }),
|
body: new URLSearchParams({ login: megaLogin, password: megaPassword, remember: "on" }),
|
||||||
redirect: "manual"
|
redirect: "manual"
|
||||||
});
|
});
|
||||||
megaCookie = (loginRes.headers.get("set-cookie") || "")
|
if (loginRes.status >= 400) {
|
||||||
.split(",")
|
return { ok: false, error: `Mega-Web login failed with HTTP ${loginRes.status}` };
|
||||||
|
}
|
||||||
|
megaCookie = loginRes.headers.getSetCookie()
|
||||||
.map((chunk) => chunk.split(";")[0].trim())
|
.map((chunk) => chunk.split(";")[0].trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
if (!megaCookie) {
|
if (!megaCookie) {
|
||||||
return { ok: false, error: "Mega-Web login failed" };
|
return { ok: false, error: "Mega-Web login returned no session cookie" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,4 +294,4 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main().catch((e) => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
@ -37,7 +37,8 @@ function runWithInput(command, args, input) {
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
input,
|
input,
|
||||||
stdio: ["pipe", "pipe", "pipe"]
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
timeout: 10000
|
||||||
});
|
});
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
const stderr = String(result.stderr || "").trim();
|
const stderr = String(result.stderr || "").trim();
|
||||||
@ -95,15 +96,17 @@ function getGiteaRepo() {
|
|||||||
|
|
||||||
const preferredBase = normalizeBaseUrl(process.env.GITEA_BASE_URL || process.env.FORGEJO_BASE_URL || "https://git.24-music.de");
|
const preferredBase = normalizeBaseUrl(process.env.GITEA_BASE_URL || process.env.FORGEJO_BASE_URL || "https://git.24-music.de");
|
||||||
|
|
||||||
|
const preferredProtocol = preferredBase ? new URL(preferredBase).protocol : "https:";
|
||||||
|
|
||||||
for (const remote of remotes) {
|
for (const remote of remotes) {
|
||||||
try {
|
try {
|
||||||
const remoteUrl = runCapture("git", ["remote", "get-url", remote]);
|
const remoteUrl = runCapture("git", ["remote", "get-url", remote]);
|
||||||
const parsed = parseRemoteUrl(remoteUrl);
|
const parsed = parseRemoteUrl(remoteUrl);
|
||||||
const remoteBase = `https://${parsed.host}`.toLowerCase();
|
const remoteBase = `https://${parsed.host}`.toLowerCase();
|
||||||
if (preferredBase && remoteBase !== preferredBase.toLowerCase()) {
|
if (preferredBase && remoteBase !== preferredBase.toLowerCase().replace(/^http:/, "https:")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return { remote, ...parsed, baseUrl: `https://${parsed.host}` };
|
return { remote, ...parsed, baseUrl: `${preferredProtocol}//${parsed.host}` };
|
||||||
} catch {
|
} catch {
|
||||||
// try next remote
|
// try next remote
|
||||||
}
|
}
|
||||||
@ -179,7 +182,8 @@ function updatePackageVersion(rootDir, version) {
|
|||||||
const packagePath = path.join(rootDir, "package.json");
|
const packagePath = path.join(rootDir, "package.json");
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||||
if (String(packageJson.version || "") === version) {
|
if (String(packageJson.version || "") === version) {
|
||||||
throw new Error(`package.json is already at version ${version}`);
|
process.stdout.write(`package.json is already at version ${version}, skipping update.\n`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
packageJson.version = version;
|
packageJson.version = version;
|
||||||
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
||||||
@ -257,9 +261,31 @@ async function createOrGetRelease(baseApi, tag, authHeader, notes) {
|
|||||||
async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) {
|
async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) {
|
||||||
for (const fileName of files) {
|
for (const fileName of files) {
|
||||||
const filePath = path.join(releaseDir, fileName);
|
const filePath = path.join(releaseDir, fileName);
|
||||||
const fileData = fs.readFileSync(filePath);
|
const fileSize = fs.statSync(filePath).size;
|
||||||
const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`;
|
const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`;
|
||||||
const response = await apiRequest("POST", uploadUrl, authHeader, fileData, "application/octet-stream");
|
|
||||||
|
// Stream large files instead of loading them entirely into memory
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
const response = await fetch(uploadUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: authHeader,
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Content-Length": String(fileSize)
|
||||||
|
},
|
||||||
|
body: fileStream,
|
||||||
|
duplex: "half"
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = text ? JSON.parse(text) : null;
|
||||||
|
} catch {
|
||||||
|
parsed = text;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
process.stdout.write(`Uploaded: ${fileName}\n`);
|
process.stdout.write(`Uploaded: ${fileName}\n`);
|
||||||
continue;
|
continue;
|
||||||
@ -268,7 +294,7 @@ async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, f
|
|||||||
process.stdout.write(`Skipped existing asset: ${fileName}\n`);
|
process.stdout.write(`Skipped existing asset: ${fileName}\n`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(response.body)}`);
|
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(parsed)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,17 +316,18 @@ async function main() {
|
|||||||
|
|
||||||
ensureNoTrackedChanges();
|
ensureNoTrackedChanges();
|
||||||
ensureTagMissing(tag);
|
ensureTagMissing(tag);
|
||||||
|
|
||||||
|
if (args.dryRun) {
|
||||||
|
process.stdout.write(`Dry run: would release ${tag}. No changes made.\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updatePackageVersion(rootDir, version);
|
updatePackageVersion(rootDir, version);
|
||||||
|
|
||||||
process.stdout.write(`Building release artifacts for ${tag}...\n`);
|
process.stdout.write(`Building release artifacts for ${tag}...\n`);
|
||||||
run(NPM_EXECUTABLE, ["run", "release:win"]);
|
run(NPM_EXECUTABLE, ["run", "release:win"]);
|
||||||
const assets = ensureAssetsExist(rootDir, version);
|
const assets = ensureAssetsExist(rootDir, version);
|
||||||
|
|
||||||
if (args.dryRun) {
|
|
||||||
process.stdout.write(`Dry run complete. Assets exist for ${tag}.\n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
run("git", ["add", "package.json"]);
|
run("git", ["add", "package.json"]);
|
||||||
run("git", ["commit", "-m", `Release ${tag}`]);
|
run("git", ["commit", "-m", `Release ${tag}`]);
|
||||||
run("git", ["push", repo.remote, "main"]);
|
run("git", ["push", repo.remote, "main"]);
|
||||||
|
|||||||
@ -285,7 +285,7 @@ export class AppController {
|
|||||||
|
|
||||||
public exportBackup(): string {
|
public exportBackup(): string {
|
||||||
const settings = { ...this.settings };
|
const settings = { ...this.settings };
|
||||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken", "ddownloadPassword"];
|
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"];
|
||||||
for (const key of SENSITIVE_KEYS) {
|
for (const key of SENSITIVE_KEYS) {
|
||||||
const val = settings[key];
|
const val = settings[key];
|
||||||
if (typeof val === "string" && val.length > 0) {
|
if (typeof val === "string" && val.length > 0) {
|
||||||
@ -307,7 +307,7 @@ export class AppController {
|
|||||||
return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" };
|
return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" };
|
||||||
}
|
}
|
||||||
const importedSettings = parsed.settings as AppSettings;
|
const importedSettings = parsed.settings as AppSettings;
|
||||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken", "ddownloadPassword"];
|
const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"];
|
||||||
for (const key of SENSITIVE_KEYS) {
|
for (const key of SENSITIVE_KEYS) {
|
||||||
const val = (importedSettings as Record<string, unknown>)[key];
|
const val = (importedSettings as Record<string, unknown>)[key];
|
||||||
if (typeof val === "string" && val.startsWith("***")) {
|
if (typeof val === "string" && val.startsWith("***")) {
|
||||||
|
|||||||
@ -164,7 +164,7 @@ async function decryptDlcLocal(filePath: string): Promise<ParsedPackageInput[]>
|
|||||||
const dlcData = content.slice(0, -88);
|
const dlcData = content.slice(0, -88);
|
||||||
|
|
||||||
const rcUrl = DLC_SERVICE_URL.replace("{KEY}", encodeURIComponent(dlcKey));
|
const rcUrl = DLC_SERVICE_URL.replace("{KEY}", encodeURIComponent(dlcKey));
|
||||||
const rcResponse = await fetch(rcUrl, { method: "GET" });
|
const rcResponse = await fetch(rcUrl, { method: "GET", signal: AbortSignal.timeout(30000) });
|
||||||
if (!rcResponse.ok) {
|
if (!rcResponse.ok) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -217,7 +217,8 @@ async function tryDcryptUpload(fileContent: Buffer, fileName: string): Promise<s
|
|||||||
|
|
||||||
const response = await fetch(DCRYPT_UPLOAD_URL, {
|
const response = await fetch(DCRYPT_UPLOAD_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: form
|
body: form,
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
if (response.status === 413) {
|
if (response.status === 413) {
|
||||||
return null;
|
return null;
|
||||||
@ -235,7 +236,8 @@ async function tryDcryptPaste(fileContent: Buffer): Promise<string[] | null> {
|
|||||||
|
|
||||||
const response = await fetch(DCRYPT_PASTE_URL, {
|
const response = await fetch(DCRYPT_PASTE_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: form
|
body: form,
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
if (response.status === 413) {
|
if (response.status === 413) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -1154,6 +1154,9 @@ export class DebridService {
|
|||||||
|
|
||||||
private options: DebridServiceOptions;
|
private options: DebridServiceOptions;
|
||||||
|
|
||||||
|
private cachedDdownloadClient: DdownloadClient | null = null;
|
||||||
|
private cachedDdownloadKey = "";
|
||||||
|
|
||||||
public constructor(settings: AppSettings, options: DebridServiceOptions = {}) {
|
public constructor(settings: AppSettings, options: DebridServiceOptions = {}) {
|
||||||
this.settings = cloneSettings(settings);
|
this.settings = cloneSettings(settings);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@ -1163,6 +1166,16 @@ export class DebridService {
|
|||||||
this.settings = cloneSettings(next);
|
this.settings = cloneSettings(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDdownloadClient(login: string, password: string): DdownloadClient {
|
||||||
|
const key = `${login}\0${password}`;
|
||||||
|
if (this.cachedDdownloadClient && this.cachedDdownloadKey === key) {
|
||||||
|
return this.cachedDdownloadClient;
|
||||||
|
}
|
||||||
|
this.cachedDdownloadClient = new DdownloadClient(login, password);
|
||||||
|
this.cachedDdownloadKey = key;
|
||||||
|
return this.cachedDdownloadClient;
|
||||||
|
}
|
||||||
|
|
||||||
public async resolveFilenames(
|
public async resolveFilenames(
|
||||||
links: string[],
|
links: string[],
|
||||||
onResolved?: (link: string, fileName: string) => void,
|
onResolved?: (link: string, fileName: string) => void,
|
||||||
@ -1338,7 +1351,7 @@ export class DebridService {
|
|||||||
return new AllDebridClient(settings.allDebridToken).unrestrictLink(link, signal);
|
return new AllDebridClient(settings.allDebridToken).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
if (provider === "ddownload") {
|
if (provider === "ddownload") {
|
||||||
return new DdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal);
|
return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
|
return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,7 +261,7 @@ export function startDebugServer(mgr: DownloadManager, baseDir: string): void {
|
|||||||
const port = getPort(baseDir);
|
const port = getPort(baseDir);
|
||||||
|
|
||||||
server = http.createServer(handleRequest);
|
server = http.createServer(handleRequest);
|
||||||
server.listen(port, "0.0.0.0", () => {
|
server.listen(port, "127.0.0.1", () => {
|
||||||
logger.info(`Debug-Server gestartet auf Port ${port}`);
|
logger.info(`Debug-Server gestartet auf Port ${port}`);
|
||||||
});
|
});
|
||||||
server.on("error", (err) => {
|
server.on("error", (err) => {
|
||||||
|
|||||||
@ -20,6 +20,23 @@ import {
|
|||||||
UiSnapshot
|
UiSnapshot
|
||||||
} from "../shared/types";
|
} from "../shared/types";
|
||||||
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK, DISK_BUSY_THRESHOLD_MS } from "./constants";
|
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK, DISK_BUSY_THRESHOLD_MS } from "./constants";
|
||||||
|
|
||||||
|
// Reference counter for NODE_TLS_REJECT_UNAUTHORIZED to avoid race conditions
|
||||||
|
// when multiple parallel downloads need TLS verification disabled (e.g. DDownload).
|
||||||
|
let tlsSkipRefCount = 0;
|
||||||
|
function acquireTlsSkip(): void {
|
||||||
|
tlsSkipRefCount += 1;
|
||||||
|
if (tlsSkipRefCount === 1) {
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function releaseTlsSkip(): void {
|
||||||
|
tlsSkipRefCount -= 1;
|
||||||
|
if (tlsSkipRefCount <= 0) {
|
||||||
|
tlsSkipRefCount = 0;
|
||||||
|
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
import { cleanupCancelledPackageArtifactsAsync } from "./cleanup";
|
import { cleanupCancelledPackageArtifactsAsync } from "./cleanup";
|
||||||
import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
|
import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
|
||||||
import { clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
|
import { clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
|
||||||
@ -3212,11 +3229,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
if (item.status !== "completed") continue;
|
if (item.status !== "completed") continue;
|
||||||
const fs = item.fullStatus || "";
|
const fullSt = item.fullStatus || "";
|
||||||
// Only relabel items with active extraction status (e.g. "Entpacken 45%", "Passwort prüfen")
|
// Only relabel items with active extraction status (e.g. "Entpacken 45%", "Passwort prüfen")
|
||||||
// Skip items that were merely waiting ("Entpacken - Ausstehend", "Entpacken - Warten auf Parts")
|
// Skip items that were merely waiting ("Entpacken - Ausstehend", "Entpacken - Warten auf Parts")
|
||||||
// as they were never actively extracting and "abgebrochen" would be misleading.
|
// as they were never actively extracting and "abgebrochen" would be misleading.
|
||||||
if (/^Entpacken\b/i.test(fs) && !/Ausstehend/i.test(fs) && !/Warten/i.test(fs) && !isExtractedLabel(fs)) {
|
if (/^Entpacken\b/i.test(fullSt) && !/Ausstehend/i.test(fullSt) && !/Warten/i.test(fullSt) && !isExtractedLabel(fullSt)) {
|
||||||
item.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
|
item.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
const pkg = this.session.packages[item.packageId];
|
const pkg = this.session.packages[item.packageId];
|
||||||
@ -3305,7 +3322,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
|
|
||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
if (item.provider !== "realdebrid" && item.provider !== "megadebrid" && item.provider !== "bestdebrid" && item.provider !== "alldebrid") {
|
if (item.provider !== "realdebrid" && item.provider !== "megadebrid" && item.provider !== "bestdebrid" && item.provider !== "alldebrid" && item.provider !== "ddownload") {
|
||||||
item.provider = null;
|
item.provider = null;
|
||||||
}
|
}
|
||||||
if (item.status === "cancelled" && item.fullStatus === "Gestoppt") {
|
if (item.status === "cancelled" && item.fullStatus === "Gestoppt") {
|
||||||
@ -5152,16 +5169,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const connectTimeoutMs = getDownloadConnectTimeoutMs();
|
const connectTimeoutMs = getDownloadConnectTimeoutMs();
|
||||||
let connectTimer: NodeJS.Timeout | null = null;
|
let connectTimer: NodeJS.Timeout | null = null;
|
||||||
const connectAbortController = new AbortController();
|
const connectAbortController = new AbortController();
|
||||||
const prevTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
if (skipTlsVerify) acquireTlsSkip();
|
||||||
try {
|
try {
|
||||||
if (connectTimeoutMs > 0) {
|
if (connectTimeoutMs > 0) {
|
||||||
connectTimer = setTimeout(() => {
|
connectTimer = setTimeout(() => {
|
||||||
connectAbortController.abort("connect_timeout");
|
connectAbortController.abort("connect_timeout");
|
||||||
}, connectTimeoutMs);
|
}, connectTimeoutMs);
|
||||||
}
|
}
|
||||||
if (skipTlsVerify) {
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
||||||
}
|
|
||||||
response = await fetch(directUrl, {
|
response = await fetch(directUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers,
|
headers,
|
||||||
@ -5181,10 +5195,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
if (skipTlsVerify) {
|
if (skipTlsVerify) releaseTlsSkip();
|
||||||
if (prevTlsReject === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
||||||
else process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevTlsReject;
|
|
||||||
}
|
|
||||||
if (connectTimer) {
|
if (connectTimer) {
|
||||||
clearTimeout(connectTimer);
|
clearTimeout(connectTimer);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,26 @@ function removeSubstMapping(mapping: SubstMapping): void {
|
|||||||
logger.info(`subst ${mapping.drive}: entfernt`);
|
logger.info(`subst ${mapping.drive}: entfernt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cleanupStaleSubstDrives(): void {
|
||||||
|
if (process.platform !== "win32") return;
|
||||||
|
try {
|
||||||
|
const result = spawnSync("subst", [], { stdio: "pipe", timeout: 5000 });
|
||||||
|
const output = String(result.stdout || "");
|
||||||
|
for (const line of output.split("\n")) {
|
||||||
|
const match = line.match(/^([A-Z]):\\: => (.+)/i);
|
||||||
|
if (!match) continue;
|
||||||
|
const drive = match[1].toUpperCase();
|
||||||
|
const target = match[2].trim();
|
||||||
|
if (/\\rd-extract-|\\Real-Debrid-Downloader/i.test(target)) {
|
||||||
|
spawnSync("subst", [`${drive}:`, "/d"], { stdio: "pipe", timeout: 5000 });
|
||||||
|
logger.info(`Stale subst ${drive}: entfernt (${target})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore — subst cleanup is best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let resolvedExtractorCommand: string | null = null;
|
let resolvedExtractorCommand: string | null = null;
|
||||||
let resolveFailureReason = "";
|
let resolveFailureReason = "";
|
||||||
let resolveFailureAt = 0;
|
let resolveFailureAt = 0;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { IPC_CHANNELS } from "../shared/ipc";
|
|||||||
import { getLogFilePath, logger } from "./logger";
|
import { getLogFilePath, logger } from "./logger";
|
||||||
import { APP_NAME } from "./constants";
|
import { APP_NAME } from "./constants";
|
||||||
import { extractHttpLinksFromText } from "./utils";
|
import { extractHttpLinksFromText } from "./utils";
|
||||||
|
import { cleanupStaleSubstDrives } from "./extractor";
|
||||||
|
|
||||||
/* ── IPC validation helpers ────────────────────────────────────── */
|
/* ── IPC validation helpers ────────────────────────────────────── */
|
||||||
function validateString(value: unknown, name: string): string {
|
function validateString(value: unknown, name: string): string {
|
||||||
@ -81,7 +82,7 @@ function createWindow(): BrowserWindow {
|
|||||||
responseHeaders: {
|
responseHeaders: {
|
||||||
...details.responseHeaders,
|
...details.responseHeaders,
|
||||||
"Content-Security-Policy": [
|
"Content-Security-Policy": [
|
||||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.real-debrid.com https://codeberg.org https://bestdebrid.com https://api.alldebrid.com https://www.mega-debrid.eu"
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.real-debrid.com https://codeberg.org https://bestdebrid.com https://api.alldebrid.com https://www.mega-debrid.eu https://git.24-music.de https://ddownload.com https://ddl.to"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -188,7 +189,12 @@ function startClipboardWatcher(): void {
|
|||||||
}
|
}
|
||||||
lastClipboardText = normalizeClipboardText(clipboard.readText());
|
lastClipboardText = normalizeClipboardText(clipboard.readText());
|
||||||
clipboardTimer = setInterval(() => {
|
clipboardTimer = setInterval(() => {
|
||||||
const text = normalizeClipboardText(clipboard.readText());
|
let text: string;
|
||||||
|
try {
|
||||||
|
text = normalizeClipboardText(clipboard.readText());
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (text === lastClipboardText || !text.trim()) {
|
if (text === lastClipboardText || !text.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -481,6 +487,7 @@ app.on("second-instance", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
cleanupStaleSubstDrives();
|
||||||
registerIpcHandlers();
|
registerIpcHandlers();
|
||||||
mainWindow = createWindow();
|
mainWindow = createWindow();
|
||||||
bindMainWindowLifecycle(mainWindow);
|
bindMainWindowLifecycle(mainWindow);
|
||||||
@ -493,6 +500,9 @@ app.whenReady().then(() => {
|
|||||||
bindMainWindowLifecycle(mainWindow);
|
bindMainWindowLifecycle(mainWindow);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("App startup failed:", error);
|
||||||
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
|
|||||||
@ -228,22 +228,23 @@ export class MegaWebFallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
|
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
|
||||||
|
const overallSignal = withTimeoutSignal(signal, 180000);
|
||||||
return this.runExclusive(async () => {
|
return this.runExclusive(async () => {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(overallSignal);
|
||||||
const creds = this.getCredentials();
|
const creds = this.getCredentials();
|
||||||
if (!creds.login.trim() || !creds.password.trim()) {
|
if (!creds.login.trim() || !creds.password.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) {
|
if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) {
|
||||||
await this.login(creds.login, creds.password, signal);
|
await this.login(creds.login, creds.password, overallSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
const generated = await this.generate(link, signal);
|
const generated = await this.generate(link, overallSignal);
|
||||||
if (!generated) {
|
if (!generated) {
|
||||||
this.cookie = "";
|
this.cookie = "";
|
||||||
await this.login(creds.login, creds.password, signal);
|
await this.login(creds.login, creds.password, overallSignal);
|
||||||
const retry = await this.generate(link, signal);
|
const retry = await this.generate(link, overallSignal);
|
||||||
if (!retry) {
|
if (!retry) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -261,7 +262,7 @@ export class MegaWebFallback {
|
|||||||
fileSize: null,
|
fileSize: null,
|
||||||
retriesUsed: 0
|
retriesUsed: 0
|
||||||
};
|
};
|
||||||
}, signal);
|
}, overallSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public invalidateSession(): void {
|
public invalidateSession(): void {
|
||||||
|
|||||||
@ -76,7 +76,12 @@ async function cleanupOldSessionLogs(dir: string, maxAgeDays: number): Promise<v
|
|||||||
|
|
||||||
export function initSessionLog(baseDir: string): void {
|
export function initSessionLog(baseDir: string): void {
|
||||||
sessionLogsDir = path.join(baseDir, "session-logs");
|
sessionLogsDir = path.join(baseDir, "session-logs");
|
||||||
fs.mkdirSync(sessionLogsDir, { recursive: true });
|
try {
|
||||||
|
fs.mkdirSync(sessionLogsDir, { recursive: true });
|
||||||
|
} catch {
|
||||||
|
sessionLogsDir = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = formatTimestamp();
|
const timestamp = formatTimestamp();
|
||||||
sessionLogPath = path.join(sessionLogsDir, `session_${timestamp}.txt`);
|
sessionLogPath = path.join(sessionLogsDir, `session_${timestamp}.txt`);
|
||||||
|
|||||||
@ -113,7 +113,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
allDebridToken: asText(settings.allDebridToken),
|
allDebridToken: asText(settings.allDebridToken),
|
||||||
ddownloadLogin: asText(settings.ddownloadLogin),
|
ddownloadLogin: asText(settings.ddownloadLogin),
|
||||||
ddownloadPassword: asText(settings.ddownloadPassword),
|
ddownloadPassword: asText(settings.ddownloadPassword),
|
||||||
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n/g, "\n"),
|
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
||||||
rememberToken: Boolean(settings.rememberToken),
|
rememberToken: Boolean(settings.rememberToken),
|
||||||
providerPrimary: settings.providerPrimary,
|
providerPrimary: settings.providerPrimary,
|
||||||
providerSecondary: settings.providerSecondary,
|
providerSecondary: settings.providerSecondary,
|
||||||
|
|||||||
@ -794,7 +794,8 @@ async function downloadFile(url: string, targetPath: string, onProgress?: Update
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const chunks: Buffer[] = [];
|
const tempPath = targetPath + ".tmp";
|
||||||
|
const writeStream = fs.createWriteStream(tempPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
@ -808,27 +809,39 @@ async function downloadFile(url: string, targetPath: string, onProgress?: Update
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const buf = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
const buf = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
||||||
chunks.push(buf);
|
if (!writeStream.write(buf)) {
|
||||||
|
await new Promise<void>((resolve) => writeStream.once("drain", resolve));
|
||||||
|
}
|
||||||
downloadedBytes += buf.byteLength;
|
downloadedBytes += buf.byteLength;
|
||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
emitDownloadProgress(false);
|
emitDownloadProgress(false);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
writeStream.destroy();
|
||||||
|
await fs.promises.rm(tempPath, { force: true }).catch(() => {});
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearIdleTimer();
|
clearIdleTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
writeStream.end(() => resolve());
|
||||||
|
writeStream.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
if (idleTimedOut) {
|
if (idleTimedOut) {
|
||||||
|
await fs.promises.rm(tempPath, { force: true }).catch(() => {});
|
||||||
throw new Error(`Update Download Body Timeout nach ${Math.ceil(idleTimeoutMs / 1000)}s`);
|
throw new Error(`Update Download Body Timeout nach ${Math.ceil(idleTimeoutMs / 1000)}s`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileBuffer = Buffer.concat(chunks);
|
if (totalBytes && downloadedBytes !== totalBytes) {
|
||||||
if (totalBytes && fileBuffer.byteLength !== totalBytes) {
|
await fs.promises.rm(tempPath, { force: true }).catch(() => {});
|
||||||
throw new Error(`Update Download unvollständig (${fileBuffer.byteLength} / ${totalBytes} Bytes)`);
|
throw new Error(`Update Download unvollständig (${downloadedBytes} / ${totalBytes} Bytes)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.writeFile(targetPath, fileBuffer);
|
await fs.promises.rename(tempPath, targetPath);
|
||||||
emitDownloadProgress(true);
|
emitDownloadProgress(true);
|
||||||
logger.info(`Update-Download abgeschlossen: ${targetPath} (${fileBuffer.byteLength} Bytes)`);
|
logger.info(`Update-Download abgeschlossen: ${targetPath} (${downloadedBytes} Bytes)`);
|
||||||
|
|
||||||
return { expectedBytes: totalBytes };
|
return { expectedBytes: totalBytes };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "",
|
token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "",
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
||||||
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||||
@ -115,15 +115,6 @@ function extractHoster(url: string): string {
|
|||||||
} catch { return ""; }
|
} catch { return ""; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHoster(item: DownloadItem): string {
|
|
||||||
const hoster = extractHoster(item.url);
|
|
||||||
const label = hoster || "-";
|
|
||||||
if (item.provider) {
|
|
||||||
return `${label} via ${providerLabels[item.provider]}`;
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingsSubTabs: { key: SettingsSubTab; label: string }[] = [
|
const settingsSubTabs: { key: SettingsSubTab; label: string }[] = [
|
||||||
{ key: "allgemein", label: "Allgemein" },
|
{ key: "allgemein", label: "Allgemein" },
|
||||||
{ key: "accounts", label: "Accounts" },
|
{ key: "accounts", label: "Accounts" },
|
||||||
@ -1878,10 +1869,12 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const executeDeleteSelection = useCallback((ids: Set<string>): void => {
|
const executeDeleteSelection = useCallback((ids: Set<string>): void => {
|
||||||
const current = snapshotRef.current;
|
const current = snapshotRef.current;
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
if (current.session.items[id]) void window.rd.removeItem(id);
|
if (current.session.items[id]) promises.push(window.rd.removeItem(id));
|
||||||
else if (current.session.packages[id]) void window.rd.cancelPackage(id);
|
else if (current.session.packages[id]) promises.push(window.rd.cancelPackage(id));
|
||||||
}
|
}
|
||||||
|
void Promise.all(promises).catch(() => {});
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -1924,28 +1917,28 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const onExportBackup = async (): Promise<void> => {
|
const onExportBackup = async (): Promise<void> => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.exportBackup();
|
const result = await window.rd.exportBackup();
|
||||||
if (result.saved) {
|
if (result.saved) {
|
||||||
showToast("Sicherung exportiert");
|
showToast("Sicherung exportiert");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, (error) => {
|
||||||
showToast(`Sicherung fehlgeschlagen: ${String(error)}`, 2600);
|
showToast(`Sicherung fehlgeschlagen: ${String(error)}`, 2600);
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImportBackup = async (): Promise<void> => {
|
const onImportBackup = async (): Promise<void> => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
try {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.importBackup();
|
const result = await window.rd.importBackup();
|
||||||
if (result.restored) {
|
if (result.restored) {
|
||||||
showToast(result.message, 4000);
|
showToast(result.message, 4000);
|
||||||
} else if (result.message !== "Abgebrochen") {
|
} else if (result.message !== "Abgebrochen") {
|
||||||
showToast(`Sicherung laden fehlgeschlagen: ${result.message}`, 3000);
|
showToast(`Sicherung laden fehlgeschlagen: ${result.message}`, 3000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}, (error) => {
|
||||||
showToast(`Sicherung laden fehlgeschlagen: ${String(error)}`, 2600);
|
showToast(`Sicherung laden fehlgeschlagen: ${String(error)}`, 2600);
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMenuRestart = (): void => {
|
const onMenuRestart = (): void => {
|
||||||
@ -2279,7 +2272,7 @@ export function App(): ReactElement {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (snapshot.session.paused) {
|
if (snapshot.session.paused) {
|
||||||
setSnapshot((prev) => ({ ...prev, session: { ...prev.session, paused: false } }));
|
setSnapshot((prev) => ({ ...prev, session: { ...prev.session, paused: false } }));
|
||||||
void window.rd.togglePause();
|
void window.rd.togglePause().catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
void onStartDownloads();
|
void onStartDownloads();
|
||||||
}
|
}
|
||||||
@ -2293,7 +2286,7 @@ export function App(): ReactElement {
|
|||||||
disabled={!snapshot.canPause || snapshot.session.paused}
|
disabled={!snapshot.canPause || snapshot.session.paused}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSnapshot((prev) => ({ ...prev, session: { ...prev.session, paused: true } }));
|
setSnapshot((prev) => ({ ...prev, session: { ...prev.session, paused: true } }));
|
||||||
void window.rd.togglePause();
|
void window.rd.togglePause().catch(() => {});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="5" y="3" width="4.5" height="18" rx="1" fill="currentColor" /><rect x="14.5" y="3" width="4.5" height="18" rx="1" fill="currentColor" /></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="5" y="3" width="4.5" height="18" rx="1" fill="currentColor" /><rect x="14.5" y="3" width="4.5" height="18" rx="1" fill="currentColor" /></svg>
|
||||||
@ -2520,7 +2513,7 @@ export function App(): ReactElement {
|
|||||||
}}>Ausgewählte entfernen ({selectedHistoryIds.size})</button>
|
}}>Ausgewählte entfernen ({selectedHistoryIds.size})</button>
|
||||||
)}
|
)}
|
||||||
{historyEntries.length > 0 && (
|
{historyEntries.length > 0 && (
|
||||||
<button className="btn btn-danger" onClick={() => { void window.rd.clearHistory().then(() => { setHistoryEntries([]); setSelectedHistoryIds(new Set()); }); }}>Verlauf leeren</button>
|
<button className="btn btn-danger" onClick={() => { void window.rd.clearHistory().then(() => { setHistoryEntries([]); setSelectedHistoryIds(new Set()); }).catch(() => {}); }}>Verlauf leeren</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{historyEntries.length === 0 && <div className="empty">Noch keine abgeschlossenen Pakete im Verlauf.</div>}
|
{historyEntries.length === 0 && <div className="empty">Noch keine abgeschlossenen Pakete im Verlauf.</div>}
|
||||||
@ -2607,7 +2600,7 @@ export function App(): ReactElement {
|
|||||||
<span>{entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}</span>
|
<span>{entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="history-actions">
|
<div className="history-actions">
|
||||||
<button className="btn" onClick={() => { void window.rd.removeHistoryEntry(entry.id).then(() => { setHistoryEntries((prev) => prev.filter((e) => e.id !== entry.id)); setSelectedHistoryIds((prev) => { const n = new Set(prev); n.delete(entry.id); return n; }); }); }}>Eintrag entfernen</button>
|
<button className="btn" onClick={() => { void window.rd.removeHistoryEntry(entry.id).then(() => { setHistoryEntries((prev) => prev.filter((e) => e.id !== entry.id)); setSelectedHistoryIds((prev) => { const n = new Set(prev); n.delete(entry.id); return n; }); }).catch(() => {}); }}>Eintrag entfernen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -3052,8 +3045,8 @@ export function App(): ReactElement {
|
|||||||
<button className="ctx-menu-item" onClick={() => {
|
<button className="ctx-menu-item" onClick={() => {
|
||||||
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
||||||
const itemIds = [...selectedIds].filter((id) => { const it = snapshot.session.items[id]; return it && startableStatuses.has(it.status); });
|
const itemIds = [...selectedIds].filter((id) => { const it = snapshot.session.items[id]; return it && startableStatuses.has(it.status); });
|
||||||
if (pkgIds.length > 0) void window.rd.startPackages(pkgIds);
|
if (pkgIds.length > 0) void window.rd.startPackages(pkgIds).catch(() => {});
|
||||||
if (itemIds.length > 0) void window.rd.startItems(itemIds);
|
if (itemIds.length > 0) void window.rd.startItems(itemIds).catch(() => {});
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}>Ausgewählte Downloads starten{multi ? ` (${selectedIds.size})` : ""}</button>
|
}}>Ausgewählte Downloads starten{multi ? ` (${selectedIds.size})` : ""}</button>
|
||||||
)}
|
)}
|
||||||
@ -3063,7 +3056,7 @@ export function App(): ReactElement {
|
|||||||
<div className="ctx-menu-sep" />
|
<div className="ctx-menu-sep" />
|
||||||
{hasPackages && !contextMenu.itemId && (
|
{hasPackages && !contextMenu.itemId && (
|
||||||
<button className="ctx-menu-item" onClick={() => {
|
<button className="ctx-menu-item" onClick={() => {
|
||||||
for (const id of selectedIds) { if (snapshot.session.packages[id]) void window.rd.togglePackage(id); }
|
for (const id of selectedIds) { if (snapshot.session.packages[id]) void window.rd.togglePackage(id).catch(() => {}); }
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}>
|
}}>
|
||||||
{multi ? `Alle ${selectedIds.size} umschalten` : (snapshot.session.packages[contextMenu.packageId]?.enabled ? "Deaktivieren" : "Aktivieren")}
|
{multi ? `Alle ${selectedIds.size} umschalten` : (snapshot.session.packages[contextMenu.packageId]?.enabled ? "Deaktivieren" : "Aktivieren")}
|
||||||
@ -3088,7 +3081,7 @@ export function App(): ReactElement {
|
|||||||
{hasPackages && !contextMenu.itemId && (
|
{hasPackages && !contextMenu.itemId && (
|
||||||
<button className="ctx-menu-item" onClick={() => {
|
<button className="ctx-menu-item" onClick={() => {
|
||||||
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
|
||||||
for (const id of pkgIds) void window.rd.resetPackage(id);
|
for (const id of pkgIds) void window.rd.resetPackage(id).catch(() => {});
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}>Zurücksetzen{multi ? ` (${[...selectedIds].filter((id) => snapshot.session.packages[id]).length})` : ""}</button>
|
}}>Zurücksetzen{multi ? ` (${[...selectedIds].filter((id) => snapshot.session.packages[id]).length})` : ""}</button>
|
||||||
)}
|
)}
|
||||||
@ -3097,7 +3090,7 @@ export function App(): ReactElement {
|
|||||||
const itemIds = multi
|
const itemIds = multi
|
||||||
? [...selectedIds].filter((id) => snapshot.session.items[id])
|
? [...selectedIds].filter((id) => snapshot.session.items[id])
|
||||||
: [contextMenu.itemId!];
|
: [contextMenu.itemId!];
|
||||||
void window.rd.resetItems(itemIds);
|
void window.rd.resetItems(itemIds).catch(() => {});
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}>Zurücksetzen{multi ? ` (${[...selectedIds].filter((id) => snapshot.session.items[id]).length})` : ""}</button>
|
}}>Zurücksetzen{multi ? ` (${[...selectedIds].filter((id) => snapshot.session.items[id]).length})` : ""}</button>
|
||||||
)}
|
)}
|
||||||
@ -3129,7 +3122,7 @@ export function App(): ReactElement {
|
|||||||
const itemIds = [...selectedIds].filter((id) => snapshot.session.items[id]);
|
const itemIds = [...selectedIds].filter((id) => snapshot.session.items[id]);
|
||||||
const skippable = itemIds.filter((id) => { const it = snapshot.session.items[id]; return it && (it.status === "queued" || it.status === "reconnect_wait"); });
|
const skippable = itemIds.filter((id) => { const it = snapshot.session.items[id]; return it && (it.status === "queued" || it.status === "reconnect_wait"); });
|
||||||
if (skippable.length === 0) return null;
|
if (skippable.length === 0) return null;
|
||||||
return <button className="ctx-menu-item" onClick={() => { void window.rd.skipItems(skippable); setContextMenu(null); }}>Überspringen{skippable.length > 1 ? ` (${skippable.length})` : ""}</button>;
|
return <button className="ctx-menu-item" onClick={() => { void window.rd.skipItems(skippable).catch(() => {}); setContextMenu(null); }}>Überspringen{skippable.length > 1 ? ` (${skippable.length})` : ""}</button>;
|
||||||
})()}
|
})()}
|
||||||
{hasPackages && (
|
{hasPackages && (
|
||||||
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
||||||
@ -3214,7 +3207,7 @@ export function App(): ReactElement {
|
|||||||
)}
|
)}
|
||||||
<div className="ctx-menu-sep" />
|
<div className="ctx-menu-sep" />
|
||||||
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
||||||
void window.rd.clearHistory().then(() => { setHistoryEntries([]); setSelectedHistoryIds(new Set()); });
|
void window.rd.clearHistory().then(() => { setHistoryEntries([]); setSelectedHistoryIds(new Set()); }).catch(() => {});
|
||||||
setHistoryCtxMenu(null);
|
setHistoryCtxMenu(null);
|
||||||
}}>Verlauf leeren</button>
|
}}>Verlauf leeren</button>
|
||||||
</div>
|
</div>
|
||||||
@ -3228,8 +3221,8 @@ export function App(): ReactElement {
|
|||||||
<div className="link-popup-list">
|
<div className="link-popup-list">
|
||||||
{linkPopup.links.map((link, i) => (
|
{linkPopup.links.map((link, i) => (
|
||||||
<div key={i} className="link-popup-row">
|
<div key={i} className="link-popup-row">
|
||||||
<span className="link-popup-name link-popup-click" title={`${link.name}\nKlicken zum Kopieren`} onClick={() => { void navigator.clipboard.writeText(link.name); showToast("Name kopiert"); }}>{link.name}</span>
|
<span className="link-popup-name link-popup-click" title={`${link.name}\nKlicken zum Kopieren`} onClick={() => { void navigator.clipboard.writeText(link.name).then(() => showToast("Name kopiert")).catch(() => showToast("Kopieren fehlgeschlagen")); }}>{link.name}</span>
|
||||||
<span className="link-popup-url link-popup-click" title={`${link.url}\nKlicken zum Kopieren`} onClick={() => { void navigator.clipboard.writeText(link.url); showToast("Link kopiert"); }}>{link.url}</span>
|
<span className="link-popup-url link-popup-click" title={`${link.url}\nKlicken zum Kopieren`} onClick={() => { void navigator.clipboard.writeText(link.url).then(() => showToast("Link kopiert")).catch(() => showToast("Kopieren fehlgeschlagen")); }}>{link.url}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -3237,15 +3230,13 @@ export function App(): ReactElement {
|
|||||||
{linkPopup.isPackage && (
|
{linkPopup.isPackage && (
|
||||||
<button className="btn" onClick={() => {
|
<button className="btn" onClick={() => {
|
||||||
const text = linkPopup.links.map((l) => l.name).join("\n");
|
const text = linkPopup.links.map((l) => l.name).join("\n");
|
||||||
void navigator.clipboard.writeText(text);
|
void navigator.clipboard.writeText(text).then(() => showToast("Alle Namen kopiert")).catch(() => showToast("Kopieren fehlgeschlagen"));
|
||||||
showToast("Alle Namen kopiert");
|
|
||||||
}}>Alle Namen kopieren</button>
|
}}>Alle Namen kopieren</button>
|
||||||
)}
|
)}
|
||||||
{linkPopup.isPackage && (
|
{linkPopup.isPackage && (
|
||||||
<button className="btn" onClick={() => {
|
<button className="btn" onClick={() => {
|
||||||
const text = linkPopup.links.map((l) => l.url).join("\n");
|
const text = linkPopup.links.map((l) => l.url).join("\n");
|
||||||
void navigator.clipboard.writeText(text);
|
void navigator.clipboard.writeText(text).then(() => showToast("Alle Links kopiert")).catch(() => showToast("Kopieren fehlgeschlagen"));
|
||||||
showToast("Alle Links kopiert");
|
|
||||||
}}>Alle Links kopieren</button>
|
}}>Alle Links kopieren</button>
|
||||||
)}
|
)}
|
||||||
<button className="btn" onClick={() => setLinkPopup(null)}>Schließen</button>
|
<button className="btn" onClick={() => setLinkPopup(null)}>Schließen</button>
|
||||||
|
|||||||
@ -1639,6 +1639,7 @@ td {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctx-menu {
|
.ctx-menu {
|
||||||
|
|||||||
@ -269,6 +269,7 @@ describe("buildAutoRenameBaseName", () => {
|
|||||||
const result = buildAutoRenameBaseName("Show.S99.720p-4sf", "show.s99e999.720p.mkv");
|
const result = buildAutoRenameBaseName("Show.S99.720p-4sf", "show.s99e999.720p.mkv");
|
||||||
// SCENE_EPISODE_RE allows up to 3-digit episodes and 2-digit seasons
|
// SCENE_EPISODE_RE allows up to 3-digit episodes and 2-digit seasons
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!).toContain("S99E999");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Real-world scene release patterns
|
// Real-world scene release patterns
|
||||||
@ -343,6 +344,7 @@ describe("buildAutoRenameBaseName", () => {
|
|||||||
const result = buildAutoRenameBaseName("Show.S01-4sf", "show.s01e01.mkv");
|
const result = buildAutoRenameBaseName("Show.S01-4sf", "show.s01e01.mkv");
|
||||||
// "mkv" should not be treated as part of the filename match
|
// "mkv" should not be treated as part of the filename match
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!).toContain("S01E01");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not match episode-like patterns in codec strings", () => {
|
it("does not match episode-like patterns in codec strings", () => {
|
||||||
@ -373,6 +375,7 @@ describe("buildAutoRenameBaseName", () => {
|
|||||||
// Extreme edge case - sanitizeFilename trims leading dots
|
// Extreme edge case - sanitizeFilename trims leading dots
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result!).toContain("S01E01");
|
expect(result!).toContain("S01E01");
|
||||||
|
expect(result!).toContain("-4sf");
|
||||||
expect(result!).not.toContain(".S01E01.S01E01"); // no duplication
|
expect(result!).not.toContain(".S01E01.S01E01"); // no duplication
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -317,7 +317,7 @@ describe("debrid service", () => {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const abortTimer = setTimeout(() => {
|
const abortTimer = setTimeout(() => {
|
||||||
controller.abort("test");
|
controller.abort("test");
|
||||||
}, 25);
|
}, 200);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expect(service.unrestrictLink("https://rapidgator.net/file/abort-mega-web", controller.signal)).rejects.toThrow(/aborted/i);
|
await expect(service.unrestrictLink("https://rapidgator.net/file/abort-mega-web", controller.signal)).rejects.toThrow(/aborted/i);
|
||||||
|
|||||||
@ -36,12 +36,8 @@ afterEach(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractor jvm backend", () => {
|
describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm backend", () => {
|
||||||
it("extracts zip archives through SevenZipJBinding backend", async () => {
|
it("extracts zip archives through SevenZipJBinding backend", async () => {
|
||||||
if (!hasJavaRuntime() || !hasJvmExtractorRuntime()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.RD_EXTRACT_BACKEND = "jvm";
|
process.env.RD_EXTRACT_BACKEND = "jvm";
|
||||||
|
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-extract-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-extract-"));
|
||||||
@ -70,10 +66,6 @@ describe("extractor jvm backend", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("respects ask/skip conflict mode in jvm backend", async () => {
|
it("respects ask/skip conflict mode in jvm backend", async () => {
|
||||||
if (!hasJavaRuntime() || !hasJvmExtractorRuntime()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.RD_EXTRACT_BACKEND = "jvm";
|
process.env.RD_EXTRACT_BACKEND = "jvm";
|
||||||
|
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-extract-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-extract-"));
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import {
|
|||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
const originalExtractBackend = process.env.RD_EXTRACT_BACKEND;
|
const originalExtractBackend = process.env.RD_EXTRACT_BACKEND;
|
||||||
|
const originalStatfs = fs.promises.statfs;
|
||||||
|
const originalZipEntryMemoryLimit = process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.RD_EXTRACT_BACKEND = "legacy";
|
process.env.RD_EXTRACT_BACKEND = "legacy";
|
||||||
@ -29,6 +31,12 @@ afterEach(() => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.RD_EXTRACT_BACKEND = originalExtractBackend;
|
process.env.RD_EXTRACT_BACKEND = originalExtractBackend;
|
||||||
}
|
}
|
||||||
|
(fs.promises as any).statfs = originalStatfs;
|
||||||
|
if (originalZipEntryMemoryLimit === undefined) {
|
||||||
|
delete process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB;
|
||||||
|
} else {
|
||||||
|
process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = originalZipEntryMemoryLimit;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("extractor", () => {
|
describe("extractor", () => {
|
||||||
@ -574,7 +582,6 @@ describe("extractor", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps original ZIP size guard error when external fallback is unavailable", async () => {
|
it("keeps original ZIP size guard error when external fallback is unavailable", async () => {
|
||||||
const previousLimit = process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB;
|
|
||||||
process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = "8";
|
process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = "8";
|
||||||
|
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
|
||||||
@ -588,32 +595,20 @@ describe("extractor", () => {
|
|||||||
zip.addFile("large.bin", Buffer.alloc(9 * 1024 * 1024, 7));
|
zip.addFile("large.bin", Buffer.alloc(9 * 1024 * 1024, 7));
|
||||||
zip.writeZip(zipPath);
|
zip.writeZip(zipPath);
|
||||||
|
|
||||||
try {
|
const result = await extractPackageArchives({
|
||||||
const result = await extractPackageArchives({
|
packageDir,
|
||||||
packageDir,
|
targetDir,
|
||||||
targetDir,
|
cleanupMode: "none",
|
||||||
cleanupMode: "none",
|
conflictMode: "overwrite",
|
||||||
conflictMode: "overwrite",
|
removeLinks: false,
|
||||||
removeLinks: false,
|
removeSamples: false
|
||||||
removeSamples: false
|
});
|
||||||
});
|
expect(result.extracted).toBe(0);
|
||||||
expect(result.extracted).toBe(0);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(String(result.lastError)).toMatch(/ZIP-Eintrag.*groß/i);
|
||||||
expect(String(result.lastError)).toMatch(/ZIP-Eintrag.*groß/i);
|
|
||||||
} finally {
|
|
||||||
if (previousLimit === undefined) {
|
|
||||||
delete process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB;
|
|
||||||
} else {
|
|
||||||
process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = previousLimit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches resume-state archive names case-insensitively on Windows", async () => {
|
it.skipIf(process.platform !== "win32")("matches resume-state archive names case-insensitively on Windows", async () => {
|
||||||
if (process.platform !== "win32") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
const packageDir = path.join(root, "pkg");
|
const packageDir = path.join(root, "pkg");
|
||||||
@ -650,23 +645,18 @@ describe("extractor", () => {
|
|||||||
zip.addFile("test.txt", Buffer.alloc(1024, 0x41));
|
zip.addFile("test.txt", Buffer.alloc(1024, 0x41));
|
||||||
zip.writeZip(path.join(packageDir, "test.zip"));
|
zip.writeZip(path.join(packageDir, "test.zip"));
|
||||||
|
|
||||||
const originalStatfs = fs.promises.statfs;
|
|
||||||
(fs.promises as any).statfs = async () => ({ bfree: 1, bsize: 1 });
|
(fs.promises as any).statfs = async () => ({ bfree: 1, bsize: 1 });
|
||||||
|
|
||||||
try {
|
await expect(
|
||||||
await expect(
|
extractPackageArchives({
|
||||||
extractPackageArchives({
|
packageDir,
|
||||||
packageDir,
|
targetDir,
|
||||||
targetDir,
|
cleanupMode: "none" as any,
|
||||||
cleanupMode: "none" as any,
|
conflictMode: "overwrite" as any,
|
||||||
conflictMode: "overwrite" as any,
|
removeLinks: false,
|
||||||
removeLinks: false,
|
removeSamples: false,
|
||||||
removeSamples: false,
|
})
|
||||||
})
|
).rejects.toThrow(/Nicht genug Speicherplatz/);
|
||||||
).rejects.toThrow(/Nicht genug Speicherplatz/);
|
|
||||||
} finally {
|
|
||||||
(fs.promises as any).statfs = originalStatfs;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("proceeds when disk space is sufficient", async () => {
|
it("proceeds when disk space is sufficient", async () => {
|
||||||
|
|||||||
@ -166,7 +166,7 @@ describe("mega-web-fallback", () => {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
controller.abort("test");
|
controller.abort("test");
|
||||||
}, 30);
|
}, 200);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i);
|
await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i);
|
||||||
|
|||||||
@ -153,7 +153,7 @@ async function main(): Promise<void> {
|
|||||||
createStoragePaths(path.join(tempRoot, "state-pause"))
|
createStoragePaths(path.join(tempRoot, "state-pause"))
|
||||||
);
|
);
|
||||||
manager2.addPackages([{ name: "pause", links: ["https://dummy/slow"] }]);
|
manager2.addPackages([{ name: "pause", links: ["https://dummy/slow"] }]);
|
||||||
manager2.start();
|
await manager2.start();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||||
const paused = manager2.togglePause();
|
const paused = manager2.togglePause();
|
||||||
assert(paused, "Pause konnte nicht aktiviert werden");
|
assert(paused, "Pause konnte nicht aktiviert werden");
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { setLogListener } from "../src/main/logger";
|
|||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
// Ensure session log is shut down between tests
|
||||||
|
shutdownSessionLog();
|
||||||
// Ensure listener is cleared between tests
|
// Ensure listener is cleared between tests
|
||||||
setLogListener(null);
|
setLogListener(null);
|
||||||
for (const dir of tempDirs.splice(0)) {
|
for (const dir of tempDirs.splice(0)) {
|
||||||
@ -45,7 +47,7 @@ describe("session-log", () => {
|
|||||||
logger.info("Test-Nachricht für Session-Log");
|
logger.info("Test-Nachricht für Session-Log");
|
||||||
|
|
||||||
// Wait for flush (200ms interval + margin)
|
// Wait for flush (200ms interval + margin)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
const content = fs.readFileSync(logPath, "utf8");
|
const content = fs.readFileSync(logPath, "utf8");
|
||||||
expect(content).toContain("Test-Nachricht für Session-Log");
|
expect(content).toContain("Test-Nachricht für Session-Log");
|
||||||
@ -79,7 +81,7 @@ describe("session-log", () => {
|
|||||||
const { logger } = await import("../src/main/logger");
|
const { logger } = await import("../src/main/logger");
|
||||||
logger.info("Nach-Shutdown-Nachricht");
|
logger.info("Nach-Shutdown-Nachricht");
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
const content = fs.readFileSync(logPath, "utf8");
|
const content = fs.readFileSync(logPath, "utf8");
|
||||||
expect(content).not.toContain("Nach-Shutdown-Nachricht");
|
expect(content).not.toContain("Nach-Shutdown-Nachricht");
|
||||||
@ -137,7 +139,7 @@ describe("session-log", () => {
|
|||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("multiple sessions create different files", () => {
|
it("multiple sessions create different files", async () => {
|
||||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-"));
|
||||||
tempDirs.push(baseDir);
|
tempDirs.push(baseDir);
|
||||||
|
|
||||||
@ -146,10 +148,7 @@ describe("session-log", () => {
|
|||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
|
|
||||||
// Small delay to ensure different timestamp
|
// Small delay to ensure different timestamp
|
||||||
const start = Date.now();
|
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||||
while (Date.now() - start < 1100) {
|
|
||||||
// busy-wait for 1.1 seconds to get different second in filename
|
|
||||||
}
|
|
||||||
|
|
||||||
initSessionLog(baseDir);
|
initSessionLog(baseDir);
|
||||||
const path2 = getSessionLogPath();
|
const path2 = getSessionLogPath();
|
||||||
|
|||||||
@ -12,5 +12,5 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"types": ["node", "vite/client"]
|
"types": ["node", "vite/client"]
|
||||||
},
|
},
|
||||||
"include": ["src", "tests", "vite.config.ts"]
|
"include": ["src", "tests", "vite.config.mts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user