fix(torrent): harden magnet metadata enrichment

- Preserve magnet display names before metadata resolution
- Bound optional magnet probes separately from required metadata work
- Fence stale Add-window probes and temporary Torrent metadata across modal races
- Compact responsive Torrent peer metrics across locales
This commit is contained in:
NimBold
2026-08-25 03:27:49 +03:30
parent 51db4fc6b7
commit ba491ccd7d
13 changed files with 110 additions and 44 deletions
+57 -14
View File
@@ -168,9 +168,11 @@ export const AddDownloadsModal = () => {
const [urls, setUrls] = useState('');
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
const [parsedItems, setParsedItems] = useState<AddDownloadDraftRow[]>([]);
const [metadataSchedulerRevision, setMetadataSchedulerRevision] = useState(0);
const parsedItemsRef = useRef<AddDownloadDraftRow[]>([]);
const addModalOpenRef = useRef(isAddModalOpen);
const metadataRequestsRef = useRef(new Set<string>());
const magnetMetadataRequestsRef = useRef(new Set<string>());
const playlistRequestsRef = useRef(new Set<string>());
const latestPlaylistRequestRef = useRef(new Map<string, string>());
const cachedTorrentDraftIdsRef = useRef(new Set<string>());
@@ -361,7 +363,6 @@ export const AddDownloadsModal = () => {
pendingLastUsedDownloadDirectoryRef.current = null;
setUrls('');
setPlaylistExpansions({});
playlistRequestsRef.current.clear();
latestPlaylistRequestRef.current.clear();
playlistMediaSelectionsRef.current.clear();
setPlaylistMediaTypeSelections({});
@@ -391,8 +392,9 @@ export const AddDownloadsModal = () => {
setUrls(initialUrls);
setParsedItems([]);
setPlaylistExpansions({});
metadataRequestsRef.current.clear();
playlistRequestsRef.current.clear();
// Keep active request ownership across rapid modal close/reopen cycles.
// The promises remove their own keys in finally; clearing these sets here
// would let stale native probes multiply beyond the concurrency bounds.
latestPlaylistRequestRef.current.clear();
playlistMediaSelectionsRef.current.clear();
setPlaylistMediaTypeSelections({});
@@ -593,13 +595,28 @@ export const AddDownloadsModal = () => {
]);
useEffect(() => {
if (!isAddModalOpen) return;
const maxConcurrentMetadataRequests = 4;
// Magnet inspection is optional enrichment. Keep it in a separate lane
// so a stalled public magnet cannot consume all slots needed to validate
// required HTTP, media, or .torrent metadata in the same Add window.
const maxConcurrentMagnetMetadataRequests = 2;
for (const row of parsedItems) {
if (row.status !== 'loading' || row.selected === false) continue;
const isDirectMagnetEnrichment = row.isTorrent === true
&& isMagnetUrl(row.sourceUrl)
&& !row.torrentPath
&& row.torrentMetadataStatus !== 'ready'
&& row.torrentMetadataStatus !== 'error';
if ((row.status !== 'loading' && !isDirectMagnetEnrichment) || row.selected === false) continue;
if (row.isPlaylist && playlistExpansions[row.sourceUrl]) continue;
const requestKey = `${row.id}:${row.generation}`;
const requestSet = row.isPlaylist ? playlistRequestsRef.current : metadataRequestsRef.current;
const requestSet = isDirectMagnetEnrichment
? magnetMetadataRequestsRef.current
: row.isPlaylist ? playlistRequestsRef.current : metadataRequestsRef.current;
if (requestSet.has(requestKey)) continue;
if (metadataRequestsRef.current.size + playlistRequestsRef.current.size >= maxConcurrentMetadataRequests) {
if (isDirectMagnetEnrichment) {
if (magnetMetadataRequestsRef.current.size >= maxConcurrentMagnetMetadataRequests) continue;
} else if (metadataRequestsRef.current.size + playlistRequestsRef.current.size >= maxConcurrentMetadataRequests) {
break;
}
requestSet.add(requestKey);
@@ -612,6 +629,7 @@ export const AddDownloadsModal = () => {
}
void (async () => {
let shouldWakeMetadataScheduler = true;
try {
const settingsStore = useSettingsStore.getState();
const login = getSiteLogin(row.sourceUrl, settingsStore);
@@ -637,14 +655,22 @@ export const AddDownloadsModal = () => {
&& currentRow.generation === row.generation
&& (currentRow.torrentCacheId || `${currentRow.id}-${currentRow.generation}`) === torrentCacheId
);
if (torrentData.torrentPath && isCurrentTorrentDraft) {
if (!isCurrentTorrentDraft) {
cachedTorrentDraftIdsRef.current.delete(torrentCacheId);
if (torrentData.torrentPath) {
void invoke('remove_torrent_metadata', { id: torrentCacheId }).catch(error => {
console.warn('Failed to remove stale torrent metadata:', error);
});
}
return;
}
if (torrentData.torrentPath) {
cachedTorrentDraftIdsRef.current.add(torrentCacheId);
} else if (torrentData.torrentPath) {
void invoke('remove_torrent_metadata', { id: torrentCacheId }).catch(error => {
console.warn('Failed to remove stale torrent metadata:', error);
});
} else {
cachedTorrentDraftIdsRef.current.delete(torrentCacheId);
}
const totalBytes = torrentData.totalBytes || undefined;
shouldWakeMetadataScheduler = false;
setParsedItems(current => updateRowIfCurrent(
current,
row.id,
@@ -655,15 +681,16 @@ export const AddDownloadsModal = () => {
downloadUrl: !row.sourceUrl.trim().toLowerCase().startsWith('magnet:')
? 'torrent:' + torrentData.infoHash
: row.sourceUrl,
file: canonicalizeDownloadFileName(torrentData.name),
size: totalBytes ? formatBytes(totalBytes) : undefined,
sizeBytes: totalBytes,
status: 'ready',
isTorrent: true,
torrentMetadataStatus: isMagnetUrl(row.sourceUrl) ? 'ready' : currentRow.torrentMetadataStatus,
torrentPath: torrentData.torrentPath,
torrentCacheId,
torrentInfoHash: torrentData.infoHash,
torrentFiles: torrentData.files,
file: canonicalizeDownloadFileName(torrentData.name?.trim() || currentRow.file),
selectedTorrentFileIndices: currentRow.selectedTorrentFileIndices
?.filter(index => torrentData.files.some(file => file.index === index))
})
@@ -672,6 +699,7 @@ export const AddDownloadsModal = () => {
}
const proxy = await getProxyArgs(settingsStore);
if (login && !useAuth && !keychainAccessReady && !keychainPromptDismissed) {
shouldWakeMetadataScheduler = false;
settingsStore.setShowKeychainModal(true);
return;
}
@@ -701,7 +729,10 @@ export const AddDownloadsModal = () => {
};
if (row.isPlaylist) {
if (playlistExpansions[row.sourceUrl]) return;
if (playlistExpansions[row.sourceUrl]) {
shouldWakeMetadataScheduler = false;
return;
}
const playlistData = await fetchMediaPlaylistMetadataDeduped({
...mediaMetadataArgs,
url: contextUrl
@@ -710,6 +741,7 @@ export const AddDownloadsModal = () => {
if (!playlistData.entries.length) {
throw new Error(t($ => $.addDownloads.playlistNoEntries));
}
shouldWakeMetadataScheduler = false;
setPlaylistExpansions(current => ({
...current,
[row.sourceUrl]: playlistData
@@ -758,6 +790,7 @@ export const AddDownloadsModal = () => {
? requestedFormatIndex
: fallbackFormatIndex >= 0 ? fallbackFormatIndex : 0;
const selectedFormat = mappedFormats[selectedFormatIndex];
shouldWakeMetadataScheduler = false;
setParsedItems(current => updateRowIfCurrent(
current,
row.id,
@@ -806,6 +839,7 @@ export const AddDownloadsModal = () => {
// its expiry. The metadata response remains useful for filename,
// size, and resumability.
const nextDownloadUrl = durableDownloadUrl(row.sourceUrl);
shouldWakeMetadataScheduler = false;
setParsedItems(current => updateRowIfCurrent(
current,
row.id,
@@ -839,6 +873,7 @@ export const AddDownloadsModal = () => {
].some(prefix => errorMessage.startsWith(prefix))
? 'unsafe-url' as const
: undefined;
shouldWakeMetadataScheduler = false;
setParsedItems(current => updateRowIfCurrent(
current,
row.id,
@@ -849,7 +884,9 @@ export const AddDownloadsModal = () => {
downloadUrl: currentRow.sourceUrl,
size: undefined,
sizeBytes: undefined,
status: 'metadata-error',
status: currentRow.isTorrent && isMagnetUrl(currentRow.sourceUrl)
? 'ready'
: 'metadata-error',
formats: undefined,
selectedFormat: undefined,
...(currentRow.isTorrent
@@ -858,6 +895,7 @@ export const AddDownloadsModal = () => {
torrentCacheId: undefined,
torrentInfoHash: undefined,
torrentFiles: undefined,
torrentMetadataStatus: isMagnetUrl(currentRow.sourceUrl) ? 'error' : undefined,
selectedTorrentFileIndices: undefined
}
: {}),
@@ -869,12 +907,17 @@ export const AddDownloadsModal = () => {
));
} finally {
requestSet.delete(requestKey);
if (shouldWakeMetadataScheduler) {
setMetadataSchedulerRevision(revision => revision + 1);
}
}
})();
}
}, [
isAddModalOpen,
keychainAccessReady,
keychainPromptDismissed,
metadataSchedulerRevision,
parsedItems,
pendingAddFilename,
pendingAddMediaUrls,
+1 -1
View File
@@ -1332,7 +1332,7 @@ export const PropertiesWindowApp = () => {
<div className="properties-metric-card"><Download size={14} /><div className="properties-metric-content"><span className="properties-metric-label">{t($ => $.properties.size)}</span><strong className="properties-metric-value">{formatDownloadBytes(snapshot.downloadedBytes ?? 0)} / {total}</strong></div></div>
<div className="properties-metric-card"><Gauge size={14} /><div className="properties-metric-content"><span className="properties-metric-label">{t($ => $.properties.speed)}</span><strong className="properties-metric-value">{allocationPending ? '—' : snapshot.speed || '—'}</strong></div></div>
<div className="properties-metric-card"><Timer size={14} /><div className="properties-metric-content"><span className="properties-metric-label">{t($ => $.properties.eta)}</span><strong className="properties-metric-value">{allocationPending ? '—' : snapshot.eta || '—'}</strong></div></div>
{connectionPresentation.showHeaderMetric && <div className="properties-metric-card"><Users size={14} /><div className="properties-metric-content"><span className={`properties-metric-label ${connectionPresentation.labelKey === 'torrentPeersSeeders' ? 'properties-metric-label--wide' : ''}`}>{connectionHeaderLabel}</span>{connectionValue}</div></div>}
{connectionPresentation.showHeaderMetric && <div className="properties-metric-card"><Users size={14} /><div className="properties-metric-content"><span className="properties-metric-label">{connectionHeaderLabel}</span>{connectionValue}</div></div>}
{isTorrent && <>
<div className="properties-metric-card"><Upload size={14} /><div className="properties-metric-content"><span className="properties-metric-label">{t($ => $.properties.torrentUploaded)}</span><strong className="properties-metric-value">{formatDownloadBytes(snapshot.torrentUploadedBytes ?? 0)}</strong></div></div>
<div className="properties-metric-card"><Activity size={14} /><div className="properties-metric-content"><span className="properties-metric-label">{t($ => $.properties.torrentRatio)}</span><strong className="properties-metric-value">{formatTorrentRatio(snapshot.torrentUploadedBytes ?? 0, snapshot.downloadedBytes ?? 0, 'en-US')}</strong></div></div>
+1 -1
View File
@@ -362,7 +362,7 @@ const common = {
torrentSeededDuration: 'Seeded',
torrentSeedTimeHint: 'How long this Torrent may continue seeding after its files finish downloading. Leave blank to use the default.',
torrentConnectedPeers: 'Peers',
torrentPeersSeeders: 'Connected peers / connected seeders',
torrentPeersSeeders: 'Peers/Seeds',
torrentConnectedPeerMetric: '{{peers}} connected peers / {{seeders}} connected seeders',
torrentPeerDetailsUnavailable: '{{connected}} connected peers reported, but peer details are not available yet.',
torrentPeerCountDifference: 'Connected status: {{connectedPeers}} peers / {{connectedSeeders}} seeders. The peer-details response lists {{listedPeers}} peers / {{listedSeeders}} seeders.',
+1 -1
View File
@@ -362,7 +362,7 @@ const fa = {
torrentSeededDuration: 'مدت سید',
torrentSeedTimeHint: 'مدتی که تورنت پس از تکمیل دانلود به سید ادامه می‌دهد. برای استفاده از پیش‌فرض خالی بگذارید.',
torrentConnectedPeers: 'همتاها',
torrentPeersSeeders: 'همتاهای متصل / سیدهای متصل',
torrentPeersSeeders: 'همتاها / سیدها',
torrentConnectedPeerMetric: '{{peers}} همتای متصل / {{seeders}} سید متصل',
torrentPeerDetailsUnavailable: '{{connected}} همتای متصل گزارش شده، اما جزئیات همتاها هنوز در دسترس نیست.',
torrentPeerCountDifference: 'وضعیت اتصال: {{connectedPeers}} همتا / {{connectedSeeders}} سید. پاسخ جزئیات همتاها {{listedPeers}} همتا / {{listedSeeders}} سید را فهرست کرده است.',
+1 -1
View File
@@ -362,7 +362,7 @@ const he = {
torrentSeededDuration: 'משך שיתוף',
torrentSeedTimeHint: 'משך הזמן שבו הטורנט ימשיך לשתף לאחר סיום ההורדה. השאר ריק כדי להשתמש בברירת המחדל.',
torrentConnectedPeers: 'עמיתים',
torrentPeersSeeders: 'עמיתים מחוברים / משתפים מחוברים',
torrentPeersSeeders: 'עמיתים / משתפים',
torrentConnectedPeerMetric: '{{peers}} עמיתים מחוברים / {{seeders}} משתפים מחוברים',
torrentPeerDetailsUnavailable: 'דווחו {{connected}} עמיתים מחוברים, אך פרטי העמיתים עדיין אינם זמינים.',
torrentPeerCountDifference: 'מחוברים: {{connectedPeers}} עמיתים / {{connectedSeeders}} משתפים. תגובת פרטי העמיתים מציגה {{listedPeers}} עמיתים / {{listedSeeders}} משתפים.',
+1 -1
View File
@@ -362,7 +362,7 @@ const ru = {
torrentSeededDuration: 'Время раздачи',
torrentSeedTimeHint: 'Как долго Torrent продолжает раздачу после завершения загрузки. Оставьте пустым для значения по умолчанию.',
torrentConnectedPeers: 'Пиры',
torrentPeersSeeders: одключённые пиры / подключённые сиды',
torrentPeersSeeders: иры / Сиды',
torrentConnectedPeerMetric: '{{peers}} подключённых пиров / {{seeders}} подключённых сидов',
torrentPeerDetailsUnavailable: 'Подключённых пиров: {{connected}}, но сведения о них пока недоступны.',
torrentPeerCountDifference: 'Подключено: {{connectedPeers}} пиров / {{connectedSeeders}} сидов. В ответе со сведениями о пирах указано: {{listedPeers}} пиров / {{listedSeeders}} сидов.',
+1 -1
View File
@@ -362,7 +362,7 @@ const uk = {
torrentSeededDuration: 'Час роздачі',
torrentSeedTimeHint: 'Як довго Torrent продовжує роздачу після завершення завантаження. Залиште порожнім для значення за замовчуванням.',
torrentConnectedPeers: 'Піри',
torrentPeersSeeders: 'Підключені піри / підключені сіди',
torrentPeersSeeders: 'Піри / Сіди',
torrentConnectedPeerMetric: '{{peers}} підключених пірів / {{seeders}} підключених сідів',
torrentPeerDetailsUnavailable: 'Підключених пірів: {{connected}}, але відомості про них поки недоступні.',
torrentPeerCountDifference: 'Підключено: {{connectedPeers}} пірів / {{connectedSeeders}} сідів. У відповіді з відомостями про піри зазначено: {{listedPeers}} пірів / {{listedSeeders}} сідів.',
+1 -1
View File
@@ -362,7 +362,7 @@ const zhCN = {
torrentSeededDuration: '做种时长',
torrentSeedTimeHint: '文件下载完成后继续做种的时长。留空以使用默认值。',
torrentConnectedPeers: '连接数',
torrentPeersSeeders: '已连接节点 / 已连接做种节点',
torrentPeersSeeders: '节点 / 做种',
torrentConnectedPeerMetric: '{{peers}} 个已连接节点 / {{seeders}} 个已连接做种节点',
torrentPeerDetailsUnavailable: '检测到 {{connected}} 个已连接节点,但其详细信息暂时不可用。',
torrentPeerCountDifference: '连接状态:{{connectedPeers}} 个节点 / {{connectedSeeders}} 个做种节点。对等节点详情响应列出 {{listedPeers}} 个节点 / {{listedSeeders}} 个做种节点。',
+1 -14
View File
@@ -798,7 +798,7 @@ html[data-list-density="relaxed"] {
.properties-window-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(min(145px, 100%), 1fr));
gap: 7px;
margin-top: 11px;
}
@@ -842,16 +842,6 @@ html[data-list-density="relaxed"] {
white-space: nowrap;
}
.properties-metric-card > .properties-metric-content > .properties-metric-label--wide {
overflow: visible;
font-size: 9px;
letter-spacing: 0.01em;
line-height: 1.15;
min-height: 2.3em;
text-overflow: clip;
white-space: normal;
}
.properties-metric-card > .properties-metric-content > .properties-metric-value {
overflow: hidden;
color: hsl(var(--text-primary));
@@ -1421,9 +1411,6 @@ html[data-list-density="relaxed"] {
display: inline-block;
}
.properties-window-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 620px) {
+12 -6
View File
@@ -97,7 +97,9 @@ describe('add download metadata workflow', () => {
expect(rows[0]).toMatchObject({
isTorrent: true,
isMedia: false,
status: 'ready'
status: 'ready',
file: 'Example',
torrentMetadataStatus: 'loading'
});
expect(rows[1]).toMatchObject({
isTorrent: true,
@@ -536,20 +538,22 @@ describe('add download metadata workflow', () => {
expect(refreshed[1]).toMatchObject({ status: 'loading', generation: 5 });
});
it('refreshes an admitted magnet only when metadata is explicitly requested', () => {
it('does not duplicate an in-flight magnet probe and refreshes it after failure', () => {
const magnet = 'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567';
const admitted = reconcileDownloadRows(magnet, [])[0];
expect(admitted.status).toBe('ready');
expect(canSubmitMetadataRows([admitted])).toBe(true);
expect(isMetadataRefreshableRow(admitted)).toBe(false);
const refreshed = refreshFailedMetadataRows([admitted])[0];
const failed = { ...admitted, torrentMetadataStatus: 'error' as const };
const refreshed = refreshFailedMetadataRows([failed])[0];
expect(refreshed).toMatchObject({
status: 'loading',
generation: 2,
torrentCacheId: `${admitted.id}-2`,
});
expect(isMetadataRefreshableRow(admitted)).toBe(true);
expect(isMetadataRefreshableRow(failed)).toBe(true);
expect(isMetadataRefreshableRow({ ...admitted, status: 'loading' })).toBe(false);
expect(isMetadataRefreshableRow(row())).toBe(false);
});
@@ -589,7 +593,8 @@ describe('add download metadata workflow', () => {
expect(refreshed).toMatchObject({
status: 'loading',
generation: 2,
torrentCacheId: 'row-1-2'
torrentCacheId: 'row-1-2',
torrentMetadataStatus: 'loading'
});
expect(refreshed.torrentPath).toBeUndefined();
expect(refreshed.torrentInfoHash).toBeUndefined();
@@ -616,7 +621,8 @@ describe('add download metadata workflow', () => {
expect(migrated).toMatchObject({
status: 'ready',
downloadUrl: magnet,
isTorrent: true
isTorrent: true,
torrentMetadataStatus: 'loading'
});
expect(migrated.torrentPath).toBeUndefined();
expect(migrated.torrentCacheId).toBeUndefined();
+12 -3
View File
@@ -61,6 +61,8 @@ export interface AddDownloadDraftRow {
torrentCacheId?: string;
torrentInfoHash?: string;
torrentFiles?: TorrentFile[];
/** Best-effort metadata enrichment state for directly admitted magnets. */
torrentMetadataStatus?: 'loading' | 'ready' | 'error';
selectedTorrentFileIndices?: number[];
torrentSeedTime?: number;
torrentSeedRatio?: number;
@@ -120,7 +122,8 @@ export const isMagnetTorrentRow = (
export const isMetadataRefreshableRow = (row: AddDownloadDraftRow): boolean =>
row.status !== 'loading'
&& (row.status === 'metadata-error' || isMagnetTorrentRow(row));
&& (row.status === 'metadata-error'
|| (isMagnetTorrentRow(row) && row.torrentMetadataStatus !== 'loading'));
type ParsedInput = {
identity: string;
@@ -291,6 +294,7 @@ export const reconcileDownloadRows = (
? canonicalizeDownloadFileName(requestedFilename || fileNameFromUrl(input.sourceUrl))
: preserved.file,
status: input.isTorrent && isMagnetUrl(input.sourceUrl) ? 'ready' : 'loading',
torrentMetadataStatus: input.isTorrent && isMagnetUrl(input.sourceUrl) ? 'loading' : undefined,
generation: nextGeneration,
requestContextVersion,
isMedia: preserved.isMedia || forcedMedia || Boolean(input.playlistSourceUrl),
@@ -332,6 +336,7 @@ export const reconcileDownloadRows = (
downloadUrl: input.sourceUrl,
status: 'ready',
isTorrent: true,
torrentMetadataStatus: 'loading',
torrentPath: undefined,
torrentCacheId: undefined,
torrentInfoHash: undefined,
@@ -359,8 +364,8 @@ export const reconcileDownloadRows = (
file: fallback,
// A magnet already contains the transfer identity. Metadata is useful
// for the preview, but it is not required to admit the transfer. Keep
// the optional probe behind Refresh Metadata so the Add window never
// blocks a valid magnet on the bounded native probe timeout.
// the probe best-effort so the Add window never blocks a valid magnet
// on the bounded native probe timeout.
status: input.valid
? input.isTorrent && isMagnetUrl(input.sourceUrl) ? 'ready' : 'loading'
: 'invalid',
@@ -383,6 +388,9 @@ export const reconcileDownloadRows = (
torrentCacheId: input.valid && (input.isTorrent || forceTorrentUrls.has(input.sourceUrl))
? `${id}-${generation}`
: undefined,
torrentMetadataStatus: input.valid && input.isTorrent && isMagnetUrl(input.sourceUrl)
? 'loading'
: undefined,
selected: input.selected !== false
};
});
@@ -446,6 +454,7 @@ export const refreshFailedMetadataRows = (
torrentCacheId: `${row.id}-${generation}`,
torrentInfoHash: undefined,
torrentFiles: undefined,
torrentMetadataStatus: isMagnetTorrentRow(row) ? 'loading' : undefined,
selectedTorrentFileIndices: undefined
}
: {})
+15
View File
@@ -3,6 +3,7 @@ import type { DownloadItem } from '../bindings/DownloadItem';
import {
downloadFileNamesMatch,
downloadFileNameWithSuffix,
fileNameFromUrl,
downloadMediaKindsMatch,
MAX_DOWNLOAD_FILENAME_BYTES,
canonicalizeDownloadFileName,
@@ -50,6 +51,20 @@ describe('download category detection', () => {
});
});
describe('download names from URLs', () => {
it('uses a magnet display name before metadata is resolved', () => {
expect(fileNameFromUrl(
'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=Pizza+House+Simulator+%28v1.0%29',
)).toBe('Pizza House Simulator (v1.0)');
});
it('keeps the generic fallback for unnamed magnets', () => {
expect(fileNameFromUrl(
'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567',
)).toBe('download');
});
});
describe('download persistence progress snapshots', () => {
it('does not write active byte counters on every progress event', () => {
const persisted = redactDownloadForPersistence(item('downloading'));
+6
View File
@@ -492,6 +492,12 @@ export const categoryForDownload = (
export const fileNameFromUrl = (rawUrl: string): string => {
try {
const url = new URL(rawUrl);
if (url.protocol === 'magnet:') {
const displayName = url.searchParams.get('dn')?.trim();
if (displayName) {
return displayName.replace(/[\/\\?%*:|"<>]/g, '-');
}
}
const pathName = url.pathname.split('/').filter(Boolean).pop();
if (pathName) {
const decoded = decodeURIComponent(pathName).trim();