diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index c6d5f12..fa75c97 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -120,6 +120,7 @@ pub enum DownloadCategory { Documents, Pictures, Applications, + Torrents, Other, } diff --git a/src-tauri/src/parity.rs b/src-tauri/src/parity.rs index abcc795..ffc6a53 100644 --- a/src-tauri/src/parity.rs +++ b/src-tauri/src/parity.rs @@ -486,7 +486,9 @@ pub fn get_file_category(filename: String) -> DownloadCategory { "run", "sh", "bin", "jar", ]; - if music_exts.contains(&ext.as_str()) { + if ext == "torrent" { + DownloadCategory::Torrents + } else if music_exts.contains(&ext.as_str()) { DownloadCategory::Musics } else if movie_exts.contains(&ext.as_str()) { DownloadCategory::Movies @@ -689,3 +691,21 @@ pub fn is_supported_media(url: String) -> bool { } false } + +#[cfg(test)] +mod tests { + use super::get_file_category; + use crate::ipc::DownloadCategory; + + #[test] + fn classifies_torrent_files_as_torrents() { + assert!(matches!( + get_file_category("Example.TORRENT".to_string()), + DownloadCategory::Torrents + )); + assert!(matches!( + get_file_category("Example.mkv".to_string()), + DownloadCategory::Movies + )); + } +} diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 595c3fb..078e847 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -693,6 +693,7 @@ fn default_category_subfolders() -> HashMap { ("Documents", "Documents"), ("Pictures", "Pictures"), ("Applications", "Applications"), + ("Torrents", "Torrents"), ("Other", "Other"), ] .into_iter() @@ -762,6 +763,7 @@ fn migrate_location_settings(state: &mut Value) -> Result<(), String> { ("Documents", "Documents"), ("Pictures", "Images"), ("Applications", "Apps"), + ("Torrents", "Torrents"), ("Other", "Other"), ]; for (category, alias) in aliases { @@ -1044,6 +1046,7 @@ mod tests { assert_eq!(settings.base_download_folder, "/Users/test/Downloads"); assert_eq!(settings.category_subfolders["Movies"], "Movies"); + assert_eq!(settings.category_subfolders["Torrents"], "Torrents"); assert!(!settings.category_directory_overrides.contains_key("Movies")); assert_eq!( settings.category_directory_overrides["Documents"], diff --git a/src/bindings/DownloadCategory.ts b/src/bindings/DownloadCategory.ts index 3ba62cb..81938b2 100644 --- a/src/bindings/DownloadCategory.ts +++ b/src/bindings/DownloadCategory.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DownloadCategory = "Musics" | "Movies" | "Compressed" | "Documents" | "Pictures" | "Applications" | "Other"; +export type DownloadCategory = "Musics" | "Movies" | "Compressed" | "Documents" | "Pictures" | "Applications" | "Torrents" | "Other"; diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 57eb161..1df7824 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -888,7 +888,7 @@ export const AddDownloadsModal = () => { if (first.status !== 'ready' && first.status !== 'metadata-error') return; void resolveCategoryDestination( useSettingsStore.getState(), - categoryForFileName(first.file) + categoryForFileName(first.file, first.isTorrent === true) ).then(location => { if (requestId === locationResolutionRequestRef.current) { setSaveLocation(location); @@ -940,8 +940,8 @@ export const AddDownloadsModal = () => { } }; - const categoryLocationForFile = (fileName: string) => { - const category = categoryForFileName(fileName); + const categoryLocationForFile = (fileName: string, isTorrent = false) => { + const category = categoryForFileName(fileName, isTorrent); return resolveCategoryDestination(useSettingsStore.getState(), category); }; @@ -963,12 +963,13 @@ export const AddDownloadsModal = () => { fileName: string, finalLocation: string, useSharedDestination: boolean, - destinationOverride?: string + destinationOverride?: string, + isTorrent = false ): Promise => { if (destinationOverride) return destinationOverride; const root = useSharedDestination ? finalLocation - : await categoryLocationForFile(fileName); + : await categoryLocationForFile(fileName, isTorrent); return saveInDedicatedFolder ? resolveSubfolderDestination(root, dedicatedFolderName) : root; @@ -1113,7 +1114,9 @@ export const AddDownloadsModal = () => { const suggestedLocation = await destinationForFile( item.file, finalLocation, - isSaveLocationManual + isSaveLocationManual, + undefined, + item.isTorrent === true ); const selected = await open({ directory: true, @@ -1160,7 +1163,8 @@ export const AddDownloadsModal = () => { finalFile, finalLocation, useSharedDestination, - destinationOverrides[i] + destinationOverrides[i], + item.isTorrent === true ); const urlMatch = store.downloads.find(d => @@ -1342,7 +1346,8 @@ export const AddDownloadsModal = () => { finalFile, finalLocation, useSharedDestination, - destinationOverrides[idx] + destinationOverrides[idx], + item.isTorrent === true ); let count = 1; @@ -1358,7 +1363,8 @@ export const AddDownloadsModal = () => { candidateFile, finalLocation, useSharedDestination, - destinationOverrides[candidateIndex] + destinationOverrides[candidateIndex], + candidate.isTorrent === true ); batchTargets.push({ location: candidateLocation, fileName: candidateFile }); } @@ -1419,7 +1425,8 @@ export const AddDownloadsModal = () => { finalFile, finalLocation, useSharedDestination, - destinationOverrides[idx] + destinationOverrides[idx], + item.isTorrent === true ); const store = useDownloadStore.getState(); let existingItem = conflict?.existingDownloadId @@ -1531,7 +1538,7 @@ export const AddDownloadsModal = () => { ? mediaFileNameForSelectedFormat(item.file, item) : canonicalizeDownloadFileName(item.file); let formatSelector = mediaFormatSelectorForRow(item); - const category = categoryForFileName(finalFile); + const category = categoryForFileName(finalFile, item.isTorrent === true); const added = await addDownload({ id, url: item.downloadUrl, @@ -1559,7 +1566,8 @@ export const AddDownloadsModal = () => { finalFile, finalLocation, useSharedDestination, - destinationOverrides[itemIndex] + destinationOverrides[itemIndex], + item.isTorrent === true ) : undefined, isMedia: item.isMedia, diff --git a/src/components/DownloadItem.tsx b/src/components/DownloadItem.tsx index 59ebc96..ae852f1 100644 --- a/src/components/DownloadItem.tsx +++ b/src/components/DownloadItem.tsx @@ -273,8 +273,8 @@ export const DownloadItem = React.memo(({ ) : null} {download.isTorrent ? ( - $.addDownloads.torrentFiles)}> - {t($ => $.addDownloads.torrentFiles)} + $.addDownloads.torrent)}> + {t($ => $.addDownloads.torrent)} ) : null} diff --git a/src/components/DownloadTable.tsx b/src/components/DownloadTable.tsx index 962a779..1273c3c 100644 --- a/src/components/DownloadTable.tsx +++ b/src/components/DownloadTable.tsx @@ -6,7 +6,7 @@ import { useToast } from '../contexts/ToastContext'; import { useSettingsStore } from '../store/useSettingsStore'; import { SidebarFilter } from './Sidebar'; import { - Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, + Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, Magnet, ArrowDownCircle, ArrowUp, ArrowDown, Command, ChevronUp, ChevronDown, MoreHorizontal, AlignLeft, AlignCenter, AlignRight, GripVertical } from 'lucide-react'; @@ -1850,6 +1850,7 @@ export const DownloadTable: React.FC = ({ filter, onSummaryC case 'Documents': return t($ => $.navigation.categories.documents); case 'Pictures': return t($ => $.navigation.categories.pictures); case 'Applications': return t($ => $.navigation.categories.applications); + case 'Torrents': return t($ => $.navigation.categories.torrents); case 'Other': return t($ => $.navigation.categories.other); default: return filter; } @@ -2008,6 +2009,7 @@ export const DownloadTable: React.FC = ({ filter, onSummaryC case 'Applications': return ; case 'Pictures': return ; case 'Compressed': return ; + case 'Torrents': return ; case 'Other': return ; default: return ; } diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index cb42a0c..9ece69d 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -813,6 +813,7 @@ runEngineChecks(false); case 'Documents': return t($ => $.navigation.categories.documents); case 'Pictures': return t($ => $.navigation.categories.pictures); case 'Applications': return t($ => $.navigation.categories.applications); + case 'Torrents': return t($ => $.navigation.categories.torrents); default: return t($ => $.navigation.categories.other); } }; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0e3f0a0..02f8328 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useLayoutEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { Inbox, Zap, CheckCircle2, CircleDashed, - Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion, + Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion, Magnet, List, CalendarClock, Gauge, Bug, Settings, Plus, Play, Pause, Edit2, Trash2, PanelLeft, ChevronDown, type LucideIcon @@ -403,6 +403,7 @@ export const Sidebar: React.FC = (props) => { $.navigation.categories.documents)} filter="Documents" /> $.navigation.categories.pictures)} filter="Pictures" /> $.navigation.categories.applications)} filter="Applications" /> + $.navigation.categories.torrents)} filter="Torrents" /> $.navigation.categories.other)} filter="Other" /> diff --git a/src/i18n/catalogs/en.ts b/src/i18n/catalogs/en.ts index dbf22a9..83809a6 100644 --- a/src/i18n/catalogs/en.ts +++ b/src/i18n/catalogs/en.ts @@ -14,6 +14,7 @@ const common = { documents: 'Documents', pictures: 'Pictures', applications: 'Applications', + torrents: 'Torrents', other: 'Other', }, folders: 'Folders', @@ -667,6 +668,7 @@ const common = { refreshMetadata: 'Refresh Metadata', files: 'Files', torrentFiles: 'Torrent files', + torrent: 'Torrent', chooseTorrentFiles: 'Add .torrent files', torrentMetadataPending: 'Aria2 will resolve the magnet metadata when the transfer starts.', torrentSeeding: 'Torrent seeding', diff --git a/src/i18n/catalogs/fa.ts b/src/i18n/catalogs/fa.ts index b1ed327..ab64921 100644 --- a/src/i18n/catalogs/fa.ts +++ b/src/i18n/catalogs/fa.ts @@ -14,6 +14,7 @@ const fa = { documents: 'اسناد', pictures: 'تصاویر', applications: 'برنامه‌ها', + torrents: 'تورنت‌ها', other: 'سایر', }, folders: 'پوشه‌ها', @@ -667,6 +668,7 @@ const fa = { refreshMetadata: 'تازه‌سازی متادیتا', files: 'فایل‌ها', torrentFiles: 'فایل‌های تورنت', + torrent: 'تورنت', chooseTorrentFiles: 'افزودن فایل‌های .torrent', torrentMetadataPending: 'آریا۲ هنگام شروع انتقال، متادیتای مگنت را دریافت می‌کند.', torrentSeeding: 'اشتراک‌گذاری تورنت', diff --git a/src/i18n/catalogs/he.ts b/src/i18n/catalogs/he.ts index 2244908..ce6a973 100644 --- a/src/i18n/catalogs/he.ts +++ b/src/i18n/catalogs/he.ts @@ -14,6 +14,7 @@ const he = { documents: 'מסמכים', pictures: 'תמונות', applications: 'יישומים', + torrents: 'טורנטים', other: 'אחר', }, folders: 'תיקיות', @@ -667,6 +668,7 @@ const he = { refreshMetadata: 'רענון מטא נתונים', files: 'קבצים', torrentFiles: 'קובצי טורנט', + torrent: 'טורנט', chooseTorrentFiles: 'הוספת קובצי ‎.torrent', torrentMetadataPending: 'Aria2 יאתר את נתוני המגנט כשההעברה תתחיל.', torrentSeeding: 'שיתוף טורנט', diff --git a/src/i18n/catalogs/ru.ts b/src/i18n/catalogs/ru.ts index bdf3339..3f945d2 100644 --- a/src/i18n/catalogs/ru.ts +++ b/src/i18n/catalogs/ru.ts @@ -14,6 +14,7 @@ const ru = { documents: 'Документы', pictures: 'Изображения', applications: 'Программы', + torrents: 'Торренты', other: 'Другое', }, folders: 'Папки', @@ -667,6 +668,7 @@ const ru = { refreshMetadata: 'Обновить метаданные', files: 'Файлы', torrentFiles: 'Торрент-файлы', + torrent: 'Торрент', chooseTorrentFiles: 'Добавить файлы .torrent', torrentMetadataPending: 'Aria2 получит метаданные магнита при запуске передачи.', torrentSeeding: 'Раздача торрента', diff --git a/src/i18n/catalogs/uk.ts b/src/i18n/catalogs/uk.ts index 59f4549..c4abd0c 100644 --- a/src/i18n/catalogs/uk.ts +++ b/src/i18n/catalogs/uk.ts @@ -14,6 +14,7 @@ const uk = { documents: 'Документи', pictures: 'Зображення', applications: 'Програми', + torrents: 'Торенти', other: 'Інше', }, folders: 'Папки', @@ -667,6 +668,7 @@ const uk = { refreshMetadata: 'Оновити метадані', files: 'Файли', torrentFiles: 'Торрент-файли', + torrent: 'Торрент', chooseTorrentFiles: 'Додати файли .torrent', torrentMetadataPending: 'Aria2 отримає метадані магнітного посилання після початку передачі.', torrentSeeding: 'Роздача торрента', diff --git a/src/i18n/catalogs/zh-CN.ts b/src/i18n/catalogs/zh-CN.ts index a69adf1..e5b92d4 100644 --- a/src/i18n/catalogs/zh-CN.ts +++ b/src/i18n/catalogs/zh-CN.ts @@ -14,6 +14,7 @@ const zhCN = { documents: '文档', pictures: '图片', applications: '应用程序', + torrents: '种子', other: '其他', }, folders: '文件夹', @@ -667,6 +668,7 @@ const zhCN = { refreshMetadata: '刷新元数据', files: '文件', torrentFiles: '种子文件', + torrent: '种子', chooseTorrentFiles: '添加 .torrent 文件', torrentMetadataPending: '传输开始时,Aria2 将解析磁力链接元数据。', torrentSeeding: 'BT 做种', diff --git a/src/store/downloadStore.ts b/src/store/downloadStore.ts index 5664a0f..1dce88e 100644 --- a/src/store/downloadStore.ts +++ b/src/store/downloadStore.ts @@ -2,7 +2,7 @@ import type { UnlistenFn } from '@tauri-apps/api/event'; import type { DownloadStatus } from '../bindings/DownloadStatus'; import { listenEvent as listen } from '../ipc'; import type { DownloadItem } from '../bindings/DownloadItem'; -import { categoryForFileName } from '../utils/downloads'; +import { categoryForDownload } from '../utils/downloads'; import { useDownloadProgressStore } from './downloadProgressStore'; import { @@ -221,7 +221,11 @@ const startDownloadListeners = async () => { } if (payload.fileName && payload.fileName !== current.fileName) { updates.fileName = payload.fileName; - updates.category = categoryForFileName(payload.fileName); + updates.category = categoryForDownload( + payload.fileName, + current.isTorrent === true, + current.category + ); } if (status !== 'downloading' && status !== 'verifying') { updates.speed = '-'; diff --git a/src/store/useDownloadStore.test.ts b/src/store/useDownloadStore.test.ts index 8812665..4ae2df3 100644 --- a/src/store/useDownloadStore.test.ts +++ b/src/store/useDownloadStore.test.ts @@ -1607,6 +1607,23 @@ describe('useDownloadStore', () => { expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything()); }); + it('normalizes new Torrent rows before resolving their default destination', async () => { + await useDownloadStore.getState().addDownload({ + id: 'torrent-default', + url: 'magnet:?xt=urn:btih:default', + fileName: 'metadata', + category: 'Other', + dateAdded: '', + isTorrent: true + }, { type: 'add-to-queue', queueId: 'queue-torrents' }); + + expect(useDownloadStore.getState().downloads[0]).toMatchObject({ + category: 'Torrents', + destination: '/Users/test/Downloads/Torrents', + status: 'staged' + }); + }); + it('inserts a newly staged queue item before paused rows', async () => { useDownloadStore.setState({ downloads: [{ diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts index 6a398ea..d7c38a3 100644 --- a/src/store/useDownloadStore.ts +++ b/src/store/useDownloadStore.ts @@ -9,7 +9,7 @@ import type { ExtensionCookieScope } from '../bindings/ExtensionCookieScope'; import type { Queue } from '../bindings/Queue'; import { useSettingsStore } from './useSettingsStore'; import { useDownloadProgressStore } from './downloadProgressStore'; -import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentFileAllocation, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads'; +import { canonicalizeDownloadFileName, categoryForDownload, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentFileAllocation, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads'; import { resolveCategoryDestination } from '../utils/downloadLocations'; @@ -418,7 +418,11 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null): if (acceptedFilename !== item.fileName) { useDownloadStore.getState().updateDownload(id, { fileName: acceptedFilename, - category: categoryForFileName(acceptedFilename) + category: categoryForDownload( + acceptedFilename, + item.isTorrent === true, + item.category + ) }); } const order = await invoke('get_pending_order', { queueId: item.queueId || MAIN_QUEUE_ID }); @@ -1614,7 +1618,8 @@ export const useDownloadStore = create((set, get) => { const settings = useSettingsStore.getState(); const normalizedItem = { ...item, - fileName: canonicalizeDownloadFileName(item.fileName) + fileName: canonicalizeDownloadFileName(item.fileName), + category: categoryForFileName(item.fileName, item.isTorrent === true) }; const destPath = await effectiveDestinationForItem(normalizedItem, settings); const queueId = action.type === 'add-to-queue' ? action.queueId : MAIN_QUEUE_ID; @@ -2541,7 +2546,11 @@ export const useDownloadStore = create((set, get) => { ...(acceptedFilenames.has(download.id) ? { fileName: acceptedFilenames.get(download.id), - category: categoryForFileName(acceptedFilenames.get(download.id)!) + category: categoryForDownload( + acceptedFilenames.get(download.id)!, + download.isTorrent === true, + download.category + ) } : {}), hasBeenDispatched: true, diff --git a/src/utils/downloadLocations.test.ts b/src/utils/downloadLocations.test.ts index 67740f4..036adbd 100644 --- a/src/utils/downloadLocations.test.ts +++ b/src/utils/downloadLocations.test.ts @@ -130,6 +130,24 @@ describe('download locations', () => { expect(await resolveCategoryDestination(automatic, 'Movies')).toBe('/Volumes/Media'); }); + it('defaults Torrent downloads to the Torrents folder and respects overrides', async () => { + const settings = normalizeDownloadLocationSettings({ + baseDownloadFolder: '/Users/test/Downloads' + }); + + expect(settings.categorySubfolders.Torrents).toBe('Torrents'); + expect(await resolveCategoryDestination(settings, 'Torrents')) + .toBe('/Users/test/Downloads/Torrents'); + + settings.categoryDirectoryOverrides.Torrents = '/Volumes/Archive/Torrents'; + expect(await resolveCategoryDestination(settings, 'Torrents')) + .toBe('/Volumes/Archive/Torrents'); + + settings.categorySubfoldersEnabled = false; + expect(await resolveCategoryDestination(settings, 'Torrents')) + .toBe('/Users/test/Downloads'); + }); + it('defaults category subfolders on and sends every category to the base folder when disabled', async () => { const automatic = normalizeDownloadLocationSettings({ baseDownloadFolder: '/Users/test/Downloads' diff --git a/src/utils/downloadLocations.ts b/src/utils/downloadLocations.ts index 6fcf42d..88dbf12 100644 --- a/src/utils/downloadLocations.ts +++ b/src/utils/downloadLocations.ts @@ -19,6 +19,7 @@ export const DOWNLOAD_CATEGORIES: DownloadCategory[] = [ 'Documents', 'Pictures', 'Applications', + 'Torrents', 'Other' ]; @@ -29,6 +30,7 @@ export const DEFAULT_CATEGORY_SUBFOLDERS: Record = { Documents: 'Documents', Pictures: 'Pictures', Applications: 'Applications', + Torrents: 'Torrents', Other: 'Other' }; @@ -245,6 +247,7 @@ export const normalizeDownloadLocationSettings = ( Documents: 'Documents', Pictures: 'Images', Applications: 'Apps', + Torrents: 'Torrents', Other: 'Other' }; diff --git a/src/utils/downloads.test.ts b/src/utils/downloads.test.ts index 7ecfe80..36300de 100644 --- a/src/utils/downloads.test.ts +++ b/src/utils/downloads.test.ts @@ -6,6 +6,8 @@ import { downloadMediaKindsMatch, MAX_DOWNLOAD_FILENAME_BYTES, canonicalizeDownloadFileName, + categoryForDownload, + categoryForFileName, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, normalizeTorrentEncryptionPolicy, @@ -33,6 +35,17 @@ const item = (status: DownloadItem['status']): DownloadItem => ({ totalIsEstimate: false }); +describe('download category detection', () => { + it('classifies torrent files and explicit Torrent rows separately from filename types', () => { + expect(categoryForFileName('Example.torrent')).toBe('Torrents'); + expect(categoryForFileName('Example', true)).toBe('Torrents'); + expect(categoryForFileName('Example.mkv', true)).toBe('Torrents'); + expect(categoryForFileName('Example.mkv')).toBe('Movies'); + expect(categoryForDownload('Renamed', true, 'Other')).toBe('Other'); + expect(categoryForDownload('Renamed', true, 'Torrents')).toBe('Torrents'); + }); +}); + describe('download persistence progress snapshots', () => { it('does not write active byte counters on every progress event', () => { const persisted = redactDownloadForPersistence(item('downloading')); diff --git a/src/utils/downloads.ts b/src/utils/downloads.ts index 36b63d8..0534a73 100644 --- a/src/utils/downloads.ts +++ b/src/utils/downloads.ts @@ -417,7 +417,11 @@ export const initMediaDomains = async () => { } }; -export const categoryForFileName = (fileName: string): DownloadCategory => { +export const categoryForFileName = ( + fileName: string, + isTorrent = false +): DownloadCategory => { + if (isTorrent || fileName.trim().toLowerCase().endsWith('.torrent')) return 'Torrents'; const ext = fileName.split('.').pop()?.toLowerCase() || ''; if (['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v', 'mpeg', 'mpg', '3gp', 'ts', 'vob'].includes(ext)) return 'Movies'; if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'm4a', 'wma', 'alac', 'ape', 'mid', 'midi'].includes(ext)) return 'Musics'; @@ -428,6 +432,15 @@ export const categoryForFileName = (fileName: string): DownloadCategory => { return 'Other'; }; +export const categoryForDownload = ( + fileName: string, + isTorrent: boolean, + existingCategory?: DownloadCategory +): DownloadCategory => { + if (isTorrent && existingCategory === 'Other') return existingCategory; + return categoryForFileName(fileName, isTorrent); +}; + export const fileNameFromUrl = (rawUrl: string): string => { try { const url = new URL(rawUrl);