show list of users in chat

このコミットが含まれているのは:
n9k 2021-04-15 11:37:04 +00:00
コミット bf2ed3c6c6
18個のファイルの変更373行の追加48822行の削除

2
app.py
ファイルの表示

@ -2,4 +2,4 @@ from website import create_app
if __name__ == '__main__':
app = create_app()
app.run(debug=False)
app.run(debug=False, threaded=True)

ファイルの表示

@ -48,11 +48,6 @@ def comment(text, token, c_response, c_token, nonce):
if viewers[token]['banned']:
failure_reason = N_BANNED
elif now < viewers[token]['comment'] + CHAT_TIMEOUT:
failure_reason = N_TOOFAST
elif len(viewers[token]['recent_comments']) + 1 >= FLOOD_THRESHOLD:
failure_reason = N_FLOOD
viewers[token]['verified'] = False
elif not viewers[token]['verified'] and c_token not in captchas:
failure_reason = N_CAPTCHA_MISSING
elif not viewers[token]['verified'] and captchas[c_token] != c_response:
@ -60,6 +55,11 @@ def comment(text, token, c_response, c_token, nonce):
elif secrets.randbelow(50) == 0:
failure_reason = N_CAPTCHA_RANDOM
viewers[token]['verified'] = False
elif now < viewers[token]['last_comment'] + CHAT_TIMEOUT:
failure_reason = N_TOOFAST
elif len(viewers[token]['recent_comments']) + 1 >= FLOOD_THRESHOLD:
failure_reason = N_FLOOD
viewers[token]['verified'] = False
else:
try:
nonces.remove(nonce)
@ -73,7 +73,7 @@ def comment(text, token, c_response, c_token, nonce):
'hidden': False,
'time': dt.strftime('%H:%M'),
'date': dt.strftime('%F %T')})
viewers[token]['comment'] = now
viewers[token]['last_comment'] = now
viewers[token]['recent_comments'].append(now)
viewers[token]['verified'] = True
behead_chat()

ファイルの表示

