This repository has been archived by the owner on Sep 14, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 119
/
music.py
639 lines (545 loc) · 24.5 KB
/
music.py
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
import discord
import asyncio
import random
import youtube_dl
import string
import os
from discord.ext import commands
from googleapiclient.discovery import build
from discord.ext.commands import command
# import pymongo
# NOTE: Import pymongo if you are using the database function commands
# NOTE: Also add `pymongo` and `dnspython` inside the requirements.txt file if you are using pymongo
# TODO: CREATE PLAYLIST SUPPORT FOR MUSIC
# NOTE: Without database, the music bot will not save your volume
# flat-playlist:True?
# extract_flat:True
# audioquality 0 best 9 worst
# format bestaudio/best or worstaudio
# 'noplaylist': None
ytdl_format_options = {
'audioquality': 5,
'format': 'bestaudio',
'outtmpl': '{}',
'restrictfilenames': True,
'flatplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': True,
'logtostderr': False,
"extractaudio": True,
"audioformat": "opus",
'quiet': True,
'no_warnings': True,
'default_search': 'auto',
# bind to ipv4 since ipv6 addresses cause issues sometimes
'source_address': '0.0.0.0'
}
# Download youtube-dl options
ytdl_download_format_options = {
'format': 'bestaudio/best',
'outtmpl': 'downloads/%(title)s.mp3',
'reactrictfilenames': True,
'noplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': False,
'logtostderr': False,
'quiet': True,
'no_warnings': True,
'default_search': 'auto',
# bind to ipv4 since ipv6 addreacses cause issues sometimes
'source_addreacs': '0.0.0.0',
'output': r'youtube-dl',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320',
}]
}
stim = {
'default_search': 'auto',
"ignoreerrors": True,
'quiet': True,
"no_warnings": True,
"simulate": True, # do not keep the video files
"nooverwrites": True,
"keepvideo": False,
"noplaylist": True,
"skip_download": False,
# bind to ipv4 since ipv6 addresses cause issues sometimes
'source_address': '0.0.0.0'
}
ffmpeg_options = {
'options': '-vn',
# 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5'
}
class Downloader(discord.PCMVolumeTransformer):
def __init__(self, source, *, data, volume=0.5):
super().__init__(source, volume)
self.data = data
self.title = data.get('title')
self.url = data.get("url")
self.thumbnail = data.get('thumbnail')
self.duration = data.get('duration')
self.views = data.get('view_count')
self.playlist = {}
@classmethod
async def video_url(cls, url, ytdl, *, loop=None, stream=False):
"""
Download the song file and data
"""
loop = loop or asyncio.get_event_loop()
data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))
song_list = {'queue': []}
if 'entries' in data:
if len(data['entries']) > 1:
playlist_titles = [title['title'] for title in data['entries']]
song_list = {'queue': playlist_titles}
song_list['queue'].pop(0)
data = data['entries'][0]
filename = data['url'] if stream else ytdl.prepare_filename(data)
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data), song_list
async def get_info(self, url):
"""
Get the info of the next song by not downloading the actual file but just the data of song/query
"""
yt = youtube_dl.YoutubeDL(stim)
down = yt.extract_info(url, download=False)
data1 = {'queue': []}
if 'entries' in down:
if len(down['entries']) > 1:
playlist_titles = [title['title'] for title in down['entries']]
data1 = {'title': down['title'], 'queue': playlist_titles}
down = down['entries'][0]['title']
return down, data1
class MusicPlayer(commands.Cog, name='Music'):
def __init__(self, bot):
self.bot = bot
# self.music=self.database.find_one('music')
self.player = {
"audio_files": []
}
# self.database_setup()
def database_setup(self):
URL = os.getenv("MONGO")
if URL is None:
return False
@property
def random_color(self):
return discord.Color.from_rgb(random.randint(1, 255), random.randint(1, 255), random.randint(1, 255))
async def yt_info(self, song):
"""
Get info from youtube
"""
API_KEY = 'API_KEY'
youtube = build('youtube', 'v3', developerKey=API_KEY)
song_data = youtube.search().list(part='snippet').execute()
return song_data[0]
@commands.Cog.listener('on_voice_state_update')
async def music_voice(self, user, before, after):
"""
Clear the server's playlist after bot leave the voice channel
"""
if after.channel is None and user.id == self.bot.user.id:
try:
self.player[user.guild.id]['queue'].clear()
except KeyError:
# NOTE: server ID not in bot's local self.player dict
# Server ID lost or was not in data before disconnecting
print(f"Failed to get guild id {user.guild.id}")
async def filename_generator(self):
"""
Generate a unique file name for the song file to be named as
"""
chars = list(string.ascii_letters+string.digits)
name = ''
for i in range(random.randint(9, 25)):
name += random.choice(chars)
if name not in self.player['audio_files']:
return name
return await self.filename_generator()
async def playlist(self, data, msg):
"""
THIS FUNCTION IS FOR WHEN YOUTUBE LINK IS A PLAYLIST
Add song into the server's playlist inside the self.player dict
"""
for i in data['queue']:
print(i)
self.player[msg.guild.id]['queue'].append(
{'title': i, 'author': msg})
async def queue(self, msg, song):
"""
Add the query/song to the queue of the server
"""
title1 = await Downloader.get_info(self, url=song)
title = title1[0]
data = title1[1]
# NOTE:needs fix here
if data['queue']:
await self.playlist(data, msg)
# NOTE: needs to be embeded to make it better output
return await msg.send(f"Added playlist {data['title']} to queue")
self.player[msg.guild.id]['queue'].append(
{'title': title, 'author': msg})
return await msg.send(f"**{title} added to queue**".title())
async def voice_check(self, msg):
"""
function used to make bot leave voice channel if music not being played for longer than 2 minutes
"""
if msg.voice_client is not None:
await asyncio.sleep(120)
if msg.voice_client is not None and msg.voice_client.is_playing() is False and msg.voice_client.is_paused() is False:
await msg.voice_client.disconnect()
async def clear_data(self, msg):
"""
Clear the local dict data
name - remove file name from dict
remove file and filename from directory
remove filename from global audio file names
"""
name = self.player[msg.guild.id]['name']
os.remove(name)
self.player['audio_files'].remove(name)
async def loop_song(self, msg):
"""
Loop the currently playing song by replaying the same audio file via `discord.PCMVolumeTransformer()`
"""
source = discord.PCMVolumeTransformer(
discord.FFmpegPCMAudio(self.player[msg.guild.id]['name']))
loop = asyncio.get_event_loop()
try:
msg.voice_client.play(
source, after=lambda a: loop.create_task(self.done(msg)))
msg.voice_client.source.volume = self.player[msg.guild.id]['volume']
# if str(msg.guild.id) in self.music:
# msg.voice_client.source.volume=self.music['vol']/100
except Exception as Error:
# Has no attribute play
print(Error) # NOTE: output back the error for later debugging
async def done(self, msg, msgId: int = None):
"""
Function to run once song completes
Delete the "Now playing" message via ID
"""
if msgId:
try:
message = await msg.channel.fetch_message(msgId)
await message.delete()
except Exception as Error:
print("Failed to get the message")
if self.player[msg.guild.id]['reset'] is True:
self.player[msg.guild.id]['reset'] = False
return await self.loop_song(msg)
if msg.guild.id in self.player and self.player[msg.guild.id]['repeat'] is True:
return await self.loop_song(msg)
await self.clear_data(msg)
if self.player[msg.guild.id]['queue']:
queue_data = self.player[msg.guild.id]['queue'].pop(0)
return await self.start_song(msg=queue_data['author'], song=queue_data['title'])
else:
await self.voice_check(msg)
async def start_song(self, msg, song):
new_opts = ytdl_format_options.copy()
audio_name = await self.filename_generator()
self.player['audio_files'].append(audio_name)
new_opts['outtmpl'] = new_opts['outtmpl'].format(audio_name)
ytdl = youtube_dl.YoutubeDL(new_opts)
download1 = await Downloader.video_url(song, ytdl=ytdl, loop=self.bot.loop)
download = download1[0]
data = download1[1]
self.player[msg.guild.id]['name'] = audio_name
emb = discord.Embed(colour=self.random_color, title='Now Playing',
description=download.title, url=download.url)
emb.set_thumbnail(url=download.thumbnail)
emb.set_footer(
text=f'Requested by {msg.author.display_name}', icon_url=msg.author.avatar_url)
loop = asyncio.get_event_loop()
if data['queue']:
await self.playlist(data, msg)
msgId = await msg.send(embed=emb)
self.player[msg.guild.id]['player'] = download
self.player[msg.guild.id]['author'] = msg
msg.voice_client.play(
download, after=lambda a: loop.create_task(self.done(msg, msgId.id)))
# if str(msg.guild.id) in self.music: #NOTE adds user's default volume if in database
# msg.voice_client.source.volume=self.music[str(msg.guild.id)]['vol']/100
msg.voice_client.source.volume = self.player[msg.guild.id]['volume']
return msg.voice_client
@command()
async def play(self, msg, *, song):
"""
Play a song with given url or title from Youtube
`Ex:` s.play Titanium David Guetta
`Command:` play(song_name)
"""
if msg.guild.id in self.player:
if msg.voice_client.is_playing() is True: # NOTE: SONG CURRENTLY PLAYING
return await self.queue(msg, song)
if self.player[msg.guild.id]['queue']:
return await self.queue(msg, song)
if msg.voice_client.is_playing() is False and not self.player[msg.guild.id]['queue']:
return await self.start_song(msg, song)
else:
# IMPORTANT: THE ONLY PLACE WHERE NEW `self.player[msg.guild.id]={}` IS CREATED
self.player[msg.guild.id] = {
'player': None,
'queue': [],
'author': msg,
'name': None,
"reset": False,
'repeat': False,
'volume': 0.5
}
return await self.start_song(msg, song)
@play.before_invoke
async def before_play(self, msg):
"""
Check voice_client
- User voice = None:
please join a voice channel
- bot voice == None:
joins the user's voice channel
- user and bot voice NOT SAME:
- music NOT Playing AND queue EMPTY
join user's voice channel
- items in queue:
please join the same voice channel as the bot to add song to queue
"""
if msg.author.voice is None:
return await msg.send('**Please join a voice channel to play music**'.title())
if msg.voice_client is None:
return await msg.author.voice.channel.connect()
if msg.voice_client.channel != msg.author.voice.channel:
# NOTE: Check player and queue
if msg.voice_client.is_playing() is False and not self.player[msg.guild.id]['queue']:
return await msg.voice_client.move_to(msg.author.voice.channel)
# NOTE: move bot to user's voice channel if queue does not exist
if self.player[msg.guild.id]['queue']:
# NOTE: user must join same voice channel if queue exist
return await msg.send("Please join the same voice channel as the bot to add song to queue")
@commands.has_permissions(manage_channels=True)
@command()
async def repeat(self, msg):
"""
Repeat the currently playing or turn off by using the command again
`Ex:` .repeat
`Command:` repeat()
"""
if msg.guild.id in self.player:
if msg.voice_client.is_playing() is True:
if self.player[msg.guild.id]['repeat'] is True:
self.player[msg.guild.id]['repeat'] = False
return await msg.message.add_reaction(emoji='✅')
self.player[msg.guild.id]['repeat'] = True
return await msg.message.add_reaction(emoji='✅')
return await msg.send("No audio currently playing")
return await msg.send("Bot not in voice channel or playing music")
@commands.has_permissions(manage_channels=True)
@command(aliases=['restart-loop'])
async def reset(self, msg):
"""
Restart the currently playing song from the begining
`Ex:` s.reset
`Command:` reset()
"""
if msg.voice_client is None:
return await msg.send(f"**{msg.author.display_name}, there is no audio currently playing from the bot.**")
if msg.author.voice is None or msg.author.voice.channel != msg.voice_client.channel:
return await msg.send(f"**{msg.author.display_name}, you must be in the same voice channel as the bot.**")
if self.player[msg.guild.id]['queue'] and msg.voice_client.is_playing() is False:
return await msg.send("**No audio currently playing or songs in queue**".title(), delete_after=25)
self.player[msg.guild.id]['reset'] = True
msg.voice_client.stop()
@commands.has_permissions(manage_channels=True)
@command()
async def skip(self, msg):
"""
Skip the current playing song
`Ex:` s.skip
`Command:` skip()
"""
if msg.voice_client is None:
return await msg.send("**No music currently playing**".title(), delete_after=60)
if msg.author.voice is None or msg.author.voice.channel != msg.voice_client.channel:
return await msg.send("Please join the same voice channel as the bot")
if not self.player[msg.guild.id]['queue'] and msg.voice_client.is_playing() is False:
return await msg.send("**No songs in queue to skip**".title(), delete_after=60)
self.player[msg.guild.id]['repeat'] = False
msg.voice_client.stop()
return await msg.message.add_reaction(emoji='✅')
@commands.has_permissions(manage_channels=True)
@command()
async def stop(self, msg):
"""
Stop the current playing songs and clear the queue
`Ex:` s.stop
`Command:` stop()
"""
if msg.voice_client is None:
return await msg.send("Bot is not connect to a voice channel")
if msg.author.voice is None:
return await msg.send("You must be in the same voice channel as the bot")
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_playing() is True or self.player[msg.guild.id]['queue']:
self.player[msg.guild.id]['queue'].clear()
self.player[msg.guild.id]['repeat'] = False
msg.voice_client.stop()
return await msg.message.add_reaction(emoji='✅')
return await msg.send(f"**{msg.author.display_name}, there is no audio currently playing or songs in queue**")
@commands.has_permissions(manage_channels=True)
@command(aliases=['get-out', 'disconnect', 'leave-voice'])
async def leave(self, msg):
"""
Disconnect the bot from the voice channel
`Ex:` s.leave
`Command:` leave()
"""
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_playing() is True or self.player[msg.guild.id]['queue']:
self.player[msg.guild.id]['queue'].clear()
msg.voice_client.stop()
return await msg.voice_client.disconnect(), await msg.message.add_reaction(emoji='✅')
return await msg.voice_client.disconnect(), await msg.message.add_reaction(emoji='✅')
if msg.author.voice is None:
return await msg.send("You must be in the same voice channel as bot to disconnect it via command")
@commands.has_permissions(manage_channels=True)
@command()
async def pause(self, msg):
"""
Pause the currently playing audio
`Ex:` s.pause
`Command:` pause()
"""
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_paused() is True:
return await msg.send("Song is already paused")
if msg.voice_client.is_paused() is False:
msg.voice_client.pause()
await msg.message.add_reaction(emoji='✅')
@commands.has_permissions(manage_channels=True)
@command()
async def resume(self, msg):
"""
Resume the currently paused audio
`Ex:` s.resume
`Command:` resume()
"""
if msg.author.voice is not None and msg.voice_client is not None:
if msg.voice_client.is_paused() is False:
return await msg.send("Song is already playing")
if msg.voice_client.is_paused() is True:
msg.voice_client.resume()
return await msg.message.add_reaction(emoji='✅')
@command(name='queue', aliases=['song-list', 'q', 'current-songs'])
async def _queue(self, msg):
"""
Show the current songs in queue
`Ex:` s.queue
`Command:` queue()
"""
if msg.voice_client is not None:
if msg.guild.id in self.player:
if self.player[msg.guild.id]['queue']:
emb = discord.Embed(
colour=self.random_color, title='queue')
emb.set_footer(
text=f'Command used by {msg.author.name}', icon_url=msg.author.avatar_url)
for i in self.player[msg.guild.id]['queue']:
emb.add_field(
name=f"**{i['author'].author.name}**", value=i['title'], inline=False)
return await msg.send(embed=emb, delete_after=120)
return await msg.send("No songs in queue")
@command(name='song-info', aliases=['song?', 'nowplaying', 'current-song'])
async def song_info(self, msg):
"""
Show information about the current playing song
`Ex:` s.song-info
`Command:` song-into()
"""
if msg.voice_client is not None and msg.voice_client.is_playing() is True:
emb = discord.Embed(colour=self.random_color, title='Currently Playing',
description=self.player[msg.guild.id]['player'].title)
emb.set_footer(
text=f"{self.player[msg.guild.id]['author'].author.name}", icon_url=msg.author.avatar_url)
emb.set_thumbnail(
url=self.player[msg.guild.id]['player'].thumbnail)
return await msg.send(embed=emb, delete_after=120)
return await msg.send(f"**No songs currently playing**".title(), delete_after=30)
@command(aliases=['move-bot', 'move-b', 'mb', 'mbot'])
async def join(self, msg, *, channel: discord.VoiceChannel = None):
"""
Make bot join a voice channel you are in if no channel is mentioned
`Ex:` .join (If voice channel name is entered, it'll join that one)
`Command:` join(channel:optional)
"""
if msg.voice_client is not None:
return await msg.send(f"Bot is already in a voice channel\nDid you mean to use {msg.prefix}moveTo")
if msg.voice_client is None:
if channel is None:
return await msg.author.voice.channel.connect(), await msg.message.add_reaction(emoji='✅')
return await channel.connect(), await msg.message.add_reaction(emoji='✅')
else:
if msg.voice_client.is_playing() is False and not self.player[msg.guild.id]['queue']:
return await msg.author.voice.channel.connect(), await msg.message.add_reaction(emoji='✅')
@join.before_invoke
async def before_join(self, msg):
if msg.author.voice is None:
return await msg.send("You are not in a voice channel")
@join.error
async def join_error(self, msg, error):
if isinstance(error, commands.BadArgument):
return msg.send(error)
if error.args[0] == 'Command raised an exception: Exception: playing':
return await msg.send("**Please join the same voice channel as the bot to add song to queue**".title())
@commands.has_permissions(manage_channels=True)
@command(aliases=['vol'])
async def volume(self, msg, vol: int):
"""
Change the volume of the bot
`Ex:` .vol 100 (200 is the max)
`Permission:` manage_channels
`Command:` volume(amount:integer)
"""
if vol > 200:
vol = 200
vol = vol/100
if msg.author.voice is not None:
if msg.voice_client is not None:
if msg.voice_client.channel == msg.author.voice.channel and msg.voice_client.is_playing() is True:
msg.voice_client.source.volume = vol
self.player[msg.guild.id]['volume'] = vol
# if (msg.guild.id) in self.music:
# self.music[str(msg.guild.id)]['vol']=vol
return await msg.message.add_reaction(emoji='✅')
return await msg.send("**Please join the same voice channel as the bot to use the command**".title(), delete_after=30)
@commands.command(brief='Download songs', description='[prefix]download <video url or title> Downloads the song')
async def download(self, ctx, *, song):
"""
Downloads the audio from given URL source and sends the audio source back to user to download from URL, the file will be removed from storage once sent.
`Ex`: .download I'll Show you K/DA
`Command`: download(url:required)
`NOTE`: file size can't exceed 8MB, otherwise it will fail to upload and cause error
"""
try:
with youtube_dl.YoutubeDL(ytdl_download_format_options) as ydl:
if "https://www.youtube.com/" in song:
download = ydl.extract_info(song, True)
else:
infosearched = ydl.extract_info(
"ytsearch:"+song, False)
download = ydl.extract_info(
infosearched['entries'][0]['webpage_url'], True)
filename = ydl.prepare_filename(download)
embed = discord.Embed(
title="Your download is ready", description="Please wait a moment while the file is beeing uploaded")
await ctx.send(embed=embed, delete_after=30)
await ctx.send(file=discord.File(filename))
os.remove(filename)
except (youtube_dl.utils.ExtractorError, youtube_dl.utils.DownloadError):
embed = discord.Embed(title="Song couldn't be downloaded", description=("Song:"+song))
await ctx.send(embed=embed)
@volume.error
async def volume_error(self, msg,error):
if isinstance(error, commands.MissingPermissions):
return await msg.send("Manage channels or admin perms required to change volume", delete_after=30)
def setup(bot):
bot.add_cog(MusicPlayer(bot))