-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmelody.py
More file actions
executable file
·255 lines (221 loc) · 9.56 KB
/
melody.py
File metadata and controls
executable file
·255 lines (221 loc) · 9.56 KB
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env python3
import os
import pygame
import threading
import cmd
from yt_dlp import YoutubeDL
from ytmusicapi import YTMusic
import builtins
from termcolor import colored
import sys
import time
from typing import Optional, Literal
class MelodyCLI(cmd.Cmd):
os.system("clear")
intro = "Welcome to Melody.CLI! Type 'help' to list commands."
prompt = "(melody) "
def __init__(self):
super().__init__()
self.youtube_music = YTMusic()
self.BASE_URL = 'https://www.youtube.com/watch?v='
self.queue: list[tuple[str, str]] = []
self.currSong: str = ""
self.queue_index: int = 0
self.autoplay: bool = True
self.is_paused: bool = False
self.playback_thread: threading.Thread | None = None
self.currently_playing: str | None = None
self.filtered_results: dict[int, list[str]] = {}
def print(self, text:str, color:Optional[Literal['black', 'grey', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'light_grey', 'dark_grey', 'light_red', 'light_green', 'light_yellow', 'light_blue', 'light_magenta', 'light_cyan', 'white']]) -> None:
"""Modified print function that prints text with colors"""
builtins.print(colored(text, color))
def do_clear(self, arg:str) -> None:
"""Clears the terminal screen"""
os.system("clear")
def do_search(self, searchString:str) -> None:
"Search for a song: search <song name>"
if not searchString.strip():
self.print("Please provide a search query!", "red")
return
search_results = self.youtube_music.search(searchString)
self.filtered_results = {}
index = 1
for result in search_results:
if result.get("category") in ["Songs", "Videos"] and "videoId" in result:
self.filtered_results[index] = [result['videoId'], result['title']]
self.print(f"{index}. {result['title']} - {result.get('duration', 'Unknown Duration')}", "yellow")
index += 1
def do_play(self, arg:str) -> None:
try:
index = int(arg)
song_id = self.filtered_results[index][0]
self.currSong = self.filtered_results[index][1]
mp3_file = self.downloadSong(song_id)
if mp3_file:
self.playSong(mp3_file)
self.generate_queue(song_id)
except (KeyError, ValueError):
self.print("Invalid index or no search results available.", "red")
except Exception as e:
self.print(f"Error: {e}", "red")
def do_next(self, arg:str) -> None:
self.play_next()
def play_next(self) -> None:
if self.queue_index < len(self.queue) - 1:
self.queue_index += 1
next_song = self.queue[self.queue_index]
self.currSong = next_song[1]
self.print(f"⏭️ Now Playing: {self.currSong}", "green")
self.playSong(self.downloadSong(next_song[0]))
else:
self.print("🎵 No more songs in queue! Fetching new songs...", "yellow")
self.generate_queue(self.queue[self.queue_index][0])
self.play_next()
def do_prev(self, arg:str) -> None:
if self.queue_index > 0:
self.queue_index -= 1
prev_song = self.queue[self.queue_index]
self.currSong = prev_song[1]
self.print(f"⏮️ Replaying: {self.currSong}", "green")
self.playSong(self.downloadSong(prev_song[0]))
else:
self.print("🚫 No previous songs!", "red")
def do_pause(self, arg:str) -> None:
if pygame.mixer.music.get_busy() and not self.is_paused:
pygame.mixer.music.pause()
self.is_paused = True
self.print("Music paused ⏸️", "yellow")
def do_resume(self, arg:str) -> None:
if self.is_paused:
pygame.mixer.music.unpause()
self.is_paused = False
self.print("Music resumed ▶️", "green")
def do_bye(self, arg:str) -> None:
self.print("Shutting down Melody CLI... 👋", "yellow")
try:
if pygame.mixer.get_init():
pygame.mixer.music.stop()
pygame.mixer.quit()
pygame.quit()
self.print("Audio system closed 🔇", "magenta")
except Exception as e:
self.print(f"Error stopping audio: {e}", "red")
if self.playback_thread and self.playback_thread.is_alive():
self.print("Waiting for playback thread to close... ⏳", "yellow")
self.playback_thread.join(timeout=2)
self.print("Goodbye! 👋", "green")
sys.exit(0)
def do_autoplay(self, arg:str) -> None:
"Toggle autoplay ON/OFF"
self.autoplay = not self.autoplay
status = "ON" if self.autoplay else "OFF"
self.print(f"Autoplay is now {status} 🔁", "cyan")
def generate_queue(self, current_video_id:str) -> None:
"Fetch related songs and build a queue"
self.queue = []
self.queue_index = 0
try:
related_songs = self.youtube_music.get_watch_playlist(current_video_id).get("tracks", [])
if not related_songs:
self.print("No related songs found!", "red")
return
for track in related_songs:
self.queue.append((track["videoId"], track["title"]))
self.print(f"🎶 Queue Updated! {len(self.queue)} songs added.", "cyan")
except Exception as e:
self.print(f"Error fetching related songs: {e}", "red")
def do_queue(self, arg:str) -> None:
"Show the queue"
self.print("\n🎶 Queue", color="cyan")
for idx, songTuple in enumerate(self.queue, start=1):
self.print(f"{idx}. {songTuple[1]}", color="cyan")
def do_queueplay(self, arg:str) -> None:
"Play a specific song from the queue: queueplay <index>"
try:
index = int(arg) - 1
if 0 <= index < len(self.queue):
self.queue_index = index
song_id, title = self.queue[self.queue_index]
self.currSong = title
self.print(f"🎵 Playing from queue: {title}", "green")
mp3_file = self.downloadSong(song_id)
if mp3_file:
self.playSong(mp3_file)
else:
self.print("⚠️ Failed to play song.", "red")
else:
self.print("❌ Index out of range!", "red")
except ValueError:
self.print("❌ Please enter a valid number. Usage: queueplay <index>", "red")
except Exception as e:
self.print(f"⚠️ Error playing from queue: {e}", "red")
def do_clearqueue(self,current_video_id:str) -> None:
"Clear the queue"
self.queue = []
self.queue_index = 0
self.print("🧹 Queue has been cleared", "green")
def downloadSong(self, videoID: str) -> str:
file_path = f"temp_audio/{videoID}.opus"
if os.path.exists(file_path):
self.print(f"🎵 Using cached song: {file_path}", "green")
return file_path
url = f"{self.BASE_URL}{videoID}"
os.makedirs("temp_audio", exist_ok=True)
ydl_opts = {
'format': 'ba', # ba = best audio, typically webm container with opus audio
'outtmpl': file_path.replace(".opus", ".%(ext)s"),
'quiet': True,
'no_warnings': True,
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'opus',
}],
'http_headers': { # Add HTTP headers
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
},
}
try:
self.print(f"⬇️ Downloading: {url}", "yellow")
with YoutubeDL(ydl_opts) as ydl:
ydl.extract_info(url, download=True)
self.print(f"🎵 Downloaded track: {file_path}", "green")
return file_path
except Exception as e:
self.print(f"❌ Error downloading: {e}", "red")
return None
def playSong(self, audio_file:str) -> None:
def _play():
try:
pygame.init()
pygame.mixer.init()
pygame.mixer.music.load(audio_file)
pygame.mixer.music.play()
self.is_paused = False
self.display_now_playing()
while pygame.mixer.music.get_busy() or self.is_paused:
time.sleep(1)
if self.autoplay and self.queue_index < len(self.queue) - 1:
self.play_next()
except Exception as e:
self.print(f"Playback error: {e}", "red")
self.currently_playing = audio_file
self.playback_thread = threading.Thread(target=_play)
self.playback_thread.start()
def display_now_playing(self) -> None:
print("\n" + "="*40)
print(f"🎶 NOW PLAYING: {colored(self.currSong, 'cyan')}")
print("="*40 + "\n")
def clear_now_playing(self) -> None:
print("\n" + "="*40)
print("🎵 No song is currently playing.")
print("="*40 + "\n")
if __name__ == "__main__":
try:
MelodyCLI().cmdloop()
except KeyboardInterrupt:
print("\n💥 Keyboard Interrupt! Shutting down Melody CLI...")
pygame.mixer.music.stop()
pygame.mixer.quit()
pygame.quit()
sys.exit(0)