Fix start-conflict skip behavior and release v1.4.67
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
647679f581
commit
e7f0b1d1fd
24
CHANGELOG.md
24
CHANGELOG.md
@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
||||||
|
|
||||||
|
## 1.4.67 - 2026-03-01
|
||||||
|
|
||||||
|
Hotfix fuer einen kritischen Start-Konflikt-Datenverlust und zusaetzliche Renamer-Haertung fuer reale Scene-Muster.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Start-Konflikt `Überspringen` loescht keine Pakete/Items mehr:
|
||||||
|
- Bereits entpackte Dateien bleiben erhalten.
|
||||||
|
- Offene Downloads bleiben in der Queue und koennen normal fortgesetzt werden.
|
||||||
|
- Laufende Tasks werden dabei als Paket-Stop statt als Cancel behandelt.
|
||||||
|
- Start-Konflikt-Dialogtext in der UI praezisiert:
|
||||||
|
- `Entpacktes überspringen` statt missverstaendlichem `Überspringen`.
|
||||||
|
- Klare Info, dass nur erneutes Entpacken uebersprungen wird.
|
||||||
|
- Auto-Renamer verbessert:
|
||||||
|
- Erkennt jetzt auch Episode-only Tokens wie `e01`/`e02` mit Staffel-Hinweis aus dem Ordner.
|
||||||
|
- Akzeptiert lowercase Group-Suffixe wie `-tmsf`.
|
||||||
|
- Robuster bei Source-Formaten wie `4sf-bs-720p-s01e05`.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Neue/angepasste Tests in:
|
||||||
|
- `tests/download-manager.test.ts` (Start-Konflikt-Skip behaelt Paket + Partial-Queue)
|
||||||
|
- `tests/auto-rename.test.ts` (e01/e02, lowercase suffix, odd source order)
|
||||||
|
|
||||||
## 1.4.66 - 2026-03-01
|
## 1.4.66 - 2026-03-01
|
||||||
|
|
||||||
Hotfix fuer haengende "Link wird umgewandelt"-Faelle (insbesondere Mega-Web-Pfad), bei denen nur ein App-Neustart geholfen hat.
|
Hotfix fuer haengende "Link wird umgewandelt"-Faelle (insbesondere Mega-Web-Pfad), bei denen nur ein App-Neustart geholfen hat.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.66",
|
"version": "1.4.67",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.66",
|
"version": "1.4.67",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.66",
|
"version": "1.4.67",
|
||||||
"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",
|
||||||
|
|||||||
@ -290,11 +290,61 @@ const SCENE_GROUP_SUFFIX_RE = /-(?=[A-Za-z0-9]{2,}$)(?=[A-Za-z0-9]*[A-Z])[A-Za-z
|
|||||||
const SCENE_EPISODE_RE = /(?:^|[._\-\s])s(\d{1,2})e(\d{1,3})(?:[._\-\s]|$)/i;
|
const SCENE_EPISODE_RE = /(?:^|[._\-\s])s(\d{1,2})e(\d{1,3})(?:[._\-\s]|$)/i;
|
||||||
const SCENE_SEASON_ONLY_RE = /(^|[._\-\s])s\d{1,2}(?=[._\-\s]|$)/i;
|
const SCENE_SEASON_ONLY_RE = /(^|[._\-\s])s\d{1,2}(?=[._\-\s]|$)/i;
|
||||||
const SCENE_SEASON_CAPTURE_RE = /(?:^|[._\-\s])s(\d{1,2})(?=[._\-\s]|$)/i;
|
const SCENE_SEASON_CAPTURE_RE = /(?:^|[._\-\s])s(\d{1,2})(?=[._\-\s]|$)/i;
|
||||||
|
const SCENE_EPISODE_ONLY_RE = /(?:^|[._\-\s])e(?:p(?:isode)?)?\s*0*(\d{1,3})(?:[._\-\s]|$)/i;
|
||||||
const SCENE_PART_TOKEN_RE = /(?:^|[._\-\s])(?:teil|part)\s*0*(\d{1,3})(?=[._\-\s]|$)/i;
|
const SCENE_PART_TOKEN_RE = /(?:^|[._\-\s])(?:teil|part)\s*0*(\d{1,3})(?=[._\-\s]|$)/i;
|
||||||
const SCENE_COMPACT_EPISODE_CODE_RE = /(?:^|[._\-\s])(\d{3,4})(?=$|[._\-\s])/;
|
const SCENE_COMPACT_EPISODE_CODE_RE = /(?:^|[._\-\s])(\d{3,4})(?=$|[._\-\s])/;
|
||||||
const SCENE_RP_TOKEN_RE = /(?:^|[._\-\s])rp(?:[._\-\s]|$)/i;
|
const SCENE_RP_TOKEN_RE = /(?:^|[._\-\s])rp(?:[._\-\s]|$)/i;
|
||||||
const SCENE_REPACK_TOKEN_RE = /(?:^|[._\-\s])repack(?:[._\-\s]|$)/i;
|
const SCENE_REPACK_TOKEN_RE = /(?:^|[._\-\s])repack(?:[._\-\s]|$)/i;
|
||||||
const SCENE_QUALITY_TOKEN_RE = /([._\-\s])((?:4320|2160|1440|1080|720|576|540|480|360)p)(?=[._\-\s]|$)/i;
|
const SCENE_QUALITY_TOKEN_RE = /([._\-\s])((?:4320|2160|1440|1080|720|576|540|480|360)p)(?=[._\-\s]|$)/i;
|
||||||
|
const SCENE_GROUP_SUFFIX_FALLBACK_RE = /-([A-Za-z0-9]{2,})$/;
|
||||||
|
const SCENE_NON_GROUP_SUFFIXES = new Set([
|
||||||
|
"x264",
|
||||||
|
"x265",
|
||||||
|
"h264",
|
||||||
|
"h265",
|
||||||
|
"avc",
|
||||||
|
"hevc",
|
||||||
|
"web",
|
||||||
|
"webrip",
|
||||||
|
"webdl",
|
||||||
|
"bluray",
|
||||||
|
"bdrip",
|
||||||
|
"hdtv",
|
||||||
|
"dvdrip",
|
||||||
|
"remux"
|
||||||
|
]);
|
||||||
|
|
||||||
|
function hasSceneGroupSuffix(fileName: string): boolean {
|
||||||
|
const text = String(fileName || "").trim();
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SCENE_GROUP_SUFFIX_RE.test(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackMatch = text.match(SCENE_GROUP_SUFFIX_FALLBACK_RE);
|
||||||
|
const suffix = String(fallbackMatch?.[1] || "").trim();
|
||||||
|
if (!suffix) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = suffix.toLowerCase();
|
||||||
|
if (SCENE_NON_GROUP_SUFFIXES.has(lower)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^\d+p$/.test(lower) || /^\d+$/.test(lower)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^\d/.test(suffix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/4s(?:f|j)/i.test(suffix) && !/^(?:4sf|4sj)$/i.test(suffix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /[a-z]/i.test(suffix);
|
||||||
|
}
|
||||||
|
|
||||||
export function extractEpisodeToken(fileName: string): string | null {
|
export function extractEpisodeToken(fileName: string): string | null {
|
||||||
const match = String(fileName || "").match(SCENE_EPISODE_RE);
|
const match = String(fileName || "").match(SCENE_EPISODE_RE);
|
||||||
@ -343,6 +393,18 @@ function extractPartEpisodeNumber(fileName: string): number | null {
|
|||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractEpisodeOnlyNumber(fileName: string): number | null {
|
||||||
|
const match = String(fileName || "").match(SCENE_EPISODE_ONLY_RE);
|
||||||
|
if (!match?.[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const episode = Number(match[1]);
|
||||||
|
if (!Number.isFinite(episode) || episode <= 0 || episode > 999) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return episode;
|
||||||
|
}
|
||||||
|
|
||||||
function extractCompactEpisodeToken(fileName: string, seasonHint: number | null): string | null {
|
function extractCompactEpisodeToken(fileName: string, seasonHint: number | null): string | null {
|
||||||
const trimmed = String(fileName || "").trim();
|
const trimmed = String(fileName || "").trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@ -407,6 +469,15 @@ function resolveEpisodeTokenForAutoRename(sourceFileName: string, folderNames: s
|
|||||||
const seasonTokenHint = extractSeasonToken(sourceFileName)
|
const seasonTokenHint = extractSeasonToken(sourceFileName)
|
||||||
?? folderNames.map((folderName) => extractSeasonToken(folderName)).find(Boolean)
|
?? folderNames.map((folderName) => extractSeasonToken(folderName)).find(Boolean)
|
||||||
?? null;
|
?? null;
|
||||||
|
const episodeOnly = extractEpisodeOnlyNumber(sourceFileName)
|
||||||
|
?? folderNames.map((folderName) => extractEpisodeOnlyNumber(folderName)).find((value) => Number.isFinite(value) && (value as number) > 0)
|
||||||
|
?? null;
|
||||||
|
if (seasonTokenHint && episodeOnly) {
|
||||||
|
return {
|
||||||
|
token: `${seasonTokenHint}E${String(episodeOnly).padStart(2, "0")}`,
|
||||||
|
fromPart: false
|
||||||
|
};
|
||||||
|
}
|
||||||
const seasonHint = seasonTokenHint ? Number(seasonTokenHint.slice(1)) : null;
|
const seasonHint = seasonTokenHint ? Number(seasonTokenHint.slice(1)) : null;
|
||||||
const compactEpisode = extractCompactEpisodeToken(sourceFileName, seasonHint);
|
const compactEpisode = extractCompactEpisodeToken(sourceFileName, seasonHint);
|
||||||
if (compactEpisode) {
|
if (compactEpisode) {
|
||||||
@ -493,7 +564,7 @@ export function buildAutoRenameBaseName(folderName: string, sourceFileName: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLegacy4sf4sjFolder = SCENE_RELEASE_FOLDER_RE.test(normalizedFolderName);
|
const isLegacy4sf4sjFolder = SCENE_RELEASE_FOLDER_RE.test(normalizedFolderName);
|
||||||
const isSceneGroupFolder = SCENE_GROUP_SUFFIX_RE.test(normalizedFolderName);
|
const isSceneGroupFolder = hasSceneGroupSuffix(normalizedFolderName);
|
||||||
if (!isLegacy4sf4sjFolder && !isSceneGroupFolder) {
|
if (!isLegacy4sf4sjFolder && !isSceneGroupFolder) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -551,7 +622,7 @@ export function buildAutoRenameBaseNameFromFoldersWithOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let target = buildAutoRenameBaseName(folderName, normalizedSourceFileName);
|
let target = buildAutoRenameBaseName(folderName, normalizedSourceFileName);
|
||||||
if (!target && resolvedEpisode && SCENE_GROUP_SUFFIX_RE.test(folderName) && (folderHasSeason || folderHasEpisode)) {
|
if (!target && resolvedEpisode && hasSceneGroupSuffix(folderName) && (folderHasSeason || folderHasEpisode)) {
|
||||||
target = applyEpisodeTokenToFolderName(folderName, resolvedEpisode.token);
|
target = applyEpisodeTokenToFolderName(folderName, resolvedEpisode.token);
|
||||||
}
|
}
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@ -560,14 +631,14 @@ export function buildAutoRenameBaseNameFromFoldersWithOptions(
|
|||||||
|
|
||||||
if (resolvedEpisode
|
if (resolvedEpisode
|
||||||
&& forceEpisodeForSeasonFolder
|
&& forceEpisodeForSeasonFolder
|
||||||
&& SCENE_GROUP_SUFFIX_RE.test(target)
|
&& hasSceneGroupSuffix(target)
|
||||||
&& !extractEpisodeToken(target)
|
&& !extractEpisodeToken(target)
|
||||||
&& SCENE_SEASON_ONLY_RE.test(target)) {
|
&& SCENE_SEASON_ONLY_RE.test(target)) {
|
||||||
target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token);
|
target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedEpisode?.fromPart
|
if (resolvedEpisode?.fromPart
|
||||||
&& SCENE_GROUP_SUFFIX_RE.test(target)
|
&& hasSceneGroupSuffix(target)
|
||||||
&& !extractEpisodeToken(target)
|
&& !extractEpisodeToken(target)
|
||||||
&& SCENE_SEASON_ONLY_RE.test(target)) {
|
&& SCENE_SEASON_ONLY_RE.test(target)) {
|
||||||
target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token);
|
target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token);
|
||||||
@ -1153,31 +1224,61 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (policy === "skip") {
|
if (policy === "skip") {
|
||||||
|
let hadPendingItems = false;
|
||||||
for (const itemId of pkg.itemIds) {
|
for (const itemId of pkg.itemIds) {
|
||||||
|
const item = this.session.items[itemId];
|
||||||
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.status === "queued" || item.status === "reconnect_wait") {
|
||||||
|
hadPendingItems = true;
|
||||||
|
}
|
||||||
|
|
||||||
const active = this.activeTasks.get(itemId);
|
const active = this.activeTasks.get(itemId);
|
||||||
if (active) {
|
if (active) {
|
||||||
active.abortReason = "cancel";
|
active.abortReason = "package_toggle";
|
||||||
active.abortController.abort("cancel");
|
active.abortController.abort("package_toggle");
|
||||||
}
|
}
|
||||||
this.releaseTargetPath(itemId);
|
|
||||||
|
if (item.status === "queued" || item.status === "reconnect_wait") {
|
||||||
|
item.status = "queued";
|
||||||
|
item.speedBps = 0;
|
||||||
|
item.lastError = "";
|
||||||
|
item.fullStatus = "Wartet";
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.session.running) {
|
||||||
this.runItemIds.delete(itemId);
|
this.runItemIds.delete(itemId);
|
||||||
this.runOutcomes.delete(itemId);
|
this.runOutcomes.delete(itemId);
|
||||||
this.itemContributedBytes.delete(itemId);
|
|
||||||
this.retryAfterByItem.delete(itemId);
|
|
||||||
delete this.session.items[itemId];
|
|
||||||
this.itemCount = Math.max(0, this.itemCount - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.retryAfterByItem.delete(itemId);
|
||||||
|
this.retryStateByItem.delete(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
|
||||||
if (postProcessController && !postProcessController.signal.aborted) {
|
if (postProcessController && !postProcessController.signal.aborted) {
|
||||||
postProcessController.abort("cancel");
|
postProcessController.abort("skip");
|
||||||
}
|
}
|
||||||
this.packagePostProcessAbortControllers.delete(packageId);
|
this.packagePostProcessAbortControllers.delete(packageId);
|
||||||
this.packagePostProcessTasks.delete(packageId);
|
this.packagePostProcessTasks.delete(packageId);
|
||||||
delete this.session.packages[packageId];
|
|
||||||
this.session.packageOrder = this.session.packageOrder.filter((id) => id !== packageId);
|
|
||||||
this.runPackageIds.delete(packageId);
|
|
||||||
this.runCompletedPackages.delete(packageId);
|
|
||||||
this.hybridExtractRequeue.delete(packageId);
|
this.hybridExtractRequeue.delete(packageId);
|
||||||
|
|
||||||
|
if (!this.session.running) {
|
||||||
|
this.runPackageIds.delete(packageId);
|
||||||
|
}
|
||||||
|
this.runCompletedPackages.delete(packageId);
|
||||||
|
|
||||||
|
const items = pkg.itemIds
|
||||||
|
.map((itemId) => this.session.items[itemId])
|
||||||
|
.filter(Boolean) as DownloadItem[];
|
||||||
|
const hasPendingNow = items.some((item) => item.status === "queued" || item.status === "reconnect_wait");
|
||||||
|
if (hadPendingItems || hasPendingNow) {
|
||||||
|
pkg.status = pkg.enabled ? "queued" : "paused";
|
||||||
|
}
|
||||||
|
pkg.updatedAt = nowMs();
|
||||||
|
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
return { skipped: true, overwritten: false };
|
return { skipped: true, overwritten: false };
|
||||||
@ -1661,7 +1762,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
.map((value) => String(value || "").trim())
|
.map((value) => String(value || "").trim())
|
||||||
.filter((value) => value.length > 0);
|
.filter((value) => value.length > 0);
|
||||||
const fallbackTemplate = [...normalizedCandidates].reverse().find((folderName) => {
|
const fallbackTemplate = [...normalizedCandidates].reverse().find((folderName) => {
|
||||||
return SCENE_GROUP_SUFFIX_RE.test(folderName) && Boolean(extractSeasonToken(folderName));
|
return hasSceneGroupSuffix(folderName) && Boolean(extractSeasonToken(folderName));
|
||||||
}) || "";
|
}) || "";
|
||||||
if (!fallbackTemplate) {
|
if (!fallbackTemplate) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -1897,6 +1897,7 @@ export function App(): ReactElement {
|
|||||||
<p>
|
<p>
|
||||||
<strong>{startConflictPrompt.entry.packageName}</strong> ist im Ziel bereits vorhanden.
|
<strong>{startConflictPrompt.entry.packageName}</strong> ist im Ziel bereits vorhanden.
|
||||||
</p>
|
</p>
|
||||||
|
<p>Bei "Überspringen" wird nur das erneute Entpacken übersprungen - offene Downloads bleiben in der Queue.</p>
|
||||||
<p className="modal-path" title={startConflictPrompt.entry.extractDir}>{startConflictPrompt.entry.extractDir}</p>
|
<p className="modal-path" title={startConflictPrompt.entry.extractDir}>{startConflictPrompt.entry.extractDir}</p>
|
||||||
<label className="toggle-line">
|
<label className="toggle-line">
|
||||||
<input
|
<input
|
||||||
@ -1915,7 +1916,7 @@ export function App(): ReactElement {
|
|||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => closeStartConflictPrompt({ policy: "skip", applyToAll: startConflictPrompt.applyToAll })}
|
onClick={() => closeStartConflictPrompt({ policy: "skip", applyToAll: startConflictPrompt.applyToAll })}
|
||||||
>
|
>
|
||||||
Überspringen
|
Entpacktes überspringen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn danger"
|
className="btn danger"
|
||||||
|
|||||||
@ -512,4 +512,48 @@ describe("buildAutoRenameBaseNameFromFolders", () => {
|
|||||||
);
|
);
|
||||||
expect(result).toBe("Lethal.Weapon.S02E11.German.DD51.Dubbed.DL.720p.AmazonHD.x264-TVS");
|
expect(result).toBe("Lethal.Weapon.S02E11.German.DD51.Dubbed.DL.720p.AmazonHD.x264-TVS");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps episode-only token e01 via season folder hint and keeps REPACK", () => {
|
||||||
|
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
|
||||||
|
[
|
||||||
|
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-TMSF"
|
||||||
|
],
|
||||||
|
"tmsf-cheatderbetrug-e01-720p-repack",
|
||||||
|
{ forceEpisodeForSeasonFolder: true }
|
||||||
|
);
|
||||||
|
expect(result).toBe("Cheat.der.Betrug.S01E01.GERMAN.REPACK.720p.WEB.h264-TMSF");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps episode-only token e02 via season folder hint", () => {
|
||||||
|
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
|
||||||
|
[
|
||||||
|
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-TMSF"
|
||||||
|
],
|
||||||
|
"tmsf-cheatderbetrug-e02-720p",
|
||||||
|
{ forceEpisodeForSeasonFolder: true }
|
||||||
|
);
|
||||||
|
expect(result).toBe("Cheat.der.Betrug.S01E02.GERMAN.720p.WEB.h264-TMSF");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps renaming for odd source order like 4sf-bs-720p-s01e05", () => {
|
||||||
|
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
|
||||||
|
[
|
||||||
|
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-TMSF"
|
||||||
|
],
|
||||||
|
"4sf-bs-720p-s01e05",
|
||||||
|
{ forceEpisodeForSeasonFolder: true }
|
||||||
|
);
|
||||||
|
expect(result).toBe("Cheat.der.Betrug.S01E05.GERMAN.720p.WEB.h264-TMSF");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts lowercase scene group suffixes", () => {
|
||||||
|
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
|
||||||
|
[
|
||||||
|
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-tmsf"
|
||||||
|
],
|
||||||
|
"tmsf-cheatderbetrug-e01-720p",
|
||||||
|
{ forceEpisodeForSeasonFolder: true }
|
||||||
|
);
|
||||||
|
expect(result).toBe("Cheat.der.Betrug.S01E01.GERMAN.720p.WEB.h264-tmsf");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1910,8 +1910,104 @@ describe("download manager", () => {
|
|||||||
|
|
||||||
const result = await manager.resolveStartConflict(packageId, "skip");
|
const result = await manager.resolveStartConflict(packageId, "skip");
|
||||||
expect(result.skipped).toBe(true);
|
expect(result.skipped).toBe(true);
|
||||||
expect(manager.getSnapshot().session.packages[packageId]).toBeUndefined();
|
const snapshot = manager.getSnapshot();
|
||||||
expect(manager.getSnapshot().session.items[itemId]).toBeUndefined();
|
expect(snapshot.session.packages[packageId]).toBeDefined();
|
||||||
|
expect(snapshot.session.packages[packageId]?.status).toBe("queued");
|
||||||
|
expect(snapshot.session.items[itemId]).toBeDefined();
|
||||||
|
expect(snapshot.session.items[itemId]?.status).toBe("queued");
|
||||||
|
expect(snapshot.session.items[itemId]?.fullStatus).toBe("Wartet");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps already completed items when skipping start conflict", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const packageId = "skip-partial-pkg";
|
||||||
|
const completedItemId = "skip-partial-completed";
|
||||||
|
const pendingItemId = "skip-partial-pending";
|
||||||
|
const now = Date.now() - 5000;
|
||||||
|
const outputDir = path.join(root, "downloads", "skip-partial");
|
||||||
|
const extractDir = path.join(root, "extract", "skip-partial");
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(extractDir, "existing.mkv"), "x", "utf8");
|
||||||
|
const completedTarget = path.join(outputDir, "skip-partial.part01.rar");
|
||||||
|
fs.writeFileSync(completedTarget, "part", "utf8");
|
||||||
|
|
||||||
|
const session = emptySession();
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = {
|
||||||
|
id: packageId,
|
||||||
|
name: "skip-partial",
|
||||||
|
outputDir,
|
||||||
|
extractDir,
|
||||||
|
status: "queued",
|
||||||
|
itemIds: [completedItemId, pendingItemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
session.items[completedItemId] = {
|
||||||
|
id: completedItemId,
|
||||||
|
packageId,
|
||||||
|
url: "https://dummy/skip-partial/completed",
|
||||||
|
provider: "realdebrid",
|
||||||
|
status: "completed",
|
||||||
|
retries: 0,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 123,
|
||||||
|
totalBytes: 123,
|
||||||
|
progressPercent: 100,
|
||||||
|
fileName: "skip-partial.part01.rar",
|
||||||
|
targetPath: completedTarget,
|
||||||
|
resumable: true,
|
||||||
|
attempts: 1,
|
||||||
|
lastError: "",
|
||||||
|
fullStatus: "Entpackt",
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
session.items[pendingItemId] = {
|
||||||
|
id: pendingItemId,
|
||||||
|
packageId,
|
||||||
|
url: "https://dummy/skip-partial/pending",
|
||||||
|
provider: null,
|
||||||
|
status: "queued",
|
||||||
|
retries: 0,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: null,
|
||||||
|
progressPercent: 0,
|
||||||
|
fileName: "skip-partial.part02.rar",
|
||||||
|
targetPath: path.join(outputDir, "skip-partial.part02.rar"),
|
||||||
|
resumable: true,
|
||||||
|
attempts: 0,
|
||||||
|
lastError: "",
|
||||||
|
fullStatus: "Wartet",
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "rd-token",
|
||||||
|
outputDir: path.join(root, "downloads"),
|
||||||
|
extractDir: path.join(root, "extract")
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await manager.resolveStartConflict(packageId, "skip");
|
||||||
|
expect(result.skipped).toBe(true);
|
||||||
|
const snapshot = manager.getSnapshot();
|
||||||
|
expect(snapshot.session.packages[packageId]).toBeDefined();
|
||||||
|
expect(snapshot.session.items[completedItemId]?.status).toBe("completed");
|
||||||
|
expect(snapshot.session.items[completedItemId]?.fullStatus).toBe("Entpackt");
|
||||||
|
expect(snapshot.session.items[pendingItemId]?.status).toBe("queued");
|
||||||
|
expect(snapshot.session.items[pendingItemId]?.fullStatus).toBe("Wartet");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves start conflict by overwriting and resetting queued package", async () => {
|
it("resolves start conflict by overwriting and resetting queued package", async () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user