[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_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))

View File

@ -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")

View File

@ -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