KiTTY, un bot Discord qui est un petit chat :) Il est basé sur une ancienne version du bot Red, sous Python 3.6 et qui a des fonctionnalités bien sympatiques !
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

2555 lines
94KB

  1. import discord
  2. from discord.ext import commands
  3. import threading
  4. import os
  5. from random import shuffle, choice
  6. from cogs.utils.dataIO import dataIO
  7. from cogs.utils import checks
  8. from cogs.utils.chat_formatting import pagify, escape
  9. from urllib.parse import urlparse
  10. from __main__ import send_cmd_help, settings
  11. from json import JSONDecodeError
  12. import re
  13. import logging
  14. import collections
  15. import copy
  16. import asyncio
  17. import math
  18. import time
  19. import inspect
  20. import subprocess
  21. import urllib.parse
  22. import datetime
  23. from enum import Enum
  24. log = logging.getLogger("red.audio")
  25. try:
  26. import youtube_dl
  27. except:
  28. youtube_dl = None
  29. try:
  30. if not discord.opus.is_loaded():
  31. discord.opus.load_opus('libopus-0.dll')
  32. except OSError: # Incorrect bitness
  33. opus = False
  34. except: # Missing opus
  35. opus = None
  36. else:
  37. opus = True
  38. youtube_dl_options = {
  39. 'source_address': '0.0.0.0',
  40. 'format': 'best',
  41. 'extractaudio': True,
  42. 'audioformat': "mp3",
  43. 'nocheckcertificate': True,
  44. 'ignoreerrors': False,
  45. 'quiet': True,
  46. 'no_warnings': True,
  47. 'outtmpl': "data/audio/cache/%(id)s",
  48. 'default_search': 'auto',
  49. 'encoding': 'utf-8'
  50. }
  51. class MaximumLength(Exception):
  52. def __init__(self, m):
  53. self.message = m
  54. def __str__(self):
  55. return self.message
  56. class YouTubeDlError(Exception):
  57. def __init__(self, m):
  58. self.message = m
  59. def __str__(self):
  60. return self.message
  61. class NotConnected(Exception):
  62. pass
  63. class AuthorNotConnected(NotConnected):
  64. pass
  65. class VoiceNotConnected(NotConnected):
  66. pass
  67. class UnauthorizedConnect(Exception):
  68. pass
  69. class UnauthorizedSpeak(Exception):
  70. pass
  71. class ChannelUserLimit(Exception):
  72. pass
  73. class UnauthorizedSave(Exception):
  74. pass
  75. class ConnectTimeout(NotConnected):
  76. pass
  77. class InvalidURL(Exception):
  78. pass
  79. class InvalidSong(InvalidURL):
  80. pass
  81. class InvalidPlaylist(InvalidSong):
  82. pass
  83. class deque(collections.deque):
  84. def __init__(self, *args, **kwargs):
  85. super().__init__(*args, **kwargs)
  86. def peek(self):
  87. ret = self.pop()
  88. self.append(ret)
  89. return copy.deepcopy(ret)
  90. def peekleft(self):
  91. ret = self.popleft()
  92. self.appendleft(ret)
  93. return copy.deepcopy(ret)
  94. class QueueKey(Enum):
  95. REPEAT = 1
  96. PLAYLIST = 2
  97. VOICE_CHANNEL_ID = 3
  98. QUEUE = 4
  99. TEMP_QUEUE = 5
  100. NOW_PLAYING = 6
  101. NOW_PLAYING_CHANNEL = 7
  102. class Song:
  103. def __init__(self, **kwargs):
  104. self.__dict__ = kwargs
  105. self.title = kwargs.pop('title', None)
  106. self.id = kwargs.pop('id', None)
  107. self.url = kwargs.pop('url', None)
  108. self.webpage_url = kwargs.pop('webpage_url', "")
  109. self.duration = kwargs.pop('duration', 60)
  110. self.start_time = kwargs.pop('start_time', None)
  111. self.end_time = kwargs.pop('end_time', None)
  112. self.thumbnail = kwargs.pop('thumbnail', None)
  113. self.view_count = kwargs.pop('view_count', None)
  114. self.rating = kwargs.pop('average_rating', None)
  115. self.song_start_time = None
  116. class QueuedSong:
  117. def __init__(self, url, channel):
  118. self.url = url
  119. self.channel = channel
  120. class Playlist:
  121. def __init__(self, server=None, sid=None, name=None, author=None, url=None,
  122. playlist=None, path=None, main_class=None, **kwargs):
  123. # when is this used? idk
  124. # what is server when it's global? None? idk
  125. self.server = server
  126. self._sid = sid
  127. self.name = name
  128. # this is an id......
  129. self.author = author
  130. self.url = url
  131. self.main_class = main_class # reference to Audio
  132. self.path = path
  133. if url is None and "link" in kwargs:
  134. self.url = kwargs.get('link')
  135. self.playlist = playlist
  136. @property
  137. def filename(self):
  138. f = "data/audio/playlists"
  139. f = os.path.join(f, self.sid, self.name + ".txt")
  140. return f
  141. def to_json(self):
  142. ret = {"author": self.author, "playlist": self.playlist,
  143. "link": self.url}
  144. return ret
  145. def is_author(self, user):
  146. """checks if the user is the author of this playlist
  147. Returns True/False"""
  148. return user.id == self.author
  149. def can_edit(self, user):
  150. """right now checks if user is mod or higher including server owner
  151. global playlists are uneditable atm
  152. dev notes:
  153. should probably be defined elsewhere later or be dynamic"""
  154. # I don't know how global playlists are handled.
  155. # Not sure if the framework is there for them to be editable.
  156. # Don't know how they are handled by Playlist
  157. # Don't know how they are handled by Audio
  158. # so let's make sure it's not global at all.
  159. if self.main_class._playlist_exists_global(self.name):
  160. return False
  161. admin_role = settings.get_server_admin(self.server)
  162. mod_role = settings.get_server_mod(self.server)
  163. is_playlist_author = self.is_author(user)
  164. is_bot_owner = user.id == settings.owner
  165. is_server_owner = self.server.owner.id == self.author
  166. is_admin = discord.utils.get(user.roles, name=admin_role) is not None
  167. is_mod = discord.utils.get(user.roles, name=mod_role) is not None
  168. return any((is_playlist_author,
  169. is_bot_owner,
  170. is_server_owner,
  171. is_admin,
  172. is_mod))
  173. # def __del__() ?
  174. def append_song(self, author, url):
  175. if not self.can_edit(author):
  176. raise UnauthorizedSave
  177. elif not self.main_class._valid_playable_url(url):
  178. raise InvalidURL
  179. else:
  180. self.playlist.append(url)
  181. self.save()
  182. def save(self):
  183. dataIO.save_json(self.path, self.to_json())
  184. @property
  185. def sid(self):
  186. if self._sid:
  187. return self._sid
  188. elif self.server:
  189. return self.server.id
  190. else:
  191. return None
  192. class Downloader(threading.Thread):
  193. def __init__(self, url, max_duration=None, download=False,
  194. cache_path="data/audio/cache", *args, **kwargs):
  195. super().__init__(*args, **kwargs)
  196. self.url = url
  197. self.max_duration = max_duration
  198. self.done = threading.Event()
  199. self.song = None
  200. self._download = download
  201. self.hit_max_length = threading.Event()
  202. self._yt = None
  203. self.error = None
  204. def run(self):
  205. try:
  206. self.get_info()
  207. if self._download:
  208. self.download()
  209. except youtube_dl.utils.DownloadError as e:
  210. self.error = str(e)
  211. except MaximumLength:
  212. self.hit_max_length.set()
  213. except OSError as e:
  214. log.warning("An OS error occurred while downloading URL '{}':\n'{}'".format(self.url, str(e)))
  215. self.done.set()
  216. def download(self):
  217. self.duration_check()
  218. if not os.path.isfile('data/audio/cache' + self.song.id):
  219. video = self._yt.extract_info(self.url)
  220. self.song = Song(**video)
  221. def duration_check(self):
  222. log.debug("duration {} for songid {}".format(self.song.duration,
  223. self.song.id))
  224. if self.max_duration and self.song.duration > self.max_duration:
  225. log.debug("songid {} too long".format(self.song.id))
  226. raise MaximumLength("songid {} has duration {} > {}".format(
  227. self.song.id, self.song.duration, self.max_duration))
  228. def get_info(self):
  229. if self._yt is None:
  230. self._yt = youtube_dl.YoutubeDL(youtube_dl_options)
  231. if "[SEARCH:]" not in self.url:
  232. video = self._yt.extract_info(self.url, download=False,
  233. process=False)
  234. else:
  235. self.url = self.url[9:]
  236. yt_id = self._yt.extract_info(
  237. self.url, download=False)["entries"][0]["id"]
  238. # Should handle errors here ^
  239. self.url = "https://youtube.com/watch?v={}".format(yt_id)
  240. video = self._yt.extract_info(self.url, download=False,
  241. process=False)
  242. if(video is not None):
  243. self.song = Song(**video)
  244. class Audio:
  245. """Music Streaming."""
  246. def __init__(self, bot, player):
  247. self.bot = bot
  248. self.queue = {} # add deque's, repeat
  249. self.downloaders = {} # sid: object
  250. self.settings = dataIO.load_json("data/audio/settings.json")
  251. self.settings_path = "data/audio/settings.json"
  252. self.server_specific_setting_keys = ["VOLUME", "VOTE_ENABLED",
  253. "VOTE_THRESHOLD", "NOPPL_DISCONNECT",
  254. "NOTIFY", "NOTIFY_CHANNEL", "TIMER_DISCONNECT"]
  255. self.cache_path = "data/audio/cache"
  256. self.local_playlist_path = "data/audio/localtracks"
  257. self._old_game = False
  258. self.skip_votes = {}
  259. self.connect_timers = {}
  260. if player == "ffmpeg":
  261. self.settings["AVCONV"] = False
  262. elif player == "avconv":
  263. self.settings["AVCONV"] = True
  264. self.save_settings()
  265. async def _add_song_status(self, song):
  266. if self._old_game is False:
  267. self._old_game = list(self.bot.servers)[0].me.game
  268. status = list(self.bot.servers)[0].me.status
  269. game = discord.Game(name=song.title, type=2)
  270. await self.bot.change_presence(status=status, game=game)
  271. log.debug('Bot status changed to song title: ' + song.title)
  272. def _add_to_queue(self, server, url, channel):
  273. if server.id not in self.queue:
  274. self._setup_queue(server)
  275. queued_song = QueuedSong(url, channel)
  276. self.queue[server.id][QueueKey.QUEUE].append(queued_song)
  277. def _add_to_temp_queue(self, server, url, channel):
  278. if server.id not in self.queue:
  279. self._setup_queue(server)
  280. queued_song = QueuedSong(url, channel)
  281. self.queue[server.id][QueueKey.TEMP_QUEUE].append(queued_song)
  282. def _addleft_to_queue(self, server, url, channel):
  283. if server.id not in self.queue:
  284. self._setup_queue()
  285. queued_song = QueuedSong(url, channel)
  286. self.queue[server.id][QueueKey.QUEUE].appendleft(queued_song)
  287. def _cache_desired_files(self):
  288. filelist = []
  289. for server in self.downloaders:
  290. song = self.downloaders[server].song
  291. try:
  292. filelist.append(song.id)
  293. except AttributeError:
  294. pass
  295. shuffle(filelist)
  296. return filelist
  297. def _cache_max(self):
  298. setting_max = self.settings["MAX_CACHE"]
  299. return max([setting_max, self._cache_min()]) # enforcing hard limit
  300. def _cache_min(self):
  301. x = self._server_count()
  302. return max([60, 48 * math.log(x) * x**0.3]) # log is not log10
  303. def _cache_required_files(self):
  304. queue = copy.deepcopy(self.queue)
  305. filelist = []
  306. for server in queue:
  307. now_playing = queue[server].get(QueueKey.NOW_PLAYING)
  308. try:
  309. filelist.append(now_playing.id)
  310. except AttributeError:
  311. pass
  312. return filelist
  313. def _cache_size(self):
  314. songs = os.listdir(self.cache_path)
  315. size = sum(map(lambda s: os.path.getsize(
  316. os.path.join(self.cache_path, s)) / 10**6, songs))
  317. return size
  318. def _cache_too_large(self):
  319. if self._cache_size() > self._cache_max():
  320. return True
  321. return False
  322. def _clear_queue(self, server):
  323. if server.id not in self.queue:
  324. return
  325. self.queue[server.id][QueueKey.QUEUE] = deque()
  326. self.queue[server.id][QueueKey.TEMP_QUEUE] = deque()
  327. async def _create_ffmpeg_player(self, server, filename, local=False, start_time=None, end_time=None):
  328. """This function will guarantee we have a valid voice client,
  329. even if one doesn't exist previously."""
  330. voice_channel_id = self.queue[server.id][QueueKey.VOICE_CHANNEL_ID]
  331. voice_client = self.voice_client(server)
  332. if voice_client is None:
  333. log.debug("not connected when we should be in sid {}".format(
  334. server.id))
  335. to_connect = self.bot.get_channel(voice_channel_id)
  336. if to_connect is None:
  337. raise VoiceNotConnected("Okay somehow we're not connected and"
  338. " we have no valid channel to"
  339. " reconnect to. In other words...LOL"
  340. " REKT.")
  341. log.debug("valid reconnect channel for sid"
  342. " {}, reconnecting...".format(server.id))
  343. await self._join_voice_channel(to_connect) # SHIT
  344. elif voice_client.channel.id != voice_channel_id:
  345. # This was decided at 3:45 EST in #advanced-testing by 26
  346. self.queue[server.id][QueueKey.VOICE_CHANNEL_ID] = voice_client.channel.id
  347. log.debug("reconnect chan id for sid {} is wrong, fixing".format(
  348. server.id))
  349. # Okay if we reach here we definitively have a working voice_client
  350. if local:
  351. song_filename = os.path.join(self.local_playlist_path, filename)
  352. else:
  353. song_filename = os.path.join(self.cache_path, filename)
  354. use_avconv = self.settings["AVCONV"]
  355. options = '-b:a 64k -bufsize 64k'
  356. before_options = ''
  357. if start_time:
  358. before_options += '-ss {}'.format(start_time)
  359. if end_time:
  360. options += ' -to {} -copyts'.format(end_time)
  361. try:
  362. voice_client.audio_player.process.kill()
  363. log.debug("killed old player")
  364. except AttributeError:
  365. pass
  366. except ProcessLookupError:
  367. pass
  368. log.debug("making player on sid {}".format(server.id))
  369. voice_client.audio_player = voice_client.create_ffmpeg_player(
  370. song_filename, use_avconv=use_avconv, options=options, before_options=before_options)
  371. # Set initial volume
  372. vol = self.get_server_settings(server)['VOLUME'] / 100
  373. voice_client.audio_player.volume = vol
  374. return voice_client # Just for ease of use, it's modified in-place
  375. # TODO: _current_playlist
  376. # TODO: _current_song
  377. def _delete_playlist(self, server, name):
  378. if not name.endswith('.txt'):
  379. name = name + ".txt"
  380. try:
  381. os.remove(os.path.join('data/audio/playlists', server.id, name))
  382. except OSError:
  383. pass
  384. except WindowsError:
  385. pass
  386. # TODO: _disable_controls()
  387. async def _disconnect_voice_client(self, server):
  388. if not self.voice_connected(server):
  389. return
  390. voice_client = self.voice_client(server)
  391. await voice_client.disconnect()
  392. async def _download_all(self, queued_song_list, channel):
  393. """
  394. Doesn't actually download, just get's info for uses like queue_list
  395. """
  396. downloaders = []
  397. for queued_song in queued_song_list:
  398. d = Downloader(queued_song.url)
  399. d.start()
  400. downloaders.append(d)
  401. while any([d.is_alive() for d in downloaders]):
  402. await asyncio.sleep(0.1)
  403. songs = [d.song for d in downloaders if d.song is not None and d.error is None]
  404. invalid_downloads = [d for d in downloaders if d.error is not None]
  405. invalid_number = len(invalid_downloads)
  406. if(invalid_number > 0):
  407. await self.bot.send_message(channel, "The queue contains {} item(s)"
  408. " that can not be played.".format(invalid_number))
  409. return songs
  410. async def _download_next(self, server, curr_dl, next_dl):
  411. """Checks to see if we need to download the next, and does.
  412. Both curr_dl and next_dl should already be started."""
  413. if curr_dl.song is None:
  414. # Only happens when the downloader thread hasn't initialized fully
  415. # There's no reason to wait if we can't compare
  416. return
  417. max_length = self.settings["MAX_LENGTH"]
  418. while next_dl.is_alive():
  419. await asyncio.sleep(0.5)
  420. error = next_dl.error
  421. if(error is not None):
  422. raise YouTubeDlError(error)
  423. if curr_dl.song.id != next_dl.song.id:
  424. log.debug("downloader ID's mismatch on sid {}".format(server.id) +
  425. " gonna start dl-ing the next thing on the queue"
  426. " id {}".format(next_dl.song.id))
  427. try:
  428. next_dl.duration_check()
  429. except MaximumLength:
  430. return
  431. self.downloaders[server.id] = Downloader(next_dl.url, max_length,
  432. download=True)
  433. self.downloaders[server.id].start()
  434. def _dump_cache(self, ignore_desired=False):
  435. reqd = self._cache_required_files()
  436. log.debug("required cache files:\n\t{}".format(reqd))
  437. opt = self._cache_desired_files()
  438. log.debug("desired cache files:\n\t{}".format(opt))
  439. prev_size = self._cache_size()
  440. for file in os.listdir(self.cache_path):
  441. if file not in reqd:
  442. if ignore_desired or file not in opt:
  443. try:
  444. os.remove(os.path.join(self.cache_path, file))
  445. except OSError:
  446. # A directory got in the cache?
  447. pass
  448. except WindowsError:
  449. # Removing a file in use, reqd failed
  450. pass
  451. post_size = self._cache_size()
  452. dumped = prev_size - post_size
  453. if not ignore_desired and self._cache_too_large():
  454. log.debug("must dump desired files")
  455. return dumped + self._dump_cache(ignore_desired=True)
  456. log.debug("dumped {} MB of audio files".format(dumped))
  457. return dumped
  458. # TODO: _enable_controls()
  459. # returns list of active voice channels
  460. # assuming list does not change during the execution of this function
  461. # if that happens, blame asyncio.
  462. def _get_active_voice_clients(self):
  463. avcs = []
  464. for vc in self.bot.voice_clients:
  465. if hasattr(vc, 'audio_player') and not vc.audio_player.is_done():
  466. avcs.append(vc)
  467. return avcs
  468. def _get_queue(self, server, limit):
  469. if server.id not in self.queue:
  470. return []
  471. ret = []
  472. for i in range(limit):
  473. try:
  474. ret.append(self.queue[server.id][QueueKey.QUEUE][i])
  475. except IndexError:
  476. pass
  477. return ret
  478. def _get_queue_nowplaying(self, server):
  479. if server.id not in self.queue:
  480. return None
  481. return self.queue[server.id][QueueKey.NOW_PLAYING]
  482. def _get_queue_nowplaying_channel(self, server):
  483. if server.id not in self.queue:
  484. return None
  485. return self.queue[server.id][QueueKey.NOW_PLAYING_CHANNEL]
  486. def _get_queue_playlist(self, server):
  487. if server.id not in self.queue:
  488. return None
  489. return self.queue[server.id][QueueKey.PLAYLIST]
  490. def _get_queue_repeat(self, server):
  491. if server.id not in self.queue:
  492. return None
  493. return self.queue[server.id][QueueKey.REPEAT]
  494. def _get_queue_tempqueue(self, server, limit):
  495. if server.id not in self.queue:
  496. return []
  497. ret = []
  498. for i in range(limit):
  499. try:
  500. ret.append(self.queue[server.id][QueueKey.TEMP_QUEUE][i])
  501. except IndexError:
  502. pass
  503. return ret
  504. async def _guarantee_downloaded(self, server, url):
  505. max_length = self.settings["MAX_LENGTH"]
  506. if server.id not in self.downloaders: # We don't have a downloader
  507. log.debug("sid {} not in downloaders, making one".format(
  508. server.id))
  509. self.downloaders[server.id] = Downloader(url, max_length)
  510. if self.downloaders[server.id].url != url: # Our downloader is old
  511. # I'm praying to Jeezus that we don't accidentally lose a running
  512. # Downloader
  513. log.debug("sid {} in downloaders but wrong url".format(server.id))
  514. self.downloaders[server.id] = Downloader(url, max_length)
  515. try:
  516. # We're assuming we have the right thing in our downloader object
  517. self.downloaders[server.id].start()
  518. log.debug("starting our downloader for sid {}".format(server.id))
  519. except RuntimeError:
  520. # Queue manager already started it for us, isn't that nice?
  521. pass
  522. # Getting info w/o download
  523. self.downloaders[server.id].done.wait()
  524. # Youtube-DL threw an exception.
  525. error = self.downloaders[server.id].error
  526. if(error is not None):
  527. raise YouTubeDlError(error)
  528. # This will throw a maxlength exception if required
  529. self.downloaders[server.id].duration_check()
  530. song = self.downloaders[server.id].song
  531. log.debug("sid {} wants to play songid {}".format(server.id, song.id))
  532. # Now we check to see if we have a cache hit
  533. cache_location = os.path.join(self.cache_path, song.id)
  534. if not os.path.exists(cache_location):
  535. log.debug("cache miss on song id {}".format(song.id))
  536. self.downloaders[server.id] = Downloader(url, max_length,
  537. download=True)
  538. self.downloaders[server.id].start()
  539. while self.downloaders[server.id].is_alive():
  540. await asyncio.sleep(0.5)
  541. song = self.downloaders[server.id].song
  542. else:
  543. log.debug("cache hit on song id {}".format(song.id))
  544. return song
  545. def _is_queue_playlist(self, server):
  546. if server.id not in self.queue:
  547. return False
  548. return self.queue[server.id][QueueKey.PLAYLIST]
  549. async def _join_voice_channel(self, channel):
  550. server = channel.server
  551. connect_time = self.connect_timers.get(server.id, 0)
  552. if time.time() < connect_time:
  553. diff = int(connect_time - time.time())
  554. raise ConnectTimeout("You are on connect cooldown for another {}"
  555. " seconds.".format(diff))
  556. if server.id in self.queue:
  557. self.queue[server.id][QueueKey.VOICE_CHANNEL_ID] = channel.id
  558. try:
  559. await asyncio.wait_for(self.bot.join_voice_channel(channel),
  560. timeout=5, loop=self.bot.loop)
  561. except asyncio.futures.TimeoutError as e:
  562. log.exception(e)
  563. self.connect_timers[server.id] = time.time() + 300
  564. raise ConnectTimeout("We timed out connecting to a voice channel,"
  565. " please try again in 10 minutes.")
  566. def _list_local_playlists(self):
  567. ret = []
  568. for thing in os.listdir(self.local_playlist_path):
  569. if os.path.isdir(os.path.join(self.local_playlist_path, thing)):
  570. ret.append(thing)
  571. log.debug("local playlists:\n\t{}".format(ret))
  572. return ret
  573. def _list_playlists(self, server):
  574. try:
  575. server = server.id
  576. except:
  577. pass
  578. path = "data/audio/playlists"
  579. old_playlists = [f[:-4] for f in os.listdir(path)
  580. if f.endswith(".txt")]
  581. path = os.path.join(path, server)
  582. if os.path.exists(path):
  583. new_playlists = [f[:-4] for f in os.listdir(path)
  584. if f.endswith(".txt")]
  585. else:
  586. new_playlists = []
  587. return list(set(old_playlists + new_playlists))
  588. def _load_playlist(self, server, name, local=True):
  589. try:
  590. server = server.id
  591. except:
  592. pass
  593. f = "data/audio/playlists"
  594. if local:
  595. f = os.path.join(f, server, name + ".txt")
  596. else:
  597. f = os.path.join(f, name + ".txt")
  598. kwargs = dataIO.load_json(f)
  599. kwargs['path'] = f
  600. kwargs['main_class'] = self
  601. kwargs['name'] = name
  602. kwargs['sid'] = server
  603. kwargs['server'] = self.bot.get_server(server)
  604. return Playlist(**kwargs)
  605. def _local_playlist_songlist(self, name):
  606. dirpath = os.path.join(self.local_playlist_path, name)
  607. return sorted(os.listdir(dirpath))
  608. def _make_local_song(self, filename):
  609. # filename should be playlist_folder/file_name
  610. folder, song = os.path.split(filename)
  611. return Song(name=song, id=filename, title=song, url=filename,
  612. webpage_url=filename)
  613. def _make_playlist(self, author, url, songlist):
  614. try:
  615. author = author.id
  616. except:
  617. pass
  618. return Playlist(author=author, url=url, playlist=songlist)
  619. def _match_sc_playlist(self, url):
  620. return self._match_sc_url(url)
  621. def _match_yt_playlist(self, url):
  622. if not self._match_yt_url(url):
  623. return False
  624. yt_playlist = re.compile(
  625. r'^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)'
  626. r'((\/playlist\?)|\/watch\?).*(list=)(.*)(&|$)')
  627. # Group 6 should be the list ID
  628. if yt_playlist.match(url):
  629. return True
  630. return False
  631. def _match_sc_url(self, url):
  632. sc_url = re.compile(
  633. r'^(https?\:\/\/)?(www\.)?(soundcloud\.com\/)')
  634. if sc_url.match(url):
  635. return True
  636. return False
  637. def _match_yt_url(self, url):
  638. yt_link = re.compile(
  639. r'^(https?\:\/\/)?(www\.|m\.)?(youtube\.com|youtu\.?be)\/.+$')
  640. if yt_link.match(url):
  641. return True
  642. return False
  643. def _match_any_url(self, url):
  644. url = urlparse(url)
  645. if url.scheme and url.netloc and url.path:
  646. return True
  647. return False
  648. # TODO: _next_songs_in_queue
  649. async def _parse_playlist(self, url):
  650. if self._match_sc_playlist(url):
  651. return await self._parse_sc_playlist(url)
  652. elif self._match_yt_playlist(url):
  653. return await self._parse_yt_playlist(url)
  654. raise InvalidPlaylist("The given URL is neither a Soundcloud or"
  655. " YouTube playlist.")
  656. async def _parse_sc_playlist(self, url):
  657. playlist = []
  658. d = Downloader(url)
  659. d.start()
  660. while d.is_alive():
  661. await asyncio.sleep(0.5)
  662. error = d.error
  663. if(error is not None):
  664. raise YouTubeDlError(error)
  665. for entry in d.song.entries:
  666. if entry["url"][4] != "s":
  667. song_url = "https{}".format(entry["url"][4:])
  668. playlist.append(song_url)
  669. else:
  670. playlist.append(entry.url)
  671. return playlist
  672. async def _parse_yt_playlist(self, url):
  673. d = Downloader(url)
  674. d.start()
  675. playlist = []
  676. while d.is_alive():
  677. await asyncio.sleep(0.5)
  678. error = d.error
  679. if(error is not None):
  680. raise YouTubeDlError(error)
  681. for entry in d.song.entries:
  682. try:
  683. song_url = "https://www.youtube.com/watch?v={}".format(
  684. entry['id'])
  685. playlist.append(song_url)
  686. except AttributeError:
  687. pass
  688. except TypeError:
  689. pass
  690. log.debug("song list:\n\t{}".format(playlist))
  691. return playlist
  692. async def _play(self, sid, url, channel):
  693. """Returns the song object of what's playing"""
  694. if type(sid) is not discord.Server:
  695. server = self.bot.get_server(sid)
  696. else:
  697. server = sid
  698. assert type(server) is discord.Server
  699. log.debug('starting to play on "{}"'.format(server.name))
  700. if self._valid_playable_url(url) or "[SEARCH:]" in url:
  701. clean_url = self._clean_url(url)
  702. try:
  703. song = await self._guarantee_downloaded(server, url)
  704. except YouTubeDlError as e:
  705. message = ("I'm unable to play '{}' because of an error:\n"
  706. "'{}'".format(clean_url, str(e)))
  707. message = escape(message, mass_mentions=True)
  708. await self.bot.send_message(channel, message)
  709. return
  710. except MaximumLength:
  711. message = ("I'm unable to play '{}' because it exceeds the "
  712. "maximum audio length.".format(clean_url))
  713. message = escape(message, mass_mentions=True)
  714. await self.bot.send_message(channel, message)
  715. return
  716. local = False
  717. else: # Assume local
  718. try:
  719. song = self._make_local_song(url)
  720. local = True
  721. except FileNotFoundError:
  722. raise
  723. song.song_start_time = datetime.datetime.now()
  724. voice_client = await self._create_ffmpeg_player(server, song.id,
  725. local=local,
  726. start_time=song.start_time,
  727. end_time=song.end_time)
  728. # That ^ creates the audio_player property
  729. voice_client.audio_player.start()
  730. log.debug("starting player on sid {}".format(server.id))
  731. return song
  732. def _play_playlist(self, server, playlist, channel):
  733. try:
  734. songlist = playlist.playlist
  735. name = playlist.name
  736. except AttributeError:
  737. songlist = playlist
  738. name = True
  739. songlist = self._songlist_change_url_to_queued_song(songlist, channel)
  740. log.debug("setting up playlist {} on sid {}".format(name, server.id))
  741. self._stop_player(server)
  742. self._stop_downloader(server)
  743. self._clear_queue(server)
  744. log.debug("finished resetting state on sid {}".format(server.id))
  745. self._setup_queue(server)
  746. self._set_queue_playlist(server, name)
  747. self._set_queue_repeat(server, True)
  748. self._set_queue(server, songlist)
  749. def _play_local_playlist(self, server, name, channel):
  750. songlist = self._local_playlist_songlist(name)
  751. ret = []
  752. for song in songlist:
  753. ret.append(os.path.join(name, song))
  754. ret_playlist = Playlist(server=server, name=name, playlist=ret)
  755. self._play_playlist(server, ret_playlist, channel)
  756. def _songlist_change_url_to_queued_song(self, songlist, channel):
  757. queued_songlist = []
  758. for song in songlist:
  759. queued_song = QueuedSong(song, channel)
  760. queued_songlist.append(queued_song)
  761. return queued_songlist
  762. def _player_count(self):
  763. count = 0
  764. queue = copy.deepcopy(self.queue)
  765. for sid in queue:
  766. server = self.bot.get_server(sid)
  767. try:
  768. vc = self.voice_client(server)
  769. if vc.audio_player.is_playing():
  770. count += 1
  771. except:
  772. pass
  773. return count
  774. def _playlist_exists(self, server, name):
  775. return self._playlist_exists_local(server, name) or \
  776. self._playlist_exists_global(name)
  777. def _playlist_exists_global(self, name):
  778. f = "data/audio/playlists"
  779. f = os.path.join(f, name + ".txt")
  780. log.debug('checking for {}'.format(f))
  781. return dataIO.is_valid_json(f)
  782. def _playlist_exists_local(self, server, name):
  783. try:
  784. server = server.id
  785. except AttributeError:
  786. pass
  787. f = "data/audio/playlists"
  788. f = os.path.join(f, server, name + ".txt")
  789. log.debug('checking for {}'.format(f))
  790. return dataIO.is_valid_json(f)
  791. def _remove_queue(self, server):
  792. if server.id in self.queue:
  793. del self.queue[server.id]
  794. async def _remove_song_status(self):
  795. if self._old_game is not False:
  796. status = list(self.bot.servers)[0].me.status
  797. await self.bot.change_presence(game=self._old_game,
  798. status=status)
  799. log.debug('Bot status returned to ' + str(self._old_game))
  800. self._old_game = False
  801. def _save_playlist(self, server, name, playlist):
  802. sid = server.id
  803. try:
  804. f = playlist.filename
  805. playlist = playlist.to_json()
  806. log.debug("got playlist object")
  807. except AttributeError:
  808. f = os.path.join("data/audio/playlists", sid, name + ".txt")
  809. head, _ = os.path.split(f)
  810. if not os.path.exists(head):
  811. os.makedirs(head)
  812. log.debug("saving playlist '{}' to {}:\n\t{}".format(name, f,
  813. playlist))
  814. dataIO.save_json(f, playlist)
  815. def _shuffle_queue(self, server):
  816. shuffle(self.queue[server.id][QueueKey.QUEUE])
  817. def _shuffle_temp_queue(self, server):
  818. shuffle(self.queue[server.id][QueueKey.TEMP_QUEUE])
  819. def _server_count(self):
  820. return max([1, len(self.bot.servers)])
  821. def _set_queue(self, server, songlist):
  822. if server.id in self.queue:
  823. self._clear_queue(server)
  824. else:
  825. self._setup_queue(server)
  826. self.queue[server.id][QueueKey.QUEUE].extend(songlist)
  827. def _set_queue_channel(self, server, channel):
  828. if server.id not in self.queue:
  829. return
  830. try:
  831. channel = channel.id
  832. except AttributeError:
  833. pass
  834. self.queue[server.id][QueueKey.VOICE_CHANNEL_ID] = channel
  835. def _set_queue_nowplaying(self, server, song, channel):
  836. if server.id not in self.queue:
  837. return
  838. self.queue[server.id][QueueKey.NOW_PLAYING] = song
  839. self.queue[server.id][QueueKey.NOW_PLAYING_CHANNEL] = channel
  840. def _set_queue_playlist(self, server, name=True):
  841. if server.id not in self.queue:
  842. self._setup_queue(server)
  843. self.queue[server.id][QueueKey.PLAYLIST] = name
  844. def _set_queue_repeat(self, server, value):
  845. if server.id not in self.queue:
  846. self._setup_queue(server)
  847. self.queue[server.id][QueueKey.REPEAT] = value
  848. def _setup_queue(self, server):
  849. self.queue[server.id] = {QueueKey.REPEAT: False, QueueKey.PLAYLIST: False,
  850. QueueKey.VOICE_CHANNEL_ID: None,
  851. QueueKey.QUEUE: deque(), QueueKey.TEMP_QUEUE: deque(),
  852. QueueKey.NOW_PLAYING: None, QueueKey.NOW_PLAYING_CHANNEL: None}
  853. def _stop(self, server):
  854. self._setup_queue(server)
  855. self._stop_player(server)
  856. self._stop_downloader(server)
  857. self.bot.loop.create_task(self._update_bot_status())
  858. async def _stop_and_disconnect(self, server):
  859. self._stop(server)
  860. await self._disconnect_voice_client(server)
  861. def _stop_downloader(self, server):
  862. if server.id not in self.downloaders:
  863. return
  864. del self.downloaders[server.id]
  865. def _stop_player(self, server):
  866. if not self.voice_connected(server):
  867. return
  868. voice_client = self.voice_client(server)
  869. if hasattr(voice_client, 'audio_player'):
  870. voice_client.audio_player.stop()
  871. # no return. they can check themselves.
  872. async def _update_bot_status(self):
  873. if self.settings["TITLE_STATUS"]:
  874. song = None
  875. try:
  876. active_servers = self._get_active_voice_clients()
  877. except:
  878. log.debug("Voice client changed while trying to update bot's"
  879. " song status")
  880. return
  881. if len(active_servers) == 1:
  882. server = active_servers[0].server
  883. song = self._get_queue_nowplaying(server)
  884. if song:
  885. await self._add_song_status(song)
  886. else:
  887. await self._remove_song_status()
  888. def _valid_playlist_name(self, name):
  889. for char in name:
  890. if char.isdigit() or char.isalpha() or char == "_":
  891. pass
  892. else:
  893. return False
  894. return True
  895. def _valid_playable_url(self, url):
  896. yt = self._match_yt_url(url)
  897. sc = self._match_sc_url(url)
  898. if yt or sc: # TODO: Add sc check
  899. return True
  900. return False
  901. def _clean_url(self, url):
  902. if(self._valid_playable_url(url)):
  903. return "<{}>".format(url)
  904. return url.replace("[SEARCH:]", "")
  905. @commands.group(pass_context=True)
  906. async def audioset(self, ctx):
  907. """Audio settings."""
  908. if ctx.invoked_subcommand is None:
  909. await send_cmd_help(ctx)
  910. return
  911. @audioset.command(name="cachemax")
  912. @checks.is_owner()
  913. async def audioset_cachemax(self, size: int):
  914. """Set the max cache size in MB"""
  915. if size < self._cache_min():
  916. await self.bot.say("Sorry, but because of the number of servers"
  917. " that your bot is in I cannot safely allow"
  918. " you to have less than {} MB of cache.".format(
  919. self._cache_min()))
  920. return
  921. self.settings["MAX_CACHE"] = size
  922. await self.bot.say("Max cache size set to {} MB.".format(size))
  923. self.save_settings()
  924. @audioset.command(name="emptydisconnect", pass_context=True)
  925. @checks.mod_or_permissions(manage_messages=True)
  926. async def audioset_emptydisconnect(self, ctx):
  927. """Toggles auto disconnection when everyone leaves the channel"""
  928. server = ctx.message.server
  929. settings = self.get_server_settings(server.id)
  930. noppl_disconnect = settings.get("NOPPL_DISCONNECT", True)
  931. self.set_server_setting(server, "NOPPL_DISCONNECT",
  932. not noppl_disconnect)
  933. if not noppl_disconnect:
  934. await self.bot.say("If there is no one left in the voice channel"
  935. " the bot will automatically disconnect after"
  936. " five minutes.")
  937. else:
  938. await self.bot.say("The bot will no longer auto disconnect"
  939. " if the voice channel is empty.")
  940. self.save_settings()
  941. @audioset.command(name="maxlength")
  942. @checks.is_owner()
  943. async def audioset_maxlength(self, length: int):
  944. """Maximum track length (seconds) for requested links"""
  945. if length <= 0:
  946. await self.bot.say("Wow, a non-positive length value...aren't"
  947. " you smart.")
  948. return
  949. self.settings["MAX_LENGTH"] = length
  950. await self.bot.say("Maximum length is now {} seconds.".format(length))
  951. self.save_settings()
  952. @checks.mod_or_permissions(manage_messages=True)
  953. @audioset.command(name="notifychannel", pass_context=True)
  954. async def audioset_notifychannel(self, ctx, channel: discord.Channel):
  955. """Sets the channel for the now playing announcement"""
  956. server = ctx.message.server
  957. if not server.me.permissions_in(channel).send_messages:
  958. await self.bot.say("No permissions to speak in that channel.")
  959. return
  960. self.set_server_setting(server, "NOTIFY_CHANNEL", channel.id)
  961. dataIO.save_json(self.settings_path, self.settings)
  962. await self.bot.send_message(channel, "I will now announce new songs here.")
  963. @audioset.command(name="notify", pass_context=True)
  964. @checks.mod_or_permissions(manage_messages=True)
  965. async def audioset_notify(self, ctx):
  966. """Sends a notification to the channel when the song changes"""
  967. server = ctx.message.server
  968. settings = self.get_server_settings(server.id)
  969. notify = settings.get("NOTIFY", True)
  970. self.set_server_setting(server, "NOTIFY", not notify)
  971. if self.get_server_settings(server)["NOTIFY_CHANNEL"] is None:
  972. self.set_server_setting(server, "NOTIFY_CHANNEL", ctx.message.channel.id)
  973. dataIO.save_json(self.settings_path, self.settings)
  974. if not notify:
  975. await self.bot.say("Now notifying when a new track plays.")
  976. else:
  977. await self.bot.say("No longer notifying when a new track plays.")
  978. self.save_settings()
  979. @audioset.command(name="player")
  980. @checks.is_owner()
  981. async def audioset_player(self):
  982. """Toggles between Ffmpeg and Avconv"""
  983. self.settings["AVCONV"] = not self.settings["AVCONV"]
  984. if self.settings["AVCONV"]:
  985. await self.bot.say("Player toggled. You're now using avconv.")
  986. else:
  987. await self.bot.say("Player toggled. You're now using ffmpeg.")
  988. self.save_settings()
  989. @audioset.command(name="status")
  990. @checks.is_owner() # cause effect is cross-server
  991. async def audioset_status(self):
  992. """Enables/disables songs' titles as status"""
  993. self.settings["TITLE_STATUS"] = not self.settings["TITLE_STATUS"]
  994. if self.settings["TITLE_STATUS"]:
  995. await self.bot.say("If only one server is playing music, songs'"
  996. " titles will now show up as status")
  997. # not updating on disable if we say disable
  998. # means don't mess with it.
  999. await self._update_bot_status()
  1000. else:
  1001. await self.bot.say("Songs' titles will no longer show up as"
  1002. " status")
  1003. self.save_settings()
  1004. @audioset.command(name="timerdisconnect", pass_context=True)
  1005. @checks.mod_or_permissions(manage_messages=True)
  1006. async def audioset_timerdisconnect(self, ctx):
  1007. """Toggles the disconnect timer"""
  1008. server = ctx.message.server
  1009. settings = self.get_server_settings(server.id)
  1010. timer_disconnect = settings.get("TIMER_DISCONNECT", True)
  1011. self.set_server_setting(server, "TIMER_DISCONNECT",
  1012. not timer_disconnect)
  1013. if not timer_disconnect:
  1014. await self.bot.say("The bot will automatically disconnect after"
  1015. " playback is stopped and five minutes have"
  1016. " elapsed. Disable this setting to stop the"
  1017. " bot from disconnecting with other music cogs"
  1018. " playing.")
  1019. else:
  1020. await self.bot.say("The bot will no longer auto disconnect"
  1021. " while other music cogs are playing.")
  1022. self.save_settings()
  1023. @audioset.command(pass_context=True, name="volume", no_pm=True)
  1024. @checks.mod_or_permissions(manage_messages=True)
  1025. async def audioset_volume(self, ctx, percent: int=None):
  1026. """Sets the volume (0 - 100)
  1027. Note: volume may be set up to 200 but you may experience clipping."""
  1028. server = ctx.message.server
  1029. if percent is None:
  1030. vol = self.get_server_settings(server)['VOLUME']
  1031. msg = "Volume is currently set to %d%%" % vol
  1032. elif percent >= 0 and percent <= 200:
  1033. self.set_server_setting(server, "VOLUME", percent)
  1034. msg = "Volume is now set to %d." % percent
  1035. if percent > 100:
  1036. msg += ("\nWarning: volume levels above 100 may result in"
  1037. " clipping")
  1038. # Set volume of playing audio
  1039. vc = self.voice_client(server)
  1040. if vc:
  1041. vc.audio_player.volume = percent / 100
  1042. self.save_settings()
  1043. else:
  1044. msg = "Volume must be between 0 and 100."
  1045. await self.bot.say(msg)
  1046. @audioset.command(pass_context=True, name="vote", no_pm=True)
  1047. @checks.mod_or_permissions(manage_messages=True)
  1048. async def audioset_vote(self, ctx, percent: int):
  1049. """Percentage needed for the masses to skip songs. 0 to disable."""
  1050. server = ctx.message.server
  1051. if percent < 0:
  1052. await self.bot.say("Can't be less than zero.")
  1053. return
  1054. elif percent > 100:
  1055. percent = 100
  1056. if percent == 0:
  1057. enabled = False
  1058. await self.bot.say("Voting disabled. All users can stop or skip.")
  1059. else:
  1060. enabled = True
  1061. await self.bot.say("Vote percentage set to {}%".format(percent))
  1062. self.set_server_setting(server, "VOTE_THRESHOLD", percent)
  1063. self.set_server_setting(server, "VOTE_ENABLED", enabled)
  1064. self.save_settings()
  1065. @commands.group(pass_context=True)
  1066. async def audiostat(self, ctx):
  1067. """General stats on audio stuff."""
  1068. if ctx.invoked_subcommand is None:
  1069. await send_cmd_help(ctx)
  1070. return
  1071. @audiostat.command(name="servers")
  1072. async def audiostat_servers(self):
  1073. """Number of servers currently playing."""
  1074. count = self._player_count()
  1075. await self.bot.say("Currently playing music in {} servers.".format(
  1076. count))
  1077. @commands.group(pass_context=True)
  1078. async def cache(self, ctx):
  1079. """Cache management tools."""
  1080. if ctx.invoked_subcommand is None:
  1081. await send_cmd_help(ctx)
  1082. return
  1083. @cache.command(name="dump")
  1084. @checks.is_owner()
  1085. async def cache_dump(self):
  1086. """Dumps the cache."""
  1087. dumped = self._dump_cache()
  1088. await self.bot.say("Dumped {:.3f} MB of audio files.".format(dumped))
  1089. @cache.command(name='stats')
  1090. async def cache_stats(self):
  1091. """Reports info about the cache.
  1092. - Current size of the cache.
  1093. - Maximum cache size. User setting or minimum, whichever is higher.
  1094. - Minimum cache size. Automatically determined by number of servers Red is running on.
  1095. """
  1096. await self.bot.say("Cache stats:\n"
  1097. "Current size: {:.2f} MB\n"
  1098. "Maximum: {:.1f} MB\n"
  1099. "Minimum: {:.1f} MB".format(self._cache_size(),
  1100. self._cache_max(),
  1101. self._cache_min()))
  1102. @commands.group(pass_context=True, hidden=True, no_pm=True)
  1103. @checks.is_owner()
  1104. async def disconnect(self, ctx):
  1105. """Disconnects from voice channel in current server."""
  1106. if ctx.invoked_subcommand is None:
  1107. server = ctx.message.server
  1108. await self._stop_and_disconnect(server)
  1109. @disconnect.command(name="all", hidden=True, no_pm=True)
  1110. async def disconnect_all(self):
  1111. """Disconnects from all voice channels."""
  1112. while len(list(self.bot.voice_clients)) != 0:
  1113. vc = list(self.bot.voice_clients)[0]
  1114. await self._stop_and_disconnect(vc.server)
  1115. await self.bot.say("done.")
  1116. @commands.command(hidden=True, pass_context=True, no_pm=True)
  1117. @checks.is_owner()
  1118. async def joinvoice(self, ctx):
  1119. """Joins your voice channel"""
  1120. author = ctx.message.author
  1121. server = ctx.message.server
  1122. voice_channel = author.voice_channel
  1123. if voice_channel is not None:
  1124. self._stop(server)
  1125. await self._join_voice_channel(voice_channel)
  1126. @commands.group(pass_context=True, no_pm=True)
  1127. async def local(self, ctx):
  1128. """Local playlists commands"""
  1129. if ctx.invoked_subcommand is None:
  1130. await send_cmd_help(ctx)
  1131. @local.command(name="start", pass_context=True, no_pm=True)
  1132. async def play_local(self, ctx, *, name):
  1133. """Plays a local playlist"""
  1134. server = ctx.message.server
  1135. author = ctx.message.author
  1136. voice_channel = author.voice_channel
  1137. channel = ctx.message.channel
  1138. # Checking already connected, will join if not
  1139. if not self.voice_connected(server):
  1140. try:
  1141. self.has_connect_perm(author, server)
  1142. except AuthorNotConnected:
  1143. await self.bot.say("You must join a voice channel before I can"
  1144. " play anything.")
  1145. return
  1146. except UnauthorizedConnect:
  1147. await self.bot.say("I don't have permissions to join your"
  1148. " voice channel.")
  1149. return
  1150. except UnauthorizedSpeak:
  1151. await self.bot.say("I don't have permissions to speak in your"
  1152. " voice channel.")
  1153. return
  1154. except ChannelUserLimit:
  1155. await self.bot.say("Your voice channel is full.")
  1156. return
  1157. else:
  1158. await self._join_voice_channel(voice_channel)
  1159. else: # We are connected but not to the right channel
  1160. if self.voice_client(server).channel != voice_channel:
  1161. pass # TODO: Perms
  1162. # Checking if playing in current server
  1163. if self.is_playing(server):
  1164. await self.bot.say("I'm already playing a song on this server!")
  1165. return # TODO: Possibly execute queue?
  1166. # If not playing, spawn a downloader if it doesn't exist and begin
  1167. # downloading the next song
  1168. if self.currently_downloading(server):
  1169. await self.bot.say("I'm already downloading a file!")
  1170. return
  1171. lists = self._list_local_playlists()
  1172. if not any(map(lambda l: os.path.split(l)[1] == name, lists)):
  1173. await self.bot.say("Local playlist not found.")
  1174. return
  1175. self._play_local_playlist(server, name, channel)
  1176. @local.command(name="list", no_pm=True)
  1177. async def list_local(self):
  1178. """Lists local playlists"""
  1179. playlists = ", ".join(self._list_local_playlists())
  1180. if playlists:
  1181. playlists = "Available local playlists:\n\n" + playlists
  1182. for page in pagify(playlists, delims=[" "]):
  1183. await self.bot.say(page)
  1184. else:
  1185. await self.bot.say("There are no playlists.")
  1186. @commands.command(pass_context=True, no_pm=True)
  1187. async def pause(self, ctx):
  1188. """Pauses the current song, `[p]resume` to continue."""
  1189. server = ctx.message.server
  1190. if not self.voice_connected(server):
  1191. await self.bot.say("Not voice connected in this server.")
  1192. return
  1193. # We are connected somewhere
  1194. voice_client = self.voice_client(server)
  1195. if not hasattr(voice_client, 'audio_player'):
  1196. await self.bot.say("Nothing playing, nothing to pause.")
  1197. elif voice_client.audio_player.is_playing():
  1198. voice_client.audio_player.pause()
  1199. await self.bot.say("Paused.")
  1200. else:
  1201. await self.bot.say("Nothing playing, nothing to pause.")
  1202. @commands.command(pass_context=True, no_pm=True)
  1203. async def play(self, ctx, *, url_or_search_terms):
  1204. """Plays a link / searches and play"""
  1205. url = url_or_search_terms
  1206. server = ctx.message.server
  1207. author = ctx.message.author
  1208. voice_channel = author.voice_channel
  1209. channel = ctx.message.channel
  1210. # Checking if playing in current server
  1211. if self.is_playing(server):
  1212. await ctx.invoke(self._queue, url=url)
  1213. return # Default to queue
  1214. # Checking already connected, will join if not
  1215. try:
  1216. self.has_connect_perm(author, server)
  1217. except AuthorNotConnected:
  1218. await self.bot.say("You must join a voice channel before I can"
  1219. " play anything.")
  1220. return
  1221. except UnauthorizedConnect:
  1222. await self.bot.say("I don't have permissions to join your"
  1223. " voice channel.")
  1224. return
  1225. except UnauthorizedSpeak:
  1226. await self.bot.say("I don't have permissions to speak in your"
  1227. " voice channel.")
  1228. return
  1229. except ChannelUserLimit:
  1230. await self.bot.say("Your voice channel is full.")
  1231. return
  1232. if not self.voice_connected(server):
  1233. await self._join_voice_channel(voice_channel)
  1234. else: # We are connected but not to the right channel
  1235. if self.voice_client(server).channel != voice_channel:
  1236. await self._stop_and_disconnect(server)
  1237. await self._join_voice_channel(voice_channel)
  1238. # If not playing, spawn a downloader if it doesn't exist and begin
  1239. # downloading the next song
  1240. if self.currently_downloading(server):
  1241. await self.bot.say("I'm already downloading a file!")
  1242. return
  1243. url = url.strip("<>")
  1244. if self._match_any_url(url):
  1245. if not self._valid_playable_url(url):
  1246. await self.bot.say("That's not a valid URL.")
  1247. return
  1248. else:
  1249. url = url.replace("/", "&#47")
  1250. url = "[SEARCH:]" + url
  1251. if "[SEARCH:]" not in url and "youtube" in url:
  1252. parsed_url = urllib.parse.urlparse(url)
  1253. query = urllib.parse.parse_qs(parsed_url.query)
  1254. query.pop("list", None)
  1255. parsed_url = parsed_url._replace(query=urllib.parse.urlencode(query, True))
  1256. url = urllib.parse.urlunparse(parsed_url)
  1257. self._stop_player(server)
  1258. self._clear_queue(server)
  1259. self._add_to_queue(server, url, channel)
  1260. @commands.command(pass_context=True, no_pm=True)
  1261. async def prev(self, ctx):
  1262. """Goes back to the last song."""
  1263. # Current song is in NOW_PLAYING
  1264. server = ctx.message.server
  1265. channel = ctx.message.channel
  1266. if self.is_playing(server):
  1267. curr_url = self._get_queue_nowplaying(server).webpage_url
  1268. last_url = None
  1269. if self._is_queue_playlist(server):
  1270. # need to reorder queue
  1271. try:
  1272. last_url = self.queue[server.id][QueueKey.QUEUE].pop()
  1273. except IndexError:
  1274. pass
  1275. log.debug("prev on sid {}, curr_url {}".format(server.id,
  1276. curr_url))
  1277. self._addleft_to_queue(server, curr_url, channel)
  1278. if last_url:
  1279. self._addleft_to_queue(server, last_url, channel)
  1280. self._set_queue_nowplaying(server, None, None)
  1281. self.voice_client(server).audio_player.stop()
  1282. await self.bot.say("Going back 1 song.")
  1283. else:
  1284. await self.bot.say("Not playing anything on this server.")
  1285. @commands.group(pass_context=True, no_pm=True)
  1286. async def playlist(self, ctx):
  1287. """Playlist management/control."""
  1288. if ctx.invoked_subcommand is None:
  1289. await send_cmd_help(ctx)
  1290. @playlist.command(pass_context=True, no_pm=True, name="create")
  1291. async def playlist_create(self, ctx, name):
  1292. """Creates an empty playlist"""
  1293. server = ctx.message.server
  1294. author = ctx.message.author
  1295. if not self._valid_playlist_name(name) or len(name) > 25:
  1296. await self.bot.say("That playlist name is invalid. It must only"
  1297. " contain alpha-numeric characters or _.")
  1298. return
  1299. # Returns a Playlist object
  1300. url = None
  1301. songlist = []
  1302. playlist = self._make_playlist(author, url, songlist)
  1303. playlist.name = name
  1304. playlist.server = server
  1305. self._save_playlist(server, name, playlist)
  1306. await self.bot.say("Empty playlist '{}' saved.".format(name))
  1307. @playlist.command(pass_context=True, no_pm=True, name="add")
  1308. async def playlist_add(self, ctx, name, url):
  1309. """Add a YouTube or Soundcloud playlist."""
  1310. server = ctx.message.server
  1311. author = ctx.message.author
  1312. if not self._valid_playlist_name(name) or len(name) > 25:
  1313. await self.bot.say("That playlist name is invalid. It must only"
  1314. " contain alpha-numeric characters or _.")
  1315. return
  1316. if self._valid_playable_url(url):
  1317. try:
  1318. await self.bot.say("Enumerating song list... This could take"
  1319. " a few moments.")
  1320. songlist = await self._parse_playlist(url)
  1321. except InvalidPlaylist:
  1322. await self.bot.say("That playlist URL is invalid.")
  1323. return
  1324. except YouTubeDlError as e:
  1325. await self.bot.say("An error occurred while enumerating the playlist:\n"
  1326. "'{}'".format(str(e)))
  1327. return
  1328. playlist = self._make_playlist(author, url, songlist)
  1329. # Returns a Playlist object
  1330. playlist.name = name
  1331. playlist.server = server
  1332. self._save_playlist(server, name, playlist)
  1333. await self.bot.say("Playlist '{}' saved. Tracks: {}".format(
  1334. name, len(songlist)))
  1335. else:
  1336. await self.bot.say("That URL is not a valid Soundcloud or YouTube"
  1337. " playlist link. If you think this is in error"
  1338. " please let us know and we'll get it"
  1339. " fixed ASAP.")
  1340. @playlist.command(pass_context=True, no_pm=True, name="append")
  1341. async def playlist_append(self, ctx, name, url):
  1342. """Appends to a playlist."""
  1343. author = ctx.message.author
  1344. server = ctx.message.server
  1345. if name not in self._list_playlists(server):
  1346. await self.bot.say("There is no playlist with that name.")
  1347. return
  1348. playlist = self._load_playlist(
  1349. server, name, local=self._playlist_exists_local(server, name))
  1350. try:
  1351. playlist.append_song(author, url)
  1352. except UnauthorizedSave:
  1353. await self.bot.say("You're not the author of that playlist.")
  1354. except InvalidURL:
  1355. await self.bot.say("Invalid link.")
  1356. else:
  1357. await self.bot.say("Done.")
  1358. @playlist.command(pass_context=True, no_pm=True, name="list")
  1359. async def playlist_list(self, ctx):
  1360. """Lists all available playlists"""
  1361. server = ctx.message.server
  1362. playlists = ", ".join(self._list_playlists(server))
  1363. if playlists:
  1364. playlists = "Available playlists:\n\n" + playlists
  1365. for page in pagify(playlists, delims=[" "]):
  1366. await self.bot.say(page)
  1367. else:
  1368. await self.bot.say("There are no playlists.")
  1369. @playlist.command(pass_context=True, no_pm=True, name="queue")
  1370. async def playlist_queue(self, ctx, url):
  1371. """Adds a song to the playlist loop.
  1372. Does NOT write to disk."""
  1373. server = ctx.message.server
  1374. channel = ctx.message.channel
  1375. if not self.voice_connected(server):
  1376. await self.bot.say("Not voice connected in this server.")
  1377. return
  1378. # We are connected somewhere
  1379. if server.id not in self.queue:
  1380. log.debug("Something went wrong, we're connected but have no"
  1381. " queue entry.")
  1382. raise VoiceNotConnected("Something went wrong, we have no internal"
  1383. " queue to modify. This should never"
  1384. " happen.")
  1385. # We have a queue to modify
  1386. self._add_to_queue(server, url, channel)
  1387. await self.bot.say("Queued.")
  1388. @playlist.command(pass_context=True, no_pm=True, name="remove")
  1389. async def playlist_remove(self, ctx, name):
  1390. """Deletes a saved playlist."""
  1391. author = ctx.message.author
  1392. server = ctx.message.server
  1393. if not self._valid_playlist_name(name):
  1394. await self.bot.say("The playlist's name contains invalid "
  1395. "characters.")
  1396. return
  1397. if not self._playlist_exists(server, name):
  1398. await self.bot.say("Playlist not found.")
  1399. return
  1400. playlist = self._load_playlist(
  1401. server, name, local=self._playlist_exists_local(server, name))
  1402. if not playlist.can_edit(author):
  1403. await self.bot.say("You do not have permissions to delete that playlist.")
  1404. return
  1405. self._delete_playlist(server, name)
  1406. await self.bot.say("Playlist deleted.")
  1407. @playlist.command(pass_context=True, no_pm=True, name="start")
  1408. async def playlist_start(self, ctx, name):
  1409. """Plays a playlist."""
  1410. server = ctx.message.server
  1411. author = ctx.message.author
  1412. voice_channel = ctx.message.author.voice_channel
  1413. channel = ctx.message.channel
  1414. caller = inspect.currentframe().f_back.f_code.co_name
  1415. if voice_channel is None:
  1416. await self.bot.say("You must be in a voice channel to start a"
  1417. " playlist.")
  1418. return
  1419. if self._playlist_exists(server, name):
  1420. if not self.voice_connected(server):
  1421. try:
  1422. self.has_connect_perm(author, server)
  1423. except AuthorNotConnected:
  1424. await self.bot.say("You must join a voice channel before"
  1425. " I can play anything.")
  1426. return
  1427. except UnauthorizedConnect:
  1428. await self.bot.say("I don't have permissions to join your"
  1429. " voice channel.")
  1430. return
  1431. except UnauthorizedSpeak:
  1432. await self.bot.say("I don't have permissions to speak in"
  1433. " your voice channel.")
  1434. return
  1435. except ChannelUserLimit:
  1436. await self.bot.say("Your voice channel is full.")
  1437. return
  1438. else:
  1439. await self._join_voice_channel(voice_channel)
  1440. self._clear_queue(server)
  1441. playlist = self._load_playlist(server, name,
  1442. local=self._playlist_exists_local(
  1443. server, name))
  1444. if caller == "playlist_start_mix":
  1445. shuffle(playlist.playlist)
  1446. self._play_playlist(server, playlist, channel)
  1447. await self.bot.say("Playlist queued.")
  1448. else:
  1449. await self.bot.say("That playlist does not exist.")
  1450. @playlist.command(pass_context=True, no_pm=True, name="mix")
  1451. async def playlist_start_mix(self, ctx, name):
  1452. """Plays and mixes a playlist."""
  1453. await self.playlist_start.callback(self, ctx, name)
  1454. @commands.command(pass_context=True, no_pm=True, name="queue")
  1455. async def _queue(self, ctx, *, url=None):
  1456. """Queues a song to play next. Extended functionality in `[p]help`
  1457. If you use `queue` when one song is playing, your new song will get
  1458. added to the song loop (if running). If you use `queue` when a
  1459. playlist is running, it will temporarily be played next and will
  1460. NOT stay in the playlist loop."""
  1461. if url is None:
  1462. return await self._queue_list(ctx)
  1463. server = ctx.message.server
  1464. channel = ctx.message.channel
  1465. if not self.voice_connected(server):
  1466. await ctx.invoke(self.play, url_or_search_terms=url)
  1467. return
  1468. # We are connected somewhere
  1469. if server.id not in self.queue:
  1470. log.debug("Something went wrong, we're connected but have no"
  1471. " queue entry.")
  1472. raise VoiceNotConnected("Something went wrong, we have no internal"
  1473. " queue to modify. This should never"
  1474. " happen.")
  1475. url = url.strip("<>")
  1476. if self._match_any_url(url):
  1477. if not self._valid_playable_url(url):
  1478. await self.bot.say("That's not a valid URL.")
  1479. return
  1480. else:
  1481. url = "[SEARCH:]" + url
  1482. if "[SEARCH:]" not in url and "youtube" in url:
  1483. parsed_url = urllib.parse.urlparse(url)
  1484. query = urllib.parse.parse_qs(parsed_url.query)
  1485. query.pop("list", None)
  1486. parsed_url = parsed_url._replace(query=urllib.parse.urlencode(query, True))
  1487. url = urllib.parse.urlunparse(parsed_url)
  1488. # We have a queue to modify
  1489. if self.queue[server.id][QueueKey.PLAYLIST]:
  1490. log.debug("queueing to the temp_queue for sid {}".format(
  1491. server.id))
  1492. self._add_to_temp_queue(server, url, channel)
  1493. else:
  1494. log.debug("queueing to the actual queue for sid {}".format(
  1495. server.id))
  1496. self._add_to_queue(server, url, channel)
  1497. await self.bot.say("Queued.")
  1498. async def _queue_list(self, ctx):
  1499. """Not a command, use `queue` with no args to call this."""
  1500. server = ctx.message.server
  1501. channel = ctx.message.channel
  1502. now_playing = self._get_queue_nowplaying(server)
  1503. if server.id not in self.queue and now_playing is None:
  1504. await self.bot.say("Nothing playing on this server!")
  1505. return
  1506. if len(self.queue[server.id][QueueKey.QUEUE]) == 0 and not self.is_playing(server):
  1507. await self.bot.say("Nothing queued on this server.")
  1508. return
  1509. colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
  1510. em = discord.Embed(description="", colour=int(colour, 16))
  1511. msg = ""
  1512. if self.is_playing(server):
  1513. msg += "\n***Currently playing:***\n{}\n".format(now_playing.title)
  1514. msg += self._draw_play(now_playing, server) + "\n" # draw play thing
  1515. if now_playing.thumbnail is None:
  1516. now_playing.thumbnail = (self.bot.user.avatar_url).replace('webp', 'png')
  1517. em.set_thumbnail(url=now_playing.thumbnail)
  1518. queued_song_list = self._get_queue(server, 10)
  1519. tempqueued_song_list = self._get_queue_tempqueue(server, 10)
  1520. await self.bot.say("Gathering information...")
  1521. queue_song_list = await self._download_all(queued_song_list, channel)
  1522. tempqueue_song_list = await self._download_all(tempqueued_song_list, channel)
  1523. song_info = []
  1524. for num, song in enumerate(tempqueue_song_list, 1):
  1525. str_duration = str(datetime.timedelta(seconds=song.duration))
  1526. try:
  1527. if song.title is None:
  1528. song_info.append("**[{}]** {.webpage_url} ({})".format(num, song, str_duration))
  1529. else:
  1530. song_info.append("**[{}]** {.title} ({})".format(num, song, str_duration))
  1531. except AttributeError:
  1532. song_info.append("**[{}]** {.webpage_url} ({})".format(num, song, str_duration))
  1533. for num, song in enumerate(queue_song_list, len(song_info) + 1):
  1534. str_duration = str(datetime.timedelta(seconds=song.duration))
  1535. if num > 10:
  1536. break
  1537. try:
  1538. if song.title is None:
  1539. song_info.append("**[{}]** {.webpage_url} ({})".format(num, song, str_duration))
  1540. else:
  1541. song_info.append("**[{}]** {.title} ({})".format(num, song, str_duration))
  1542. except AttributeError:
  1543. song_info.append("**[{}]** {.webpage_url} ({})".format(num, song, str_duration))
  1544. if song_info:
  1545. msg += "\n***Next up:***\n" + "\n".join(song_info)
  1546. em.description = msg.replace('None', '-')
  1547. more_songs = len(self.queue[server.id][QueueKey.QUEUE]) - 10
  1548. if more_songs > 0:
  1549. em.set_footer(text="And {} more songs...".format(more_songs))
  1550. await self.bot.say(embed=em)
  1551. def _draw_play(self, song, server):
  1552. song_start_time = song.song_start_time
  1553. total_time = datetime.timedelta(seconds=song.duration)
  1554. current_time = datetime.datetime.now()
  1555. elapsed_time = current_time - song_start_time
  1556. sections = 12
  1557. loc_time = round((elapsed_time/total_time) * sections) # 10 sections
  1558. bar_char = '\N{BOX DRAWINGS HEAVY HORIZONTAL}'
  1559. seek_char = '\N{RADIO BUTTON}'
  1560. play_char = '\N{BLACK RIGHT-POINTING TRIANGLE}'
  1561. try:
  1562. if self.voice_client(server).audio_player.is_playing():
  1563. play_char = '\N{BLACK RIGHT-POINTING TRIANGLE}'
  1564. else:
  1565. play_char = '\N{DOUBLE VERTICAL BAR}'
  1566. except AttributeError:
  1567. pass
  1568. msg = "\n" + play_char + " "
  1569. for i in range(sections):
  1570. if i == loc_time:
  1571. msg += seek_char
  1572. else:
  1573. msg += bar_char
  1574. msg += " `{}`/`{}`".format(str(elapsed_time)[0:7],str(total_time))
  1575. return msg
  1576. @commands.group(pass_context=True, no_pm=True)
  1577. async def repeat(self, ctx):
  1578. """Toggles REPEAT"""
  1579. server = ctx.message.server
  1580. if ctx.invoked_subcommand is None:
  1581. if self.is_playing(server):
  1582. if self.queue[server.id][QueueKey.REPEAT]:
  1583. msg = "The queue is currently looping."
  1584. else:
  1585. msg = "The queue is currently not looping."
  1586. await self.bot.say(msg)
  1587. await self.bot.say(
  1588. "Do `{}repeat toggle` to change this.".format(ctx.prefix))
  1589. else:
  1590. await self.bot.say("Play something to see this setting.")
  1591. @repeat.command(pass_context=True, no_pm=True, name="toggle")
  1592. async def repeat_toggle(self, ctx):
  1593. """Flips repeat setting."""
  1594. server = ctx.message.server
  1595. if not self.is_playing(server):
  1596. await self.bot.say("I don't have a repeat setting to flip."
  1597. " Try playing something first.")
  1598. return
  1599. self._set_queue_repeat(server, not self.queue[server.id][QueueKey.REPEAT])
  1600. repeat = self.queue[server.id][QueueKey.REPEAT]
  1601. if repeat:
  1602. await self.bot.say("Repeat toggled on.")
  1603. else:
  1604. await self.bot.say("Repeat toggled off.")
  1605. @commands.command(pass_context=True, no_pm=True)
  1606. async def resume(self, ctx):
  1607. """Resumes a paused song or playlist"""
  1608. server = ctx.message.server
  1609. if not self.voice_connected(server):
  1610. await self.bot.say("Not voice connected in this server.")
  1611. return
  1612. # We are connected somewhere
  1613. voice_client = self.voice_client(server)
  1614. if not hasattr(voice_client, 'audio_player'):
  1615. await self.bot.say("Nothing paused, nothing to resume.")
  1616. elif not voice_client.audio_player.is_done() and \
  1617. not voice_client.audio_player.is_playing():
  1618. voice_client.audio_player.resume()
  1619. await self.bot.say("Resuming.")
  1620. else:
  1621. await self.bot.say("Nothing paused, nothing to resume.")
  1622. @commands.command(pass_context=True, no_pm=True, name="shuffle")
  1623. async def _shuffle(self, ctx):
  1624. """Shuffles the current queue"""
  1625. server = ctx.message.server
  1626. if server.id not in self.queue:
  1627. await self.bot.say("I have nothing in queue to shuffle.")
  1628. return
  1629. self._shuffle_queue(server)
  1630. self._shuffle_temp_queue(server)
  1631. await self.bot.say("Queues shuffled.")
  1632. @commands.command(pass_context=True, aliases=["next"], no_pm=True)
  1633. async def skip(self, ctx):
  1634. """Skips a song, using the set threshold if the requester isn't
  1635. a mod or admin. Mods, admins and bot owner are not counted in
  1636. the vote threshold."""
  1637. msg = ctx.message
  1638. server = ctx.message.server
  1639. if self.is_playing(server):
  1640. vchan = server.me.voice_channel
  1641. vc = self.voice_client(server)
  1642. if msg.author.voice_channel == vchan:
  1643. if self.can_instaskip(msg.author):
  1644. vc.audio_player.stop()
  1645. if self._get_queue_repeat(server) is False:
  1646. self._set_queue_nowplaying(server, None, None)
  1647. await self.bot.say("Skipping...")
  1648. else:
  1649. if msg.author.id in self.skip_votes[server.id]:
  1650. self.skip_votes[server.id].remove(msg.author.id)
  1651. reply = "I removed your vote to skip."
  1652. else:
  1653. self.skip_votes[server.id].append(msg.author.id)
  1654. reply = "you voted to skip."
  1655. num_votes = len(self.skip_votes[server.id])
  1656. # Exclude bots and non-plebs
  1657. num_members = sum(not (m.bot or self.can_instaskip(m))
  1658. for m in vchan.voice_members)
  1659. vote = int(100 * num_votes / num_members)
  1660. thresh = self.get_server_settings(server)["VOTE_THRESHOLD"]
  1661. if vote >= thresh:
  1662. vc.audio_player.stop()
  1663. if self._get_queue_repeat(server) is False:
  1664. self._set_queue_nowplaying(server, None, None)
  1665. self.skip_votes[server.id] = []
  1666. await self.bot.say("Vote threshold met. Skipping...")
  1667. return
  1668. else:
  1669. reply += " Votes: %d/%d" % (num_votes, num_members)
  1670. reply += " (%d%% out of %d%% needed)" % (vote, thresh)
  1671. await self.bot.reply(reply)
  1672. else:
  1673. await self.bot.say("You need to be in the voice channel to skip the music.")
  1674. else:
  1675. await self.bot.say("Can't skip if I'm not playing.")
  1676. def can_instaskip(self, member):
  1677. server = member.server
  1678. if not self.get_server_settings(server)["VOTE_ENABLED"]:
  1679. return True
  1680. admin_role = settings.get_server_admin(server)
  1681. mod_role = settings.get_server_mod(server)
  1682. is_owner = member.id == settings.owner
  1683. is_server_owner = member == server.owner
  1684. is_admin = discord.utils.get(member.roles, name=admin_role) is not None
  1685. is_mod = discord.utils.get(member.roles, name=mod_role) is not None
  1686. nonbots = sum(not m.bot for m in member.voice_channel.voice_members)
  1687. alone = nonbots <= 1
  1688. return is_owner or is_server_owner or is_admin or is_mod or alone
  1689. @commands.command(pass_context=True, no_pm=True)
  1690. async def sing(self, ctx):
  1691. """Makes Red sing one of her songs"""
  1692. ids = ("zGTkAVsrfg8", "cGMWL8cOeAU", "vFrjMq4aL-g", "WROI5WYBU_A",
  1693. "41tIUr_ex3g", "f9O2Rjn1azc")
  1694. url = "https://www.youtube.com/watch?v={}".format(choice(ids))
  1695. await ctx.invoke(self.play, url_or_search_terms=url)
  1696. @commands.command(pass_context=True, aliases=["np"], no_pm=True)
  1697. async def song(self, ctx):
  1698. """Info about the current song."""
  1699. server = ctx.message.server
  1700. if not self.is_playing(server):
  1701. await self.bot.say("I'm not playing on this server.")
  1702. return
  1703. song = self._get_queue_nowplaying(server)
  1704. if song:
  1705. if not hasattr(song, 'creator'):
  1706. song.creator = None
  1707. if not hasattr(song, 'view_count'):
  1708. song.view_count = None
  1709. if not hasattr(song, 'uploader'):
  1710. song.uploader = None
  1711. if song.rating is None:
  1712. song.rating = 0
  1713. if song.thumbnail is None:
  1714. song.thumbnail = (self.bot.user.avatar_url).replace('webp', 'png')
  1715. if hasattr(song, 'duration'):
  1716. m, s = divmod(song.duration, 60)
  1717. h, m = divmod(m, 60)
  1718. if h:
  1719. dur = "{0}:{1:0>2}:{2:0>2}".format(h, m, s)
  1720. else:
  1721. dur = "{0}:{1:0>2}".format(m, s)
  1722. else:
  1723. dur = None
  1724. msg = ("**Author:** `{}`\n**Uploader:** `{}`\n"
  1725. "**Duration:** `{}`\n**Rating: **`{:.2f}`\n**Views:** `{}`".format(
  1726. song.creator, song.uploader, str(datetime.timedelta(seconds=song.duration)), song.rating,
  1727. song.view_count))
  1728. msg += self._draw_play(song, server) + "\n"
  1729. colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
  1730. em = discord.Embed(description="", colour=int(colour, 16))
  1731. if 'http' not in song.webpage_url:
  1732. em.set_author(name=song.title)
  1733. else:
  1734. em.set_author(name=song.title, url=song.webpage_url)
  1735. em.set_thumbnail(url=song.thumbnail)
  1736. em.description = msg.replace('None', '-')
  1737. await self.bot.say("**Currently Playing:**", embed=em)
  1738. else:
  1739. await self.bot.say("Darude - Sandstorm.")
  1740. @commands.command(pass_context=True, no_pm=True)
  1741. async def stop(self, ctx):
  1742. """Stops a currently playing song or playlist. CLEARS QUEUE."""
  1743. server = ctx.message.server
  1744. if self.is_playing(server):
  1745. if ctx.message.author.voice_channel == server.me.voice_channel:
  1746. if self.can_instaskip(ctx.message.author):
  1747. await self.bot.say('Stopping...')
  1748. self._stop(server)
  1749. else:
  1750. await self.bot.say("You can't stop music when there are other"
  1751. " people in the channel! Vote to skip"
  1752. " instead.")
  1753. else:
  1754. await self.bot.say("You need to be in the voice channel to stop the music.")
  1755. else:
  1756. await self.bot.say("Can't stop if I'm not playing.")
  1757. @commands.command(name="yt", pass_context=True, no_pm=True)
  1758. async def yt_search(self, ctx, *, search_terms: str):
  1759. """Searches and plays a video from YouTube"""
  1760. await self.bot.say("Searching...")
  1761. await ctx.invoke(self.play, url_or_search_terms=search_terms)
  1762. def is_playing(self, server):
  1763. if not self.voice_connected(server):
  1764. return False
  1765. if self.voice_client(server) is None:
  1766. return False
  1767. if not hasattr(self.voice_client(server), 'audio_player'):
  1768. return False
  1769. if self.voice_client(server).audio_player.is_done():
  1770. return False
  1771. return True
  1772. async def cache_manager(self):
  1773. while self == self.bot.get_cog("Audio"):
  1774. if self._cache_too_large():
  1775. # Our cache is too big, dumping
  1776. log.debug("cache too large ({} > {}), dumping".format(
  1777. self._cache_size(), self._cache_max()))
  1778. self._dump_cache()
  1779. await asyncio.sleep(5) # No need to run this every half second
  1780. async def cache_scheduler(self):
  1781. await asyncio.sleep(30) # Extra careful
  1782. self.bot.loop.create_task(self.cache_manager())
  1783. def currently_downloading(self, server):
  1784. if server.id in self.downloaders:
  1785. if self.downloaders[server.id].is_alive():
  1786. return True
  1787. return False
  1788. async def disconnect_timer(self):
  1789. stop_times = {}
  1790. while self == self.bot.get_cog('Audio'):
  1791. for vc in self.bot.voice_clients:
  1792. server = vc.server
  1793. if not hasattr(vc, 'audio_player') and \
  1794. (server not in stop_times or
  1795. stop_times[server] is None):
  1796. log.debug("putting sid {} in stop loop, no player".format(
  1797. server.id))
  1798. stop_times[server] = int(time.time())
  1799. if hasattr(vc, 'audio_player'):
  1800. if vc.audio_player.is_done():
  1801. if server not in stop_times or stop_times[server] is None:
  1802. log.debug("putting sid {} in stop loop".format(server.id))
  1803. stop_times[server] = int(time.time())
  1804. noppl_disconnect = self.get_server_settings(server)
  1805. noppl_disconnect = noppl_disconnect.get("NOPPL_DISCONNECT", True)
  1806. if noppl_disconnect and len(vc.channel.voice_members) == 1:
  1807. if server not in stop_times or stop_times[server] is None:
  1808. log.debug("putting sid {} in stop loop".format(server.id))
  1809. stop_times[server] = int(time.time())
  1810. elif not vc.audio_player.is_done():
  1811. stop_times[server] = None
  1812. for server in stop_times:
  1813. if stop_times[server] and \
  1814. int(time.time()) - stop_times[server] > 300:
  1815. # 5 min not playing to d/c
  1816. timer_disconnect = self.get_server_settings(server)
  1817. timer_disconnect = timer_disconnect.get("TIMER_DISCONNECT", True)
  1818. if timer_disconnect:
  1819. log.debug("dcing from sid {} after 300s".format(server.id))
  1820. self._clear_queue(server)
  1821. await self._stop_and_disconnect(server)
  1822. stop_times[server] = None
  1823. await asyncio.sleep(5)
  1824. def get_server_settings(self, server):
  1825. try:
  1826. sid = server.id
  1827. except:
  1828. sid = server
  1829. if sid not in self.settings["SERVERS"]:
  1830. self.settings["SERVERS"][sid] = {}
  1831. ret = self.settings["SERVERS"][sid]
  1832. # Not the cleanest way. Some refactoring is suggested if more settings
  1833. # have to be added
  1834. if "NOPPL_DISCONNECT" not in ret:
  1835. ret["NOPPL_DISCONNECT"] = True
  1836. if "NOTIFY" not in ret:
  1837. ret["NOTIFY"] = False
  1838. if "NOTIFY_CHANNEL" not in ret:
  1839. ret["NOTIFY_CHANNEL"] = None
  1840. if "TIMER_DISCONNECT" not in ret:
  1841. ret["TIMER_DISCONNECT"] = True
  1842. for setting in self.server_specific_setting_keys:
  1843. if setting not in ret:
  1844. # Add the default
  1845. ret[setting] = self.settings[setting]
  1846. if setting.lower() == "volume" and ret[setting] <= 1:
  1847. ret[setting] *= 100
  1848. # ^This will make it so that only users with an outdated config will
  1849. # have their volume set * 100. In theory.
  1850. self.save_settings()
  1851. return ret
  1852. def has_connect_perm(self, author, server):
  1853. channel = author.voice_channel
  1854. if channel:
  1855. is_admin = channel.permissions_for(server.me).administrator
  1856. if channel.user_limit == 0:
  1857. is_full = False
  1858. else:
  1859. is_full = len(channel.voice_members) >= channel.user_limit
  1860. if channel is None:
  1861. raise AuthorNotConnected
  1862. elif channel.permissions_for(server.me).connect is False:
  1863. raise UnauthorizedConnect
  1864. elif channel.permissions_for(server.me).speak is False:
  1865. raise UnauthorizedSpeak
  1866. elif is_full and not is_admin:
  1867. raise ChannelUserLimit
  1868. else:
  1869. return True
  1870. return False
  1871. async def queue_manager(self, sid):
  1872. """This function assumes that there's something in the queue for us to
  1873. play"""
  1874. server = self.bot.get_server(sid)
  1875. if self.get_server_settings(server)["NOTIFY"] is True:
  1876. notify_channel = self.settings["SERVERS"][server.id]["NOTIFY_CHANNEL"]
  1877. if self.get_server_settings(server)["NOTIFY"] is False:
  1878. notify_channel = None
  1879. max_length = self.settings["MAX_LENGTH"]
  1880. # This is a reference, or should be at least
  1881. temp_queue = self.queue[server.id][QueueKey.TEMP_QUEUE]
  1882. queue = self.queue[server.id][QueueKey.QUEUE]
  1883. repeat = self.queue[server.id][QueueKey.REPEAT]
  1884. last_song = self.queue[server.id][QueueKey.NOW_PLAYING]
  1885. last_song_channel = self.queue[server.id][QueueKey.NOW_PLAYING_CHANNEL]
  1886. assert temp_queue is self.queue[server.id][QueueKey.TEMP_QUEUE]
  1887. assert queue is self.queue[server.id][QueueKey.QUEUE]
  1888. # _play handles creating the voice_client and player for us
  1889. if not self.is_playing(server):
  1890. log.debug("not playing anything on sid {}".format(server.id) +
  1891. ", attempting to start a new song.")
  1892. self.skip_votes[server.id] = []
  1893. # Reset skip votes for each new song
  1894. if len(temp_queue) > 0:
  1895. # Fake queue for irdumb's temp playlist songs
  1896. log.debug("calling _play because temp_queue is non-empty")
  1897. try:
  1898. queued_song = temp_queue.popleft()
  1899. url = queued_song.url
  1900. channel = queued_song.channel
  1901. song = await self._play(sid, url, channel)
  1902. await self.display_now_playing(server, song, notify_channel)
  1903. except MaximumLength:
  1904. return
  1905. elif len(queue) > 0: # We're in the normal queue
  1906. queued_song = queue.popleft()
  1907. url = queued_song.url
  1908. channel = queued_song.channel
  1909. log.debug("calling _play on the normal queue")
  1910. try:
  1911. song = await self._play(sid, url, channel)
  1912. await self.display_now_playing(server, song, notify_channel)
  1913. except MaximumLength:
  1914. return
  1915. if repeat and last_song:
  1916. queued_last_song = QueuedSong(last_song.webpage_url, last_song_channel)
  1917. queue.append(queued_last_song)
  1918. else:
  1919. song = None
  1920. self._set_queue_nowplaying(server, song, channel)
  1921. log.debug("set now_playing for sid {}".format(server.id))
  1922. self.bot.loop.create_task(self._update_bot_status())
  1923. elif server.id in self.downloaders:
  1924. # We're playing but we might be able to download a new song
  1925. curr_dl = self.downloaders.get(server.id)
  1926. if len(temp_queue) > 0:
  1927. queued_next_song = temp_queue.peekleft()
  1928. next_url = queued_next_song.url
  1929. next_channel = queued_next_song.channel
  1930. next_dl = Downloader(next_url, max_length)
  1931. elif len(queue) > 0:
  1932. queued_next_song = queue.peekleft()
  1933. next_url = queued_next_song.url
  1934. next_channel = queued_next_song.channel
  1935. next_dl = Downloader(next_url, max_length)
  1936. else:
  1937. next_dl = None
  1938. if next_dl is not None:
  1939. try:
  1940. # Download next song
  1941. next_dl.start()
  1942. await self._download_next(server, curr_dl, next_dl)
  1943. except YouTubeDlError as e:
  1944. if len(temp_queue) > 0:
  1945. temp_queue.popleft()
  1946. elif len(queue) > 0:
  1947. queue.popleft()
  1948. clean_url = self._clean_url(next_url)
  1949. message = ("I'm unable to play '{}' because of an "
  1950. "error:\n'{}'".format(clean_url, str(e)))
  1951. message = escape(message, mass_mentions=True)
  1952. await self.bot.send_message(next_channel, message)
  1953. async def display_now_playing(self, server, song, notify_channel:int):
  1954. channel = discord.utils.get(server.channels, id=notify_channel)
  1955. if channel is None:
  1956. return
  1957. if song.title is None:
  1958. return
  1959. def to_delete(m):
  1960. if "Now Playing" in m.content and m.author == self.bot.user:
  1961. return True
  1962. else:
  1963. return False
  1964. try:
  1965. await self.bot.purge_from(channel, limit=50, check=to_delete)
  1966. except discord.errors.Forbidden:
  1967. await self.bot.say("I need permissions to manage messages in this channel.")
  1968. if song:
  1969. if not hasattr(song, 'creator'):
  1970. song.creator = None
  1971. if not hasattr(song, 'uploader'):
  1972. song.uploader = None
  1973. if song.rating is None:
  1974. song.rating = 0
  1975. if song.thumbnail is None:
  1976. song.thumbnail = (self.bot.user.avatar_url).replace('webp', 'png')
  1977. msg = ("**Author:** `{}`\n**Uploader:** `{}`\n"
  1978. "**Duration:** `{}`\n**Rating: **`{:.2f}`\n**Views:** `{}`".format(
  1979. song.creator, song.uploader, str(datetime.timedelta(seconds=song.duration)), song.rating, song.view_count))
  1980. colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
  1981. em = discord.Embed(description="", colour=int(colour, 16))
  1982. if 'http' not in song.webpage_url:
  1983. em.set_author(name=song.title)
  1984. else:
  1985. em.set_author(name=song.title, url=song.webpage_url)
  1986. em.set_thumbnail(url=song.thumbnail)
  1987. em.description = msg.replace('None', '-')
  1988. await self.bot.send_message(channel, "**Now Playing:**", embed=em)
  1989. async def queue_scheduler(self):
  1990. while self == self.bot.get_cog('Audio'):
  1991. tasks = []
  1992. queue = copy.deepcopy(self.queue)
  1993. for sid in queue:
  1994. if len(queue[sid][QueueKey.QUEUE]) == 0 and \
  1995. len(queue[sid][QueueKey.TEMP_QUEUE]) == 0:
  1996. continue
  1997. # log.debug("scheduler found a non-empty queue"
  1998. # " for sid: {}".format(sid))
  1999. tasks.append(
  2000. self.bot.loop.create_task(self.queue_manager(sid)))
  2001. completed = [t.done() for t in tasks]
  2002. while not all(completed):
  2003. completed = [t.done() for t in tasks]
  2004. await asyncio.sleep(0.5)
  2005. await asyncio.sleep(1)
  2006. async def reload_monitor(self):
  2007. while self == self.bot.get_cog('Audio'):
  2008. await asyncio.sleep(0.5)
  2009. for vc in self.bot.voice_clients:
  2010. try:
  2011. vc.audio_player.stop()
  2012. except:
  2013. pass
  2014. def save_settings(self):
  2015. dataIO.save_json('data/audio/settings.json', self.settings)
  2016. def set_server_setting(self, server, key, value):
  2017. if server.id not in self.settings["SERVERS"]:
  2018. self.settings["SERVERS"][server.id] = {}
  2019. self.settings["SERVERS"][server.id][key] = value
  2020. def voice_client(self, server):
  2021. return self.bot.voice_client_in(server)
  2022. def voice_connected(self, server):
  2023. if self.bot.is_voice_connected(server):
  2024. return True
  2025. return False
  2026. async def voice_state_update(self, before, after):
  2027. server = after.server
  2028. # Member objects
  2029. if after.voice_channel != before.voice_channel:
  2030. try:
  2031. self.skip_votes[server.id].remove(after.id)
  2032. except (ValueError, KeyError):
  2033. pass
  2034. # Either the server ID or member ID already isn't in there
  2035. if after is None:
  2036. return
  2037. if server.id not in self.queue:
  2038. return
  2039. if after != server.me:
  2040. return
  2041. # Member is the bot
  2042. if before.voice_channel != after.voice_channel:
  2043. self._set_queue_channel(after.server, after.voice_channel)
  2044. if before.mute != after.mute:
  2045. vc = self.voice_client(server)
  2046. if after.mute and vc.audio_player.is_playing():
  2047. log.debug("Just got muted, pausing")
  2048. vc.audio_player.pause()
  2049. elif not after.mute and \
  2050. (not vc.audio_player.is_playing() and
  2051. not vc.audio_player.is_done()):
  2052. log.debug("just got unmuted, resuming")
  2053. vc.audio_player.resume()
  2054. def __unload(self):
  2055. for vc in self.bot.voice_clients:
  2056. self.bot.loop.create_task(vc.disconnect())
  2057. def check_folders():
  2058. folders = ("data/audio", "data/audio/cache", "data/audio/playlists",
  2059. "data/audio/localtracks", "data/audio/sfx")
  2060. for folder in folders:
  2061. if not os.path.exists(folder):
  2062. print("Creating " + folder + " folder...")
  2063. os.makedirs(folder)
  2064. def check_files():
  2065. default = {"VOLUME": 50, "MAX_LENGTH": 3700, "VOTE_ENABLED": True,
  2066. "MAX_CACHE": 0, "SOUNDCLOUD_CLIENT_ID": None,
  2067. "TITLE_STATUS": True, "AVCONV": False, "VOTE_THRESHOLD": 50,
  2068. "SERVERS": {}}
  2069. settings_path = "data/audio/settings.json"
  2070. if not os.path.isfile(settings_path):
  2071. print("Creating default audio settings.json...")
  2072. dataIO.save_json(settings_path, default)
  2073. else: # consistency check
  2074. try:
  2075. current = dataIO.load_json(settings_path)
  2076. except JSONDecodeError:
  2077. # settings.json keeps getting corrupted for unknown reasons. Let's
  2078. # try to keep it from making the cog load fail.
  2079. dataIO.save_json(settings_path, default)
  2080. current = dataIO.load_json(settings_path)
  2081. if current.keys() != default.keys():
  2082. for key in default.keys():
  2083. if key not in current.keys():
  2084. current[key] = default[key]
  2085. print(
  2086. "Adding " + str(key) + " field to audio settings.json")
  2087. dataIO.save_json(settings_path, current)
  2088. def verify_ffmpeg_avconv():
  2089. try:
  2090. subprocess.call(["ffmpeg", "-version"], stdout=subprocess.DEVNULL)
  2091. except FileNotFoundError:
  2092. pass
  2093. else:
  2094. return "ffmpeg"
  2095. try:
  2096. subprocess.call(["avconv", "-version"], stdout=subprocess.DEVNULL)
  2097. except FileNotFoundError:
  2098. return False
  2099. else:
  2100. return "avconv"
  2101. def setup(bot):
  2102. check_folders()
  2103. check_files()
  2104. if youtube_dl is None:
  2105. raise RuntimeError("You need to run `pip3 install youtube_dl`")
  2106. if opus is False:
  2107. raise RuntimeError(
  2108. "Your opus library's bitness must match your python installation's"
  2109. " bitness. They both must be either 32bit or 64bit.")
  2110. elif opus is None:
  2111. raise RuntimeError(
  2112. "You need to install ffmpeg and opus. See \"https://github.com/"
  2113. "Twentysix26/Red-DiscordBot/wiki/Requirements\"")
  2114. player = verify_ffmpeg_avconv()
  2115. if not player:
  2116. if os.name == "nt":
  2117. msg = "ffmpeg isn't installed"
  2118. else:
  2119. msg = "Neither ffmpeg nor avconv are installed"
  2120. raise RuntimeError(
  2121. "{}.\nConsult the guide for your operating system "
  2122. "and do ALL the steps in order.\n"
  2123. "https://twentysix26.github.io/Red-Docs/\n"
  2124. "".format(msg))
  2125. n = Audio(bot, player=player) # Praise 26
  2126. bot.add_cog(n)
  2127. bot.add_listener(n.voice_state_update, 'on_voice_state_update')
  2128. bot.loop.create_task(n.queue_scheduler())
  2129. bot.loop.create_task(n.disconnect_timer())
  2130. bot.loop.create_task(n.reload_monitor())
  2131. bot.loop.create_task(n.cache_scheduler())