From cee92e1cd25955640315816582d133c0764f7500 Mon Sep 17 00:00:00 2001 From: Twentysix Date: Sat, 28 Oct 2017 02:28:51 +0200 Subject: [PATCH] [Streams] [WIP] Rewrite (#782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Streams] [WIP] Rewrite * [Streams] [WIP] Settings related commands * [Streams] Moved to Config * [Streams] Added [p]streamalert stop * [Streams] Added toggleable autodeletion * [Streams] Added [p]streamalert list * [Streams] beam.pro -> Mixer * V3 streams modifications (#847) * [Streams] implement mention command * [Streams] implement mentions in stream alert check * [Streams] move to 3 separate commands for alert mentions * [Streams] implement support for alerting for twitch community streams… (#849) * [Streams] implement support for alerting for twitch community streams (Twentysix26/Red-DiscordBot/issues/824) * [Streams] async getter implementation * [Streams] repr for TwitchCommunity class * [Streams] move and fix imports (#994) * [Streams] function rename due to duplicate names * [Streams] drop force_registration * [Streams] fix command not displaying help --- redbot/cogs/streams/__init__.py | 5 + redbot/cogs/streams/errors.py | 26 ++ redbot/cogs/streams/main.py | 487 ++++++++++++++++++++++++++++++++ redbot/cogs/streams/streams.py | 313 ++++++++++++++++++++ 4 files changed, 831 insertions(+) create mode 100644 redbot/cogs/streams/__init__.py create mode 100644 redbot/cogs/streams/errors.py create mode 100644 redbot/cogs/streams/main.py create mode 100644 redbot/cogs/streams/streams.py diff --git a/redbot/cogs/streams/__init__.py b/redbot/cogs/streams/__init__.py new file mode 100644 index 000000000..570836701 --- /dev/null +++ b/redbot/cogs/streams/__init__.py @@ -0,0 +1,5 @@ +from .main import Streams + + +def setup(bot): + bot.add_cog(Streams(bot)) diff --git a/redbot/cogs/streams/errors.py b/redbot/cogs/streams/errors.py new file mode 100644 index 000000000..433e73ee0 --- /dev/null +++ b/redbot/cogs/streams/errors.py @@ -0,0 +1,26 @@ +class StreamsError(Exception): + pass + + +class StreamNotFound(StreamsError): + pass + + +class CommunityNotFound(StreamsError): + pass + + +class APIError(StreamsError): + pass + + +class InvalidCredentials(StreamsError): + pass + + +class OfflineStream(StreamsError): + pass + + +class OfflineCommunity(StreamsError): + pass \ No newline at end of file diff --git a/redbot/cogs/streams/main.py b/redbot/cogs/streams/main.py new file mode 100644 index 000000000..7b744662a --- /dev/null +++ b/redbot/cogs/streams/main.py @@ -0,0 +1,487 @@ +import discord +from discord.ext import commands +from redbot.core import Config, checks +from redbot.core.utils.chat_formatting import pagify, box +from redbot.core.bot import Red +from .streams import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity +from .errors import OfflineStream, StreamNotFound, APIError, InvalidCredentials, CommunityNotFound, OfflineCommunity +from . import streams as StreamClasses +from collections import defaultdict +import asyncio + +CHECK_DELAY = 60 + + +class Streams: + + global_defaults = { + "tokens": {}, + "streams": [] + } + + guild_defaults = { + "autodelete": False, + "mention_everyone": False, + "mention_here": False + } + + role_defaults = { + "mention": False + } + + def __init__(self, bot: Red): + self.db = Config.get_conf(self, 26262626) + + self.db.register_global(**self.global_defaults) + + self.db.register_guild(**self.guild_defaults) + + self.db.register_role(**self.role_defaults) + + self.bot = bot + + self.streams = self.bot.loop.create_task(self.load_streams()) + self.communities = self.bot.loop.create_task(self.load_communities()) + self.task = self.bot.loop.create_task(self._stream_alerts()) + + @commands.command() + async def twitch(self, ctx, channel_name: str): + """Checks if a Twitch channel is streaming""" + token = await self.db.tokens.get_attr(TwitchStream.__name__) + stream = TwitchStream(name=channel_name, + token=token) + await self.check_online(ctx, stream) + + @commands.command() + async def hitbox(self, ctx, channel_name: str): + """Checks if a Hitbox channel is streaming""" + stream = HitboxStream(name=channel_name) + await self.check_online(ctx, stream) + + @commands.command() + async def mixer(self, ctx, channel_name: str): + """Checks if a Mixer channel is streaming""" + stream = MixerStream(name=channel_name) + await self.check_online(ctx, stream) + + @commands.command() + async def picarto(self, ctx, channel_name: str): + """Checks if a Picarto channel is streaming""" + stream = PicartoStream(name=channel_name) + await self.check_online(ctx, stream) + + async def check_online(self, ctx, stream): + try: + embed = await stream.is_online() + except OfflineStream: + await ctx.send("The stream is offline.") + except StreamNotFound: + await ctx.send("The channel doesn't seem to exist.") + except InvalidCredentials: + await ctx.send("Invalid twitch token.") + except APIError: + await ctx.send("Error contacting the API.") + else: + await ctx.send(embed=embed) + + @commands.group() + @commands.guild_only() + @checks.mod() + async def streamalert(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.bot.send_cmd_help(ctx) + + @streamalert.group(name="twitch") + async def _twitch(self, ctx): + """Twitch stream alerts""" + if isinstance(ctx.invoked_subcommand, commands.Group): + await self.bot.send_cmd_help(ctx) + + @_twitch.command(name="channel") + async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str): + """Sets a Twitch stream alert notification in the channel""" + await self.stream_alert(ctx, TwitchStream, channel_name) + + @_twitch.command(name="community") + async def twitch_alert_community(self, ctx: commands.Context, community: str): + """Sets a Twitch stream alert notification in the channel + for the specified community.""" + await self.community_alert(ctx, TwitchCommunity, community) + + @streamalert.command(name="hitbox") + async def hitbox_alert(self, ctx, channel_name: str): + """Sets a Hitbox stream alert notification in the channel""" + await self.stream_alert(ctx, HitboxStream, channel_name) + + @streamalert.command(name="mixer") + async def mixer_alert(self, ctx, channel_name: str): + """Sets a Mixer stream alert notification in the channel""" + await self.stream_alert(ctx, MixerStream, channel_name) + + @streamalert.command(name="picarto") + async def picarto_alert(self, ctx, channel_name: str): + """Sets a Picarto stream alert notification in the channel""" + await self.stream_alert(ctx, PicartoStream, channel_name) + + @streamalert.command(name="stop") + async def streamalert_stop(self, ctx, _all: bool=False): + """Stops all stream notifications in the channel + + Adding 'yes' will disable all notifications in the server""" + streams = self.streams.copy() + local_channel_ids = [c.id for c in ctx.guild.channels] + to_remove = [] + + for stream in streams: + for channel_id in stream.channels: + if channel_id == ctx.channel.id: + stream.channels.remove(channel_id) + elif _all and ctx.channel.id in local_channel_ids: + if channel_id in stream.channels: + stream.channels.remove(channel_id) + + if not stream.channels: + to_remove.append(stream) + + for stream in to_remove: + streams.remove(stream) + + self.streams = streams + await self.save_streams() + + msg = "All {}'s stream alerts have been disabled." \ + "".format("server" if _all else "channel") + + await ctx.send(msg) + + @streamalert.command(name="list") + async def streamalert_list(self, ctx): + streams_list = defaultdict(list) + guild_channels_ids = [c.id for c in ctx.guild.channels] + msg = "Active stream alerts:\n\n" + + for stream in self.streams: + for channel_id in stream.channels: + if channel_id in guild_channels_ids: + streams_list[channel_id].append(stream.name) + + if not streams_list: + await ctx.send("There are no active stream alerts in this server.") + return + + for channel_id, streams in streams_list.items(): + channel = ctx.guild.get_channel(channel_id) + msg += "** - #{}**\n{}\n".format(channel, ", ".join(streams)) + + for page in pagify(msg): + await ctx.send(page) + + async def stream_alert(self, ctx, _class, channel_name): + stream = self.get_stream(_class, channel_name) + if not stream: + token = await self.db.tokens.get_attr(_class.__name__) + stream = _class(name=channel_name, + token=token) + if not await self.check_exists(stream): + await ctx.send("That channel doesn't seem to exist.") + return + + await self.add_or_remove(ctx, stream) + + async def community_alert(self, ctx, _class, community_name): + community = self.get_community(_class, community_name) + if not community: + token = await self.db.tokens.get_attr(_class.__name__) + community = _class(name=community_name, token=token) + try: + await community.get_community_streams() + except CommunityNotFound: + await ctx.send("That community doesn't seem to exist") + return + await self.add_or_remove_community(ctx, community) + + @commands.group() + @checks.mod() + async def streamset(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.bot.send_cmd_help(ctx) + + @streamset.command() + @checks.is_owner() + async def twitchtoken(self, ctx, token: str): + tokens = await self.db.tokens() + tokens["TwitchStream"] = token + tokens["TwitchCommunity"] = token + await self.db.tokens.set(tokens) + await ctx.send("Twitch token set.") + + @streamset.group() + @commands.guild_only() + async def mention(self, ctx): + """Sets mentions for stream alerts + Types: everyone, here, role, none""" + if ctx.invoked_subcommand is None: + await self.bot.send_cmd_help(ctx) + + @mention.command() + @commands.guild_only() + async def all(self, ctx): + """Toggles everyone mention""" + guild = ctx.guild + current_setting = await self.db.guild(guild).mention_everyone() + if current_setting: + await self.db.guild(guild).mention_everyone.set(False) + await ctx.send("@\u200beveryone will no longer be mentioned " + "for a stream alert.") + else: + await self.db.guild(guild).mention_everyone.set(True) + await ctx.send("When a stream configured for stream alerts " + "comes online, @\u200beveryone will be mentioned") + + @mention.command() + @commands.guild_only() + async def online(self, ctx): + """Toggles here mention""" + guild = ctx.guild + current_setting = await self.db.guild(guild).mention_here() + if current_setting: + await self.db.guild(guild).mention_here.set(False) + await ctx.send("@\u200bhere will no longer be mentioned " + "for a stream alert.") + else: + await self.db.guild(guild).mention_here.set(True) + await ctx.send("When a stream configured for stream alerts " + "comes online, @\u200bhere will be mentioned") + + @mention.command() + @commands.guild_only() + async def role(self, ctx, role: discord.Role): + """Toggles role mention""" + current_setting = await self.db.role(role).mention() + if not role.mentionable: + await ctx.send("That role is not mentionable!") + return + if current_setting: + await self.db.role(role).mention.set(False) + await ctx.send("@\u200b{} will no longer be mentioned " + "for a stream alert".format(role.name)) + else: + await self.db.role(role).mention.set(True) + await ctx.send("When a stream configured for stream alerts " + "comes online, @\u200b{} will be mentioned" + "".format(role.name)) + + @streamset.command() + @commands.guild_only() + async def autodelete(self, ctx, on_off: bool): + """Toggles automatic deletion of notifications for streams that go offline""" + await self.db.guild(ctx.guild).autodelete.set(on_off) + if on_off: + await ctx.send("The notifications will be deleted once " + "streams go offline.") + else: + await ctx.send("Notifications will never be deleted.") + + async def add_or_remove(self, ctx, stream): + if ctx.channel.id not in stream.channels: + stream.channels.append(ctx.channel.id) + if stream not in self.streams: + self.streams.append(stream) + await ctx.send("I'll send a notification in this channel when {} " + "is online.".format(stream.name)) + else: + stream.channels.remove(ctx.channel.id) + if not stream.channels: + self.streams.remove(stream) + await ctx.send("I won't send notifications about {} in this " + "channel anymore.".format(stream.name)) + + await self.save_streams() + + async def add_or_remove_community(self, ctx, community): + if ctx.channel.id not in community.channels: + community.channels.append(ctx.channel.id) + if community not in self.communities: + self.communities.append(community) + await ctx.send("I'll send a notification in this channel when a " + "channel is streaming to the {} community" + "".format(community.name)) + else: + community.channels.remove(ctx.channel.id) + if not community.channels: + self.communities.remove(community) + await ctx.send("I won't send notifications about channels streaming " + "to the {} community in this channel anymore" + "".format(community.name)) + await self.save_communities() + + def get_stream(self, _class, name): + for stream in self.streams: + # if isinstance(stream, _class) and stream.name == name: + # return stream + # Reloading this cog causes an issue with this check ^ + # isinstance will always return False + # As a workaround, we'll compare the class' name instead. + # Good enough. + if stream.type == _class.__name__ and stream.name == name: + return stream + + def get_community(self, _class, name): + for community in self.communities: + if community.type == _class.__name__ and community.name == name: + return community + + async def check_exists(self, stream): + try: + await stream.is_online() + except OfflineStream: + pass + except: + return False + return True + + async def _stream_alerts(self): + while True: + try: + await self.check_streams() + except asyncio.CancelledError: + pass + try: + await self.check_communities() + except asyncio.CancelledError: + pass + await asyncio.sleep(CHECK_DELAY) + + async def check_streams(self): + for stream in self.streams: + try: + embed = await stream.is_online() + except OfflineStream: + for message in stream._messages_cache: + try: + autodelete = self.db.guild(message.guild).autodelete() + if autodelete: + await message.delete() + except: + pass + stream._messages_cache.clear() + except: + pass + else: + if stream._messages_cache: + continue + for channel_id in stream.channels: + channel = self.bot.get_channel(channel_id) + mention_everyone = await self.db.guild(channel.guild).mention_everyone() + mention_here = await self.db.guild(channel.guild).mention_here() + mention_roles = [] + for r in channel.guild.roles: + to_append = { + "role": r, + "enabled": await self.db.role(r).mention() + } + mention_roles.append(to_append) + mention = None + if mention_everyone or mention_here or any(mention_roles): + mention = True + if mention: + mention_str = "" + if mention_everyone: + mention_str += "@everyone " + if mention_here: + mention_str += "@here " + if any(mention_roles): + mention_str += " ".join( + [ + r["role"].mention for r in mention_roles + if r["role"].mentionable and r["enabled"] + ] + ) + mention_str = mention_str.strip() + try: + m = await channel.send( + "{}, {} is online!".format( + mention_str, stream.name + ), embed=embed + ) + stream._messages_cache.append(m) + except: + pass + else: + try: + m = await channel.send("%s is online!" % stream.name, + embed=embed) + stream._messages_cache.append(m) + except: + pass + + async def check_communities(self): + for community in self.communities: + try: + streams = community.get_community_streams() + except CommunityNotFound: + print("Community {} not found!".format(community.name)) + continue + except OfflineCommunity: + pass + else: + token = self.db.tokens().get(TwitchStream.__name__) + for channel in community.channels: + chn = self.bot.get_channel(channel) + await chn.send("Online streams for {}".format(community.name)) + for stream in streams: + stream_obj = TwitchStream( + token=token, name=stream["channel"]["name"], + id=stream["_id"] + ) + try: + emb = await stream_obj.is_online() + except: + pass + else: + for channel in community.channels: + chn = self.bot.get_channel(channel) + await chn.send(embed=emb) + + async def load_streams(self): + streams = [] + + for raw_stream in await self.db.streams(): + _class = getattr(StreamClasses, raw_stream["type"], None) + if not _class: + continue + + token = await self.db.tokens.get_attr(_class.__name__) + streams.append(_class(token=token, **raw_stream)) + + return streams + + async def load_communities(self): + communities = [] + + for raw_community in await self.db.communities(): + _class = getattr(StreamClasses, raw_community["type"], None) + if not _class: + continue + + token = await self.db.tokens.get_attr(_class.__name__) + communities.append(_class(token=token, **raw_community)) + + return communities + + async def save_streams(self): + raw_streams = [] + for stream in self.streams: + raw_streams.append(stream.export()) + + await self.db.streams.set(raw_streams) + + async def save_communities(self): + raw_communities = [] + for community in self.communities: + raw_communities.append(community.export()) + + await self.db.communities.set(raw_communities) + + def __unload(self): + self.task.cancel() diff --git a/redbot/cogs/streams/streams.py b/redbot/cogs/streams/streams.py new file mode 100644 index 000000000..3310ed472 --- /dev/null +++ b/redbot/cogs/streams/streams.py @@ -0,0 +1,313 @@ +from .errors import StreamNotFound, APIError, InvalidCredentials, OfflineStream, CommunityNotFound, OfflineCommunity +from random import choice +from string import ascii_letters +import discord +import aiohttp +import json + +TWITCH_BASE_URL = "https://api.twitch.tv" +TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/kraken/users?login=" +TWITCH_STREAMS_ENDPOINT = TWITCH_BASE_URL + "/kraken/streams/" +TWITCH_COMMUNITIES_ENDPOINT = TWITCH_BASE_URL + "/kraken/communities" + + +def rnd(url): + """Appends a random parameter to the url to avoid Discord's caching""" + return url + "?rnd=" + "".join([choice(ascii_letters) for i in range(6)]) + + +class TwitchCommunity: + def __init__(self, **kwargs): + self.name = kwargs.pop("name") + self.id = kwargs.pop("id", None) + self.channels = kwargs.pop("channels", []) + self._token = kwargs.pop("token", None) + self.type = self.__class__.__name__ + + async def get_community_id(self): + session = aiohttp.ClientSession() + headers = { + "Accept": "application/vnd.twitchtv.v5+json", + "Client-ID": str(self._token) + } + params = { + "name": self.name + } + async with session.get(TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params) as r: + data = await r.json() + await session.close() + if "status" in data and data["status"] == 404: + raise CommunityNotFound() + return data["_id"] + + async def get_community_streams(self): + if not self.id: + try: + self.id = await self.get_community_id() + except CommunityNotFound: + raise + session = aiohttp.ClientSession() + headers = { + "Accept": "application/vnd.twitchtv.v5+json", + "Client-ID": str(self._token) + } + params = { + "community_id": self.id + } + url = TWITCH_BASE_URL + "/kraken/streams" + async with session.get(url, headers=headers, params=params) as r: + data = await r.json() + if data["_total"] == 0: + raise OfflineCommunity() + else: + return data["streams"] + + def export(self): + data = {} + for k, v in self.__dict__.items(): + if not k.startswith("_"): + data[k] = v + return data + + def __repr__(self): + return "<{0.__class__.__name__}: {0.name}>".format(self) + + +class Stream: + def __init__(self, **kwargs): + self.name = kwargs.pop("name") + self.channels = kwargs.pop("channels", []) + #self.already_online = kwargs.pop("already_online", False) + self._messages_cache = [] + self.type = self.__class__.__name__ + + async def is_online(self): + raise NotImplementedError() + + def make_embed(self): + raise NotImplementedError() + + def export(self): + data = {} + for k, v in self.__dict__.items(): + if not k.startswith("_"): + data[k] = v + return data + + def __repr__(self): + return "<{0.__class__.__name__}: {0.name}>".format(self) + + +class TwitchStream(Stream): + def __init__(self, **kwargs): + self.id = kwargs.pop("id", None) + self._token = kwargs.pop("token", None) + super().__init__(**kwargs) + + async def is_online(self): + if not self.id: + self.id = await self.fetch_id() + + session = aiohttp.ClientSession() + url = TWITCH_STREAMS_ENDPOINT + self.id + header = { + 'Client-ID': str(self._token), + 'Accept': 'application/vnd.twitchtv.v5+json' + } + + async with session.get(url, headers=header) as r: + data = await r.json(encoding='utf-8') + await session.close() + if r.status == 200: + if data["stream"] is None: + #self.already_online = False + raise OfflineStream() + #self.already_online = True + # In case of rename + self.name = data["stream"]["channel"]["name"] + return self.make_embed(data) + elif r.status == 400: + raise InvalidCredentials() + elif r.status == 404: + raise StreamNotFound() + else: + raise APIError() + + async def fetch_id(self): + header = { + 'Client-ID': str(self._token), + 'Accept': 'application/vnd.twitchtv.v5+json' + } + url = TWITCH_ID_ENDPOINT + self.name + session = aiohttp.ClientSession() + + async with session.get(url, headers=header) as r: + data = await r.json() + await session.close() + + if r.status == 200: + if not data["users"]: + raise StreamNotFound() + return data["users"][0]["_id"] + elif r.status == 400: + raise InvalidCredentials() + else: + raise APIError() + + def make_embed(self, data): + channel = data["stream"]["channel"] + url = channel["url"] + logo = channel["logo"] + if logo is None: + logo = ("https://static-cdn.jtvnw.net/" + "jtv_user_pictures/xarth/404_user_70x70.png") + status = channel["status"] + if not status: + status = "Untitled broadcast" + embed = discord.Embed(title=status, url=url) + embed.set_author(name=channel["display_name"]) + embed.add_field(name="Followers", value=channel["followers"]) + embed.add_field(name="Total views", value=channel["views"]) + embed.set_thumbnail(url=logo) + if data["stream"]["preview"]["medium"]: + embed.set_image(url=rnd(data["stream"]["preview"]["medium"])) + if channel["game"]: + embed.set_footer(text="Playing: " + channel["game"]) + embed.color = 0x6441A4 + + return embed + + def __repr__(self): + return "<{0.__class__.__name__}: {0.name} (ID: {0.id})>".format(self) + + +class HitboxStream(Stream): + async def is_online(self): + session = aiohttp.ClientSession() + url = "https://api.hitbox.tv/media/live/" + self.name + + async with session.get(url) as r: + #data = await r.json(encoding='utf-8') + data = await r.text() + await session.close() + data = json.loads(data, strict=False) + if "livestream" not in data: + raise StreamNotFound() + elif data["livestream"][0]["media_is_live"] == "0": + #self.already_online = False + raise OfflineStream() + elif data["livestream"][0]["media_is_live"] == "1": + #self.already_online = True + return self.make_embed(data) + + raise APIError() + + def make_embed(self, data): + base_url = "https://edge.sf.hitbox.tv" + livestream = data["livestream"][0] + channel = livestream["channel"] + url = channel["channel_link"] + embed = discord.Embed(title=livestream["media_status"], url=url) + embed.set_author(name=livestream["media_name"]) + embed.add_field(name="Followers", value=channel["followers"]) + embed.set_thumbnail(url=base_url + channel["user_logo"]) + if livestream["media_thumbnail"]: + embed.set_image(url=rnd(base_url + livestream["media_thumbnail"])) + embed.set_footer(text="Playing: " + livestream["category_name"]) + embed.color = 0x98CB00 + + return embed + + +class MixerStream(Stream): + async def is_online(self): + url = "https://mixer.com/api/v1/channels/" + self.name + + session = aiohttp.ClientSession() + async with session.get(url) as r: + #data = await r.json(encoding='utf-8') + data = await r.text(encoding='utf-8') + await session.close() + if r.status == 200: + data = json.loads(data, strict=False) + if data["online"] is True: + #self.already_online = True + return self.make_embed(data) + else: + #self.already_online = False + raise OfflineStream() + elif r.status == 404: + raise StreamNotFound() + else: + raise APIError() + + def make_embed(self, data): + default_avatar = ("https://mixer.com/_latest/assets/images/main/" + "avatars/default.jpg") + user = data["user"] + url = "https://mixer.com/" + data["token"] + embed = discord.Embed(title=data["name"], url=url) + embed.set_author(name=user["username"]) + embed.add_field(name="Followers", value=data["numFollowers"]) + embed.add_field(name="Total views", value=data["viewersTotal"]) + if user["avatarUrl"]: + embed.set_thumbnail(url=user["avatarUrl"]) + else: + embed.set_thumbnail(url=default_avatar) + if data["thumbnail"]: + embed.set_image(url=rnd(data["thumbnail"]["url"])) + embed.color = 0x4C90F3 + if data["type"] is not None: + embed.set_footer(text="Playing: " + data["type"]["name"]) + return embed + + +class PicartoStream(Stream): + async def is_online(self): + url = "https://api.picarto.tv/v1/channel/name/" + self.name + + session = aiohttp.ClientSession() + + async with session.get(url) as r: + data = await r.text(encoding='utf-8') + await session.close() + if r.status == 200: + data = json.loads(data) + if data["online"] is True: + #self.already_online = True + return self.make_embed(data) + else: + #self.already_online = False + raise OfflineStream() + elif r.status == 404: + raise StreamNotFound() + else: + raise APIError() + + def make_embed(self, data): + avatar = rnd("https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg" + "".format(data["name"].lower())) + url = "https://picarto.tv/" + data["name"] + thumbnail = ("https://thumb.picarto.tv/thumbnail/{}.jpg" + "".format(data["name"])) + embed = discord.Embed(title=data["title"], url=url) + embed.set_author(name=data["name"]) + embed.set_image(url=rnd(thumbnail)) + embed.add_field(name="Followers", value=data["followers"]) + embed.add_field(name="Total views", value=data["viewers_total"]) + embed.set_thumbnail(url=avatar) + embed.color = 0x132332 + data["tags"] = ", ".join(data["tags"]) + + if not data["tags"]: + data["tags"] = "None" + + if data["adult"]: + data["adult"] = "NSFW | " + else: + data["adult"] = "" + + embed.color = 0x4C90F3 + embed.set_footer(text="{adult}Category: {category} | Tags: {tags}" + "".format(**data)) + return embed