@ -17,14 +17,22 @@ viewers = viewership.viewers
def new_token():
return secrets.token_hex(8)
def get_token(form=False):
token = (request.form if form else request.args).get('token')
if token == None or len(token) >= 256 or len(token) < 4:
token = request.cookies.get('token')
if token and (len(token) >= 256 or len(token) < 4):
token = None
return token
@current_app.route('/')
def index(token=None):
token = token or request.args.get('token') or request.cookies.get('token') or new_token()
token = token or get_token() or new_token()
try:
viewership.video_was_corrupted.remove(token)
except KeyError:
pass
viewership.setdefault(token)
viewership.made_request(token)
response = Response(render_template('index.html', token=token)) # TODO: add a view of the chat only, either as an arg here or another route
response.set_cookie('token', token)
return response
@ -34,17 +42,37 @@ def index(token=None):
def broadcaster():
return index(token=BROADCASTER_TOKEN)
## simple version, just reads the file from disk
#@current_app.route('/stream.m3u8')
#def playlist():
# if not stream.is_online():
# return abort(404)
#
# token = get_token() or new_token()
# try:
# viewership.video_was_corrupted.remove(token)
# except KeyError:
# pass
# response = send_from_directory(SEGMENTS_DIR, 'stream.m3u8', add_etags=False)
# response.headers['Cache-Control'] = 'no-cache'
# response.set_cookie('token', token)
# return response
@current_app.route('/stream.m3u8')
def playlist():
if not stream.is_online():
return abort(404)
token = request.args.get('token') or request.cookies.get('token') or new_token()
token = get_token() or new_token()
try:
viewership.video_was_corrupted.remove(token)
except KeyError:
pass
response = send_from_directory(SEGMENTS_DIR, 'stream.m3u8', add_etags=False)
try:
file_wrapper = wrap_file(request.environ, stream.TokenPlaylist(token))
except FileNotFoundError:
return abort(404)
response = Response(file_wrapper, mimetype='application/x-mpegURL')
response.headers['Cache-Control'] = 'no-cache'
response.set_cookie('token', token)
return response
@ -54,7 +82,7 @@ def segment_init():
if not stream.is_online():
return abort(404)
token = request.args.get('token') or request.cookies.get('token') or new_token()
token = get_token() or new_token()
try:
viewership.video_was_corrupted.remove(token)
except KeyError:
@ -69,7 +97,7 @@ def segment_arbitrary(n):
if not stream.is_online():
return abort(404)
token = request.args.get('token') or request.cookies.get('token')
token = get_token()
try:
viewership.video_was_corrupted.remove(token)
except KeyError:
@ -86,7 +114,7 @@ def segment_arbitrary(n):
def segments():
if not stream.is_online():
return abort(404)
token = request.args.get('token') or request.cookies.get('token')
token = get_token()
try:
viewership.video_was_corrupted.remove(token)
except KeyError:
@ -106,16 +134,27 @@ def segments():
# or no-js users have to load it and js users get it in heartbeat, or no-js users always load it like /chat and js users get it in heartbeat
@current_app.route('/chat')
def chat_iframe():
token = request.args.get('token') or request.cookies.get('token') or new_token()
token = get_token() or new_token()
viewership.made_request(token)
include_user_list = bool(request.args.get('users', default=0, type=int))
messages = (message for message in chat.messages if not message['hidden'])
messages = zip(messages, range(CHAT_SCROLLBACK)) # show at most CHAT_SCROLLBACK messages
messages = (message for message, _ in messages)
return render_template('chat-iframe.html', token=token, messages=messages, default_nickname=viewership.default_nickname, broadcaster=token == BROADCASTER_TOKEN, broadcaster_colour=BROADCASTER_COLOUR, debug=request.args.get('debug'))
return render_template('chat-iframe.html',
token=token,
messages=messages,
people=viewership.get_people_list(),
default_nickname=viewership.default_nickname,
broadcaster=token == BROADCASTER_TOKEN,
broadcaster_colour=BROADCASTER_COLOUR,
debug=request.args.get('debug'),
len=len)
@current_app.route('/heartbeat')
def heartbeat():
token = request.args.get('token') or request.cookies.get('token')
viewership.heartbeat(token)
token = get_token()
viewership.made_request(token)
online = stream.is_online()
start_abs, start_rel = stream.get_start(absolute=True, relative=True)
return {'viewers': viewership.count(),
@ -127,7 +166,8 @@ def heartbeat():
@current_app.route('/comment-box')
def comment_iframe(token=None):
token = token or request.args.get('token') or request.cookies.get('token') or new_token()
token = token or get_token() or new_token()
viewership.made_request(token)
try:
preset = viewership.preset_comment_iframe.pop(token)
@ -154,12 +194,14 @@ def comment_iframe(token=None):
@current_app.route('/comment', methods=['POST'])
def comment():
token = request.form.get('token') or request.cookies.get('token') or new_token()
token = get_token(form=True) or new_token()
nonce = request.form.get('nonce')
message = request.form.get('message', '').replace('\r', '').replace('\n', ' ').strip()
c_response = request.form.get('captcha')
c_token = request.form.get('captcha-token')
viewership.made_request(token)
failure_reason = chat.comment(message, token, c_response, c_token, nonce)
viewership.preset_comment_iframe[token] = {'note': failure_reason, 'message': message if failure_reason else ''}
@ -171,10 +213,12 @@ def comment():
# for changing your appearance. So this is not done for now.
@current_app.route('/settings', methods=['POST'])
def settings():
token = request.form.get('token') or request.cookies.get('token') or new_token()
token = get_token(form=True) or new_token()
nickname = request.form.get('nickname', '')
password = request.form.get('password', '')
viewership.made_request(token)
note, ok = chat.set_nickname(nickname, token)
if ok:
if request.form.get('remove-tripcode'):
@ -195,8 +239,11 @@ def mod():
@current_app.route('/stream-info')
def stream_info():
token = request.args.get('token') or request.cookies.get('token')
token = get_token()
embed_images = bool(request.args.get('embed', type=int))
viewership.made_request(token)
start_abs, start_rel = stream.get_start(absolute=True, relative=True)
online = stream.is_online()
return render_template('stream-info-iframe.html',
@ -211,6 +258,12 @@ def stream_info():
token=token,
broadcaster_colour=BROADCASTER_COLOUR)
@current_app.route('/users')
def users():
token = get_token()
viewership.made_request(token)
return render_template('users.html', token=token, people=viewership.get_people_list(), default_nickname=viewership.default_nickname, broadcaster_colour=BROADCASTER_COLOUR, len=len)
@current_app.route('/static/radial.apng')
def radial():
response = send_from_directory(DIR_STATIC, 'radial.apng', mimetype='image/png', add_etags=False)
@ -218,9 +271,15 @@ def radial():
response.expires = response.date
return response
@current_app.route('/static/external/<path:path>')
def third_party(path):
response = send_from_directory(DIR_STATIC_EXTERNAL, path, add_etags=False)
@current_app.route('/static/<fn>')
def _static(fn):
response = send_from_directory(DIR_STATIC, fn, add_etags=False)
response.headers['Cache-Control'] = 'no-cache'
return response
@current_app.route('/static/external/<fn>')
def third_party(fn):
response = send_from_directory(DIR_STATIC_EXTERNAL, fn, add_etags=False)
response.headers['Cache-Control'] = 'public, max-age=604800, immutable'
response.expires = response.date + datetime.timedelta(days=7)
return response
@ -235,4 +294,4 @@ def add_header(response):
@current_app.route('/teapot')
def teapot():
return {'short': True, 'stout': True}, 418
return {'short': True, 'stout': True}, 418

長すぎる行があるためファイル差分は表示されません

1
website/static/external/video-js.min.css vendored ノーマルファイル

長すぎる行があるためファイル差分は表示されません

ファイル差分が大きすぎるため省略します 差分を読み込み

25
website/static/external/video.min.js vendored ノーマルファイル

長すぎる行があるためファイル差分は表示されません

ファイル差分が大きすぎるため省略します 差分を読み込み

6
website/static/external/videojs-http-streaming.min.js vendored ノーマルファイル

長すぎる行があるためファイル差分は表示されません

ファイルの表示

@ -1,43 +1,77 @@
.banner {
text-align: center;
margin: 0.5em;
padding-bottom:0.5em;
border-bottom:1px solid gray;
font-size:125%;
}
.border {
border: 1px solid #434343;
}
.red {
color: #ff8280;
fill: #ff8280;
.hue-rotate {
filter: hue-rotate(150deg);
}
#stream {
height: calc(100vw * 9 / 16);
}
#chat {
#chat-window iframe {
height: calc(100vh - (100vw * 9 / 16 + 20em));
min-height: 12em;
}
#chat {
transform: scaleY(-1);
}
#noscript {
text-align: center;
margin-top: calc(-100vw * 9 / 16 / 2 - 1ex);
position: relative;
}
iframe.stream-info {
#stream-info {
min-height: 7em;
height: 7em;
}
#source {
display: none;
}
.users-logo {
width: 20px;
height: 20px;
cursor: pointer;
}
svg.users-logo {
fill: white;
}
.chat-banner-left {
float: left;
width: 1em;
margin-top: 2px;
}
.users-logo:hover {
border-radius: 6px;
background-color: #2f2f2f;
padding: 4px;
margin: -4px;
}
#users-toggle:checked + .banner .users-logo {
border-radius: 6px;
background-color: #3f3f3f;
padding: 4px;
margin: -4px;
}
#users-toggle:checked + .banner svg.users-logo {
fill: #0078e7;
}
#users-toggle:checked + .banner .users-logo img {
filter: brightness(0) saturate(100%) invert(41%) sepia(92%) saturate(5359%) hue-rotate(195deg) brightness(97%) contrast(103%); /* thanks to https://codepen.io/sosuke/pen/Pjoqqp */
}
form[target="users"] {
display: none;
}
#users-toggle:checked + .banner form[target="chat"] button {
filter: revert;
}
@media screen and (min-width:48em) {
#chat {
#chat-window iframe {
height: calc(100vh - 1px - 10em);
}
#stream {
@ -46,42 +80,36 @@ iframe.stream-info {
#noscript {
margin-top: calc(-100vw * 2 / 3 * 9 / 16 / 2 - 1ex);
}
iframe.stream-info {
#stream-info {
height: calc(100vh - 100vw * 2 / 3 * 9 / 16 - 2px);
}
#source {
display: revert;
}
}
iframe {
border:0;
width: 100%;
}
.dark-theme a:hover,
.dark-theme a:active {
color: #00b6f0;
}
.dark-theme a {
color: #a0a0a0;
text-decoration: none;
}
body.dark-theme {
background-color: #232323;
color: #f0f0f0;
}
.dark-theme input,
.dark-theme select,
.dark-theme textarea {
color: #232323;
}
/* workaround to make some custom user css work
e.g. https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/usercss/2154.user.css */
div[class="vjs-text-track-display"] {
display: none;
}
}

