From b73ab860663c9570dabae32c4410eb63579e3757 Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Tue, 26 Aug 2025 09:14:49 +0530 Subject: [PATCH] feat: added sponsorblock support and improved resume persistence --- src-tauri/src/migrations.rs | 37 ++++++++ src-tauri/tauri.conf.json | 3 + src/App.tsx | 89 +++++++++++++------ src/pages/settings.tsx | 165 ++++++++++++++++++++++++++++++++++-- src/services/database.ts | 30 +++++-- src/services/store.ts | 12 +++ src/types/download.ts | 14 +++ src/types/settings.ts | 6 ++ 8 files changed, 319 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/migrations.rs b/src-tauri/src/migrations.rs index 71faa1c..dea00a3 100644 --- a/src-tauri/src/migrations.rs +++ b/src-tauri/src/migrations.rs @@ -68,5 +68,42 @@ pub fn get_migrations() -> Vec { ); ", kind: MigrationKind::Up, + }, + Migration { + 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, }] } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3dd568c..1c77c8c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -36,6 +36,9 @@ ] }, "plugins": { + "sql": { + "preload": ["sqlite:database.db"] + }, "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNDM0I4ODcyODdGOTM4MDIKUldRQ09QbUhjb2c3UENGY1lFUVdTVWhucmJ4QzdGeW9sU3VHVFlGNWY5anZab2s4SU1rMWFsekMK", "endpoints": [ diff --git a/src/App.tsx b/src/App.tsx index 4c8a4a9..b204398 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -74,6 +74,12 @@ export default function App({ children }: { children: React.ReactNode }) { const IMPORT_COOKIES_FROM = useSettingsPageStatesStore(state => state.settings.import_cookies_from); const COOKIES_BROWSER = useSettingsPageStatesStore(state => state.settings.cookies_browser); const COOKIES_FILE = useSettingsPageStatesStore(state => state.settings.cookies_file); + const USE_SPONSORBLOCK = useSettingsPageStatesStore(state => state.settings.use_sponsorblock); + const SPONSORBLOCK_MODE = useSettingsPageStatesStore(state => state.settings.sponsorblock_mode); + const SPONSORBLOCK_REMOVE = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove); + const SPONSORBLOCK_MARK = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark); + const SPONSORBLOCK_REMOVE_CATEGORIES = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove_categories); + const SPONSORBLOCK_MARK_CATEGORIES = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark_categories); const isErrored = useDownloaderPageStatesStore((state) => state.isErrored); const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); @@ -205,6 +211,8 @@ export default function App({ children }: { children: React.ReactNode }) { if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') videoMetadata.ext = AUDIO_FORMAT; } + if (resumeState && resumeState.output_format) videoMetadata.ext = resumeState.output_format; + 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); @@ -237,53 +245,54 @@ export default function App({ children }: { children: React.ReactNode }) { args.push('--playlist-items', playlistIndex); } - if (fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) { - if (VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') { + let outputFormat = null; + if (fileType !== 'unknown' && ((VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto') || resumeState?.output_format)) { + outputFormat = resumeState?.output_format || (fileType === 'video+audio' ? VIDEO_FORMAT : (fileType === 'video' ? VIDEO_FORMAT : (fileType === 'audio' ? AUDIO_FORMAT : null))); + if ((VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') || (resumeState?.output_format && fileType === 'video+audio')) { if (ALWAYS_REENCODE_VIDEO) { - args.push('--recode-video', VIDEO_FORMAT); + args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT); } else { - args.push('--merge-output-format', VIDEO_FORMAT); + args.push('--merge-output-format', resumeState?.output_format || VIDEO_FORMAT); } } - if (VIDEO_FORMAT !== 'auto' && fileType === 'video') { + if ((VIDEO_FORMAT !== 'auto' && fileType === 'video') || (resumeState?.output_format && fileType === 'video')) { if (ALWAYS_REENCODE_VIDEO) { - args.push('--recode-video', VIDEO_FORMAT); + args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT); } else { - args.push('--remux-video', VIDEO_FORMAT); + args.push('--remux-video', resumeState?.output_format || VIDEO_FORMAT); } } - if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') { - args.push('--extract-audio', '--audio-format', AUDIO_FORMAT); + if ((AUDIO_FORMAT !== 'auto' && fileType === 'audio') || (resumeState?.output_format && fileType === 'audio')) { + args.push('--extract-audio', '--audio-format', resumeState?.output_format || AUDIO_FORMAT); } } - if (fileType !== 'unknown' && (EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) { - if (EMBED_VIDEO_METADATA && (fileType === 'video+audio' || fileType === 'video')) { + let embedMetadata = 0; + if (fileType !== 'unknown' && ((EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA) || resumeState?.embed_metadata)) { + if ((EMBED_VIDEO_METADATA || resumeState?.embed_metadata) && (fileType === 'video+audio' || fileType === 'video')) { + embedMetadata = 1; args.push('--embed-metadata'); } - if (EMBED_AUDIO_METADATA && fileType === 'audio') { + if ((EMBED_AUDIO_METADATA || resumeState?.embed_metadata) && fileType === 'audio') { + embedMetadata = 1; args.push('--embed-metadata'); } } - if (EMBED_AUDIO_THUMBNAIL && fileType === 'audio') { + let embedThumbnail = 0; + if (fileType === 'audio' && (EMBED_AUDIO_THUMBNAIL || resumeState?.embed_thumbnail)) { + embedThumbnail = 1; args.push('--embed-thumbnail'); } - - if (resumeState) { - args.push('--continue'); - } else { - args.push('--no-continue'); - } - + if (USE_PROXY && PROXY_URL) { args.push('--proxy', PROXY_URL); } - + if (USE_RATE_LIMIT && RATE_LIMIT) { args.push('--limit-rate', `${RATE_LIMIT}`); } - + if (USE_COOKIES) { if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) { args.push('--cookies-from-browser', COOKIES_BROWSER); @@ -291,6 +300,28 @@ export default function App({ children }: { children: React.ReactNode }) { args.push('--cookies', COOKIES_FILE); } } + + let sponsorblockRemove = null; + let sponsorblockMark = null; + if (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark)) { + if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) { + 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) { + sponsorblockMark = resumeState?.sponsorblock_mark || (SPONSORBLOCK_MARK === 'custom' ? ( + SPONSORBLOCK_MARK_CATEGORIES.length > 0 ? SPONSORBLOCK_MARK_CATEGORIES.join(',') : 'default' + ) : (SPONSORBLOCK_MARK)); + args.push('--sponsorblock-mark', sponsorblockMark); + } + } + + if (resumeState) { + args.push('--continue'); + } else { + args.push('--no-continue'); + } console.log('Starting download with args:', args); const command = Command.sidecar('binaries/yt-dlp', args); @@ -378,7 +409,12 @@ export default function App({ children }: { children: React.ReactNode }) { eta: currentProgress.eta || null, filepath: downloadFilePath, filetype: determineFileType(videoMetadata.vcodec, videoMetadata.acodec) || null, - filesize: videoMetadata.filesize_approx || null + filesize: videoMetadata.filesize_approx || null, + output_format: outputFormat, + embed_metadata: embedMetadata, + embed_thumbnail: embedThumbnail, + sponsorblock_remove: sponsorblockRemove, + sponsorblock_mark: sponsorblockMark }; downloadStateSaver.mutate(state, { onSuccess: (data) => { @@ -463,7 +499,12 @@ export default function App({ children }: { children: React.ReactNode }) { eta: resumeState?.eta || null, filepath: downloadFilePath, filetype: resumeState?.filetype || null, - filesize: resumeState?.filesize || null + filesize: resumeState?.filesize || null, + output_format: resumeState?.output_format || null, + embed_metadata: resumeState?.embed_metadata || 0, + embed_thumbnail: resumeState?.embed_thumbnail || 0, + sponsorblock_remove: resumeState?.sponsorblock_remove || null, + sponsorblock_mark: resumeState?.sponsorblock_mark || null } downloadStateSaver.mutate(state, { onSuccess: (data) => { diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index be1ec26..a5f84cb 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; -import { ArrowDownToLine, ArrowRight, BrushCleaning, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react"; +import { ArrowDownToLine, ArrowRight, BrushCleaning, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react"; import { cn } from "@/lib/utils"; import { useEffect } from "react"; import { useTheme } from "@/providers/themeProvider"; @@ -27,6 +27,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import * as fs from "@tauri-apps/plugin-fs"; import { join } from "@tauri-apps/api/path"; import { formatSpeed } from "@/utils"; +import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup"; const websocketPortSchema = z.object({ port: z.coerce.number({ @@ -99,6 +100,12 @@ export default function SettingsPage() { const importCookiesFrom = useSettingsPageStatesStore(state => state.settings.import_cookies_from); const cookiesBrowser = useSettingsPageStatesStore(state => state.settings.cookies_browser); const cookiesFile = useSettingsPageStatesStore(state => state.settings.cookies_file); + const useSponsorblock = useSettingsPageStatesStore(state => state.settings.use_sponsorblock); + const sponsorblockMode = useSettingsPageStatesStore(state => state.settings.sponsorblock_mode); + const sponsorblockRemove = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove); + const sponsorblockMark = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark); + const sponsorblockRemoveCategories = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove_categories); + const sponsorblockMarkCategories = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark_categories); const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port); const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort); @@ -124,6 +131,19 @@ export default function SettingsPage() { { value: 'system', icon: Monitor, label: 'System' }, ]; + const sponsorblockCategories = [ + { code: 'sponsor', label: 'Sponsorship' }, + { code: 'intro', label: 'Intro' }, + { code: 'outro', label: 'Outro' }, + { code: 'interaction', label: 'Interaction' }, + { code: 'selfpromo', label: 'Self Promotion' }, + { code: 'music_offtopic', label: 'Music Offtopic' }, + { code: 'preview', label: 'Preview' }, + { code: 'filler', label: 'Filler' }, + { code: 'poi_highlight', label: 'Point of Interest' }, + { code: 'chapter', label: 'Chapter' }, + ]; + const openLink = async (url: string, app: string | null) => { try { await invoke('open_file_with_app', { filePath: url, appName: app }).then(() => { @@ -394,9 +414,14 @@ export default function SettingsPage() { value="cookies" className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2" > Cookies + Sponsorblock
- +

Max Parallel Downloads

Set maximum number of allowed parallel downloads

@@ -442,7 +467,7 @@ export default function SettingsPage() {
- +

Theme

Choose app interface theme

@@ -465,7 +490,7 @@ export default function SettingsPage() {
- +

Download Folder

Set default download folder (directory)

@@ -525,7 +550,7 @@ export default function SettingsPage() {
- +

Video Format

Choose in which format the final video file will be saved

@@ -590,7 +615,7 @@ export default function SettingsPage() { />
- +

Embed Metadata

Wheather to embed metadata in video/audio files (info, chapters)

@@ -621,7 +646,7 @@ export default function SettingsPage() { />
- +

Proxy

Use proxy for downloads, Unblocks blocked sites in your region (download speed may affect, some sites may not work)

@@ -703,7 +728,7 @@ export default function SettingsPage() {
- +

Cookies

Use cookies to access exclusive/private (login-protected) contents from sites (use wisely, over-use can even block/ban your account)

@@ -788,6 +813,130 @@ export default function SettingsPage() {
+ + +
+ +
+

Sponsor Block

+

Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)

+
+ saveSettingsKey('use_sponsorblock', checked)} + /> + +
+ saveSettingsKey('sponsorblock_mode', value)} + disabled={!useSponsorblock} + > +
+ + +
+
+ + +
+
+
+ + saveSettingsKey('sponsorblock_remove', value)} + disabled={!useSponsorblock || sponsorblockMode !== "remove"} + > +
+ + +
+
+ + +
+
+ + +
+
+ cat.code !== 'poi_highlight' && cat.code !== 'filler').map((cat) => cat.code) : sponsorblockRemove === "all" ? sponsorblockCategories.filter((cat) => cat.code !== 'poi_highlight').map((cat) => cat.code) : []} + onValueChange={(value) => saveSettingsKey('sponsorblock_remove_categories', value)} + disabled={!useSponsorblock || sponsorblockMode !== "remove" || sponsorblockRemove !== "custom"} + > +
+ {sponsorblockCategories.map((category) => ( + category.code !== "poi_highlight" && ( + + {category.label} + + ) + ))} +
+
+
+
+ + saveSettingsKey('sponsorblock_mark', value)} + disabled={!useSponsorblock || sponsorblockMode !== "mark"} + > +
+ + +
+
+ + +
+
+ + +
+
+ cat.code) : sponsorblockMark === "all" ? sponsorblockCategories.map((cat) => cat.code) : []} + onValueChange={(value) => saveSettingsKey('sponsorblock_mark_categories', value)} + disabled={!useSponsorblock || sponsorblockMode !== "mark" || sponsorblockMark !== "custom"} + > +
+ {sponsorblockCategories.map((category) => ( + + {category.label} + + ))} +
+
+
+
diff --git a/src/services/database.ts b/src/services/database.ts index e233763..89b2edc 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -196,7 +196,12 @@ export const saveDownloadState = async (downloadState: DownloadState) => { eta = $22, filepath = $23, filetype = $24, - filesize = $25 + filesize = $25, + output_format = $26, + embed_metadata = $27, + embed_thumbnail = $28, + sponsorblock_remove = $29, + sponsorblock_mark = $30 WHERE download_id = $1`, [ downloadState.download_id, @@ -223,7 +228,12 @@ export const saveDownloadState = async (downloadState: DownloadState) => { downloadState.eta, downloadState.filepath, downloadState.filetype, - downloadState.filesize + downloadState.filesize, + downloadState.output_format, + downloadState.embed_metadata, + downloadState.embed_thumbnail, + downloadState.sponsorblock_remove, + downloadState.sponsorblock_mark ] ) } @@ -252,8 +262,13 @@ export const saveDownloadState = async (downloadState: DownloadState) => { eta, filepath, filetype, - filesize - ) 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)`, + filesize, + output_format, + embed_metadata, + embed_thumbnail, + sponsorblock_remove, + sponsorblock_mark + ) 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)`, [ downloadState.download_id, downloadState.download_status, @@ -279,7 +294,12 @@ export const saveDownloadState = async (downloadState: DownloadState) => { downloadState.eta, downloadState.filepath, downloadState.filetype, - downloadState.filesize + downloadState.filesize, + downloadState.output_format, + downloadState.embed_metadata, + downloadState.embed_thumbnail, + downloadState.sponsorblock_remove, + downloadState.sponsorblock_mark ] ) } diff --git a/src/services/store.ts b/src/services/store.ts index 68f4958..1bd12b2 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -145,6 +145,12 @@ export const useSettingsPageStatesStore = create((set) import_cookies_from: 'browser', cookies_browser: 'firefox', cookies_file: '', + use_sponsorblock: false, + sponsorblock_mode: 'remove', + sponsorblock_remove: 'default', + sponsorblock_mark: 'default', + sponsorblock_remove_categories: [], + sponsorblock_mark_categories: [], // extension settings websocket_port: 53511 }, @@ -194,6 +200,12 @@ export const useSettingsPageStatesStore = create((set) import_cookies_from: 'browser', cookies_browser: 'firefox', cookies_file: '', + use_sponsorblock: false, + sponsorblock_mode: 'remove', + sponsorblock_remove: 'default', + sponsorblock_mark: 'default', + sponsorblock_remove_categories: [], + sponsorblock_mark_categories: [], // extension settings websocket_port: 53511 }, diff --git a/src/types/download.ts b/src/types/download.ts index 5f6621e..70f7faf 100644 --- a/src/types/download.ts +++ b/src/types/download.ts @@ -37,6 +37,13 @@ export interface DownloadState { filepath: string | null; filetype: string | null; filesize: number | null; + output_format: string | null; + embed_metadata: number; + embed_thumbnail: number; + sponsorblock_remove: string | null; + sponsorblock_mark: string | null; + created_at?: string; + updated_at?: string; } export interface Download { @@ -65,6 +72,13 @@ export interface Download { filepath: string | null; filetype: string | null; filesize: number | null; + output_format: string | null; + embed_metadata: number; + embed_thumbnail: number; + sponsorblock_remove: string | null; + sponsorblock_mark: string | null; + created_at: string; + updated_at: string; } export interface DownloadProgress { diff --git a/src/types/settings.ts b/src/types/settings.ts index e03201c..6a904d5 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -26,6 +26,12 @@ export interface Settings { import_cookies_from: string; cookies_browser: string; cookies_file: string; + use_sponsorblock: boolean; + sponsorblock_mode: string; + sponsorblock_remove: string; + sponsorblock_mark: string; + sponsorblock_remove_categories: string[]; + sponsorblock_mark_categories: string[]; // extension settings websocket_port: number; } \ No newline at end of file