diff --git a/.gitignore b/.gitignore
index 1f87ab3..e62cb77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
__pycache__
*.swp
.token
-.oauth
.secret
server.conf
.*.log
diff --git a/README.md b/README.md
index 97a2859..2d1f3a1 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,34 @@ Least-favourite twitch streamers here
## Change Log
+### 1.10.0
+#### Changed
+- Refresh website design
+
+### 1.9.1
+#### Fixed
+- Server does not fail when processing API response withour `cursor` field.
+- String formatting for older python3 versions.
+
+### 1.9
+#### Fixed
+- Char escaping and various typos.
+- Unique filter for streams.
+
+#### Added
+- Runtime ouath token generation.
+
+#### Changed
+- Use threshold on amount of returned streams to resend requests.
+
+#### Removed
+- v5 (kraken) API support.
+- Daemonizer component.
+
+### 1.8
+#### Fixed
+- Search queries for v5 and v6 API
+
### 1.7
- Fixed bug with displaying special symbols in stream name
### 1.6
diff --git a/main.py b/main.py
index 327c283..bee436d 100755
--- a/main.py
+++ b/main.py
@@ -4,7 +4,7 @@
import cherrypy
from twitch import TwitchClient
-ver = '1.9.0-pre-3'
+ver = '1.10.0'
class FleastServer(object):
@@ -12,8 +12,8 @@ class FleastServer(object):
try:
with open('.token', 'r') as reader:
self.twitch_token = reader.read().strip()
- with open('.oauth', 'r') as reader:
- self.oauth_token = reader.read().strip()
+ with open('.secret', 'r') as reader:
+ self.secret = reader.read().strip()
with open('./web/fl.html', 'r') as reader:
self.index_page = reader.read()
with open('./web/fl_template_main.html', 'r') as reader:
@@ -22,7 +22,7 @@ class FleastServer(object):
self.templ_stream = reader.read()
with open('./web/fl_template_lang.html', 'r') as reader:
self.templ_lang = reader.read().splitlines()
- self.client = TwitchClient(self.twitch_token, self.oauth_token, freq=1)
+ self.client = TwitchClient(self.twitch_token, self.secret, freq=1)
except:
print("Cannot read token for twitch app or templates, abort.")
exit(1)
@@ -90,7 +90,7 @@ class FleastServer(object):
s['thumbnail_url'].format(width=320, height=180),
self.to_html(s['title']),
s['user_name'],
- s['viewer_count']) + '\n'
+ s['viewer_count'])
return self.templ_main.format(_stream_num_=len(data['streams']),
_game_name_=game,
diff --git a/twitch.py b/twitch.py
index aa12b2a..194b2fb 100644
--- a/twitch.py
+++ b/twitch.py
@@ -2,21 +2,54 @@ import cherrypy
import requests
import threading
import time
+
+from oauthlib.oauth2 import BackendApplicationClient
+from requests_oauthlib import OAuth2Session
from urllib.parse import quote
class TwitchClient:
- def __init__(self, token, oauth, freq=2):
+ def __init__(self, token, secret, freq=2):
self.token = token
- self.oauth = oauth
self.lock = threading.Lock()
- self.header_v6 = {'Client-ID': self.token, 'Authorization': 'Bearer ' + self.oauth}
+ self.header_v6 = {'Client-ID': self.token}
self.urlbase_v6 = 'https://api.twitch.tv/helix'
self.last_q = time.time()
self.delay = 1 / freq
+ # authentication
+ self.oauth = ''
+ self.oauth_token_url = 'https://id.twitch.tv/oauth2/token'
+ self.auth_secret = secret
+ oauth_clint = BackendApplicationClient(client_id=self.token)
+ self.oauth_session = OAuth2Session(client=oauth_clint)
+ self.update_oauth()
+
+ def update_oauth(self):
+ """
+ Update self.oauth token based on client id and secret.
+ :return: nothing
+ """
+ token = self.oauth_session.fetch_token(token_url=self.oauth_token_url,
+ client_secret=self.auth_secret,
+ include_client_id=True)
+ self.oauth = token['access_token']
+
+ def do_q_auth_v6(self, base, header):
+ """
+ Do query with v6 authentication header and single retry.
+ :param base: string with requesting URL
+ :param header: dictionary of http headers
+ :return: string with response or None
+ """
+ result = self.do_q(base, header | {'Authorization': 'Bearer ' + self.oauth})
+ if result is not None:
+ return result
+ self.update_oauth()
+ return self.do_q(base, header | {'Authorization': 'Bearer ' + self.oauth})
+
def do_q(self, base, header):
"""
Do query for twitch server
@@ -31,8 +64,13 @@ class TwitchClient:
if delta < self.delay:
time.sleep(delta) # Sleep remaining time
r = requests.get(base, headers=header).json()
+ error_message = r.get("error", "")
+ if len(error_message) > 0:
+ cherrypy.log('Request: fail with error "%s"' % error_message)
+ r = None
+ else:
+ cherrypy.log('Request: OK')
self.last_q = time.time()
- cherrypy.log('Request: OK')
except requests.exceptions.RequestException as e:
cherrypy.log('Request: FAIL')
cherrypy.log('Error: {}'.format(e))
@@ -62,7 +100,7 @@ class TwitchClient:
:return: string with get query result or None
"""
header, base = self.get_base('v6')
- return self.do_q(base + q, header)
+ return self.do_q_auth_v6(base + q, header)
def get_game_id_v6(self, name):
"""
@@ -72,7 +110,7 @@ class TwitchClient:
"""
header, base = self.get_base('v6')
- r = self.do_q('{}/games?name={}'.format(base, name), header)
+ r = self.do_q_auth_v6('{}/games?name={}'.format(base, name), header)
if r and r.get('data'):
return r['data'][0]['id'], r['data'][0]['name']
@@ -92,11 +130,11 @@ class TwitchClient:
header, base = self.get_base('v6')
init_q_template = "{}/streams?language={}&first={}&game_id={}"
q_template = "{}/streams?language={}&first={}&after={}&game_id={}"
- data = self.do_q(init_q_template.format(base, lang, 100, game_id[0]), header)
+ data = self.do_q_auth_v6(init_q_template.format(base, lang, 100, game_id[0]), header)
result['streams'].extend(data['data'])
while len(data.get('data', [])) > 0: # there must be non zero value, but search is kinda broken now
result['streams'].extend(data['data'])
- data = self.do_q(q_template.format(base, lang, 100, data['pagination']['cursor'], game_id[0]), header)
+ data = self.do_q_auth_v6(q_template.format(base, lang, 100, data['pagination']['cursor'], game_id[0]), header)
return self.unique_streams_v6(result)
def get_irl_live_streams_v6(self, lang):
@@ -105,17 +143,38 @@ class TwitchClient:
q_template = "{}/streams?language={}&first={}&after={}{}"
game_id = ''
- irl_ids = ["509660", "509673", "509667", "509669", "509670", "509658",
- "509672", "509671", "509664", "509663", "417752", "509659"]
+ """
+ 417752: Talk Shows & Podcasts
+ 509658: Just Chatting
+ 509659: ASMR
+ 509660: Art
+ 509663: Special Events
+ 509664: Tabletop RPGs
+ 509667: Food & Drink
+ 509669: Beauty & Body Art
+ 509670: Science & Technology
+ 509671: Fitness & Health [exclude]
+ 509672: Travel & Outdoors
+ 509673: Makers & Crafting
+ 515214: Politics [exclude]
+ 518203: Sports
+ 116747788: Pools, Hot Tubs, and Beaches
+ 272263131: Animals, Aquariums, and Zoos [exclude]
+ 1469308723: Software and Game Development
+ """
+ irl_ids = ["417752", "509658", "509659", "509660", "509663", "509664",
+ "509667", "509669", "509670", "509672", "509673", "518203",
+ "116747788", "1469308723"]
for irl_id in irl_ids:
game_id += '&game_id={}'.format(irl_id)
result = {'_total': 0, 'streams': []}
- data = self.do_q(init_q_template.format(base, lang, 100, game_id), header)
- result['streams'].extend(data['data'])
+ data = self.do_q_auth_v6(init_q_template.format(base, lang, 100, game_id), header)
while len(data.get('data', [])) > 0: # there must be non zero value, but search is kinda broken now
result['streams'].extend(data['data'])
- data = self.do_q(q_template.format(base, lang, 100, data['pagination']['cursor'], game_id), header)
+ if data['pagination'].get("cursor", None) is None: # sometimes server return results without cursor
+ break
+ data = self.do_q_auth_v6(q_template.format(base, lang, 100, data['pagination']['cursor'], game_id), header)
return self.unique_streams_v6(result)
def unique_streams_v6(self, result):
diff --git a/web/fl.html b/web/fl.html
index e554367..2a93ab7 100644
--- a/web/fl.html
+++ b/web/fl.html
@@ -3,64 +3,101 @@
FLeast
-
+
-
-
-
-
FLeast - your least favourite streamers here
-
-
-
-
-
Usage:
-
- Write the name of the game and select the language.
- The name must _exactly_ match the name of the game on twitch.tv
- i.e. csgo must be "Counter-Strike: Global Offensive".
- IRL is back! Sort of.
- You can type IRL in game field and it will output streams from
- Just Chatting, Travel & Outdoors, etc...
-
-
-
+
+
+
+
+
+
Fleast — find new favourite small streamer
+
- Developed by Alex Vanin | twitter |
- github
- Version: {_version_}
+ Search streams by specifying category from twitch.tv and the
+ language of the stream. Streams will be sorted by ascending order.
-
+
+ The category must be specified by exact name. For example,
+ if you are looking for Counter-Strike, then you should write
+ Counter-Strike: Global Offensive.
+
+
+ You can use IRL like in good old days. The output will
+ combine Just Chatting, Travel & Outdoors, and other categories.
+
- Write the name of the game and select the language.
- The name must _exactly_ match the name of the game on twitch.tv
- i.e. csgo must be "Counter-Strike: Global Offensive".
- You can type IRL in game field and it will output streams from
- Just Chatting, Travel & Outdoors, etc...
-