ファイルの表示

@ -11,7 +11,7 @@ let streamAbsoluteStart, streamRelativeStart, streamTimer, streamTimerLastUpdate
// ensure only one heartbeat is sent at a time
let heartIsBeating = false;
let streamInfoFrame = window.frames['stream-info'];
let streamInfoFrame = window.frames["stream-info"];
streamInfoFrame.addEventListener("load", function() {
console.log("stream info iframe loaded");
@ -58,9 +58,9 @@ function currentSegment() {
}
}
function updateStreamStatus(msg, color, showRefreshButton) {
function updateStreamStatus(msg, backgroundColor, showRefreshButton) {
streamStatus.innerHTML = msg;
streamLight.style.color = color;
streamLight.style.backgroundColor = backgroundColor;
if ( showRefreshButton ) {
refreshButton.style.display = null;
} else {
@ -102,6 +102,10 @@ function resetRadialLoader() {
radialLoader = newElement;
}
// TODO: this
function fitFrame(frame) {
}
// get stream info from the server (viewer count, current segment, if stream is online, etc.)
function heartbeat() {
if ( heartIsBeating ) {
@ -176,8 +180,8 @@ function heartbeat() {
}
xhr.send();
} catch (e) {
} catch ( error ) {
heartIsBeating = false;
throw e;
throw error;
}
}

ファイルの表示

@ -4,7 +4,8 @@
<meta http-equiv="conent-security-policy" content="default-src 'none'">
{% if not debug %}<meta http-equiv="refresh" content="8">{% endif %}
<style>
body {margin: 0.5em;margin-bottom: 0;min-height: calc(100vh - 1em);transform: scaleX(-1);}
body {margin: 0.5em;margin-bottom: 0;min-height: calc(100vh - 1em);}
#messages {transform: scaleX(-1);}
.inline-block {display: inline-block;}
.rotate {transform: rotate(-180deg);}
.reverse {direction: rtl;}
@ -20,11 +21,15 @@
font-size: 90%;
font-family: monospace;
vertical-align: middle;
display: inline-block;
}
.message {margin-left: 0.5em;overflow-wrap: break-word;unicode-bidi: isolate;}
.camera {transform: scaleX(-1);text-shadow: 0px 0px 6px #{{ broadcaster_colour.hex() }};cursor: help;margin-right:0.25em;}
.time {font-size: 80%;color: gray;vertical-align:middle;margin-right:0.5em;}
#users {margin-left:0.5em;}
.group-name {color:white;margin-bottom:0.125em;}
input[type="submit"] {padding: 0;margin-bottom: 0.5em;}
div#refresh {
color: white;
@ -54,7 +59,7 @@
</style>
</head>
<body>
<div id="messages">
{% if broadcaster %}
<form action="/mod" method="post">
<div class="reverse">
@ -89,6 +94,22 @@
{% endif %}
<a href="" style="text-decoration: none;"><div id="refresh" class="rotate">Manual refresh required</div></a>
</div>
<div id="users" style="display: none;">
{% with broadcaster = people['broadcaster'] %}
{% if broadcaster %}
<div style="margin-bottom:1em;">
<div class="group-name">Broadcaster</div>
<span class="camera" title="Broadcaster">🎥</span><span class="name" style="color:#{{ broadcaster['colour'].hex() }};">{{ broadcaster['nickname'] or default_nickname(broadcaster['token']) }}</span>{% if broadcaster['tripcode']['string'] %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ broadcaster['tripcode']['string'] }}</div>{% endif %}
</div>
{% endif %}
{% endwith %}
<div class="group-name">Users ({{ len(people['users']) }})</div>
{% for person in people['users'] %}
<div class="person">
<span class="name" style="color:#{{ person['colour'].hex() }};">{{ person['nickname'] or default_nickname(person['token']) }}{% with tag = person['nickname'] == None %}{% if tag %}<sup>{{ person['tag'] }}</sup>{% endif %}</span>{% if person['tripcode']['string'] %}{% if tag %}<span style="margin-right:0.125em;"></span>{% endif %}<div class="tripcode" style="background-color:#{{ person['tripcode']['background_colour'].hex() }};color:#{{ person['tripcode']['foreground_colour'].hex() }};">{{ person['tripcode']['string'] }}</div>{% endif %}{% endwith %}{% if person['token'] == token %}<span style="margin-left:0.5em;color:white;">(You)</span>{% endif %}
</div>
{% endfor %}
</div>
</body>
</html>
</html>

ファイルの表示

@ -218,4 +218,4 @@
</form>
<div id="note">{{ note }}</div>
</body>
</html>
</html>

ファイルの表示

@ -2,18 +2,41 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/external/video-js.css" rel="stylesheet">
<!-- https://unpkg.com/video.js@7.12.1/dist/video-js.min.css -->
<link href="/static/external/video-js.min.css" rel="stylesheet">
<link href="/static/external/pure-min.css" rel="stylesheet">
<link href="/static/external/grids-responsive-min.css" rel="stylesheet">
<link href="/static/platform.css" rel="stylesheet">
<script src="/static/external/video.js"></script>
<script src="/static/external/videojs-contrib-hls.js"></script>
<!-- <script src="/static/external/dash.all.debug.js"></script>
<script src="/static/external/videojs-dash.js"></script>-->
<noscript><style>#videojs {display: none;}</style></noscript>
<!-- https://unpkg.com/video.js@7.12.1/dist/video.min.js -->
<script src="/static/external/video.min.js"></script>
<!-- https://unpkg.com/@videojs/http-streaming@2.7.1/dist/videojs-http-streaming.min.js -->
<script src="/static/external/videojs-http-streaming.min.js"></script>
<noscript>
<style>
#videojs {
display: none;
}
#users-container {
display: none;
}
#users-toggle:checked ~ #chat-window #users-container {
display: revert;
}
#users-toggle:checked ~ #chat-window #chat-container {
display: none;
}
#users-toggle:checked + .banner form[target="chat"] {
display: none;
}
#users-toggle:checked + .banner form[target="users"] {
display: revert;
}
</style>
</noscript>
</head>
<body class="dark-theme">
<div class=pure-g>
<!-- TODO: get rid of PureCSS dependency here; the noscript block on top of the video is too large because the PureCSS files take longer to load -->
<div class="pure-u-1 pure-u-md-2-3">
<div id="stream" class="border">
<!-- https://stackoverflow.com/questions/41014197/how-can-i-play-a-m3u8-file-video-using-the-html5-video-element -->
@ -24,36 +47,79 @@
<video style="width: 100%;height: 100%;" controls autoplay src="{{ url_for('segments', token=token) }}">
</noscript>
</div>
<div id="stream-info-container">
<noscript><iframe class="stream-info" src="{{ url_for('stream_info', token=token, embed=1) }}"></iframe></noscript>
</div>
<div style="margin: -2.75em 0 1.5em 1.25em;"><a href="https://gitlab.com/ninya9k/onion-livestreaming" target="_blank">source code</a></div>
<div id="stream-info-container"><noscript><iframe id="stream-info" src="{{ url_for('stream_info', token=token, embed=1) }}"></iframe></noscript></div>
<div id="source" style="margin: -2.75em 0 1.5em 1.25em;"><a href="https://gitlab.com/ninya9k/onion-livestreaming" target="_blank">source code</a></div>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<div class="border">
<div class="banner" style="padding-bottom:0.5em;border-bottom:1px solid gray;padding-left: 1em;font-size:125%;">
<!-- TODO: mobile tooltip -->
<input id="users-toggle" type="checkbox" style="display: none;">
<div class="banner">
<div class="chat-banner-left">
<label for="users-toggle" title="Users in chat">
<svg class="users-logo" style="display:none;" version="1.1" viewBox="0 0 20 20" x="0px" y="0px"><g><path fill-rule="evenodd" d="M7 2a4 4 0 00-1.015 7.87c-.098.64-.651 1.13-1.318 1.13A2.667 2.667 0 002 13.667V18h2v-4.333c0-.368.298-.667.667-.667.908 0 1.732-.363 2.333-.953.601.59 1.425.953 2.333.953.369 0 .667.299.667.667V18h2v-4.333A2.667 2.667 0 009.333 11c-.667 0-1.22-.49-1.318-1.13A4.002 4.002 0 007 2zM5 6a2 2 0 104 0 2 2 0 00-4 0z" clip-rule="evenodd"></path><path d="M14 11.83V18h4v-3.75c0-.69-.56-1.25-1.25-1.25a.75.75 0 01-.75-.75v-.42a3.001 3.001 0 10-2 0z"></path></g>
</svg>
<noscript><div class="users-logo"><img src="/static/external/users.png"></div></noscript>
</label>
</div>
<span>Stream chat</span>
<form target="chat" action="/chat" method="get" class="banner" style="float: right;margin: 0;width: 1em;">
<form target="chat" action="/chat" method="get" style="float: right;margin: 0;width: 1em;">
<input id="token" type="hidden" name="token" value="{{ token }}">
<input type="checkbox" style="display:none;" name="debug">
<button style="font-weight: bold;background: none;border: none;margin: 0;padding: 0;cursor: pointer;" type="submit" class="">🔄</button>
<button class="hue-rotate" title="Refresh chat window" style="font-weight: bold;background: none;border: none;margin: 0;padding: 0;cursor: pointer;" type="submit" class="">🔄</button>
</form>
<form target="users" action="/users" method="get" style="float: right;margin: 0;width: 1em;">
<input id="token" type="hidden" name="token" value="{{ token }}">
<input type="checkbox" style="display:none;" name="debug">
<button title="Refresh chat window" style="font-weight: bold;background: none;border: none;margin: 0;padding: 0;cursor: pointer;" type="submit" class="">🔄</button>
</form>
</div>
<iframe id="chat" name="chat" style="transform: rotate(180deg);transform: scaleY(-1);" src="{{ url_for('chat_iframe', token=token) }}"></iframe>
<div id="chat-window">
<div id="chat-container"><noscript><iframe id="chat" name="chat" src="{{ url_for('chat_iframe', token=token) }}"></iframe></noscript></div>
<div id="users-container"><noscript><iframe name="users" src="{{ url_for('users') }}"></iframe></noscript></div>
</div>
<iframe style="height:6em;border-top:1px solid #434343;padding-top:0.5em;" src="{{ url_for('comment_iframe', token=token) }}"></iframe>
</div>
</div>
</div>
</div>
<script>
const streamInfoContainer = document.getElementById("stream-info-container")
const streamInfoWithEmbeds = streamInfoContainer.querySelector("*");
const streamInfoNoEmbeds = document.createElement("iframe");
streamInfoNoEmbeds.id = "stream-info";
streamInfoNoEmbeds.className = "stream-info";
streamInfoNoEmbeds.src = "{{ url_for('stream_info', token=token) }}";
streamInfoContainer.replaceChild(streamInfoNoEmbeds, streamInfoWithEmbeds);
function replaceFrameURL(frameContainerId, newUrl, newId, newName) {
const frameContainer = document.getElementById(frameContainerId);
const oldFrame = frameContainer.querySelector("*");
const newFrame = document.createElement("iframe");
newFrame.id = newId;
newFrame.name = newName;
newFrame.src = newUrl;
frameContainer.replaceChild(newFrame, oldFrame);
}
replaceFrameURL("stream-info-container", "{{ url_for('stream_info', token=token) }}", "stream-info", "");
replaceFrameURL("chat-container", "{{ url_for('chat_iframe') }}?token={{ token }}&users=1", "chat", "chat");
/* ensure that svg only appears when scripts are enabled */
for ( element of document.querySelectorAll("svg") ) {
element.style.display = null;
}
const chat = document.getElementById("chat");
const usersToggle = document.getElementById("users-toggle");
usersToggle.onchange = function() {
const chatMessages = chat.contentDocument.getElementById("messages");
const chatUsers = chat.contentDocument.getElementById("users");
if ( chatUsers == null || chatMessages == null )
return;
if ( usersToggle.checked ) {
chatMessages.style.display = "none";
chatUsers.style.display = null;
chat.style.transform = "revert";
} else {
chatUsers.style.display = "none";
chatMessages.style.display = null;
chat.style.transform = null;
}
}
chat.addEventListener("load", usersToggle.onchange);
</script>
<script src="/static/platform.js"></script>
</body>
</html>
</html>

