diff --git a/redbot/cogs/streams/streams.py b/redbot/cogs/streams/streams.py index daa2d4bfa..42091e2df 100644 --- a/redbot/cogs/streams/streams.py +++ b/redbot/cogs/streams/streams.py @@ -58,6 +58,7 @@ class Streams(commands.Cog): "live_message_mention": False, "live_message_nomention": False, "ignore_reruns": False, + "ignore_schedule": False, } role_defaults = {"mention": False} @@ -223,9 +224,9 @@ class Streams(commands.Cog): apikey = await self.bot.get_shared_api_tokens("youtube") is_name = self.check_name_or_id(channel_id_or_name) if is_name: - stream = YoutubeStream(name=channel_id_or_name, token=apikey) + stream = YoutubeStream(name=channel_id_or_name, token=apikey, config=self.config) else: - stream = YoutubeStream(id=channel_id_or_name, token=apikey) + stream = YoutubeStream(id=channel_id_or_name, token=apikey, config=self.config) await self.check_online(ctx, stream) @commands.command() @@ -398,7 +399,7 @@ class Streams(commands.Cog): is_yt = _class.__name__ == "YoutubeStream" is_twitch = _class.__name__ == "TwitchStream" if is_yt and not self.check_name_or_id(channel_name): - stream = _class(id=channel_name, token=token) + stream = _class(id=channel_name, token=token, config=self.config) elif is_twitch: await self.maybe_renew_twitch_bearer_token() stream = _class( @@ -645,6 +646,19 @@ class Streams(commands.Cog): await self.config.guild(guild).ignore_reruns.set(True) await ctx.send(_("Streams of type 'rerun' will no longer send an alert.")) + @streamset.command(name="ignoreschedule") + @commands.guild_only() + async def ignore_schedule(self, ctx: commands.Context): + """Toggle excluding YouTube streams schedules from alerts.""" + guild = ctx.guild + current_setting = await self.config.guild(guild).ignore_schedule() + if current_setting: + await self.config.guild(guild).ignore_schedule.set(False) + await ctx.send(_("Streams schedules will be included in alerts.")) + else: + await self.config.guild(guild).ignore_schedule.set(True) + await ctx.send(_("Streams schedules will no longer send an alert.")) + async def add_or_remove(self, ctx: commands.Context, stream): if ctx.channel.id not in stream.channels: stream.channels.append(ctx.channel.id) @@ -705,6 +719,16 @@ class Streams(commands.Cog): pass await asyncio.sleep(await self.config.refresh_timer()) + async def _send_stream_alert( + self, stream, channel: discord.TextChannel, embed: discord.Embed, content: str = None + ): + m = await channel.send( + content, + embed=embed, + allowed_mentions=discord.AllowedMentions(roles=True, everyone=True), + ) + stream._messages_cache.append(m) + async def check_streams(self): for stream in self.streams: with contextlib.suppress(Exception): @@ -712,9 +736,12 @@ class Streams(commands.Cog): if stream.__class__.__name__ == "TwitchStream": await self.maybe_renew_twitch_bearer_token() embed, is_rerun = await stream.is_online() + elif stream.__class__.__name__ == "YoutubeStream": + embed, is_schedule = await stream.is_online() else: embed = await stream.is_online() is_rerun = False + is_schedule = False except OfflineStream: if not stream._messages_cache: continue @@ -739,7 +766,14 @@ class Streams(commands.Cog): ignore_reruns = await self.config.guild(channel.guild).ignore_reruns() if ignore_reruns and is_rerun: continue - + ignore_schedules = await self.config.guild(channel.guild).ignore_schedule() + if ignore_schedules and is_schedule: + continue + if is_schedule: + # skip messages and mentions + await self._send_stream_alert(stream, channel, embed) + await self.save_streams() + continue await set_contextual_locales_from_guild(self.bot, channel.guild) mention_str, edited_roles = await self._get_mention_str( @@ -780,13 +814,7 @@ class Streams(commands.Cog): str(stream.name), mass_mentions=True, formatting=True ) ) - - m = await channel.send( - content, - embed=embed, - allowed_mentions=discord.AllowedMentions(roles=True, everyone=True), - ) - stream._messages_cache.append(m) + await self._send_stream_alert(stream, channel, embed, content) if edited_roles: for role in edited_roles: await role.edit(mentionable=False) @@ -855,6 +883,8 @@ class Streams(commands.Cog): raw_stream["token"] = token.get("client_id") raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None) else: + if _class.__name__ == "YoutubeStream": + raw_stream["config"] = self.config raw_stream["token"] = token streams.append(_class(**raw_stream)) diff --git a/redbot/cogs/streams/streamtypes.py b/redbot/cogs/streams/streamtypes.py index e69ed1421..f8b958862 100644 --- a/redbot/cogs/streams/streamtypes.py +++ b/redbot/cogs/streams/streamtypes.py @@ -1,9 +1,12 @@ +import contextlib import json import logging +from dateutil.parser import parse as parse_time from random import choice from string import ascii_letters +from datetime import datetime, timedelta import xml.etree.ElementTree as ET -from typing import ClassVar, Optional, List +from typing import ClassVar, Optional, List, Tuple import aiohttp import discord @@ -17,7 +20,7 @@ from .errors import ( YoutubeQuotaExceeded, ) from redbot.core.i18n import Translator -from redbot.core.utils.chat_formatting import humanize_number +from redbot.core.utils.chat_formatting import humanize_number, humanize_timedelta TWITCH_BASE_URL = "https://api.twitch.tv" TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users" @@ -86,6 +89,7 @@ class YoutubeStream(Stream): def __init__(self, **kwargs): self.id = kwargs.pop("id", None) self._token = kwargs.pop("token", None) + self._config = kwargs.pop("config") self.not_livestreams: List[str] = [] self.livestreams: List[str] = [] @@ -128,8 +132,11 @@ class YoutubeStream(Stream): if ( stream_data and stream_data != "None" - and stream_data.get("actualStartTime", None) is not None and stream_data.get("actualEndTime", None) is None + and ( + stream_data.get("actualStartTime", None) is not None + or stream_data.get("scheduledStartTime", None) is not None + ) ): if video_id not in self.livestreams: self.livestreams.append(data["items"][0]["id"]) @@ -143,24 +150,52 @@ class YoutubeStream(Stream): # info from the RSS ... but incase you don't wanna deal with fully rewritting the # code for this part, as this is only a 2 quota query. if self.livestreams: - params = {"key": self._token["api_key"], "id": self.livestreams[-1], "part": "snippet"} + params = { + "key": self._token["api_key"], + "id": self.livestreams[-1], + "part": "snippet,liveStreamingDetails", + } async with aiohttp.ClientSession() as session: async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r: data = await r.json() - return self.make_embed(data) + return await self.make_embed(data) raise OfflineStream() - def make_embed(self, data): + async def make_embed(self, data): vid_data = data["items"][0] video_url = "https://youtube.com/watch?v={}".format(vid_data["id"]) title = vid_data["snippet"]["title"] thumbnail = vid_data["snippet"]["thumbnails"]["medium"]["url"] channel_title = vid_data["snippet"]["channelTitle"] embed = discord.Embed(title=title, url=video_url) + is_schedule = False + if vid_data["liveStreamingDetails"]["scheduledStartTime"] is not None: + if "actualStartTime" not in vid_data["liveStreamingDetails"]: + start_time = parse_time(vid_data["liveStreamingDetails"]["scheduledStartTime"]) + start_in = start_time.replace(tzinfo=None) - datetime.now() + embed.description = _("This stream will start in {time}").format( + time=humanize_timedelta( + timedelta=timedelta(minutes=start_in.total_seconds() // 60) + ) # getting rid of seconds + ) + embed.timestamp = start_time + is_schedule = True + else: + # repost message + to_remove = [] + for message in self._messages_cache: + if message.embeds[0].description is discord.Embed.Empty: + continue + with contextlib.suppress(Exception): + autodelete = await self._config.guild(message.guild).autodelete() + if autodelete: + await message.delete() + to_remove.append(message.id) + self._messages_cache = [x for x in self._messages_cache if x.id not in to_remove] embed.set_author(name=channel_title) embed.set_image(url=rnd(thumbnail)) embed.colour = 0x9255A5 - return embed + return embed, is_schedule async def fetch_id(self): return await self._fetch_channel_resource("id") diff --git a/setup.cfg b/setup.cfg index ffc5ee71b..f447e53a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ install_requires = idna==2.10 markdown==3.2.2 multidict==4.7.6 + python-dateutil==2.8.1 python-Levenshtein-wheels==0.13.1 pytz==2020.1 PyYAML==5.3.1