mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[V3 Audio] Add Spotify support (#2328)
* [V3 Audio] Add Spotify support * [V3 Audio] Update LICENSE * Appeasing the style gods * Extra word removal on LICENSE * Update for #2389 Thanks to TrustyJAID for the help. * Playlist command support for Spotify URLs or codes * Add exception for dc while loading Spotify tracks * Allow Spotify urls by default in audioset restrict Matches the behavior of Spotify codes already being allowed by default. * Update audio.py * .format() moving * Added a character to try to make Travis behave
This commit is contained in:
parent
94c3a2fedd
commit
050300040c
28
LICENSE
28
LICENSE
@ -672,3 +672,31 @@ may consider it more useful to permit linking proprietary applications with
|
|||||||
the library. If this is what you want to do, use the GNU Lesser General
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License. But first, please read
|
Public License instead of this License. But first, please read
|
||||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
|
||||||
|
The Red-DiscordBot project contains subcomponents in audio.py that have a
|
||||||
|
separate copyright notice and license terms. Your use of the source code for
|
||||||
|
these subcomponents is subject to the terms and conditions of the following
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
This product bundles methods from https://github.com/Just-Some-Bots/MusicBot/
|
||||||
|
blob/master/musicbot/spotify.py which are available under an MIT license.
|
||||||
|
|
||||||
|
Copyright (c) 2015-2018 Just-Some-Bots (https://github.com/Just-Some-Bots)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import discord
|
import discord
|
||||||
from fuzzywuzzy import process
|
from fuzzywuzzy import process
|
||||||
@ -32,8 +33,8 @@ from .manager import shutdown_lavalink_server, start_lavalink_server, maybe_down
|
|||||||
|
|
||||||
_ = Translator("Audio", __file__)
|
_ = Translator("Audio", __file__)
|
||||||
|
|
||||||
__version__ = "0.0.8"
|
__version__ = "0.0.8b"
|
||||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
__author__ = ["aikaterna"]
|
||||||
|
|
||||||
log = logging.getLogger("red.audio")
|
log = logging.getLogger("red.audio")
|
||||||
|
|
||||||
@ -84,6 +85,8 @@ class Audio(commands.Cog):
|
|||||||
self._connect_task = None
|
self._connect_task = None
|
||||||
self._disconnect_task = None
|
self._disconnect_task = None
|
||||||
self._cleaned_up = False
|
self._cleaned_up = False
|
||||||
|
self.spotify_token = None
|
||||||
|
self.play_lock = {}
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
self._restart_connect()
|
self._restart_connect()
|
||||||
@ -331,6 +334,27 @@ class Audio(commands.Cog):
|
|||||||
await self.config.guild(ctx.guild).emptydc_timer.set(seconds)
|
await self.config.guild(ctx.guild).emptydc_timer.set(seconds)
|
||||||
await self.config.guild(ctx.guild).emptydc_enabled.set(enabled)
|
await self.config.guild(ctx.guild).emptydc_enabled.set(enabled)
|
||||||
|
|
||||||
|
@audioset.command()
|
||||||
|
@checks.mod_or_permissions(administrator=True)
|
||||||
|
async def jukebox(self, ctx, price: int):
|
||||||
|
"""Set a price for queueing tracks for non-mods. 0 to disable."""
|
||||||
|
if price < 0:
|
||||||
|
return await self._embed_msg(ctx, _("Can't be less than zero."))
|
||||||
|
if price == 0:
|
||||||
|
jukebox = False
|
||||||
|
await self._embed_msg(ctx, _("Jukebox mode disabled."))
|
||||||
|
else:
|
||||||
|
jukebox = True
|
||||||
|
await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_("Track queueing command price set to {price} {currency}.").format(
|
||||||
|
price=price, currency=await bank.get_currency_name(ctx.guild)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.config.guild(ctx.guild).jukebox_price.set(price)
|
||||||
|
await self.config.guild(ctx.guild).jukebox.set(jukebox)
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def maxlength(self, ctx, seconds):
|
async def maxlength(self, ctx, seconds):
|
||||||
@ -354,35 +378,6 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
await self.config.guild(ctx.guild).maxlength.set(seconds)
|
await self.config.guild(ctx.guild).maxlength.set(seconds)
|
||||||
|
|
||||||
@audioset.command()
|
|
||||||
@checks.admin_or_permissions(manage_roles=True)
|
|
||||||
async def role(self, ctx, role_name: discord.Role):
|
|
||||||
"""Set the role to use for DJ mode."""
|
|
||||||
await self.config.guild(ctx.guild).dj_role.set(role_name.id)
|
|
||||||
dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role())
|
|
||||||
await self._embed_msg(ctx, _("DJ role set to: {role.name}.").format(role=dj_role_obj))
|
|
||||||
|
|
||||||
@audioset.command()
|
|
||||||
@checks.mod_or_permissions(administrator=True)
|
|
||||||
async def jukebox(self, ctx, price: int):
|
|
||||||
"""Set a price for queueing tracks for non-mods. 0 to disable."""
|
|
||||||
if price < 0:
|
|
||||||
return await self._embed_msg(ctx, _("Can't be less than zero."))
|
|
||||||
if price == 0:
|
|
||||||
jukebox = False
|
|
||||||
await self._embed_msg(ctx, _("Jukebox mode disabled."))
|
|
||||||
else:
|
|
||||||
jukebox = True
|
|
||||||
await self._embed_msg(
|
|
||||||
ctx,
|
|
||||||
_("Track queueing command price set to {price} {currency}.").format(
|
|
||||||
price=price, currency=await bank.get_currency_name(ctx.guild)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.config.guild(ctx.guild).jukebox_price.set(price)
|
|
||||||
await self.config.guild(ctx.guild).jukebox.set(jukebox)
|
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.mod_or_permissions(manage_messages=True)
|
@checks.mod_or_permissions(manage_messages=True)
|
||||||
async def notify(self, ctx):
|
async def notify(self, ctx):
|
||||||
@ -406,6 +401,14 @@ class Audio(commands.Cog):
|
|||||||
ctx, _("Commercial links only: {true_or_false}.").format(true_or_false=not restrict)
|
ctx, _("Commercial links only: {true_or_false}.").format(true_or_false=not restrict)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@audioset.command()
|
||||||
|
@checks.admin_or_permissions(manage_roles=True)
|
||||||
|
async def role(self, ctx, role_name: discord.Role):
|
||||||
|
"""Set the role to use for DJ mode."""
|
||||||
|
await self.config.guild(ctx.guild).dj_role.set(role_name.id)
|
||||||
|
dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role())
|
||||||
|
await self._embed_msg(ctx, _("DJ role set to: {role.name}.").format(role=dj_role_obj))
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
async def settings(self, ctx):
|
async def settings(self, ctx):
|
||||||
"""Show the current settings."""
|
"""Show the current settings."""
|
||||||
@ -461,6 +464,33 @@ class Audio(commands.Cog):
|
|||||||
embed = discord.Embed(colour=await ctx.embed_colour(), description=box(msg, lang="ini"))
|
embed = discord.Embed(colour=await ctx.embed_colour(), description=box(msg, lang="ini"))
|
||||||
return await ctx.send(embed=embed)
|
return await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@audioset.command()
|
||||||
|
@checks.is_owner()
|
||||||
|
async def spotifyapi(self, ctx):
|
||||||
|
"""Instructions to set the Spotify API tokens."""
|
||||||
|
message = _(
|
||||||
|
f"1. Go to Spotify developers and log in with your Spotify account\n"
|
||||||
|
"(https://developer.spotify.com/dashboard/applications)\n"
|
||||||
|
'2. Click "Create An App"\n'
|
||||||
|
"3. Fill out the form provided with your app name, etc.\n"
|
||||||
|
'4. When asked if you\'re developing commercial integration select "No"\n'
|
||||||
|
"5. Accept the terms and conditions.\n"
|
||||||
|
"6. Copy your client ID and your client secret into:\n"
|
||||||
|
"`{prefix}set api spotify client_id,your_client_id "
|
||||||
|
"client_secret,your_client_secret`"
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
|
@checks.is_owner()
|
||||||
|
@audioset.command()
|
||||||
|
async def status(self, ctx):
|
||||||
|
"""Enable/disable tracks' titles as status."""
|
||||||
|
status = await self.config.status()
|
||||||
|
await self.config.status.set(not status)
|
||||||
|
await self._embed_msg(
|
||||||
|
ctx, _("Song titles as status: {true_or_false}.").format(true_or_false=not status)
|
||||||
|
)
|
||||||
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
@checks.mod_or_permissions(administrator=True)
|
@checks.mod_or_permissions(administrator=True)
|
||||||
async def thumbnail(self, ctx):
|
async def thumbnail(self, ctx):
|
||||||
@ -493,21 +523,30 @@ class Audio(commands.Cog):
|
|||||||
await self.config.guild(ctx.guild).vote_percent.set(percent)
|
await self.config.guild(ctx.guild).vote_percent.set(percent)
|
||||||
await self.config.guild(ctx.guild).vote_enabled.set(enabled)
|
await self.config.guild(ctx.guild).vote_enabled.set(enabled)
|
||||||
|
|
||||||
@checks.is_owner()
|
|
||||||
@audioset.command()
|
@audioset.command()
|
||||||
async def status(self, ctx):
|
@checks.is_owner()
|
||||||
"""Enable/disable tracks' titles as status."""
|
async def youtubeapi(self, ctx):
|
||||||
status = await self.config.status()
|
"""Instructions to set the YouTube API key."""
|
||||||
await self.config.status.set(not status)
|
message = _(
|
||||||
await self._embed_msg(
|
f"1. Go to Google Developers Console and log in with your Google account.\n"
|
||||||
ctx, _("Song titles as status: {true_or_false}.").format(true_or_false=not status)
|
"(https://console.developers.google.com/)\n"
|
||||||
)
|
"2. You should be prompted to create a new project (name does not matter).\n"
|
||||||
|
"3. Click on Enable APIs and Services at the top.\n"
|
||||||
|
"4. In the list of APIs choose or search for YouTube Data API v3 and click on it. Choose Enable.\n"
|
||||||
|
"5. Click on Credentials on the left navigation bar.\n"
|
||||||
|
"6. Click on Create Credential at the top.\n"
|
||||||
|
'7. At the top click the link for "API key".\n'
|
||||||
|
"8. No application restrictions are needed. Click Create at the bottom.\n"
|
||||||
|
"9. You now have a key to add to `{prefix}set api youtube api_key,your_api_key`"
|
||||||
|
).format(prefix=ctx.prefix)
|
||||||
|
await ctx.maybe_send_embed(message)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def audiostats(self, ctx):
|
async def audiostats(self, ctx):
|
||||||
"""Audio stats."""
|
"""Audio stats."""
|
||||||
server_num = len([p for p in lavalink.players if p.current is not None])
|
server_num = len([p for p in lavalink.players if p.current is not None])
|
||||||
|
total_num = len([p for p in lavalink.players])
|
||||||
server_list = []
|
server_list = []
|
||||||
|
|
||||||
for p in lavalink.players:
|
for p in lavalink.players:
|
||||||
@ -549,7 +588,7 @@ class Audio(commands.Cog):
|
|||||||
servers = "\n".join(server_list)
|
servers = "\n".join(server_list)
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
colour=await ctx.embed_colour(),
|
colour=await ctx.embed_colour(),
|
||||||
title=_("Connected in {num} servers:").format(num=server_num),
|
title=_("Playing in {num}/{total} servers:").format(num=server_num, total=total_num),
|
||||||
description=servers,
|
description=servers,
|
||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
@ -605,6 +644,7 @@ class Audio(commands.Cog):
|
|||||||
):
|
):
|
||||||
return await self._embed_msg(ctx, _("There are other people listening to music."))
|
return await self._embed_msg(ctx, _("There are other people listening to music."))
|
||||||
else:
|
else:
|
||||||
|
self._play_lock(ctx, False)
|
||||||
await lavalink.get_player(ctx.guild.id).stop()
|
await lavalink.get_player(ctx.guild.id).stop()
|
||||||
await lavalink.get_player(ctx.guild.id).disconnect()
|
await lavalink.get_player(ctx.guild.id).disconnect()
|
||||||
|
|
||||||
@ -977,6 +1017,7 @@ class Audio(commands.Cog):
|
|||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
async def play(self, ctx, *, query):
|
async def play(self, ctx, *, query):
|
||||||
"""Play a URL or search for a track."""
|
"""Play a URL or search for a track."""
|
||||||
|
|
||||||
guild_data = await self.config.guild(ctx.guild).all()
|
guild_data = await self.config.guild(ctx.guild).all()
|
||||||
restrict = await self.config.restrict()
|
restrict = await self.config.restrict()
|
||||||
if restrict:
|
if restrict:
|
||||||
@ -1021,6 +1062,13 @@ class Audio(commands.Cog):
|
|||||||
return await self._embed_msg(ctx, _("No tracks to play."))
|
return await self._embed_msg(ctx, _("No tracks to play."))
|
||||||
query = query.strip("<>")
|
query = query.strip("<>")
|
||||||
|
|
||||||
|
if "open.spotify.com" in query:
|
||||||
|
query = "spotify:{}".format(
|
||||||
|
re.sub("(http[s]?:\/\/)?(open.spotify.com)\/", "", query).replace("/", ":")
|
||||||
|
)
|
||||||
|
if query.startswith("spotify:"):
|
||||||
|
return await self._get_spotify_tracks(ctx, query)
|
||||||
|
|
||||||
if query.startswith("localtrack:"):
|
if query.startswith("localtrack:"):
|
||||||
await self._localtracks_check(ctx)
|
await self._localtracks_check(ctx)
|
||||||
query = query.replace("localtrack:", "").replace(
|
query = query.replace("localtrack:", "").replace(
|
||||||
@ -1030,9 +1078,116 @@ class Audio(commands.Cog):
|
|||||||
if not self._match_url(query) and not (query.lower().endswith(allowed_files)):
|
if not self._match_url(query) and not (query.lower().endswith(allowed_files)):
|
||||||
query = "ytsearch:{}".format(query)
|
query = "ytsearch:{}".format(query)
|
||||||
|
|
||||||
tracks = await player.get_tracks(query)
|
await self._enqueue_tracks(ctx, query)
|
||||||
if not tracks:
|
|
||||||
return await self._embed_msg(ctx, _("Nothing found."))
|
async def _get_spotify_tracks(self, ctx, query):
|
||||||
|
if ctx.invoked_with == "play":
|
||||||
|
enqueue_tracks = True
|
||||||
|
else:
|
||||||
|
enqueue_tracks = False
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
api_data = await self._check_api_tokens()
|
||||||
|
guild_data = await self.config.guild(ctx.guild).all()
|
||||||
|
if "open.spotify.com" in query:
|
||||||
|
query = "spotify:{}".format(
|
||||||
|
re.sub("(http[s]?:\/\/)?(open.spotify.com)\/", "", query).replace("/", ":")
|
||||||
|
)
|
||||||
|
if query.startswith("spotify:"):
|
||||||
|
if (
|
||||||
|
not api_data["spotify_client_id"]
|
||||||
|
or not api_data["spotify_client_secret"]
|
||||||
|
or not api_data["youtube_api"]
|
||||||
|
):
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_(
|
||||||
|
"The owner needs to set the Spotify client ID, Spotify client secret, "
|
||||||
|
"and YouTube API key before Spotify URLs or codes can be used. "
|
||||||
|
"\nSee `{prefix}audioset youtubeapi` and `{prefix}audioset spotifyapi` "
|
||||||
|
"for instructions."
|
||||||
|
).format(prefix=ctx.prefix),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if self.play_lock[ctx.message.guild.id]:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("Wait until the playlist has finished loading.")
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
parts = query.split(":")
|
||||||
|
if "track" in parts:
|
||||||
|
res = await self._make_spotify_req(
|
||||||
|
"https://api.spotify.com/v1/tracks/{0}".format(parts[-1])
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
query = "{} {}".format(res["artists"][0]["name"], res["name"])
|
||||||
|
if enqueue_tracks:
|
||||||
|
return await self._enqueue_tracks(ctx, query)
|
||||||
|
else:
|
||||||
|
tracks = await player.get_tracks(f"ytsearch:{query}")
|
||||||
|
if not tracks:
|
||||||
|
return await self._embed_msg(ctx, _("Nothing found."))
|
||||||
|
single_track = []
|
||||||
|
single_track.append(tracks[0])
|
||||||
|
return single_track
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_(
|
||||||
|
"The Spotify API key or client secret has not been set properly. "
|
||||||
|
"\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||||
|
).format(prefix=ctx.prefix),
|
||||||
|
)
|
||||||
|
elif "album" in parts:
|
||||||
|
query = parts[-1]
|
||||||
|
self._play_lock(ctx, True)
|
||||||
|
track_list = await self._spotify_playlist(
|
||||||
|
ctx, "album", api_data["youtube_api"], query
|
||||||
|
)
|
||||||
|
if not track_list:
|
||||||
|
self._play_lock(ctx, False)
|
||||||
|
return
|
||||||
|
if enqueue_tracks:
|
||||||
|
return await self._enqueue_tracks(ctx, track_list)
|
||||||
|
else:
|
||||||
|
return track_list
|
||||||
|
elif "playlist" in parts:
|
||||||
|
query = parts[-1]
|
||||||
|
self._play_lock(ctx, True)
|
||||||
|
if "user" in parts:
|
||||||
|
track_list = await self._spotify_playlist(
|
||||||
|
ctx, "user_playlist", api_data["youtube_api"], query
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
track_list = await self._spotify_playlist(
|
||||||
|
ctx, "playlist", api_data["youtube_api"], query
|
||||||
|
)
|
||||||
|
if not track_list:
|
||||||
|
self._play_lock(ctx, False)
|
||||||
|
return
|
||||||
|
if enqueue_tracks:
|
||||||
|
return await self._enqueue_tracks(ctx, track_list)
|
||||||
|
else:
|
||||||
|
return track_list
|
||||||
|
|
||||||
|
else:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("This doesn't seem to be a valid Spotify URL or code.")
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _enqueue_tracks(self, ctx, query):
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
guild_data = await self.config.guild(ctx.guild).all()
|
||||||
|
if type(query) is not list:
|
||||||
|
if not query.startswith("http"):
|
||||||
|
query = f"ytsearch:{query}"
|
||||||
|
tracks = await player.get_tracks(query)
|
||||||
|
if not tracks:
|
||||||
|
return await self._embed_msg(ctx, _("Nothing found."))
|
||||||
|
else:
|
||||||
|
tracks = query
|
||||||
|
|
||||||
queue_duration = await self._queue_duration(ctx)
|
queue_duration = await self._queue_duration(ctx)
|
||||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||||
@ -1071,14 +1226,20 @@ class Audio(commands.Cog):
|
|||||||
if not player.current:
|
if not player.current:
|
||||||
await player.play()
|
await player.play()
|
||||||
else:
|
else:
|
||||||
single_track = tracks[0]
|
try:
|
||||||
if guild_data["maxlength"] > 0:
|
single_track = tracks[0]
|
||||||
if self._track_limit(ctx, single_track, guild_data["maxlength"]):
|
if guild_data["maxlength"] > 0:
|
||||||
player.add(ctx.author, single_track)
|
if self._track_limit(ctx, single_track, guild_data["maxlength"]):
|
||||||
|
player.add(ctx.author, single_track)
|
||||||
|
else:
|
||||||
|
return await self._embed_msg(ctx, _("Track exceeds maximum length."))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return await self._embed_msg(ctx, _("Track exceeds maximum length."))
|
player.add(ctx.author, single_track)
|
||||||
else:
|
except IndexError:
|
||||||
player.add(ctx.author, single_track)
|
return await self._embed_msg(
|
||||||
|
ctx, _("Nothing found. Check your Lavalink logs for details.")
|
||||||
|
)
|
||||||
|
|
||||||
if "localtracks" in single_track.uri:
|
if "localtracks" in single_track.uri:
|
||||||
if not single_track.title == "Unknown title":
|
if not single_track.title == "Unknown title":
|
||||||
@ -1105,6 +1266,131 @@ class Audio(commands.Cog):
|
|||||||
if not player.current:
|
if not player.current:
|
||||||
await player.play()
|
await player.play()
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
if type(query) is list:
|
||||||
|
self._play_lock(ctx, False)
|
||||||
|
|
||||||
|
async def _spotify_playlist(self, ctx, stype, yt_key, query):
|
||||||
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
|
spotify_info = []
|
||||||
|
if stype == "album":
|
||||||
|
r = await self._make_spotify_req("https://api.spotify.com/v1/albums/{0}".format(query))
|
||||||
|
else:
|
||||||
|
r = await self._make_spotify_req(
|
||||||
|
"https://api.spotify.com/v1/playlists/{0}/tracks".format(query)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if r["error"]["status"] == 401:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx,
|
||||||
|
_(
|
||||||
|
"The Spotify API key or client secret has not been set properly. "
|
||||||
|
"\nUse `{prefix}audioset spotifyapi` for instructions."
|
||||||
|
).format(prefix=ctx.prefix),
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
spotify_info.extend(r["tracks"]["items"])
|
||||||
|
except KeyError:
|
||||||
|
spotify_info.extend(r["items"])
|
||||||
|
except KeyError:
|
||||||
|
return await self._embed_msg(
|
||||||
|
ctx, _("This doesn't seem to be a valid Spotify URL or code.")
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if r["next"] is not None:
|
||||||
|
r = await self._make_spotify_req(r["next"])
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except KeyError:
|
||||||
|
if r["tracks"]["next"] is not None:
|
||||||
|
r = await self._make_spotify_req(r["tracks"]["next"])
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
embed1 = discord.Embed(
|
||||||
|
colour=await ctx.embed_colour(), title=_("Please wait, adding tracks...")
|
||||||
|
)
|
||||||
|
playlist_msg = await ctx.send(embed=embed1)
|
||||||
|
track_list = []
|
||||||
|
track_count = 0
|
||||||
|
now = int(time.time())
|
||||||
|
for i in spotify_info:
|
||||||
|
if stype == "album":
|
||||||
|
song_info = "{} {}".format(i["name"], i["artists"][0]["name"])
|
||||||
|
else:
|
||||||
|
song_info = "{} {}".format(i["track"]["name"], i["track"]["artists"][0]["name"])
|
||||||
|
try:
|
||||||
|
track_url = await self._youtube_api_search(yt_key, song_info)
|
||||||
|
except:
|
||||||
|
error_embed = discord.Embed(
|
||||||
|
colour=await ctx.embed_colour(),
|
||||||
|
title=_(
|
||||||
|
"The YouTube API key has not been set properly.\n"
|
||||||
|
"Use `{prefix}audioset youtubeapi` for instructions."
|
||||||
|
).format(prefix=ctx.prefix),
|
||||||
|
)
|
||||||
|
await playlist_msg.edit(embed=error_embed)
|
||||||
|
return None
|
||||||
|
# let's complain about errors
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
yt_track = await player.get_tracks(track_url)
|
||||||
|
except (RuntimeError, aiohttp.client_exceptions.ServerDisconnectedError):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
track_list.append(yt_track[0])
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
track_count += 1
|
||||||
|
if (track_count % 5 == 0) or (track_count == len(spotify_info)):
|
||||||
|
embed2 = discord.Embed(
|
||||||
|
colour=await ctx.embed_colour(),
|
||||||
|
title=_("Loading track {num}/{total}...").format(
|
||||||
|
num=track_count, total=len(spotify_info)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if track_count == 5:
|
||||||
|
five_time = int(time.time()) - now
|
||||||
|
if track_count >= 5:
|
||||||
|
remain_tracks = len(spotify_info) - track_count
|
||||||
|
time_remain = (remain_tracks / 5) * five_time
|
||||||
|
if track_count < len(spotify_info):
|
||||||
|
seconds = self._dynamic_time(int(time_remain))
|
||||||
|
if track_count == len(spotify_info):
|
||||||
|
seconds = "0s"
|
||||||
|
embed2.set_footer(
|
||||||
|
text=_("Approximate time remaining: {seconds}").format(seconds=seconds)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await playlist_msg.edit(embed=embed2)
|
||||||
|
except discord.errors.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(track_list) == 0:
|
||||||
|
embed3 = discord.Embed(
|
||||||
|
colour=await ctx.embed_colour(),
|
||||||
|
title=_(
|
||||||
|
"Nothing found.\nThe YouTube API key may be invalid "
|
||||||
|
"or you may be rate limited on YouTube's search service.\n"
|
||||||
|
"Check the YouTube API key again and follow the instructions "
|
||||||
|
"at `{prefix}audioset youtubeapi`."
|
||||||
|
).format(prefix=ctx.prefix),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return await playlist_msg.edit(embed=embed3)
|
||||||
|
except discord.errors.NotFound:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
await playlist_msg.delete()
|
||||||
|
except discord.errors.NotFound:
|
||||||
|
pass
|
||||||
|
return track_list
|
||||||
|
|
||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -1680,21 +1966,42 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
async def _playlist_tracks(self, ctx, player, query):
|
async def _playlist_tracks(self, ctx, player, query):
|
||||||
search = False
|
search = False
|
||||||
|
tracklist = []
|
||||||
if type(query) is tuple:
|
if type(query) is tuple:
|
||||||
query = " ".join(query)
|
query = " ".join(query)
|
||||||
if not query.startswith("http"):
|
if "open.spotify.com" in query:
|
||||||
query = " ".join(query)
|
query = "spotify:{}".format(
|
||||||
query = "ytsearch:{}".format(query)
|
re.sub("(http[s]?:\/\/)?(open.spotify.com)\/", "", query).replace("/", ":")
|
||||||
search = True
|
)
|
||||||
tracks = await player.get_tracks(query)
|
if query.startswith("spotify:"):
|
||||||
if not tracks:
|
try:
|
||||||
return await self._embed_msg(ctx, _("Nothing found."))
|
if self.play_lock[ctx.message.guild.id]:
|
||||||
tracklist = []
|
return await self._embed_msg(
|
||||||
if not search:
|
ctx, _("Wait until the playlist has finished loading.")
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
tracks = await self._get_spotify_tracks(ctx, query)
|
||||||
|
if not tracks:
|
||||||
|
return await self._embed_msg(ctx, _("Nothing found."))
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
track_obj = self._track_creator(player, other_track=track)
|
track_obj = self._track_creator(player, other_track=track)
|
||||||
tracklist.append(track_obj)
|
tracklist.append(track_obj)
|
||||||
|
self._play_lock(ctx, False)
|
||||||
|
elif not query.startswith("http"):
|
||||||
|
query = " ".join(query)
|
||||||
|
query = "ytsearch:{}".format(query)
|
||||||
|
search = True
|
||||||
|
tracks = await player.get_tracks(query)
|
||||||
|
if not tracks:
|
||||||
|
return await self._embed_msg(ctx, _("Nothing found."))
|
||||||
else:
|
else:
|
||||||
|
tracks = await player.get_tracks(query)
|
||||||
|
if not search and len(tracklist) == 0:
|
||||||
|
for track in tracks:
|
||||||
|
track_obj = self._track_creator(player, other_track=track)
|
||||||
|
tracklist.append(track_obj)
|
||||||
|
elif len(tracklist) == 0:
|
||||||
track_obj = self._track_creator(player, other_track=tracks[0])
|
track_obj = self._track_creator(player, other_track=tracks[0])
|
||||||
tracklist.append(track_obj)
|
tracklist.append(track_obj)
|
||||||
return tracklist
|
return tracklist
|
||||||
@ -2715,6 +3022,17 @@ class Audio(commands.Cog):
|
|||||||
|
|
||||||
self._restart_connect()
|
self._restart_connect()
|
||||||
|
|
||||||
|
async def _check_api_tokens(self):
|
||||||
|
spotify = await self.bot.db.api_tokens.get_raw(
|
||||||
|
"spotify", default={"client_id": "", "client_secret": ""}
|
||||||
|
)
|
||||||
|
youtube = await self.bot.db.api_tokens.get_raw("youtube", default={"api_key": ""})
|
||||||
|
return {
|
||||||
|
"spotify_client_id": spotify["client_id"],
|
||||||
|
"spotify_client_secret": spotify["client_secret"],
|
||||||
|
"youtube_api": youtube["api_key"],
|
||||||
|
}
|
||||||
|
|
||||||
async def _check_external(self):
|
async def _check_external(self):
|
||||||
external = await self.config.use_external_lavalink()
|
external = await self.config.use_external_lavalink()
|
||||||
if not external:
|
if not external:
|
||||||
@ -2881,6 +3199,12 @@ class Audio(commands.Cog):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _play_lock(self, ctx, tf):
|
||||||
|
if tf:
|
||||||
|
self.play_lock[ctx.message.guild.id] = True
|
||||||
|
else:
|
||||||
|
self.play_lock[ctx.message.guild.id] = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _player_check(ctx):
|
def _player_check(ctx):
|
||||||
try:
|
try:
|
||||||
@ -2972,6 +3296,7 @@ class Audio(commands.Cog):
|
|||||||
"vimeo.com",
|
"vimeo.com",
|
||||||
"mixer.com",
|
"mixer.com",
|
||||||
"twitch.tv",
|
"twitch.tv",
|
||||||
|
"spotify.com",
|
||||||
"localtracks",
|
"localtracks",
|
||||||
]
|
]
|
||||||
query_url = urlparse(url)
|
query_url = urlparse(url)
|
||||||
@ -2989,6 +3314,80 @@ class Audio(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _youtube_api_search(self, yt_key, query):
|
||||||
|
params = {"q": query, "part": "id", "key": yt_key, "maxResults": 1, "type": "video"}
|
||||||
|
yt_url = "https://www.googleapis.com/youtube/v3/search"
|
||||||
|
async with self.session.request("GET", yt_url, params=params) as r:
|
||||||
|
if r.status == 400:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
search_response = await r.json()
|
||||||
|
for search_result in search_response.get("items", []):
|
||||||
|
if search_result["id"]["kind"] == "youtube#video":
|
||||||
|
return "https://www.youtube.com/watch?v={}".format(search_result["id"]["videoId"])
|
||||||
|
|
||||||
|
# Spotify-related methods below are originally from: https://github.com/Just-Some-Bots/MusicBot/blob/master/musicbot/spotify.py
|
||||||
|
|
||||||
|
async def _check_token(self, token):
|
||||||
|
now = int(time.time())
|
||||||
|
return token["expires_at"] - now < 60
|
||||||
|
|
||||||
|
async def _get_spotify_token(self):
|
||||||
|
if self.spotify_token and not await self._check_token(self.spotify_token):
|
||||||
|
return self.spotify_token["access_token"]
|
||||||
|
token = await self._request_token()
|
||||||
|
if token is None:
|
||||||
|
log.debug("Requested a token from Spotify, did not end up getting one.")
|
||||||
|
try:
|
||||||
|
token["expires_at"] = int(time.time()) + token["expires_in"]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
self.spotify_token = token
|
||||||
|
log.debug("Created a new access token for Spotify: {0}".format(token))
|
||||||
|
return self.spotify_token["access_token"]
|
||||||
|
|
||||||
|
async def _make_get(self, url, headers=None):
|
||||||
|
async with self.session.request("GET", url, headers=headers) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
log.debug(
|
||||||
|
"Issue making GET request to {0}: [{1.status}] {2}".format(
|
||||||
|
url, r, await r.json()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return await r.json()
|
||||||
|
|
||||||
|
async def _make_post(self, url, payload, headers=None):
|
||||||
|
async with self.session.post(url, data=payload, headers=headers) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
log.debug(
|
||||||
|
"Issue making POST request to {0}: [{1.status}] {2}".format(
|
||||||
|
url, r, await r.json()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return await r.json()
|
||||||
|
|
||||||
|
async def _make_spotify_req(self, url):
|
||||||
|
token = await self._get_spotify_token()
|
||||||
|
return await self._make_get(url, headers={"Authorization": "Bearer {0}".format(token)})
|
||||||
|
|
||||||
|
def _make_token_auth(self, client_id, client_secret):
|
||||||
|
auth_header = base64.b64encode((client_id + ":" + client_secret).encode("ascii"))
|
||||||
|
return {"Authorization": "Basic %s" % auth_header.decode("ascii")}
|
||||||
|
|
||||||
|
async def _request_token(self):
|
||||||
|
self.client_id = await self.bot.db.api_tokens.get_raw("spotify", default={"client_id": ""})
|
||||||
|
self.client_secret = await self.bot.db.api_tokens.get_raw(
|
||||||
|
"spotify", default={"client_secret": ""}
|
||||||
|
)
|
||||||
|
payload = {"grant_type": "client_credentials"}
|
||||||
|
headers = self._make_token_auth(
|
||||||
|
self.client_id["client_id"], self.client_secret["client_secret"]
|
||||||
|
)
|
||||||
|
r = await self._make_post(
|
||||||
|
"https://accounts.spotify.com/api/token", payload=payload, headers=headers
|
||||||
|
)
|
||||||
|
return r
|
||||||
|
|
||||||
async def on_voice_state_update(self, member, before, after):
|
async def on_voice_state_update(self, member, before, after):
|
||||||
if after.channel != before.channel:
|
if after.channel != before.channel:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user