ファイルの表示

@ -211,4 +211,4 @@
var streamRelativeStart = {{ stream_start_rel_json }};
</script>
</body>
</html>
</html>

ファイルの表示

@ -35,4 +35,8 @@ def gen_colour(seed, background=BACKGROUND_COLOUR, *avoid):
return colour
if best_score == None or score > best_score:
best_colour = colour
return best_colour
return best_colour
def tag(colour):
tag = ((colour[2] & 0xf0) >> 2) | ((colour[1] & 0xf0) >> 6) | ((colour[2] & 0xf0) >> 10)
return f'#{tag:03x}'

ファイルの表示

@ -1,9 +1,11 @@
import os
import re
import time
from flask import abort
from website.constants import SEGMENTS_DIR, SEGMENTS_M3U8, SEGMENT_INIT, STREAM_PIDFILE, STREAM_START, STREAM_TITLE
RE_SEGMENT = re.compile(r'stream(?P<number>\d+).m4s')
RE_SEGMENT_OR_INIT = re.compile(r'\b(stream(?P<number>\d+)\.m4s|init\.mp4)\b')
RE_SEGMENT = re.compile(r'stream(?P<number>\d+)\.m4s')
def _segment_number(fn):
if fn == SEGMENT_INIT: return None
@ -72,4 +74,34 @@ def get_start(absolute=True, relative=False):
elif absolute:
return start
elif relative:
return diff
return diff
class TokenPlaylist:
'''
Append '?token={token}' to each segment in the playlist
'''
def __init__(self, token):
self.token = token
self.fp = open(SEGMENTS_M3U8)
self.leftover = b''
def read(self, n):
if self.token == None:
return self.fp.read(n)
leftover = self.leftover
chunk = b''
while True:
line = self.fp.readline()
if len(line) == 0:
break
injected_line = RE_SEGMENT_OR_INIT.sub(lambda match: f'{match.group()}?token={self.token}', line)
chunk += injected_line.encode()
if len(chunk) >= n:
chunk, self.leftover = chunk[:n], chunk[n:]
break
return leftover + chunk
def close(self):
self.fp.close()

