[Streams] Twitch API v5 (#718)

Twitch API v5 makes use of IDs instead of usernames.
To migrate the current data, the cog will attempt to fetch and save all the missing stream IDs on loading (in chunks of 100)
This commit is contained in:
Twentysix 2017-04-18 23:27:00 +02:00 committed by GitHub
parent 6b725560f6
commit 719088d438

View File

@ -71,7 +71,8 @@ class Streams:
regex = r'^(https?\:\/\/)?(www\.)?(twitch\.tv\/)' regex = r'^(https?\:\/\/)?(www\.)?(twitch\.tv\/)'
stream = re.sub(regex, '', stream) stream = re.sub(regex, '', stream)
try: try:
embed = await self.twitch_online(stream) data = await self.fetch_twitch_ids(stream, raise_if_none=True)
embed = await self.twitch_online(data[0]["_id"])
except OfflineStream: except OfflineStream:
await self.bot.say(stream + " is offline.") await self.bot.say(stream + " is offline.")
except StreamNotFound: except StreamNotFound:
@ -117,7 +118,7 @@ class Streams:
stream = re.sub(regex, '', stream) stream = re.sub(regex, '', stream)
channel = ctx.message.channel channel = ctx.message.channel
try: try:
await self.twitch_online(stream) data = await self.fetch_twitch_ids(stream, raise_if_none=True)
except StreamNotFound: except StreamNotFound:
await self.bot.say("That stream doesn't exist.") await self.bot.say("That stream doesn't exist.")
return return
@ -129,12 +130,11 @@ class Streams:
"See `{}streamset twitchtoken`" "See `{}streamset twitchtoken`"
"".format(ctx.prefix)) "".format(ctx.prefix))
return return
except OfflineStream:
pass
enabled = self.enable_or_disable_if_active(self.twitch_streams, enabled = self.enable_or_disable_if_active(self.twitch_streams,
stream, stream,
channel) channel,
_id=data[0]["_id"])
if enabled: if enabled:
await self.bot.say("Alert activated. I will notify this channel " await self.bot.say("Alert activated. I will notify this channel "
@ -298,15 +298,17 @@ class Streams:
elif data["livestream"][0]["media_is_live"] == "0": elif data["livestream"][0]["media_is_live"] == "0":
raise OfflineStream() raise OfflineStream()
elif data["livestream"][0]["media_is_live"] == "1": elif data["livestream"][0]["media_is_live"] == "1":
data = self.hitbox_embed(data) return self.hitbox_embed(data)
return data
raise APIError() raise APIError()
async def twitch_online(self, stream): async def twitch_online(self, stream):
session = aiohttp.ClientSession() session = aiohttp.ClientSession()
url = "https://api.twitch.tv/kraken/streams/" + stream url = "https://api.twitch.tv/kraken/streams/" + stream
header = {'Client-ID': self.settings.get("TWITCH_TOKEN", "")} header = {
'Client-ID': self.settings.get("TWITCH_TOKEN", ""),
'Accept': 'application/vnd.twitchtv.v5+json'
}
async with session.get(url, headers=header) as r: async with session.get(url, headers=header) as r:
data = await r.json(encoding='utf-8') data = await r.json(encoding='utf-8')
@ -314,8 +316,7 @@ class Streams:
if r.status == 200: if r.status == 200:
if data["stream"] is None: if data["stream"] is None:
raise OfflineStream() raise OfflineStream()
embed = self.twitch_embed(data) return self.twitch_embed(data)
return embed
elif r.status == 400: elif r.status == 400:
raise InvalidCredentials() raise InvalidCredentials()
elif r.status == 404: elif r.status == 404:
@ -330,8 +331,7 @@ class Streams:
data = await r.json(encoding='utf-8') data = await r.json(encoding='utf-8')
if r.status == 200: if r.status == 200:
if data["online"] is True: if data["online"] is True:
data = self.beam_embed(data) return self.beam_embed(data)
return data
else: else:
raise OfflineStream() raise OfflineStream()
elif r.status == 404: elif r.status == 404:
@ -339,6 +339,36 @@ class Streams:
else: else:
raise APIError() raise APIError()
async def fetch_twitch_ids(self, *streams, raise_if_none=False):
def chunks(l):
for i in range(0, len(l), 100):
yield l[i:i + 100]
base_url = "https://api.twitch.tv/kraken/users?login="
header = {
'Client-ID': self.settings.get("TWITCH_TOKEN", ""),
'Accept': 'application/vnd.twitchtv.v5+json'
}
results = []
for streams_list in chunks(streams):
session = aiohttp.ClientSession()
url = base_url + ",".join(streams_list)
async with session.get(url, headers=header) as r:
data = await r.json()
if r.status == 200:
results.extend(data["users"])
elif r.status == 400:
raise InvalidCredentials()
else:
raise APIError()
await session.close()
if not results and raise_if_none:
raise StreamNotFound()
return results
def twitch_embed(self, data): def twitch_embed(self, data):
channel = data["stream"]["channel"] channel = data["stream"]["channel"]
url = channel["url"] url = channel["url"]
@ -396,7 +426,7 @@ class Streams:
embed.set_footer(text="Playing: " + data["type"]["name"]) embed.set_footer(text="Playing: " + data["type"]["name"])
return embed return embed
def enable_or_disable_if_active(self, streams, stream, channel): def enable_or_disable_if_active(self, streams, stream, channel, _id=None):
"""Returns True if enabled or False if disabled""" """Returns True if enabled or False if disabled"""
for i, s in enumerate(streams): for i, s in enumerate(streams):
if s["NAME"] != stream: if s["NAME"] != stream:
@ -411,15 +441,29 @@ class Streams:
streams[i]["CHANNELS"].append(channel.id) streams[i]["CHANNELS"].append(channel.id)
return True return True
streams.append({"CHANNELS": [channel.id], data = {"CHANNELS": [channel.id],
"NAME": stream, "NAME": stream,
"ALREADY_ONLINE": False}) "ALREADY_ONLINE": False}
if _id:
data["ID"] = _id
streams.append(data)
return True return True
async def stream_checker(self): async def stream_checker(self):
CHECK_DELAY = 60 CHECK_DELAY = 60
try:
await self._migration_twitch_v5()
except InvalidCredentials:
print("Error during convertion of twitch usernames to IDs: "
"invalid token")
except Exception as e:
print("Error during convertion of twitch usernames to IDs: "
"{}".format(e))
while self == self.bot.get_cog("Streams"): while self == self.bot.get_cog("Streams"):
save = False save = False
@ -428,10 +472,16 @@ class Streams:
(self.beam_streams, self.beam_online)) (self.beam_streams, self.beam_online))
for streams_list, parser in streams: for streams_list, parser in streams:
if parser == self.twitch_online:
_type = "ID"
else:
_type = "NAME"
for stream in streams_list: for stream in streams_list:
key = (parser, stream["NAME"]) if _type not in stream:
continue
key = (parser, stream[_type])
try: try:
embed = await parser(stream["NAME"]) embed = await parser(stream[_type])
except OfflineStream: except OfflineStream:
if stream["ALREADY_ONLINE"]: if stream["ALREADY_ONLINE"]:
stream["ALREADY_ONLINE"] = False stream["ALREADY_ONLINE"] = False
@ -483,6 +533,28 @@ class Streams:
"""Avoids Discord's caching""" """Avoids Discord's caching"""
return "?rnd=" + "".join([choice(ascii_letters) for i in range(6)]) return "?rnd=" + "".join([choice(ascii_letters) for i in range(6)])
async def _migration_twitch_v5(self):
# Migration of old twitch streams to API v5
to_convert = []
for stream in self.twitch_streams:
if "ID" not in stream:
to_convert.append(stream["NAME"])
if not to_convert:
return
results = await self.fetch_twitch_ids(*to_convert)
for stream in self.twitch_streams:
for result in results:
if stream["NAME"].lower() == result["name"].lower():
stream["ID"] = result["_id"]
# We might as well delete the invalid / renamed ones
self.twitch_streams = [s for s in self.twitch_streams if "ID" in s]
dataIO.save_json("data/streams/twitch.json", self.twitch_streams)
def check_folders(): def check_folders():
if not os.path.exists("data/streams"): if not os.path.exists("data/streams"):