Support for v5 twitch api

-Using v5 Twitch API
-Caching for userids
-Using the largest thumbnail available
-Adding description if available
-Changed feed date to creation time
-Changed feed title to use display name
-Some extra error handling + cleanup
This commit is contained in:
Laszlo Zeke 2017-01-21 20:13:40 +01:00
parent 1a69557460
commit cbb5c654c5
3 changed files with 68 additions and 31 deletions

View File

@ -1,9 +1,10 @@
application: twitchrss
version: a002
runtime: python27 runtime: python27
api_version: 1
threadsafe: true threadsafe: true
handlers: handlers:
- url: /favicon\.ico
static_files: favicon.ico
upload: favicon\.ico
- url: /.* - url: /.*
script: twitchrss.app script: twitchrss.app

BIN
TwitchRSS/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -1,5 +1,5 @@
# #
# Copyright 2016 Laszlo Zeke # Copyright 2017, 2016 Laszlo Zeke
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -24,6 +24,13 @@ from feedformatter import Feed
from google.appengine.api import memcache from google.appengine.api import memcache
TWITCH_CLIENT_ID = 'Insert_key_here' TWITCH_CLIENT_ID = 'Insert_key_here'
VODCACHE_PREFIX = 'vodcache'
USERIDCACHE_PREFIX = 'userid'
VOD_URL_TEMPLATE = 'https://api.twitch.tv/kraken/channels/%s/videos?broadcast_type=archive,highlight,upload'
USERID_URL_TEMPLATE = 'https://api.twitch.tv/kraken/users?login=%s'
VODCACHE_LIFETIME = 120
USERIDCACHE_LIFETIME = 0 # No expire
class MainPage(webapp2.RequestHandler): class MainPage(webapp2.RequestHandler):
def get(self): def get(self):
@ -51,22 +58,33 @@ class MainPage(webapp2.RequestHandler):
class RSSVoDServer(webapp2.RequestHandler): class RSSVoDServer(webapp2.RequestHandler):
def get(self, channel): def get(self, channel):
channel_json = self.lookup_cache(channel) userid_json = self.fetch_userid(channel)
if channel_json == '': (channel_display_name, channel_id) = self.extract_userid(json.loads(userid_json))
channel_json = self.fetch_json(channel) channel_json = self.fetch_vods(channel_id)
if channel_json == '':
self.abort(404)
else:
self.store_cache(channel, channel_json)
decoded_json = json.loads(channel_json) decoded_json = json.loads(channel_json)
rss_data = self.construct_rss(channel, decoded_json) rss_data = self.construct_rss(channel, decoded_json, channel_display_name)
self.response.headers['Content-Type'] = 'application/xhtml+xml' self.response.headers['Content-Type'] = 'application/xhtml+xml'
self.response.write(rss_data) self.response.write(rss_data)
def fetch_userid(self, channel_name):
return self.fetch_or_cache_object(channel_name, USERIDCACHE_PREFIX, USERID_URL_TEMPLATE, USERIDCACHE_LIFETIME)
def fetch_vods(self, channel_id):
return self.fetch_or_cache_object(channel_id, VODCACHE_PREFIX, VOD_URL_TEMPLATE, VODCACHE_LIFETIME)
def fetch_or_cache_object(self, channel, key_prefix, url_template, cache_time):
json_data = self.lookup_cache(channel, key_prefix)
if json_data == '':
json_data = self.fetch_json(channel, url_template)
if json_data == '':
self.abort(404)
else:
self.store_cache(channel, json_data, key_prefix, cache_time)
return json_data
@staticmethod @staticmethod
def lookup_cache(channel_name): def lookup_cache(channel_name, key_prefix):
cached_data = memcache.get('vodcache:%s' % channel_name) cached_data = memcache.get('%s:v5:%s' % (key_prefix, channel_name))
if cached_data is not None: if cached_data is not None:
logging.debug('Cache hit for %s' % channel_name) logging.debug('Cache hit for %s' % channel_name)
return cached_data return cached_data
@ -75,43 +93,58 @@ class RSSVoDServer(webapp2.RequestHandler):
return '' return ''
@staticmethod @staticmethod
def store_cache(channel_name, data): def store_cache(channel_name, data, key_prefix, cache_lifetime):
try: try:
logging.debug('Cached data for %s' % channel_name) logging.debug('Cached data for %s' % channel_name)
memcache.set('vodcache:%s' % channel_name, data, 120) memcache.set('%s:v5:%s' % (key_prefix, channel_name), data, cache_lifetime)
except: except BaseException as e:
logging.warning('Memcache exception: %s' % e)
return return
@staticmethod @staticmethod
def fetch_json(channel): def fetch_json(id, url_template):
url = 'https://api.twitch.tv/kraken/channels/%s/videos?broadcasts=true' % channel url = url_template % id
headers = { headers = {
'Accept': 'application/vnd.twitchtv.v3+json', 'Accept': 'application/vnd.twitchtv.v5+json',
'Client-ID': TWITCH_CLIENT_ID 'Client-ID': TWITCH_CLIENT_ID
} }
request = urllib2.Request(url, headers=headers) request = urllib2.Request(url, headers=headers)
try: try:
result = urllib2.urlopen(request) result = urllib2.urlopen(request)
logging.debug('Fetch from twitch for %s with code %s' % (channel, result.getcode())) logging.debug('Fetch from twitch for %s with code %s' % (id, result.getcode()))
return result.read() return result.read()
except urllib2.URLError, e: except urllib2.URLError as e:
logging.warning("Fetch exception caught: %s" % e)
return '' return ''
def construct_rss(self, channel_name, vods_info): def extract_userid(self, user_info):
userlist = user_info.get('users')
if userlist is None or len(userlist) < 1:
logging.info('No such user found.')
self.abort(404)
# Get the first id in the list
userid = userlist[0].get('_id')
username = userlist[0].get('display_name')
if username and userid:
return username, userid
else:
logging.warning('Userid is not found in %s' % user_info)
self.abort(404)
def construct_rss(self, channel_name, vods_info, display_name):
feed = Feed() feed = Feed()
# Set the feed/channel level properties # Set the feed/channel level properties
feed.feed["title"] = "%s's Twitch video RSS" % channel_name feed.feed["title"] = "%s's Twitch video RSS" % display_name
feed.feed["link"] = "https://twitchrss.appspot.com/" feed.feed["link"] = "https://twitchrss.appspot.com/"
feed.feed["author"] = "Twitch RSS Gen" feed.feed["author"] = "Twitch RSS Gen"
feed.feed["description"] = "The RSS Feed of %s's videos on Twitch" % channel_name feed.feed["description"] = "The RSS Feed of %s's videos on Twitch" % display_name
# Create an item # Create an item
try: try:
if vods_info['videos'] is not None: if vods_info['videos'] is not None:
for vod in vods_info['videos']: for vod in vods_info['videos']:
item = {} item = {}
link = ""
if vod["status"] == "recording": if vod["status"] == "recording":
link = "http://www.twitch.tv/%s" % channel_name link = "http://www.twitch.tv/%s" % channel_name
item["title"] = "%s - LIVE" % vod['title'] item["title"] = "%s - LIVE" % vod['title']
@ -119,15 +152,18 @@ class RSSVoDServer(webapp2.RequestHandler):
link = vod['url'] link = vod['url']
item["title"] = vod['title'] item["title"] = vod['title']
item["link"] = link item["link"] = link
item["description"] = "<a href=\"%s\"><img src=\"%s\" /></a>" % (link, vod['preview']) item["description"] = "<a href=\"%s\"><img src=\"%s\" /></a>" % (link, vod['preview']['large'])
d = datetime.datetime.strptime(vod['recorded_at'], '%Y-%m-%dT%H:%M:%SZ') if vod.get('description_html'):
item["description"] += "<br/>" + vod['description_html']
d = datetime.datetime.strptime(vod['created_at'], '%Y-%m-%dT%H:%M:%SZ')
item["pubDate"] = d.timetuple() item["pubDate"] = d.timetuple()
item["guid"] = vod['_id'] item["guid"] = vod['_id']
if vod["status"] == "recording": # To show a different news item when live is over if vod["status"] == "recording": # To show a different news item when recording is over
item["guid"] += "_live" item["guid"] += "_live"
item["ttl"] = '10' item["ttl"] = '10'
feed.items.append(item) feed.items.append(item)
except KeyError: except KeyError as e:
logging.warning('Issue with json: %s\nException: %s' % (vods_info, e))
self.abort(404) self.abort(404)
return feed.format_rss2_string() return feed.format_rss2_string()