[Streams] Support Youtube stream schedules (#4615)

* Catch scheduled livestreams

* Announce scheduled streams and starts

* Add setting, fix bugs

* Add new dependency

* Black reformat

* Fix the duplicated messages bug

* Do not send messages for schedules

* Format embed
This commit is contained in:
El Laggron 2020-11-18 21:48:36 +01:00 committed by GitHub
parent 13ca9a6c2e
commit 6060da0f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 18 deletions

View File

@ -58,6 +58,7 @@ class Streams(commands.Cog):
"live_message_mention": False, "live_message_mention": False,
"live_message_nomention": False, "live_message_nomention": False,
"ignore_reruns": False, "ignore_reruns": False,
"ignore_schedule": False,
} }
role_defaults = {"mention": False} role_defaults = {"mention": False}
@ -223,9 +224,9 @@ class Streams(commands.Cog):
apikey = await self.bot.get_shared_api_tokens("youtube") apikey = await self.bot.get_shared_api_tokens("youtube")
is_name = self.check_name_or_id(channel_id_or_name) is_name = self.check_name_or_id(channel_id_or_name)
if is_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: 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) await self.check_online(ctx, stream)
@commands.command() @commands.command()
@ -398,7 +399,7 @@ class Streams(commands.Cog):
is_yt = _class.__name__ == "YoutubeStream" is_yt = _class.__name__ == "YoutubeStream"
is_twitch = _class.__name__ == "TwitchStream" is_twitch = _class.__name__ == "TwitchStream"
if is_yt and not self.check_name_or_id(channel_name): 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: elif is_twitch:
await self.maybe_renew_twitch_bearer_token() await self.maybe_renew_twitch_bearer_token()
stream = _class( stream = _class(
@ -645,6 +646,19 @@ class Streams(commands.Cog):
await self.config.guild(guild).ignore_reruns.set(True) await self.config.guild(guild).ignore_reruns.set(True)
await ctx.send(_("Streams of type 'rerun' will no longer send an alert.")) 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): async def add_or_remove(self, ctx: commands.Context, stream):
if ctx.channel.id not in stream.channels: if ctx.channel.id not in stream.channels:
stream.channels.append(ctx.channel.id) stream.channels.append(ctx.channel.id)
@ -705,6 +719,16 @@ class Streams(commands.Cog):
pass pass
await asyncio.sleep(await self.config.refresh_timer()) 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): async def check_streams(self):
for stream in self.streams: for stream in self.streams:
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
@ -712,9 +736,12 @@ class Streams(commands.Cog):
if stream.__class__.__name__ == "TwitchStream": if stream.__class__.__name__ == "TwitchStream":
await self.maybe_renew_twitch_bearer_token() await self.maybe_renew_twitch_bearer_token()
embed, is_rerun = await stream.is_online() embed, is_rerun = await stream.is_online()
elif stream.__class__.__name__ == "YoutubeStream":
embed, is_schedule = await stream.is_online()
else: else:
embed = await stream.is_online() embed = await stream.is_online()
is_rerun = False is_rerun = False
is_schedule = False
except OfflineStream: except OfflineStream:
if not stream._messages_cache: if not stream._messages_cache:
continue continue
@ -739,7 +766,14 @@ class Streams(commands.Cog):
ignore_reruns = await self.config.guild(channel.guild).ignore_reruns() ignore_reruns = await self.config.guild(channel.guild).ignore_reruns()
if ignore_reruns and is_rerun: if ignore_reruns and is_rerun:
continue 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) await set_contextual_locales_from_guild(self.bot, channel.guild)
mention_str, edited_roles = await self._get_mention_str( 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 str(stream.name), mass_mentions=True, formatting=True
) )
) )
await self._send_stream_alert(stream, channel, embed, content)
m = await channel.send(
content,
embed=embed,
allowed_mentions=discord.AllowedMentions(roles=True, everyone=True),
)
stream._messages_cache.append(m)
if edited_roles: if edited_roles:
for role in edited_roles: for role in edited_roles:
await role.edit(mentionable=False) await role.edit(mentionable=False)
@ -855,6 +883,8 @@ class Streams(commands.Cog):
raw_stream["token"] = token.get("client_id") raw_stream["token"] = token.get("client_id")
raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None) raw_stream["bearer"] = self.ttv_bearer_cache.get("access_token", None)
else: else:
if _class.__name__ == "YoutubeStream":
raw_stream["config"] = self.config
raw_stream["token"] = token raw_stream["token"] = token
streams.append(_class(**raw_stream)) streams.append(_class(**raw_stream))

View File

@ -1,9 +1,12 @@
import contextlib
import json import json
import logging import logging
from dateutil.parser import parse as parse_time
from random import choice from random import choice
from string import ascii_letters from string import ascii_letters
from datetime import datetime, timedelta
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import ClassVar, Optional, List from typing import ClassVar, Optional, List, Tuple
import aiohttp import aiohttp
import discord import discord
@ -17,7 +20,7 @@ from .errors import (
YoutubeQuotaExceeded, YoutubeQuotaExceeded,
) )
from redbot.core.i18n import Translator 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_BASE_URL = "https://api.twitch.tv"
TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users" TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/helix/users"
@ -86,6 +89,7 @@ class YoutubeStream(Stream):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
self._token = kwargs.pop("token", None) self._token = kwargs.pop("token", None)
self._config = kwargs.pop("config")
self.not_livestreams: List[str] = [] self.not_livestreams: List[str] = []
self.livestreams: List[str] = [] self.livestreams: List[str] = []
@ -128,8 +132,11 @@ class YoutubeStream(Stream):
if ( if (
stream_data stream_data
and stream_data != "None" 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("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: if video_id not in self.livestreams:
self.livestreams.append(data["items"][0]["id"]) 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 # 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. # code for this part, as this is only a 2 quota query.
if self.livestreams: 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 aiohttp.ClientSession() as session:
async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r: async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r:
data = await r.json() data = await r.json()
return self.make_embed(data) return await self.make_embed(data)
raise OfflineStream() raise OfflineStream()
def make_embed(self, data): async def make_embed(self, data):
vid_data = data["items"][0] vid_data = data["items"][0]
video_url = "https://youtube.com/watch?v={}".format(vid_data["id"]) video_url = "https://youtube.com/watch?v={}".format(vid_data["id"])
title = vid_data["snippet"]["title"] title = vid_data["snippet"]["title"]
thumbnail = vid_data["snippet"]["thumbnails"]["medium"]["url"] thumbnail = vid_data["snippet"]["thumbnails"]["medium"]["url"]
channel_title = vid_data["snippet"]["channelTitle"] channel_title = vid_data["snippet"]["channelTitle"]
embed = discord.Embed(title=title, url=video_url) 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_author(name=channel_title)
embed.set_image(url=rnd(thumbnail)) embed.set_image(url=rnd(thumbnail))
embed.colour = 0x9255A5 embed.colour = 0x9255A5
return embed return embed, is_schedule
async def fetch_id(self): async def fetch_id(self):
return await self._fetch_channel_resource("id") return await self._fetch_channel_resource("id")

View File

@ -50,6 +50,7 @@ install_requires =
idna==2.10 idna==2.10
markdown==3.2.2 markdown==3.2.2
multidict==4.7.6 multidict==4.7.6
python-dateutil==2.8.1
python-Levenshtein-wheels==0.13.1 python-Levenshtein-wheels==0.13.1
pytz==2020.1 pytz==2020.1
PyYAML==5.3.1 PyYAML==5.3.1