ファイルの表示

@ -24,11 +24,12 @@ def default_nickname(token):
return ANON_DEFAULT_NICKNAME
def setdefault(token):
if token in viewers:
if token in viewers or token == None:
return
viewers[token] = {'token': token,
'comment': float('-inf'),
'heartbeat': int(time.time()),
'last_comment': float('-inf'),
'last_request': float('-inf'),
'first_request': float('-inf'),
'verified': False,
'recent_comments': [],
'nickname': None,
@ -36,17 +37,21 @@ def setdefault(token):
'banned': False,
'tripcode': tripcode.default(),
'broadcaster': False}
c = viewers[token]['colour']
tag = ((c[2] & 0xf0) >> 2) | ((c[1] & 0xf0) >> 6) | ((c[2] & 0xf0) >> 10)
viewers[token]['tag'] = f'#{tag:03x}'
viewers[token]['tag'] = colour.tag(viewers[token]['colour'])
if token == BROADCASTER_TOKEN:
viewers[token]['broadcaster'] = True
viewers[token]['colour'] = BROADCASTER_COLOUR
viewers[token]['verified'] = True
def heartbeat(token):
# TODO: generalise this and reduce the number of keys in last_request; comment is used for flood detection and the rest is for get_user_list
def made_request(token):
if token == None:
return
now = int(time.time())
setdefault(token)
viewers[token]['heartbeat'] = int(time.time())
if viewers[token]['first_request'] == float('-inf'):
viewers[token]['first_request'] = now
viewers[token]['last_request'] = now
def view_segment(n, token=None, check_exists=True):
# n is None if segment_hook is called in ConcatenatedSegments and the current segment is init.mp4
@ -60,18 +65,20 @@ def view_segment(n, token=None, check_exists=True):
with lock:
now = int(time.time())
segment_views.setdefault(n, []).append({'time': now, 'token': token})
if token:
made_request(token)
print(f'seg{n}: {token}')
def count_site_tokens():
'''
Return the number of viewers who have sent a heartbeat or commented in the last 30 seconds
'''
n = 0
now = int(time.time())
for token in set(viewers):
if max(viewers[token]['heartbeat'], viewers[token]['comment']) >= now - VIEW_COUNTING_PERIOD:
n += 1
return n
#def count_site_tokens():
# '''
# Return the number of viewers who have sent a heartbeat or commented in the last 30 seconds
# '''
# n = 0
# now = int(time.time())
# for token in set(viewers):
# if max(viewers[token]['last_request']['heartbeat'], viewers[token]['last_request']['comment']) >= now - VIEW_COUNTING_PERIOD:
# n += 1
# return n
# TODO: account for the stream restarting; segments will be out of order
def count_segment_views(exclude_token_views=True):
@ -139,4 +146,20 @@ def count():
with lock:
a, b = count_segment_tokens(), count_segment_views(exclude_token_views=True)
print(f'count_segment_tokens={a}; count_segment_views={b}')
return a + b
return a + b
# TODO: separate users into watching and not watching
def get_people_list():
now = int(time.time())
users = filter(lambda token: viewers[token]['first_request'] > float('-inf'), viewers)
users = filter(lambda token: now - viewers[token]['last_request'] < 24, users)
users = sorted(users, key=lambda token: viewers[token]['first_request'])
people = {'broadcaster': None, 'users': []}
for token in users:
if viewers[token]['broadcaster']:
people['broadcaster'] = viewers[token]
else:
people['users'].append(viewers[token])
return people