-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdisco-bot.py
More file actions
418 lines (353 loc) · 13.1 KB
/
Copy pathdisco-bot.py
File metadata and controls
418 lines (353 loc) · 13.1 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
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
import discord
from discord.ext import commands
import asyncio
import collections
import re
import copy
# --
# Some parts of this bot come from https://github.com/Twentysix26/Red-DiscordBot
# His bot helped us to understand some mechanics.
# Many thanks to him.
# --
try:
import youtube_dl
except:
youtube_dl = None
try:
if not discord.opus.is_loaded():
discord.opus.load_opus('libopus-0.dll')
except OSError: # Incorrect bitness
opus = False
except: # Missing opus
opus = None
else:
opus = True
description = '''A bot to stream music.'''
bot = commands.Bot(command_prefix='!', description=description)
# -----------------
# CONSTANTES
# -----------------
YT_DL_OPTIONS = {
'source_address': '0.0.0.0',
'format': 'bestaudio/best',
'extractaudio': True,
'audioformat': "mp3",
'outtmpl': '%(id)s',
'noplaylist': False,
'yesplaylist': True,
'nocheckcertificate': True,
'ignoreerrors': True,
'quiet': True,
'no_warnings': True,
'outtmpl': "data/audio/cache/%(id)s",
'default_search': 'auto'
}
# -----------------
# CLASSES
# -----------------
class Song:
def __init__(self, **kwargs):
self.__dict__ = kwargs
self.title = kwargs.pop('title', None)
self.id = kwargs.pop('id', None)
self.url = kwargs.pop('webpage_url', None)
self.duration = kwargs.pop('duration', 60)
class Deque(collections.deque):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def peek(self):
if self:
song = self.pop()
self.append(song)
return copy.deepcopy(song)
return False
# -----------------
# GLOBALS
# -----------------
my_playlist = Deque()
current_song = None
client = discord.Client()
player = None;
stop_demand = False
voice = None
current_volume = 1.0;
paused = False;
# -----------------
# ERRORS
# -----------------
error_unreadable_video = ":exclamation: Cette vidéo n'existe pas (plus) OU a été bloquée pour des raisons de droits d'auteur."
error_empty_playlist = ":exclamation: La playlist est vide."
error_not_playing = ":exclamation: Aucune musique en cours."
# -----------------
# DISCORD.PY
# -----------------
@bot.event
async def on_ready():
print('Logged in as')
print(bot.user.name)
print(bot.user.id)
print('------')
@bot.command(pass_context=True)
async def addsong(ctx, url : str):
"""Add the song specified by url into the playlist."""
if is_valid_yt_url(url):
info = await get_song_info(url)
if info:
if info['extractor_key'] == 'Youtube':
await add_song_to_playlist(info, ctx.message.channel)
elif info['extractor_key'] == 'YoutubePlaylist':
await bot.say(':recycle: Chargement de la playlist...')
print(info['entries'])
for song in info['entries']:
url = f"https://www.youtube.com/watch?v={song['url']}"
print(url)
info = await get_song_info(url)
if info:
await add_song_to_playlist(info, ctx.message.channel)
else:
await bot.say(error_unreadable_video)
await bot.say(':white_check_mark: Playlist chargée.')
else:
await bot.say(error_unreadable_video)
else:
await bot.say(':exclamation: Ceci n\'est pas un lien valide...')
@bot.command()
async def playlist():
"""Display the current content of the playlist."""
count = 0
for song in reversed(my_playlist):
song_title = song.title
song_duration = song.duration
song_url = song.url
count += 1
hms = hms_to_string(seconds_to_hms(song.duration))
await bot.say(f"{count}. {song_title} :clock10: {hms}")
if count == 0:
await bot.say(error_empty_playlist)
@bot.command(pass_context=True)
async def play(ctx):
"""Join the caller voice channel and start playing the songs in the playlist."""
global current_song
global player
global stop_demand
global voice
server = ctx.message.server
author = ctx.message.author
voice_channel = author.voice_channel
if current_song is None:
if my_playlist:
# Envoie du bot dans le channel de author
tmp_voice = await _join_voice_channel(voice_channel)
# If the bot is not already in the given voice channel
if tmp_voice != False:
voice = tmp_voice
await bot.say(':arrow_forward: Début de la lecture.')
while my_playlist and not stop_demand:
print("Lis un morceau")
current_song = my_playlist.pop()
beforeArgs = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5"
player = await voice.create_ytdl_player(current_song.url, ytdl_options=YT_DL_OPTIONS, before_options=beforeArgs)
player.volume = current_volume
player.start()
await display_song_playing_info(ctx.message.channel)
# TODO: Attente passive
while not player.is_done():
await asyncio.sleep(1) # Attend tant que la musique en cours n'est pas finie
if not stop_demand:
await bot.say(':white_check_mark: La playlist est terminée.')
stop_demand = False
current_song = None
set_paused(False)
else:
await bot.say(error_empty_playlist)
else:
await bot.say(':exclamation: Musique déjà en cours.')
@bot.command(pass_context=True)
async def peek(ctx):
"""Display info about the next song in the playlist."""
if my_playlist:
await display_next_song_info(ctx.message.channel);
else:
await bot.say(error_empty_playlist)
@bot.command(pass_context=True)
async def song(ctx):
"""Display info about the song currently playing."""
if not current_song is None:
await display_song_playing_info(ctx.message.channel)
else:
await bot.say(error_not_playing)
@bot.command()
async def pause():
"""Enable pause mode which pauses the lecture of the current song and the playlist."""
if not current_song is None:
if not paused:
set_paused(True)
player.pause();
await bot.say(":pause_button: Lecture mise en pause. (Reprendre la lecture avec !resume)")
else:
await bot.say(":exclamation: Lecture déjà en pause.")
else:
await bot.say(error_not_playing)
@bot.command()
async def resume():
"""Disable pause mode which continues the reading of the current song and the playlist."""
if not current_song is None:
if paused:
set_paused(False)
player.resume();
await bot.say(":arrow_forward: Reprise de la lecture.")
else:
await bot.say(":exclamation: La lecture est déjà en cours.")
else:
await bot.say(error_not_playing)
@bot.command()
async def next():
"""Stop the current song and play the next one in the playlist."""
if not current_song is None:
player.stop(); # Stop the current song
await bot.say(":track_next: Passage au morceau suivant.")
else:
await bot.say(error_not_playing)
@bot.command()
async def stop():
"""Stop completly the reading of the current song and the playlist."""
# Stop the player completly
global stop_demand
if not current_song is None:
stop_demand = True
my_playlist.append(current_song) # Put back on top of the playlist the song playing when stopped
player.stop();
await bot.say(":stop_button: Lecture stoppée. (Reprendre la lecture avec !play)")
else:
await bot.say(error_not_playing)
@bot.command()
async def clear():
"""Clear the playlist."""
my_playlist.clear()
await bot.say(":grey_exclamation: La playlist a été vidée.")
@bot.command()
async def dellast():
"""Remove the last song added to the playlist from it."""
del_song = my_playlist.popleft()
song_title = del_song.title
await bot.say(f":grey_exclamation: {song_title} [Retiré de la playlist]" )
@bot.command(pass_context=True)
async def volume(ctx):
"""Change volume of the bot stream."""
arg = ctx.message.content.split()
if(len(arg) > 1):
await set_volume(arg[1], ctx.message.channel)
else:
await display_volume(ctx.message.channel)
@bot.command()
async def helpbot():
"""Display help for the bot's commands"""
help = '- **!help** : Display help about the bot’s commands\n'\
'- **!addsong [YT URL]** : Add a song or songs from a playlist to the'\
'bot’s queue. Only argument is YouTube URL of your song/playlist.\n'\
'- **!playlist** : Display the current playlist of the bot.\n'\
'- **!play** : Start reading the songs in the playlist (you must be in a'\
'voice channel!)\n'\
'- **!peek** : Display information about the next song.\n'\
'- **!song** : Display information about the current song.\n'\
'- **!next** : Skip the current song and start reading the next one.\n'\
'- **!pause** : Pause the reading.\n'\
'- **!resume** : Resume the reading.\n'\
'- **!stop** : Stop completly the lecture.\n'\
'- **!clear** : Empty the playlist.\n'\
'- **!dellast** : Delete the last song added to the playlist.\n'\
'- **!volume [number]** : When called without arguments only display the'\
'current volume, when you specify a number (between 0 - 200) adjust'\
'the current volume.\n'
await bot.say(help)
# -----------------
# METHODES
# -----------------
async def display_next_song_info(channel):
"""Display information of the next song to come."""
peeked_song = my_playlist.peek()
if peeked_song:
song_title = peeked_song.title
await bot.send_message(channel, f":track_next: Prochain morceau : {song_title}")
else:
await bot.send_message(channel, ":track_next: Prochain morceau : Y'a plus rien après wesh!")
async def display_song_playing_info(channel):
"""Display info from the song playing currently and the next one to come."""
song_title = current_song.title
hms = hms_to_string(seconds_to_hms(current_song.duration))
await bot.send_message(channel, f":musical_note: Ecoute : {song_title} :clock10: {hms}")
await display_next_song_info(channel);
async def get_song_info(url):
"""Return song object with details from url video."""
yt = youtube_dl.YoutubeDL(YT_DL_OPTIONS)
return yt.extract_info(url, download=False, process=False)
def is_valid_yt_url(url):
"""Verify if url is a valid YouTube link."""
yt_link = re.compile(r'^(https?\:\/\/)?(www\.|m\.)?(youtube\.com|youtu\.?be)\/.+$') # C'est pas nous qui l'avons fait...
return yt_link.match(url)
async def _join_voice_channel(channel):
"""The bot joins the given audio channel."""
# Pour l'instant on admet un usage sur un seul voice channel à la fois
for voice in bot.voice_clients:
if voice.channel.id == channel.id:
print("Already in that voice channel")
return False
voice = await bot.join_voice_channel(channel)
print("Connection to a voice channel")
return voice
async def display_volume(channel):
"""Display the current player volume."""
await bot.send_message(channel, f":speaker: Volume à {current_volume * 100:.0f}%" )
async def set_volume(val, channel):
"""Set the new volume to val."""
global current_volume
try:
val = int(val)
except ValueError:
print("That's not an int!")
if 0 <= val <=200:
current_volume = val/100.
if player:
player.volume = current_volume
await display_volume(channel)
else:
await bot.send_message(channel, ":exclamation: Le volume doit être compris entre 0 et 200." )
async def add_song_to_playlist(info, channel):
"""Add a song into the playlist."""
if check_duration(info['duration']):
new_song = Song(**info)
my_playlist.appendleft(new_song)
title = new_song.title
hms = hms_to_string(seconds_to_hms(new_song.duration))
await bot.send_message(channel, f":notes: Nouvelle entrée : {title}. :clock10: {hms}")
else:
await bot.send_message(channel, ":exclamation: La durée d'un morceau ne doit pas excéder 10 minutes.")
def seconds_to_hms(seconds):
"""Convert given seconds into hms."""
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
return(h, m, s)
def hms_to_string(hms):
"""Convert hms into a string."""
h = ""
m = ""
s = ""
if hms[0] != 0:
h = str(hms[0]) + "h "
if hms[1] != 0:
m = str(hms[1]) + "mn "
if hms[2] != 0:
s = str(hms[2]) + "s"
return h + m + s
def check_duration(duration):
"""Check the duration given in arg."""
return duration <= 600 # 10 minutes
def get_token():
"""Get the bot token from text file."""
file = open("token.txt", "r")
return file.read()
def set_paused(bool):
"""Set the state of the pause."""
global paused
paused = bool
bot.run(get_token())