1+ #!/usr/bin/env python3
2+ """
3+ OptiFetch 2026 - Updated: separate audio and video download flows.
4+ Drop-in replacement for your script. Keeps settings persistence and adds
5+ distinct "Download Video" and "Download Audio" actions.
6+ """
7+ import yt_dlp
8+ import os
9+ import json
10+ import shutil
11+ import sys
12+
13+ CONFIG_FILE = "youtube_downloader_config.json"
14+ DEFAULT_SAVE_PATH = os .path .expanduser ("~/Downloads" )
15+
16+ DEFAULT_CONFIG = {
17+ "save_path" : DEFAULT_SAVE_PATH ,
18+ "resolution" : "best" ,
19+ "format" : "mp4" , # used for video downloads: mp4, mkv, webm
20+ "mp3_bitrate" : "192" # used for audio downloads
21+ }
22+
23+ current_settings = {}
24+
25+ # -------------------- Utilities --------------------
26+ def find_ffmpeg ():
27+ ffmpeg_path = shutil .which ('ffmpeg' )
28+ if ffmpeg_path :
29+ print (f"✓ ffmpeg found at: { ffmpeg_path } " )
30+ else :
31+ print ("⚠️ WARNING: ffmpeg not found in PATH!" )
32+ print (" Please install ffmpeg or add it to your system PATH" )
33+ return ffmpeg_path
34+
35+ def load_config ():
36+ if os .path .exists (CONFIG_FILE ):
37+ try :
38+ with open (CONFIG_FILE , 'r' , encoding = 'utf-8' ) as f :
39+ cfg = json .load (f )
40+ merged = DEFAULT_CONFIG .copy ()
41+ merged .update (cfg )
42+ return merged
43+ except Exception :
44+ return DEFAULT_CONFIG .copy ()
45+ return DEFAULT_CONFIG .copy ()
46+
47+ def save_config (config ):
48+ try :
49+ with open (CONFIG_FILE , 'w' , encoding = 'utf-8' ) as f :
50+ json .dump (config , f , indent = 4 )
51+ print ("✓ Configuration saved permanently!" )
52+ except Exception as e :
53+ print (f"Error saving config: { e } " )
54+
55+ def get_current_settings ():
56+ cfg = load_config ()
57+ cfg .update (current_settings )
58+ return cfg
59+
60+ def ensure_save_path (path ):
61+ try :
62+ os .makedirs (path , exist_ok = True )
63+ return True
64+ except Exception as e :
65+ print (f"Error creating save path '{ path } ': { e } " )
66+ return False
67+
68+ # -------------------- UI --------------------
69+ def display_main_menu ():
70+ print ("\n " + "=" * 50 )
71+ print (" OptiFetch 2026 - YouTube Downloader" )
72+ print ("=" * 50 )
73+ print ("1. Download Video" )
74+ print ("2. Download Audio (MP3)" )
75+ print ("3. View Current Settings" )
76+ print ("4. Change Settings (Temporary)" )
77+ print ("5. Change Settings (Permanent)" )
78+ print ("6. Reset to Default Settings" )
79+ print ("7. Exit" )
80+ print ("=" * 50 )
81+
82+ def display_settings (settings ):
83+ print ("\n " + "-" * 50 )
84+ print (" CURRENT SETTINGS" )
85+ print ("-" * 50 )
86+ print (f"Save Path: { settings ['save_path' ]} " )
87+ print (f"Resolution: { settings ['resolution' ]} " )
88+ print (f"Video Format: { settings ['format' ]} " )
89+ print (f"MP3 Bitrate: { settings .get ('mp3_bitrate' )} " )
90+ print ("-" * 50 )
91+
92+ def change_resolution ():
93+ print ("\n Resolution Options:" )
94+ resolutions = {
95+ '1' : ('best' , 'Best Quality (auto-select)' ),
96+ '2' : ('1080' , '1080p (Full HD)' ),
97+ '3' : ('720' , '720p (HD)' ),
98+ '4' : ('480' , '480p (SD)' ),
99+ '5' : ('360' , '360p (Low)' ),
100+ }
101+ for key , (_ , name ) in resolutions .items ():
102+ print (f" { key } . { name } " )
103+ choice = input ("Select resolution (1-5): " ).strip ()
104+ return resolutions .get (choice , ('best' ,))[0 ]
105+
106+ def change_video_format ():
107+ print ("\n Video Format Options:" )
108+ formats = {
109+ '1' : ('mp4' , 'MP4 (Most Compatible)' ),
110+ '2' : ('mkv' , 'MKV (Higher Quality)' ),
111+ '3' : ('webm' , 'WEBM (Web Format)' ),
112+ }
113+ for key , (_ , name ) in formats .items ():
114+ print (f" { key } . { name } " )
115+ choice = input ("Select format (1-3): " ).strip ()
116+ return formats .get (choice , ('mp4' ,))[0 ]
117+
118+ def change_save_path ():
119+ print ("\n Current save path:" , DEFAULT_SAVE_PATH )
120+ path = input ("Enter new save path (or press Enter to keep current): " ).strip ()
121+ if not path :
122+ return None
123+ if not os .path .exists (path ):
124+ create = input (f"Path doesn't exist. Create it? (y/n): " ).strip ().lower ()
125+ if create == 'y' :
126+ try :
127+ os .makedirs (path , exist_ok = True )
128+ return path
129+ except Exception as e :
130+ print (f"Error creating path: { e } " )
131+ return None
132+ else :
133+ return None
134+ return path
135+
136+ def temporary_settings_menu ():
137+ global current_settings
138+ print ("\n " + "=" * 50 )
139+ print (" TEMPORARY SETTINGS (This Session Only)" )
140+ print ("=" * 50 )
141+ while True :
142+ settings = get_current_settings ()
143+ print (f"\n 1. Resolution: { settings ['resolution' ]} " )
144+ print (f"2. Video Format: { settings ['format' ]} " )
145+ print (f"3. Save Path: { settings ['save_path' ]} " )
146+ print (f"4. MP3 Bitrate: { settings .get ('mp3_bitrate' )} " )
147+ print ("5. Back to Main Menu" )
148+ choice = input ("\n Select option to change (1-5): " ).strip ()
149+ if choice == '1' :
150+ current_settings ['resolution' ] = change_resolution ()
151+ elif choice == '2' :
152+ current_settings ['format' ] = change_video_format ()
153+ elif choice == '3' :
154+ path = change_save_path ()
155+ if path :
156+ current_settings ['save_path' ] = path
157+ elif choice == '4' :
158+ br = input ("Enter MP3 bitrate (e.g., 128, 192, 320) [192]: " ).strip () or "192"
159+ current_settings ['mp3_bitrate' ] = br
160+ elif choice == '5' :
161+ break
162+ else :
163+ print ("Invalid choice!" )
164+
165+ def permanent_settings_menu ():
166+ cfg = load_config ()
167+ print ("\n " + "=" * 50 )
168+ print (" PERMANENT SETTINGS (Saved for Future Use)" )
169+ print ("=" * 50 )
170+ while True :
171+ print (f"\n 1. Resolution: { cfg ['resolution' ]} " )
172+ print (f"2. Video Format: { cfg ['format' ]} " )
173+ print (f"3. Save Path: { cfg ['save_path' ]} " )
174+ print (f"4. MP3 Bitrate: { cfg .get ('mp3_bitrate' )} " )
175+ print ("5. Back to Main Menu" )
176+ choice = input ("\n Select option to change (1-5): " ).strip ()
177+ if choice == '1' :
178+ cfg ['resolution' ] = change_resolution ()
179+ save_config (cfg )
180+ elif choice == '2' :
181+ cfg ['format' ] = change_video_format ()
182+ save_config (cfg )
183+ elif choice == '3' :
184+ path = change_save_path ()
185+ if path :
186+ cfg ['save_path' ] = path
187+ save_config (cfg )
188+ elif choice == '4' :
189+ br = input ("Enter MP3 bitrate (e.g., 128, 192, 320) [192]: " ).strip () or "192"
190+ cfg ['mp3_bitrate' ] = br
191+ save_config (cfg )
192+ elif choice == '5' :
193+ break
194+ else :
195+ print ("Invalid choice!" )
196+
197+ # -------------------- Download helpers --------------------
198+ def build_video_format_option (resolution , format_type ):
199+ if resolution == 'best' :
200+ return f'bestvideo[ext={ format_type } ]+bestaudio[ext=m4a]/best[ext={ format_type } ]/best'
201+ else :
202+ return f'bestvideo[height<={ resolution } ][ext={ format_type } ]+bestaudio/best[height<={ resolution } ]/best'
203+
204+ def progress_hook (d ):
205+ if d .get ('status' ) == 'finished' :
206+ print ('\n ✓ Finished downloading, post-processing...' )
207+
208+ # -------------------- Separate download flows --------------------
209+ def download_video (url ):
210+ settings = get_current_settings ()
211+ if not url :
212+ print ("Invalid URL." )
213+ return
214+
215+ if not ensure_save_path (settings ['save_path' ]):
216+ return
217+
218+ # Check ffmpeg presence when needed (merging or audio extraction)
219+ ffmpeg_needed = True # yt-dlp uses ffmpeg for merging and audio extraction
220+ ffmpeg_path = find_ffmpeg ()
221+ if ffmpeg_needed and not ffmpeg_path :
222+ print ("\n ❌ Error: ffmpeg is required for merging/conversion." )
223+ print (" Please install ffmpeg or add it to your PATH." )
224+ print (" - winget install ffmpeg" )
225+ print (" - or download from: https://ffmpeg.org/download.html" )
226+ return
227+
228+ out_template = os .path .join (settings ['save_path' ], '%(title)s.%(ext)s' )
229+ ydl_opts = {
230+ 'format' : build_video_format_option (settings ['resolution' ], settings ['format' ]),
231+ 'outtmpl' : out_template ,
232+ 'merge_output_format' : settings ['format' ],
233+ 'ffmpeg_location' : ffmpeg_path ,
234+ 'noplaylist' : True ,
235+ 'progress_hooks' : [progress_hook ],
236+ 'quiet' : False ,
237+ 'no_warnings' : True ,
238+ }
239+
240+ print (f"\n 📥 Downloading VIDEO:" )
241+ print (f" Resolution: { settings ['resolution' ]} " )
242+ print (f" Container: { settings ['format' ]} " )
243+ print (f" Save Path: { settings ['save_path' ]} \n " )
244+
245+ try :
246+ with yt_dlp .YoutubeDL (ydl_opts ) as ydl :
247+ ydl .download ([url ])
248+ print ("✓ Video download complete." )
249+ except Exception as e :
250+ print (f"Error downloading video: { e } " )
251+
252+ def download_audio_mp3 (url ):
253+ settings = get_current_settings ()
254+ if not url :
255+ print ("Invalid URL." )
256+ return
257+
258+ if not ensure_save_path (settings ['save_path' ]):
259+ return
260+
261+ ffmpeg_path = find_ffmpeg ()
262+ if not ffmpeg_path :
263+ print ("\n ❌ Error: ffmpeg is required to extract/convert audio." )
264+ print (" Please install ffmpeg or add it to your PATH." )
265+ return
266+
267+ out_template = os .path .join (settings ['save_path' ], '%(title)s.%(ext)s' )
268+ bitrate = str (settings .get ('mp3_bitrate' , DEFAULT_CONFIG ['mp3_bitrate' ]))
269+
270+ ydl_opts = {
271+ 'format' : 'bestaudio/best' ,
272+ 'outtmpl' : out_template ,
273+ 'ffmpeg_location' : ffmpeg_path ,
274+ 'noplaylist' : True ,
275+ 'progress_hooks' : [progress_hook ],
276+ 'quiet' : False ,
277+ 'no_warnings' : True ,
278+ 'postprocessors' : [{
279+ 'key' : 'FFmpegExtractAudio' ,
280+ 'preferredcodec' : 'mp3' ,
281+ 'preferredquality' : bitrate
282+ }],
283+ }
284+
285+ print (f"\n 📥 Downloading AUDIO (MP3):" )
286+ print (f" Bitrate: { bitrate } kbps" )
287+ print (f" Save Path: { settings ['save_path' ]} \n " )
288+
289+ try :
290+ with yt_dlp .YoutubeDL (ydl_opts ) as ydl :
291+ ydl .download ([url ])
292+ print ("✓ Audio download complete." )
293+ except Exception as e :
294+ print (f"Error downloading audio: { e } " )
295+
296+ # -------------------- Main --------------------
297+ def main ():
298+ print (" (By Utilizing This Software, You Agree To The Terms And Conditions. \n https://github.com/yosuf-e/OptiFetch/blob/main/README.md)" )
299+ print ("\n 🎥 Welcome to OptiFetch!" )
300+
301+ while True :
302+ display_main_menu ()
303+ choice = input ("Select option (1-7): " ).strip ()
304+ if choice == '1' :
305+ url = input ("\n Enter video URL: " ).strip ()
306+ download_video (url )
307+ elif choice == '2' :
308+ url = input ("\n Enter video/audio URL (will produce MP3): " ).strip ()
309+ download_audio_mp3 (url )
310+ elif choice == '3' :
311+ display_settings (get_current_settings ())
312+ elif choice == '4' :
313+ temporary_settings_menu ()
314+ elif choice == '5' :
315+ permanent_settings_menu ()
316+ elif choice == '6' :
317+ confirm = input ("\n Reset to default settings? (y/n): " ).strip ().lower ()
318+ if confirm == 'y' :
319+ save_config (DEFAULT_CONFIG .copy ())
320+ current_settings .clear ()
321+ print ("✓ Reset to defaults!" )
322+ elif choice == '7' :
323+ print ("\n Goodbye!" )
324+ break
325+ else :
326+ print ("Invalid choice! Please try again." )
327+
328+ if __name__ == "__main__" :
329+ try :
330+ main ()
331+ except KeyboardInterrupt :
332+ print ("\n Interrupted by user. Exiting." )
333+ sys .exit (0 )
0 commit comments