Compare commits
10 commits
b2b48c2ea1
...
a2c4582a0c
Author | SHA1 | Date | |
---|---|---|---|
a2c4582a0c | |||
e322994842 | |||
5daa97af3d | |||
8d8226a5fa | |||
538f50847e | |||
821389605a | |||
ca2b4f4d59 | |||
be894106a3 | |||
6ca8f3cd3c | |||
6367ab7ffe |
9 changed files with 293 additions and 237 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,7 +1,6 @@
|
||||||
__pycache__
|
__pycache__
|
||||||
*.swp
|
*.swp
|
||||||
.token
|
.token
|
||||||
.oauth
|
|
||||||
.secret
|
.secret
|
||||||
server.conf
|
server.conf
|
||||||
.*.log
|
.*.log
|
||||||
|
|
28
README.md
28
README.md
|
@ -3,6 +3,34 @@ Least-favourite twitch streamers here
|
||||||
|
|
||||||
|
|
||||||
## Change Log
|
## 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
|
### 1.7
|
||||||
- Fixed bug with displaying special symbols in stream name
|
- Fixed bug with displaying special symbols in stream name
|
||||||
### 1.6
|
### 1.6
|
||||||
|
|
10
main.py
10
main.py
|
@ -4,7 +4,7 @@
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from twitch import TwitchClient
|
from twitch import TwitchClient
|
||||||
|
|
||||||
ver = '1.9.0-pre-3'
|
ver = '1.10.0'
|
||||||
|
|
||||||
|
|
||||||
class FleastServer(object):
|
class FleastServer(object):
|
||||||
|
@ -12,8 +12,8 @@ class FleastServer(object):
|
||||||
try:
|
try:
|
||||||
with open('.token', 'r') as reader:
|
with open('.token', 'r') as reader:
|
||||||
self.twitch_token = reader.read().strip()
|
self.twitch_token = reader.read().strip()
|
||||||
with open('.oauth', 'r') as reader:
|
with open('.secret', 'r') as reader:
|
||||||
self.oauth_token = reader.read().strip()
|
self.secret = reader.read().strip()
|
||||||
with open('./web/fl.html', 'r') as reader:
|
with open('./web/fl.html', 'r') as reader:
|
||||||
self.index_page = reader.read()
|
self.index_page = reader.read()
|
||||||
with open('./web/fl_template_main.html', 'r') as reader:
|
with open('./web/fl_template_main.html', 'r') as reader:
|
||||||
|
@ -22,7 +22,7 @@ class FleastServer(object):
|
||||||
self.templ_stream = reader.read()
|
self.templ_stream = reader.read()
|
||||||
with open('./web/fl_template_lang.html', 'r') as reader:
|
with open('./web/fl_template_lang.html', 'r') as reader:
|
||||||
self.templ_lang = reader.read().splitlines()
|
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:
|
except:
|
||||||
print("Cannot read token for twitch app or templates, abort.")
|
print("Cannot read token for twitch app or templates, abort.")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
@ -90,7 +90,7 @@ class FleastServer(object):
|
||||||
s['thumbnail_url'].format(width=320, height=180),
|
s['thumbnail_url'].format(width=320, height=180),
|
||||||
self.to_html(s['title']),
|
self.to_html(s['title']),
|
||||||
s['user_name'],
|
s['user_name'],
|
||||||
s['viewer_count']) + '\n'
|
s['viewer_count'])
|
||||||
|
|
||||||
return self.templ_main.format(_stream_num_=len(data['streams']),
|
return self.templ_main.format(_stream_num_=len(data['streams']),
|
||||||
_game_name_=game,
|
_game_name_=game,
|
||||||
|
|
85
twitch.py
85
twitch.py
|
@ -2,21 +2,54 @@ import cherrypy
|
||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from oauthlib.oauth2 import BackendApplicationClient
|
||||||
|
from requests_oauthlib import OAuth2Session
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
||||||
class TwitchClient:
|
class TwitchClient:
|
||||||
def __init__(self, token, oauth, freq=2):
|
def __init__(self, token, secret, freq=2):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.oauth = oauth
|
|
||||||
self.lock = threading.Lock()
|
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.urlbase_v6 = 'https://api.twitch.tv/helix'
|
||||||
|
|
||||||
self.last_q = time.time()
|
self.last_q = time.time()
|
||||||
self.delay = 1 / freq
|
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):
|
def do_q(self, base, header):
|
||||||
"""
|
"""
|
||||||
Do query for twitch server
|
Do query for twitch server
|
||||||
|
@ -31,8 +64,13 @@ class TwitchClient:
|
||||||
if delta < self.delay:
|
if delta < self.delay:
|
||||||
time.sleep(delta) # Sleep remaining time
|
time.sleep(delta) # Sleep remaining time
|
||||||
r = requests.get(base, headers=header).json()
|
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()
|
self.last_q = time.time()
|
||||||
cherrypy.log('Request: OK')
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
cherrypy.log('Request: FAIL')
|
cherrypy.log('Request: FAIL')
|
||||||
cherrypy.log('Error: {}'.format(e))
|
cherrypy.log('Error: {}'.format(e))
|
||||||
|
@ -62,7 +100,7 @@ class TwitchClient:
|
||||||
:return: string with get query result or None
|
:return: string with get query result or None
|
||||||
"""
|
"""
|
||||||
header, base = self.get_base('v6')
|
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):
|
def get_game_id_v6(self, name):
|
||||||
"""
|
"""
|
||||||
|
@ -72,7 +110,7 @@ class TwitchClient:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
header, base = self.get_base('v6')
|
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'):
|
if r and r.get('data'):
|
||||||
return r['data'][0]['id'], r['data'][0]['name']
|
return r['data'][0]['id'], r['data'][0]['name']
|
||||||
|
|
||||||
|
@ -92,11 +130,11 @@ class TwitchClient:
|
||||||
header, base = self.get_base('v6')
|
header, base = self.get_base('v6')
|
||||||
init_q_template = "{}/streams?language={}&first={}&game_id={}"
|
init_q_template = "{}/streams?language={}&first={}&game_id={}"
|
||||||
q_template = "{}/streams?language={}&first={}&after={}&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'])
|
result['streams'].extend(data['data'])
|
||||||
while len(data.get('data', [])) > 0: # there must be non zero value, but search is kinda broken now
|
while len(data.get('data', [])) > 0: # there must be non zero value, but search is kinda broken now
|
||||||
result['streams'].extend(data['data'])
|
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)
|
return self.unique_streams_v6(result)
|
||||||
|
|
||||||
def get_irl_live_streams_v6(self, lang):
|
def get_irl_live_streams_v6(self, lang):
|
||||||
|
@ -105,17 +143,38 @@ class TwitchClient:
|
||||||
q_template = "{}/streams?language={}&first={}&after={}{}"
|
q_template = "{}/streams?language={}&first={}&after={}{}"
|
||||||
|
|
||||||
game_id = ''
|
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:
|
for irl_id in irl_ids:
|
||||||
game_id += '&game_id={}'.format(irl_id)
|
game_id += '&game_id={}'.format(irl_id)
|
||||||
|
|
||||||
result = {'_total': 0, 'streams': []}
|
result = {'_total': 0, 'streams': []}
|
||||||
data = self.do_q(init_q_template.format(base, lang, 100, game_id), header)
|
data = self.do_q_auth_v6(init_q_template.format(base, lang, 100, game_id), header)
|
||||||
result['streams'].extend(data['data'])
|
|
||||||
while len(data.get('data', [])) > 0: # there must be non zero value, but search is kinda broken now
|
while len(data.get('data', [])) > 0: # there must be non zero value, but search is kinda broken now
|
||||||
result['streams'].extend(data['data'])
|
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)
|
return self.unique_streams_v6(result)
|
||||||
|
|
||||||
def unique_streams_v6(self, result):
|
def unique_streams_v6(self, result):
|
||||||
|
|
141
web/fl.html
141
web/fl.html
|
@ -3,64 +3,101 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>FLeast</title>
|
<title>FLeast</title>
|
||||||
<link rel="stylesheet" href="./style.css">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="shortcut icon" href="./favicon.ico" type="image/png">
|
<link rel="shortcut icon" href="./favicon.ico" type="image/png">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<style>
|
||||||
<section id="header">
|
.container-sm {{
|
||||||
<strong>
|
max-width: 1450px;
|
||||||
<h1>FLeast - your least favourite streamers here</h1>
|
}}
|
||||||
</strong>
|
|
||||||
</section>
|
|
||||||
<section id="pageContent">
|
|
||||||
<article>
|
|
||||||
<h2>Usage:</h2>
|
|
||||||
<p>
|
|
||||||
Write the name of the game and select the language.<br>
|
|
||||||
The name must _exactly_ match the name of the game on twitch.tv
|
|
||||||
i.e. csgo must be "Counter-Strike: Global Offensive".<br><br>
|
|
||||||
<b>IRL is back!</b> Sort of.<br>
|
|
||||||
You can type <b>IRL</b> in game field and it will output streams from
|
|
||||||
<i>Just Chatting</i>, <i>Travel & Outdoors</i>, etc...
|
|
||||||
</p>
|
|
||||||
<form method="get" action="./">
|
|
||||||
<table class="Input">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<label>Game </label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>Language </label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input name="game" type="text" maxlength="255" value=""/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select class="element select medium" name="lang">
|
|
||||||
{_opt_langs_}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="submit" value="Search" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</section>
|
body {{
|
||||||
<footer>
|
background-color: #333333;
|
||||||
|
}}
|
||||||
|
|
||||||
|
footer {{
|
||||||
|
background-color: #AEC6CF;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.strname {{
|
||||||
|
font-weight: bold;
|
||||||
|
word-break: break-word;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.struser {{
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0f0
|
||||||
|
}}
|
||||||
|
|
||||||
|
.strviews {{
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f00
|
||||||
|
}}
|
||||||
|
|
||||||
|
.grid {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 340px));
|
||||||
|
justify-content: space-around;
|
||||||
|
row-gap: 30px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container-sm text-white">
|
||||||
|
<br>
|
||||||
|
<h1>Fleast — find new favourite small streamer</h1>
|
||||||
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
Developed by Alex Vanin | <a href="https://twitter.com/AlexVanin" target="_blank">twitter</a> |
|
Search streams by specifying category from twitch.tv and the
|
||||||
<a href="https://github.com/AlexVanin/fleast" target="_blank">github</a><br>
|
language of the stream.<br>Streams will be sorted by <b>ascending</b> order.
|
||||||
Version: {_version_}
|
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
<p>
|
||||||
|
The category must be specified by <b>exact name</b>.<br>For example,
|
||||||
|
if you are looking for <i>Counter-Strike</i>, then you should write
|
||||||
|
<i>Counter-Strike: Global Offensive</i>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can use <b>IRL</b> like in good old days.<br>The output will
|
||||||
|
combine <i>Just Chatting</i>, <i>Travel & Outdoors</i>, and other categories.
|
||||||
|
</p>
|
||||||
|
<form method="get" action="./">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Category
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Language
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input name="game" type="text" class="form-control-sm" maxlength="255" value="" placeholder="Specify category"/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="lang" class="form-control-sm">
|
||||||
|
{_opt_langs_}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="submit" value="Search" class="btn btn-primary btn-sm"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<footer>
|
||||||
|
<div class="text-end text-black p-2">
|
||||||
|
Developed by Alexey Vanin | <a href="https://twitter.com/AlexVanin" target="_blank">twitter</a> |
|
||||||
|
<a href="https://github.com/AlexVanin/fleast" target="_blank">github</a><br>
|
||||||
|
Version: {_version_}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<option value="ru" {}>russian</option>
|
<option value="ru" {}>🇷🇺Russian</option>
|
||||||
<option value="en" {}>english</option>
|
<option value="en" {}>🇬🇧English </option>
|
||||||
<option value="de" {}>german</option>
|
<option value="de" {}>🇩🇪German </option>
|
||||||
<option value="zh-tw" {}>chinese (TW)</option>
|
<option value="es" {}>🇪🇸Spanish</option>
|
||||||
<option value="fi" {}>finnish</option>
|
<option value="fr" {}>🇫🇷French</option>
|
||||||
<option value="fr" {}>french</option>
|
<option value="pl" {}>🇵🇱Polish</option>
|
||||||
<option value="it" {}>italian</option>
|
<option value="ja" {}>🇯🇵Japanese</option>
|
||||||
<option value="ja" {}>japanese</option>
|
<option value="zh-tw" {}>🇨🇳Chinese</option>
|
||||||
<option value="ko" {}>korean</option>
|
<option value="ko" {}>🇰🇷Korean</option>
|
||||||
<option value="no" {}>norwegian</option>
|
<option value="fi" {}>🇫🇮Finnish</option>
|
||||||
<option value="pl" {}>polish</option>
|
<option value="no" {}>🇳🇴Norwegian</option>
|
||||||
<option value="es" {}>spanish</option>
|
<option value="sv" {}>🇸🇪Swedish</option>
|
||||||
<option value="sv" {}>swedish</option>
|
|
|
@ -3,66 +3,99 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>FLeast</title>
|
<title>FLeast</title>
|
||||||
<link rel="stylesheet" href="./style.css">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="shortcut icon" href="./favicon.ico" type="image/png">
|
<link rel="shortcut icon" href="./favicon.ico" type="image/png">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container-sm {{
|
||||||
|
max-width: 1450px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
background-color: #333333;
|
||||||
|
}}
|
||||||
|
|
||||||
|
footer {{
|
||||||
|
background-color: #AEC6CF;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.strname {{
|
||||||
|
font-weight: bold;
|
||||||
|
word-break: break-word;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.struser {{
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0f0
|
||||||
|
}}
|
||||||
|
|
||||||
|
.strviews {{
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f00
|
||||||
|
}}
|
||||||
|
|
||||||
|
.grid {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 340px));
|
||||||
|
justify-content: space-around;
|
||||||
|
row-gap: 30px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<section id="header">
|
<div class="container-sm text-white">
|
||||||
<strong><h1>FLeast - your least favourite streamers here</h1></strong>
|
<br>
|
||||||
</section>
|
<h1>Fleast — find new favourite small streamer</h1>
|
||||||
<section id="pageContent">
|
<hr>
|
||||||
<article>
|
<p>
|
||||||
<h2>Usage:</h2>
|
The category must be specified by <b>exact name</b>.<br>Search
|
||||||
<p>
|
<b>IRL</b> to find streams from <i>Just Chatting</i>,
|
||||||
Write the name of the game and select the language.<br>
|
<i>Travel & Outdoors</i>, and other categories.
|
||||||
The name must _exactly_ match the name of the game on twitch.tv
|
</p>
|
||||||
i.e. csgo must be "Counter-Strike: Global Offensive".<br><br>
|
<form method="get" action="./">
|
||||||
You can type <b>IRL</b> in game field and it will output streams from
|
<table>
|
||||||
<i>Just Chatting</i>, <i>Travel & Outdoors</i>, etc...
|
<tr>
|
||||||
</p>
|
<td>
|
||||||
<form method="get" action="./">
|
Category
|
||||||
<table class="Input">
|
</td>
|
||||||
<tbody>
|
<td>
|
||||||
<tr>
|
Language
|
||||||
<td>
|
</td>
|
||||||
<label>Game </label>
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<label>Language </label>
|
<input name="game" type="text" class="form-control-sm" maxlength="255" value="{_game_name_}" placeholder="Specify category"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
<tr>
|
<select name="lang" class="form-control-sm">
|
||||||
<td>
|
|
||||||
<input name="game" type="text" maxlength="255" value=""/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select class="element select medium" name="lang">
|
|
||||||
{_opt_langs_}
|
{_opt_langs_}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="submit" value="Search" />
|
<input type="submit" value="Search" class="btn btn-primary btn-sm"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
<hr>
|
||||||
<article>
|
<h4> Found {_stream_num_} streams:</h4>
|
||||||
<h2>Found {_stream_num_} streams: </h2><br>
|
<div class="grid">
|
||||||
<div class="container">
|
|
||||||
{_stream_list_}
|
{_stream_list_}
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<footer>
|
||||||
|
<div class="text-end text-black p-2">
|
||||||
|
Developed by Alexey Vanin | <a href="https://twitter.com/AlexVanin" target="_blank">twitter</a> |
|
||||||
|
<a href="https://github.com/AlexVanin/fleast" target="_blank">github</a><br>
|
||||||
|
Version: {_version_}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</footer>
|
||||||
</section>
|
</div>
|
||||||
<footer>
|
|
||||||
<p>
|
|
||||||
Developed by Alex Vanin | <a href="https://twitter.com/AlexVanin" target="_blank">twitter</a> |
|
|
||||||
<a href="https://github.com/AlexVanin/fleast" target="_blank">github</a><br>
|
|
||||||
Version: {_version_}
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="inner">
|
<div>
|
||||||
<a href="{0}" target="_blank"><img src={1}></a><br>
|
<a href="{0}" target="_blank"><img src={1}></a>
|
||||||
<div class="strname">{2}</div>
|
<div class="strname">{2}</div>
|
||||||
<span class="struser">{3}</span>
|
<span class="struser">{3}</span>
|
||||||
<span class="strviews"> :{4}</span>
|
<span class="strviews"> :{4}</span>
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
body{background: #333333; line-height:1; font-family: arial;}
|
|
||||||
h1{font-size: 25px;}h2{font-size: 21px;}h3{font-size: 18px;}h4{font-size: 16px;}
|
|
||||||
table{border-collapse:collapse;border-spacing:0}
|
|
||||||
hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}
|
|
||||||
|
|
||||||
#pageContent {
|
|
||||||
margin:0;padding:0;border:0;outline:0
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: auto;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header {
|
|
||||||
padding:10px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
float: left;
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
aside {
|
|
||||||
float: right;
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
article {
|
|
||||||
border-bottom: 2px dotted #999;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
article h2 {
|
|
||||||
font-weight: normal;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
article p {
|
|
||||||
|
|
||||||
}
|
|
||||||
main section {
|
|
||||||
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
background: #AEC6CF;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: auto;
|
|
||||||
clear: both;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
footer p {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside > div {
|
|
||||||
margin: 10px auto;
|
|
||||||
background: #AEC6CF;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body > section {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 30px 0px;
|
|
||||||
border-bottom: 1px solid #999;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.Input td, table.Input th {
|
|
||||||
border: 0px solid #AAAAAA;
|
|
||||||
padding: 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0px 0px 0px 10px
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner {
|
|
||||||
overflow: auto;
|
|
||||||
word-wrap: break-word;
|
|
||||||
background-color: transparent;
|
|
||||||
width: 330px;
|
|
||||||
padding: 0px 0px 30px 0px
|
|
||||||
}
|
|
||||||
|
|
||||||
.strname {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.struser {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #0f0
|
|
||||||
}
|
|
||||||
.strviews{
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #f00
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue