220 lines
7.2 KiB
Python
220 lines
7.2 KiB
Python
#
|
|
# Copyright 2020 Laszlo Zeke
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
from cachetools import cached, TTLCache, LRUCache
|
|
from feedformatter import Feed
|
|
from flask import abort, Flask, request
|
|
from io import BytesIO
|
|
from os import environ
|
|
import datetime
|
|
import gzip
|
|
import time
|
|
import json
|
|
import logging
|
|
import re
|
|
import urllib
|
|
|
|
|
|
VOD_URL_TEMPLATE = 'https://api.twitch.tv/helix/videos?user_id=%s&type=all'
|
|
USERID_URL_TEMPLATE = 'https://api.twitch.tv/helix/users?login=%s'
|
|
VODCACHE_LIFETIME = 10 * 60
|
|
USERIDCACHE_LIFETIME = 24 * 60 * 60
|
|
CHANNEL_FILTER = re.compile("^[a-zA-Z0-9_]{2,25}$")
|
|
TWITCH_CLIENT_ID = environ.get("TWITCH_CLIENT_ID")
|
|
TWITCH_SECRET = environ.get("TWITCH_SECRET")
|
|
TWITCH_OAUTH_TOKEN = ""
|
|
TWITCH_OAUTH_EXPIRE_EPOCH = 0
|
|
logging.basicConfig(level=logging.DEBUG if environ.get('DEBUG') else logging.INFO)
|
|
|
|
if not TWITCH_CLIENT_ID:
|
|
raise Exception("Twitch API client id is not set.")
|
|
if not TWITCH_SECRET:
|
|
raise Exception("Twitch API secret env variable not set.")
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
def authorize():
|
|
global TWITCH_OAUTH_TOKEN
|
|
global TWITCH_OAUTH_EXPIRE_EPOCH
|
|
|
|
# return if token has not expired
|
|
if (TWITCH_OAUTH_EXPIRE_EPOCH >= round(time.time())):
|
|
return
|
|
|
|
logging.debug("requesting a new oauth token")
|
|
data = {
|
|
'client_id': TWITCH_CLIENT_ID,
|
|
'client_secret': TWITCH_SECRET,
|
|
'grant_type': 'client_credentials',
|
|
}
|
|
url = 'https://id.twitch.tv/oauth2/token'
|
|
request = urllib.request.Request(url, data=urllib.parse.urlencode(data).encode("utf-8"), method='POST')
|
|
retries = 0
|
|
while retries < 3:
|
|
try:
|
|
result = urllib.request.urlopen(request, timeout=3)
|
|
r = json.loads(result.read().decode("utf-8"))
|
|
TWITCH_OAUTH_TOKEN = r['access_token']
|
|
TWITCH_OAUTH_EXPIRE_EPOCH = int(r['expires_in']) + round(time.time())
|
|
logging.debug("oauth token aquired")
|
|
return
|
|
except Exception as e:
|
|
logging.warning("Fetch exception caught: %s" % e)
|
|
retries += 1
|
|
abort(503)
|
|
|
|
@app.route('/vod/<string:channel>', methods=['GET', 'HEAD'])
|
|
def vod(channel):
|
|
if CHANNEL_FILTER.match(channel):
|
|
return get_inner(channel)
|
|
else:
|
|
abort(404)
|
|
|
|
|
|
@app.route('/vodonly/<string:channel>', methods=['GET', 'HEAD'])
|
|
def vodonly(channel):
|
|
if CHANNEL_FILTER.match(channel):
|
|
return get_inner(channel, add_live=False)
|
|
else:
|
|
abort(404)
|
|
|
|
|
|
def get_inner(channel, add_live=True):
|
|
userid_json = fetch_userid(channel)
|
|
if not userid_json:
|
|
abort(404)
|
|
|
|
(channel_display_name, channel_id) = extract_userid(json.loads(userid_json)['data'][0])
|
|
|
|
channel_json = fetch_vods(channel_id)
|
|
if not channel_json:
|
|
abort(404)
|
|
|
|
decoded_json = json.loads(channel_json)['data']
|
|
rss_data = construct_rss(channel, decoded_json, channel_display_name, add_live)
|
|
headers = {'Content-Type': 'application/rss+xml'}
|
|
|
|
if 'gzip' in request.headers.get("Accept-Encoding", ''):
|
|
headers['Content-Encoding'] = 'gzip'
|
|
rss_data = gzip.compress(rss_data)
|
|
|
|
return rss_data, headers
|
|
|
|
|
|
@cached(cache=TTLCache(maxsize=3000, ttl=USERIDCACHE_LIFETIME))
|
|
def fetch_userid(channel_name):
|
|
return fetch_json(channel_name, USERID_URL_TEMPLATE)
|
|
|
|
|
|
@cached(cache=TTLCache(maxsize=500, ttl=VODCACHE_LIFETIME))
|
|
def fetch_vods(channel_id):
|
|
return fetch_json(channel_id, VOD_URL_TEMPLATE)
|
|
|
|
|
|
def fetch_json(id, url_template):
|
|
#update the oauth token
|
|
authorize()
|
|
|
|
url = url_template % id
|
|
headers = {
|
|
'Authorization': 'Bearer '+TWITCH_OAUTH_TOKEN,
|
|
'Client-Id': TWITCH_CLIENT_ID,
|
|
'Accept-Encoding': 'gzip'
|
|
}
|
|
request = urllib.request.Request(url, headers=headers)
|
|
retries = 0
|
|
while retries < 3:
|
|
try:
|
|
result = urllib.request.urlopen(request, timeout=3)
|
|
logging.debug('Fetch from twitch for %s with code %s' % (id, result.getcode()))
|
|
if result.info().get('Content-Encoding') == 'gzip':
|
|
logging.debug('Fetched gzip content')
|
|
return gzip.decompress(result.read())
|
|
return result.read()
|
|
except Exception as e:
|
|
logging.warning("Fetch exception caught: %s" % e)
|
|
retries += 1
|
|
abort(503)
|
|
|
|
|
|
def extract_userid(user_info):
|
|
# Get the first id in the list
|
|
userid = user_info['id']
|
|
username = user_info['display_name']
|
|
if username and userid:
|
|
return username, userid
|
|
else:
|
|
logging.warning('Userid is not found in %s' % user_info)
|
|
abort(404)
|
|
|
|
|
|
def construct_rss(channel_name, vods_info, display_name, add_live=True):
|
|
feed = Feed()
|
|
|
|
# Set the feed/channel level properties
|
|
feed.feed["title"] = "%s's Twitch video RSS" % display_name
|
|
feed.feed["link"] = "https://twitchrss.appspot.com/"
|
|
feed.feed["author"] = "Twitch RSS Generated"
|
|
feed.feed["description"] = "The RSS Feed of %s's videos on Twitch" % display_name
|
|
feed.feed["ttl"] = '10'
|
|
|
|
# Create an item
|
|
try:
|
|
if vods_info:
|
|
for vod in vods_info:
|
|
item = {}
|
|
|
|
# @madiele: in twitch new API the current stream now it's not bundled in the same request
|
|
# maybe to be re-implemented later on
|
|
|
|
#if vod["status"] == "recording":
|
|
# if not add_live:
|
|
# continue
|
|
# link = "http://www.twitch.tv/%s" % channel_name
|
|
# item["title"] = "%s - LIVE" % vod['title']
|
|
# item["category"] = "live"
|
|
#else:
|
|
link = vod['url']
|
|
item["title"] = vod['title']
|
|
item["category"] = vod['type']
|
|
|
|
item["link"] = link
|
|
item["description"] = "<a href=\"%s\"><img src=\"%s\" /></a>" % (link, vod['thumbnail_url'].replace("%{width}", "512").replace("%{height}","288"))
|
|
|
|
#@madiele: for some reason the new API does not have the game field anymore...
|
|
#if vod.get('game'):
|
|
# item["description"] += "<br/>" + vod['game']
|
|
|
|
if vod.get('description'):
|
|
item["description"] += "<br/>" + vod['description']
|
|
d = datetime.datetime.strptime(vod['created_at'], '%Y-%m-%dT%H:%M:%SZ')
|
|
item["pubDate"] = d.timetuple()
|
|
item["guid"] = vod['id']
|
|
#if vod["status"] == "recording": # To show a different news item when recording is over
|
|
# item["guid"] += "_live"
|
|
feed.items.append(item)
|
|
except KeyError as e:
|
|
logging.warning('Issue with json: %s\nException: %s' % (vods_info, e))
|
|
abort(404)
|
|
|
|
return feed.format_rss2_string()
|
|
|
|
|
|
# For debug
|
|
if __name__ == "__main__":
|
|
app.run(host='127.0.0.1', port=8080, debug=True)
|