feat(downloads): reuse unfinished filename matches

Fixes #26
This commit is contained in:
NimBold
2026-07-20 19:02:46 +03:30
parent c133556d38
commit e0cb124720
12 changed files with 460 additions and 134 deletions
+31 -1
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest';
import type { DownloadItem } from '../bindings/DownloadItem';
import { redactDownloadForPersistence, resolveDownloadConnections } from './downloads';
import {
downloadFileNamesMatch,
downloadMediaKindsMatch,
redactDownloadForPersistence,
resolveDownloadConnections
} from './downloads';
const item = (status: DownloadItem['status']): DownloadItem => ({
id: 'download-1',
@@ -56,3 +61,28 @@ describe('download connection resolution', () => {
expect(resolveDownloadConnections(Number.NaN, 8)).toBe(8);
});
});
describe('download filename matching', () => {
it('matches case and path spelling while preserving the actual filename', () => {
expect(downloadFileNamesMatch(
'Media\\Example.Show.S01E01.MKV',
'example.show.s01e01.mkv'
)).toBe(true);
});
it('does not collapse distinct extensions or names', () => {
expect(downloadFileNamesMatch('example.zip', 'example.tar')).toBe(false);
expect(downloadFileNamesMatch('example-1.zip', 'example.zip')).toBe(false);
});
it('does not auto-match weak metadata fallback names', () => {
expect(downloadFileNamesMatch('download', 'download')).toBe(false);
expect(downloadFileNamesMatch('identifier', 'IDENTIFIER')).toBe(false);
expect(downloadFileNamesMatch('real-file.bin', 'real-file.bin')).toBe(true);
});
it('treats omitted media flags as ordinary downloads', () => {
expect(downloadMediaKindsMatch(undefined, false)).toBe(true);
expect(downloadMediaKindsMatch(undefined, true)).toBe(false);
});
});
+23
View File
@@ -132,6 +132,29 @@ export const canonicalizeDownloadFileName = (fileName: string): string => {
return sanitized && sanitized !== '.' && sanitized !== '..' ? sanitized : 'download';
};
/**
* Compare metadata-derived names without allowing path spelling or case to
* turn the same download into a second queue entry. Keep the extension and
* the rest of the name intact: URL query strings are not part of this value.
*/
export const normalizeDownloadFileNameForMatch = (fileName: string): string =>
canonicalizeDownloadFileName(fileName).normalize('NFKC').toLowerCase();
export const downloadMediaKindsMatch = (
left: boolean | undefined,
right: boolean | undefined
): boolean => Boolean(left) === Boolean(right);
const WEAK_DOWNLOAD_FILE_NAMES = new Set(['download', 'identifier', 'view', 'uc']);
export const downloadFileNamesMatch = (left: string, right: string): boolean => {
const normalizedLeft = normalizeDownloadFileNameForMatch(left);
const normalizedRight = normalizeDownloadFileNameForMatch(right);
return !WEAK_DOWNLOAD_FILE_NAMES.has(normalizedLeft)
&& !WEAK_DOWNLOAD_FILE_NAMES.has(normalizedRight)
&& normalizedLeft === normalizedRight;
};
export const isMediaUrl = (rawUrl: string): boolean => {
try {
const url = new URL(rawUrl);