From 7193083b6bf25b16f3b1dba8e8b04f15be236b67 Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Mon, 6 Oct 2025 21:25:08 +0530 Subject: [PATCH] refactor: switched to yt-dlp way of path resolution, optimized database migrations and improved queuing --- package-lock.json | 28 +++++- package.json | 1 + src-tauri/src/migrations.rs | 145 ++++----------------------- src/App.tsx | 140 +++++++++++++++++--------- src/providers/appContextProvider.tsx | 2 +- src/services/database.ts | 22 ++-- src/services/mutations.ts | 6 +- src/types/download.ts | 4 +- 8 files changed, 157 insertions(+), 191 deletions(-) diff --git a/package-lock.json b/package-lock.json index 033b891..0c2e64b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "recharts": "^3.1.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", + "ulid": "^3.0.1", "vaul": "^1.1.2", "zod": "^4.1.0", "zustand": "^5.0.8" @@ -141,6 +142,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2948,6 +2950,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.85.5" }, @@ -3396,6 +3399,7 @@ "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -3406,6 +3410,7 @@ "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3416,6 +3421,7 @@ "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -3479,6 +3485,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3771,7 +3778,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4371,6 +4379,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4412,6 +4421,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4442,6 +4452,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -4454,6 +4465,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -4477,6 +4489,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -4653,7 +4666,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4864,6 +4878,15 @@ "node": ">=14.17" } }, + "node_modules/ulid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.1.tgz", + "integrity": "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q==", + "license": "MIT", + "bin": { + "ulid": "dist/cli.js" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -4995,6 +5018,7 @@ "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index 34f284d..eb0a9be 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "recharts": "^3.1.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", + "ulid": "^3.0.1", "vaul": "^1.1.2", "zod": "^4.1.0", "zustand": "^5.0.8" diff --git a/src-tauri/src/migrations.rs b/src-tauri/src/migrations.rs index 1e5ccbf..0236d97 100644 --- a/src-tauri/src/migrations.rs +++ b/src-tauri/src/migrations.rs @@ -73,119 +73,7 @@ pub fn get_migrations() -> Vec { version: 2, description: "add_columns_to_downloads", sql: " - ALTER TABLE downloads ADD COLUMN output_format TEXT; - ALTER TABLE downloads ADD COLUMN embed_metadata INTEGER NOT NULL DEFAULT 0; - ALTER TABLE downloads ADD COLUMN embed_thumbnail INTEGER NOT NULL DEFAULT 0; - ALTER TABLE downloads ADD COLUMN sponsorblock_remove TEXT; - ALTER TABLE downloads ADD COLUMN sponsorblock_mark TEXT; - ALTER TABLE downloads ADD COLUMN created_at TEXT; - ALTER TABLE downloads ADD COLUMN updated_at TEXT; - - -- Update existing rows with current timestamp - UPDATE downloads SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL; - UPDATE downloads SET updated_at = CURRENT_TIMESTAMP WHERE updated_at IS NULL; - - CREATE TRIGGER IF NOT EXISTS update_downloads_updated_at - AFTER UPDATE ON downloads - FOR EACH ROW - BEGIN - UPDATE downloads SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; - END; - - -- Create trigger for new inserts to set created_at and updated_at - CREATE TRIGGER IF NOT EXISTS set_downloads_timestamps - AFTER INSERT ON downloads - FOR EACH ROW - WHEN NEW.created_at IS NULL OR NEW.updated_at IS NULL - BEGIN - UPDATE downloads - SET created_at = COALESCE(NEW.created_at, CURRENT_TIMESTAMP), - updated_at = COALESCE(NEW.updated_at, CURRENT_TIMESTAMP) - WHERE id = NEW.id; - END; - ", - kind: MigrationKind::Up, - }, - Migration { - version: 3, - description: "add_use_aria2_column_to_downloads_with_proper_position", - sql: " - -- Create temporary table with the new column in the correct position - CREATE TABLE downloads_temp ( - id INTEGER PRIMARY KEY NOT NULL, - download_id TEXT UNIQUE NOT NULL, - download_status TEXT NOT NULL, - video_id TEXT NOT NULL, - format_id TEXT NOT NULL, - subtitle_id TEXT, - queue_index INTEGER, - playlist_id TEXT, - playlist_index INTEGER, - resolution TEXT, - ext TEXT, - abr REAL, - vbr REAL, - acodec TEXT, - vcodec TEXT, - dynamic_range TEXT, - process_id INTEGER, - status TEXT, - progress REAL, - total INTEGER, - downloaded INTEGER, - speed REAL, - eta INTEGER, - filepath TEXT, - filetype TEXT, - filesize INTEGER, - output_format TEXT, - embed_metadata INTEGER NOT NULL DEFAULT 0, - embed_thumbnail INTEGER NOT NULL DEFAULT 0, - sponsorblock_remove TEXT, - sponsorblock_mark TEXT, - use_aria2 INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (video_id) REFERENCES video_info (video_id), - FOREIGN KEY (playlist_id) REFERENCES playlist_info (playlist_id) - ); - - -- Copy all data from original table to temporary table - INSERT INTO downloads_temp SELECT - id, download_id, download_status, video_id, format_id, subtitle_id, - queue_index, playlist_id, playlist_index, resolution, ext, abr, vbr, - acodec, vcodec, dynamic_range, process_id, status, progress, total, - downloaded, speed, eta, filepath, filetype, filesize, output_format, - embed_metadata, embed_thumbnail, sponsorblock_remove, sponsorblock_mark, - 0, -- use_aria2 default value - created_at, updated_at - FROM downloads; - - -- Drop existing triggers for the original table - DROP TRIGGER IF EXISTS update_downloads_updated_at; - DROP TRIGGER IF EXISTS set_downloads_timestamps; - - -- Drop the original table - DROP TABLE downloads; - - -- Rename temporary table to original name - ALTER TABLE downloads_temp RENAME TO downloads; - - -- Create only the update trigger (as insert trigger not needed anymore) - CREATE TRIGGER IF NOT EXISTS update_downloads_updated_at - AFTER UPDATE ON downloads - FOR EACH ROW - BEGIN - UPDATE downloads SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; - END; - ", - kind: MigrationKind::Up, - }, - Migration { - version: 4, - description: "add_custom_command_column_to_downloads", - sql: " - -- Create temporary table with the new column in the correct position + -- Create temporary table with all new columns CREATE TABLE downloads_temp ( id INTEGER PRIMARY KEY NOT NULL, download_id TEXT UNIQUE NOT NULL, @@ -220,33 +108,38 @@ pub fn get_migrations() -> Vec { sponsorblock_mark TEXT, use_aria2 INTEGER NOT NULL DEFAULT 0, custom_command TEXT, + queue_config TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (video_id) REFERENCES video_info (video_id), FOREIGN KEY (playlist_id) REFERENCES playlist_info (playlist_id) ); - - -- Copy all data from original table to temporary table - INSERT INTO downloads_temp SELECT + + -- Copy all data from original table to temporary table with default values for new columns + INSERT INTO downloads_temp SELECT id, download_id, download_status, video_id, format_id, subtitle_id, queue_index, playlist_id, playlist_index, resolution, ext, abr, vbr, acodec, vcodec, dynamic_range, process_id, status, progress, total, - downloaded, speed, eta, filepath, filetype, filesize, output_format, - embed_metadata, embed_thumbnail, sponsorblock_remove, sponsorblock_mark, - use_aria2, NULL, -- custom_command default value - created_at, updated_at + downloaded, speed, eta, filepath, filetype, filesize, + NULL, -- output_format + 0, -- embed_metadata + 0, -- embed_thumbnail + NULL, -- sponsorblock_remove + NULL, -- sponsorblock_mark + 0, -- use_aria2 + NULL, -- custom_command + NULL, -- queue_config + CURRENT_TIMESTAMP, -- created_at + CURRENT_TIMESTAMP -- updated_at FROM downloads; - - -- Drop existing triggers for the original table - DROP TRIGGER IF EXISTS update_downloads_updated_at; -- Drop the original table DROP TABLE downloads; - + -- Rename temporary table to original name ALTER TABLE downloads_temp RENAME TO downloads; - - -- Re-Create the update trigger + + -- Create trigger for updating updated_at timestamp CREATE TRIGGER IF NOT EXISTS update_downloads_updated_at AFTER UPDATE ON downloads FOR EACH ROW diff --git a/src/App.tsx b/src/App.tsx index f8086f6..2bbca3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { arch, exeExtension } from "@tauri-apps/plugin-os"; import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path"; import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store"; -import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils"; +import { determineFileType, generateVideoId, isObjEmpty, parseProgressLine } from "@/utils"; import { Command } from "@tauri-apps/plugin-shell"; import { RawVideoInfo } from "@/types/video"; import { useDeleteDownloadState, useSaveDownloadState, useSavePlaylistInfo, useSaveVideoInfo, useUpdateDownloadFilePath, useUpdateDownloadStatus } from "@/services/mutations"; @@ -27,7 +27,8 @@ import useAppUpdater from "@/helpers/use-app-updater"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { toast } from "sonner"; import { useLogger } from "@/helpers/use-logger"; -import { DownloadConfiguration } from "./types/settings"; +import { DownloadConfiguration } from "@/types/settings"; +import { ulid } from "ulid"; export default function App({ children }: { children: React.ReactNode }) { const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates(); @@ -45,7 +46,6 @@ export default function App({ children }: { children: React.ReactNode }) { const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid); - // const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings); const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings); const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey); const appVersion = useSettingsPageStatesStore(state => state.appVersion); @@ -91,7 +91,6 @@ export default function App({ children }: { children: React.ReactNode }) { const isErrored = useDownloaderPageStatesStore((state) => state.isErrored); const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId); - const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration); const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored); const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected); const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId); @@ -122,7 +121,7 @@ export default function App({ children }: { children: React.ReactNode }) { const hasRunYtDlpAutoUpdateRef = useRef(false); const isRegisteredToMacOsRef = useRef(false); - const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState): Promise => { + const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration): Promise => { try { const args = [url, '--dump-single-json', '--no-warnings']; if (formatId) args.push('-f', formatId); @@ -132,6 +131,17 @@ export default function App({ children }: { children: React.ReactNode }) { if (STRICT_DOWNLOADABILITY_CHECK && !formatId) args.push('--check-all-formats'); if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-formats'); + if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfig?.custom_command) || resumeState?.custom_command) { + let customCommandArgs = null; + if (resumeState?.custom_command) { + customCommandArgs = resumeState.custom_command; + } else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig?.custom_command)) { + let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig?.custom_command); + customCommandArgs = customCommand ? customCommand.args : ''; + } + if (customCommandArgs && customCommandArgs.trim() !== '') args.push(...customCommandArgs.split(' ')); + } + if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL); if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) { if (FORCE_INTERNET_PROTOCOL === 'ipv4') { @@ -146,6 +156,19 @@ export default function App({ children }: { children: React.ReactNode }) { } else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) { args.push('--cookies', COOKIES_FILE); } + } + if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark))) { + if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) { + let sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? ( + SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default' + ) : (SPONSORBLOCK_REMOVE)); + args.push('--sponsorblock-remove', sponsorblockRemove); + } else if (SPONSORBLOCK_MODE === 'mark' || resumeState?.sponsorblock_mark) { + let sponsorblockMark = resumeState?.sponsorblock_mark || (SPONSORBLOCK_MARK === 'custom' ? ( + SPONSORBLOCK_MARK_CATEGORIES.length > 0 ? SPONSORBLOCK_MARK_CATEGORIES.join(',') : 'default' + ) : (SPONSORBLOCK_MARK)); + args.push('--sponsorblock-mark', sponsorblockMark); + } }; const command = Command.sidecar('binaries/yt-dlp', args); @@ -246,20 +269,27 @@ export default function App({ children }: { children: React.ReactNode }) { const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain); const playlistId = isPlaylist ? (resumeState?.playlist_id || generateVideoId(videoMetadata.playlist_id, videoMetadata.webpage_url_domain)) : null; - const downloadId = resumeState?.download_id || generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain); - const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`); - const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`); - let downloadFilePath = resumeState?.filepath || await join(downloadDirPath, sanitizeFilename(`${videoMetadata.title}_${videoMetadata.resolution || 'unknown'}[${videoMetadata.id}].${videoMetadata.ext}`)); + const downloadId = resumeState?.download_id || ulid() /*generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain)*/; + // const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`); + // const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`); + // let downloadFilePath = resumeState?.filepath || await join(downloadDirPath, sanitizeFilename(`${videoMetadata.title}_${videoMetadata.resolution || 'unknown'}[${videoMetadata.id}].${videoMetadata.ext}`)); + let downloadFilePath: string | null = null; let processPid: number | null = null; const args = [ url, '--newline', '--progress-template', 'status:%(progress.status)s,progress:%(progress._percent_str)s,speed:%(progress.speed)f,downloaded:%(progress.downloaded_bytes)d,total:%(progress.total_bytes)d,eta:%(progress.eta)d', + '--paths', + `temp:${tempDownloadDirPath}`, + '--paths', + `home:${downloadDirPath}`, '--output', - tempDownloadPathForYtdlp, - // '--ffmpeg-location', - // ffmpegPath, + `%(title)s_%(resolution|unknown)s[${downloadId}].%(ext)s`, + '--windows-filenames', + '--restrict-filenames', + '--exec', + 'after_move:echo Finalpath: {}', '-f', selectedFormat, '--no-mtime', @@ -277,11 +307,11 @@ export default function App({ children }: { children: React.ReactNode }) { } let customCommandArgs = null; - if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfiguration.custom_command) || resumeState?.custom_command) { + if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfig.custom_command) || resumeState?.custom_command) { if (resumeState?.custom_command) { customCommandArgs = resumeState.custom_command; - } else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfiguration.custom_command)) { - let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfiguration.custom_command); + } else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig.custom_command)) { + let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig.custom_command); customCommandArgs = customCommand ? customCommand.args : ''; } @@ -326,10 +356,10 @@ export default function App({ children }: { children: React.ReactNode }) { } let embedMetadata = 0; - if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) { - const shouldEmbedForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfiguration.embed_metadata === null)); - const shouldEmbedForAudio = fileType === 'audio' && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfiguration.embed_metadata === null)); - const shouldEmbedForUnknown = fileType === 'unknown' && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata); + if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) { + const shouldEmbedForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfig.embed_metadata === null)); + const shouldEmbedForAudio = fileType === 'audio' && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfig.embed_metadata === null)); + const shouldEmbedForUnknown = fileType === 'unknown' && (downloadConfig.embed_metadata || resumeState?.embed_metadata); if (shouldEmbedForUnknown || shouldEmbedForVideo || shouldEmbedForAudio) { embedMetadata = 1; @@ -338,7 +368,7 @@ export default function App({ children }: { children: React.ReactNode }) { } let embedThumbnail = 0; - if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfiguration.embed_thumbnail || resumeState?.embed_thumbnail || (fileType === 'audio' && EMBED_AUDIO_THUMBNAIL && downloadConfiguration.embed_thumbnail === null))) { + if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (fileType === 'audio' && EMBED_AUDIO_THUMBNAIL && downloadConfig.embed_thumbnail === null))) { embedThumbnail = 1; args.push('--embed-thumbnail'); } @@ -412,32 +442,7 @@ export default function App({ children }: { children: React.ReactNode }) { setErroredDownloadId(downloadId); } } else { - if (await fs.exists(tempDownloadPath)) { - downloadFilePath = await generateSafeFilePath(downloadFilePath); - LOG.info('NEODLP', `yt-dlp download completed with id: ${downloadId}, moving downloaded file from: "${tempDownloadPath}" to final destination: "${downloadFilePath}"`); - await fs.copyFile(tempDownloadPath, downloadFilePath); - await fs.remove(tempDownloadPath); - } - - downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath }, { - onSuccess: (data) => { - console.log("Download filepath updated successfully:", data); - queryClient.invalidateQueries({ queryKey: ['download-states'] }); - }, - onError: (error) => { - console.error("Failed to update download filepath:", error); - } - }) - - downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, { - onSuccess: (data) => { - console.log("Download status updated successfully:", data); - queryClient.invalidateQueries({ queryKey: ['download-states'] }); - }, - onError: (error) => { - console.error("Failed to update download status:", error); - } - }) + LOG.info(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code}`); } }); @@ -499,6 +504,7 @@ export default function App({ children }: { children: React.ReactNode }) { sponsorblock_mark: sponsorblockMark, use_aria2: useAria2, custom_command: customCommandArgs, + queue_config: null }; downloadStateSaver.mutate(state, { onSuccess: (data) => { @@ -512,6 +518,36 @@ export default function App({ children }: { children: React.ReactNode }) { } else { console.log(line); if (line.trim() !== '') LOG.info(`YT-DLP Download ${downloadId}`, line); + + if (line.startsWith('Finalpath: ')) { + downloadFilePath = line.replace('Finalpath: ', '').trim().replace(/^"|"$/g, ''); + const downloadedFileExt = downloadFilePath.split('.').pop(); + + // Update completion status after a short delay to ensure database states are propagated correctly + console.log(`Download completed with ID: ${downloadId}, updating filepath and status after 1s delay...`); + setTimeout(() => { + LOG.info('NEODLP', `yt-dlp download completed with id: ${downloadId}`); + downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath as string, ext: downloadedFileExt as string }, { + onSuccess: (data) => { + console.log("Download filepath updated successfully:", data); + queryClient.invalidateQueries({ queryKey: ['download-states'] }); + }, + onError: (error) => { + console.error("Failed to update download filepath:", error); + } + }); + + downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, { + onSuccess: (data) => { + console.log("Download status updated successfully:", data); + queryClient.invalidateQueries({ queryKey: ['download-states'] }); + }, + onError: (error) => { + console.error("Failed to update download status:", error); + } + }); + }, 1000); + } } }); @@ -591,7 +627,8 @@ export default function App({ children }: { children: React.ReactNode }) { sponsorblock_remove: resumeState?.sponsorblock_remove || null, sponsorblock_mark: resumeState?.sponsorblock_mark || null, use_aria2: resumeState?.use_aria2 || 0, - custom_command: resumeState?.custom_command || null + custom_command: resumeState?.custom_command || null, + queue_config: resumeState?.queue_config || ((!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) ? null : JSON.stringify(downloadConfig)) } downloadStateSaver.mutate(state, { onSuccess: (data) => { @@ -683,7 +720,7 @@ export default function App({ children }: { children: React.ReactNode }) { await startDownload( downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url, downloadState.format_id, - { + downloadState.queue_config ? JSON.parse(downloadState.queue_config) : { output_format: null, embed_metadata: null, embed_thumbnail: null, @@ -785,7 +822,12 @@ export default function App({ children }: { children: React.ReactNode }) { await startDownload( downloadToStart.url, downloadToStart.format_id, - downloadConfiguration, + downloadToStart.queue_config ? JSON.parse(downloadToStart.queue_config) : { + output_format: null, + embed_metadata: null, + embed_thumbnail: null, + custom_command: null + }, downloadToStart.subtitle_id, downloadToStart ); diff --git a/src/providers/appContextProvider.tsx b/src/providers/appContextProvider.tsx index 0f239dc..b825c44 100644 --- a/src/providers/appContextProvider.tsx +++ b/src/providers/appContextProvider.tsx @@ -4,7 +4,7 @@ import { RawVideoInfo } from '@/types/video'; import { createContext, useContext } from 'react'; interface AppContextType { - fetchVideoMetadata: (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState) => Promise; + fetchVideoMetadata: (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration) => Promise; startDownload: (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise; pauseDownload: (state: DownloadState) => Promise; resumeDownload: (state: DownloadState) => Promise; diff --git a/src/services/database.ts b/src/services/database.ts index 7d202e2..5c09f57 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -203,7 +203,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => { sponsorblock_remove = $29, sponsorblock_mark = $30, use_aria2 = $31, - custom_command = $32 + custom_command = $32, + queue_config = $33 WHERE download_id = $1`, [ downloadState.download_id, @@ -237,7 +238,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => { downloadState.sponsorblock_remove, downloadState.sponsorblock_mark, downloadState.use_aria2, - downloadState.custom_command + downloadState.custom_command, + downloadState.queue_config ] ) } @@ -273,8 +275,9 @@ export const saveDownloadState = async (downloadState: DownloadState) => { sponsorblock_remove, sponsorblock_mark, use_aria2, - custom_command - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)`, + custom_command, + queue_config + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33)`, [ downloadState.download_id, downloadState.download_status, @@ -307,7 +310,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => { downloadState.sponsorblock_remove, downloadState.sponsorblock_mark, downloadState.use_aria2, - downloadState.custom_command + downloadState.custom_command, + downloadState.queue_config ] ) } @@ -320,11 +324,11 @@ export const updateDownloadStatus = async (download_id: string, download_status: ) } -export const updateDownloadFilePath = async (download_id: string, filepath: string) => { +export const updateDownloadFilePath = async (download_id: string, filepath: string, ext: string) => { const db = await Database.load('sqlite:database.db') return await db.execute( - 'UPDATE downloads SET filepath = $2 WHERE download_id = $1', - [download_id, filepath] + 'UPDATE downloads SET filepath = $2, ext = $3 WHERE download_id = $1', + [download_id, filepath, ext] ) } @@ -451,4 +455,4 @@ export const deleteKvPair = async (key: string) => { 'DELETE FROM kv_store WHERE key = $1', [key] ) -} \ No newline at end of file +} diff --git a/src/services/mutations.ts b/src/services/mutations.ts index 9283198..f587eb8 100644 --- a/src/services/mutations.ts +++ b/src/services/mutations.ts @@ -31,8 +31,8 @@ export function useUpdateDownloadStatus() { export function useUpdateDownloadFilePath() { return useMutation({ - mutationFn: (data: { download_id: string; filepath: string }) => - updateDownloadFilePath(data.download_id, data.filepath) + mutationFn: (data: { download_id: string; filepath: string, ext: string }) => + updateDownloadFilePath(data.download_id, data.filepath, data.ext) }) } @@ -64,4 +64,4 @@ export function useDeleteKvPair() { return useMutation({ mutationFn: (key: string) => deleteKvPair(key) }) -} \ No newline at end of file +} diff --git a/src/types/download.ts b/src/types/download.ts index e51fdce..c4a3327 100644 --- a/src/types/download.ts +++ b/src/types/download.ts @@ -44,6 +44,7 @@ export interface DownloadState { sponsorblock_mark: string | null; use_aria2: number; custom_command: string | null; + queue_config: string | null; created_at?: string; updated_at?: string; } @@ -81,6 +82,7 @@ export interface Download { sponsorblock_mark: string | null; use_aria2: number; custom_command: string | null; + queue_config: string | null; created_at: string; updated_at: string; } @@ -92,4 +94,4 @@ export interface DownloadProgress { downloaded: number | null; total: number | null; eta: number | null; -} \ No newline at end of file +}