@@ -20,12 +20,13 @@ def __init__(self):
2020 self .currently_playing = None
2121 self .playback_thread = None
2222 self .is_paused = False
23- self .manual_stop = False
2423 self .BASE_URL = 'https://www.youtube.com/watch?v='
2524 self .currSong = ""
2625 self .autoplay = True
2726 self .downloaded_tracks = []
28-
27+ self .max_cache_size = 10
28+ self .queue = []
29+ self .queue_index = 0
2930
3031 def print (self , text , color ):
3132 builtins .print (colored (text , color ))
@@ -39,54 +40,57 @@ def do_autoplay(self, arg):
3940 status = "ON" if self .autoplay else "OFF"
4041 self .print (f"Autoplay is now { status } 🔁" , "cyan" )
4142
42-
4343 def do_search (self , searchString ):
4444 "Search for a song: search <song name>"
45+ if not searchString .strip ():
46+ self .print ("Please provide a search query!" , "red" )
47+ return
48+
4549 search_results = self .youtube_music .search (searchString )
4650 self .filtered_results = {}
4751 index = 1
4852 for result in search_results :
49- if result [ "category" ] in ["Songs" , "Videos" ]:
53+ if result . get ( "category" ) in ["Songs" , "Videos" ] and "videoId" in result :
5054 self .filtered_results [index ] = [result ['videoId' ], result ['title' ]]
51- self .print (f"{ index } . { result ['title' ]} - { result [ 'duration' ] } " , "yellow" )
55+ self .print (f"{ index } . { result ['title' ]} - { result . get ( 'duration' , 'Unknown Duration' ) } " , "yellow" )
5256 index += 1
5357
5458 def do_play (self , arg ):
5559 try :
56- song_id = self .filtered_results [int (arg )][0 ]
57- self .currSong = self .filtered_results [int (arg )][1 ]
60+ index = int (arg )
61+ song_id = self .filtered_results [index ][0 ]
62+ self .currSong = self .filtered_results [index ][1 ]
5863 mp3_file = self .downloadSong (song_id )
5964 if mp3_file :
6065 self .playSong (mp3_file )
6166 self .generate_queue (song_id )
67+ except (KeyError , ValueError ):
68+ self .print ("Invalid index or no search results available." , "red" )
6269 except Exception as e :
6370 self .print (f"Error: { e } " , "red" )
64-
71+
6572 def generate_queue (self , current_video_id ):
6673 "Fetch related songs and build a queue"
6774 self .queue = []
68- related_songs = self .youtube_music .get_watch_playlist (current_video_id )["tracks" ]
69-
70- if not related_songs :
71- self .print ("No related songs found!" , "red" )
72- return
73-
74- for track in related_songs :
75- self .queue .append ((track ["videoId" ], track ["title" ]))
76-
7775 self .queue_index = 0
78- self .print (f"🎶 Queue Updated! { len (self .queue )} songs added." , "cyan" )
76+ try :
77+ related_songs = self .youtube_music .get_watch_playlist (current_video_id ).get ("tracks" , [])
78+ if not related_songs :
79+ self .print ("No related songs found!" , "red" )
80+ return
81+ for track in related_songs :
82+ self .queue .append ((track ["videoId" ], track ["title" ]))
83+ self .print (f"🎶 Queue Updated! { len (self .queue )} songs added." , "cyan" )
84+ except Exception as e :
85+ self .print (f"Error fetching related songs: { e } " , "red" )
7986
8087 def do_queue (self , arg ):
8188 "Show the queue"
82- idx = 1
8389 self .print ("\n 🎶 Queue" , color = "cyan" )
84- for songTuple in self .queue :
90+ for idx , songTuple in enumerate ( self .queue , start = 1 ) :
8591 self .print (f"{ idx } . { songTuple [1 ]} " , color = "cyan" )
86- idx += 1
8792
8893 def play_next (self ):
89- "Play the next song in the queue"
9094 if self .queue_index < len (self .queue ) - 1 :
9195 self .queue_index += 1
9296 next_song = self .queue [self .queue_index ]
@@ -99,16 +103,9 @@ def play_next(self):
99103 self .play_next ()
100104
101105 def do_next (self , arg ):
102- "Skip to the next song in queue"
103- if self .queue_index < len (self .queue ) - 1 :
104- self .play_next ()
105- else :
106- self .print ("🎵 Queue empty! Fetching new songs..." , "yellow" )
107- self .generate_queue (self .queue [self .queue_index ][0 ]) # Get new queue
108- self .play_next ()
106+ self .play_next ()
109107
110108 def do_prev (self , arg ):
111- "Play the previous song in queue"
112109 if self .queue_index > 0 :
113110 self .queue_index -= 1
114111 prev_song = self .queue [self .queue_index ]
@@ -119,47 +116,45 @@ def do_prev(self, arg):
119116 self .print ("🚫 No previous songs!" , "red" )
120117
121118 def do_pause (self , arg ):
122- "Pause the currently playing song"
123119 if pygame .mixer .music .get_busy () and not self .is_paused :
124120 pygame .mixer .music .pause ()
125121 self .is_paused = True
126122 self .print ("Music paused ⏸️" , "yellow" )
127123
128124 def do_resume (self , arg ):
129- "Resume a paused song"
130125 if self .is_paused :
131126 pygame .mixer .music .unpause ()
132127 self .is_paused = False
133128 self .print ("Music resumed ▶️" , "green" )
134129
135-
136- def do_stop (self , arg ):
137- "Stop the currently playing song and prevent autoplay"
138- if self .currently_playing :
139- self .manual_stop = True
140- pygame .mixer .music .stop ()
141- self .currently_playing = None
142- self .is_paused = False
143- self .clear_now_playing ()
144- self .print ("Music stopped ⏹️" , "yellow" )
145-
146-
147130 def do_bye (self , arg ):
148- "Exit the application"
149- self .print ("Goodbye! 👋" , "yellow" )
131+ self .print ("Shutting down Melody CLI... 👋" , "yellow" )
132+ try :
133+ if pygame .mixer .get_init ():
134+ pygame .mixer .music .stop ()
135+ pygame .mixer .quit ()
136+ pygame .quit ()
137+ self .print ("Audio system closed 🔇" , "magenta" )
138+ except Exception as e :
139+ self .print (f"Error stopping audio: { e } " , "red" )
140+
141+ if self .playback_thread and self .playback_thread .is_alive ():
142+ self .print ("Waiting for playback thread to close... ⏳" , "yellow" )
143+ self .playback_thread .join (timeout = 2 )
144+
145+ self .print ("Goodbye! 👋" , "green" )
150146 sys .exit (0 )
151147
152148 def downloadSong (self , videoID ):
153- "Check if song exists in cache, else download it."
154149 file_path = f"temp_audio/{ videoID } .mp3"
155-
150+
156151 if os .path .exists (file_path ):
157152 self .print (f"🎵 Using cached song: { file_path } " , "green" )
158153 return file_path
159154
160155 url = f"{ self .BASE_URL } { videoID } "
161156 os .makedirs ("temp_audio" , exist_ok = True )
162-
157+
163158 ydl_opts = {
164159 'format' : 'bestaudio/best' ,
165160 'outtmpl' : file_path .replace (".mp3" , ".%(ext)s" ),
@@ -171,51 +166,66 @@ def downloadSong(self, videoID):
171166 'preferredquality' : '192' ,
172167 }],
173168 }
174-
169+
175170 try :
176171 with YoutubeDL (ydl_opts ) as ydl :
177172 ydl .extract_info (url , download = True )
173+ self .cleanup_cache ()
178174 return file_path
179175 except Exception as e :
180176 self .print (f"❌ Error downloading: { e } " , "red" )
181177 return None
182178
179+ def cleanup_cache (self ):
180+ "Keep only the latest N files in the temp_audio folder"
181+ try :
182+ files = [os .path .join ("temp_audio" , f ) for f in os .listdir ("temp_audio" ) if f .endswith (".mp3" )]
183+ files .sort (key = os .path .getmtime , reverse = True )
184+ for f in files [self .max_cache_size :]:
185+ os .remove (f )
186+ self .print (f"Deleted old cached file: { f } " , "magenta" )
187+ except Exception as e :
188+ self .print (f"Error cleaning up cache: { e } " , "red" )
189+
183190 def playSong (self , mp3_file ):
184191 def _play ():
185- pygame .init ()
186- pygame .mixer .init ()
187- pygame .mixer .music .load (mp3_file )
188- pygame .mixer .music .play ()
189- self .is_paused = False
190- self .manual_stop = False
191- self .display_now_playing ()
192-
193- while pygame .mixer .music .get_busy () or self .is_paused :
194- time .sleep (1 )
195-
196- was_manual = self .manual_stop
197-
198- self .manual_stop = False
199-
200- if not was_manual and self .autoplay and self .queue_index < len (self .queue ) - 1 :
201- self .play_next ()
192+ try :
193+ pygame .init ()
194+ pygame .mixer .init ()
195+ pygame .mixer .music .load (mp3_file )
196+ pygame .mixer .music .play ()
197+ self .is_paused = False
198+ self .display_now_playing ()
199+
200+ while pygame .mixer .music .get_busy () or self .is_paused :
201+ time .sleep (1 )
202+
203+ if self .autoplay and self .queue_index < len (self .queue ) - 1 :
204+ self .play_next ()
205+ except Exception as e :
206+ self .print (f"Playback error: { e } " , "red" )
202207
203208 self .currently_playing = mp3_file
204209 self .playback_thread = threading .Thread (target = _play )
205210 self .playback_thread .start ()
206211
207-
208212 def display_now_playing (self ):
209213 print ("\n " + "=" * 40 )
210214 print (f"🎶 NOW PLAYING: { colored (self .currSong , 'cyan' )} " )
211215 print ("=" * 40 + "\n " )
212216
213217 def clear_now_playing (self ):
214- print ("\n " + " " * 40 , end = "\r " )
215218 print ("\n " + "=" * 40 )
216219 print ("🎵 No song is currently playing." )
217220 print ("=" * 40 + "\n " )
218221
219222
220223if __name__ == "__main__" :
221- MelodyCLI ().cmdloop ()
224+ try :
225+ MelodyCLI ().cmdloop ()
226+ except KeyboardInterrupt :
227+ print ("\n 💥 Keyboard Interrupt! Shutting down Melody CLI..." )
228+ pygame .mixer .music .stop ()
229+ pygame .mixer .quit ()
230+ pygame .quit ()
231+ sys .exit (0 )
0 commit comments