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:
parent
1a69557460
commit
cbb5c654c5
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue