Keep track of stream viewership (number of viewers)
このコミットが含まれているのは:
コミット
e0f2f26c71
|
@ -89,8 +89,9 @@ def create_app(config_file):
|
||||||
|
|
||||||
# State for tasks
|
# State for tasks
|
||||||
app.users_update_buffer = set()
|
app.users_update_buffer = set()
|
||||||
app.stream_uptime = None
|
|
||||||
app.stream_title = None
|
app.stream_title = None
|
||||||
|
app.stream_uptime = None
|
||||||
|
app.stream_viewership = None
|
||||||
|
|
||||||
# Background tasks' asyncio.sleep tasks, cancelled on shutdown
|
# Background tasks' asyncio.sleep tasks, cancelled on shutdown
|
||||||
app.background_sleep = set()
|
app.background_sleep = set()
|
||||||
|
|
|
@ -76,6 +76,9 @@ def get_presence(timestamp, user):
|
||||||
|
|
||||||
return Presence.ABSENT
|
return Presence.ABSENT
|
||||||
|
|
||||||
|
def is_watching(timestamp, user):
|
||||||
|
return get_presence(timestamp, user) == Presence.WATCHING
|
||||||
|
|
||||||
def is_listed(timestamp, user):
|
def is_listed(timestamp, user):
|
||||||
return (
|
return (
|
||||||
get_presence(timestamp, user)
|
get_presence(timestamp, user)
|
||||||
|
|
|
@ -2,11 +2,11 @@ from quart import current_app, request, render_template, redirect, url_for, esca
|
||||||
|
|
||||||
from anonstream.captcha import get_random_captcha_digest_for
|
from anonstream.captcha import get_random_captcha_digest_for
|
||||||
from anonstream.chat import add_chat_message, Rejected
|
from anonstream.chat import add_chat_message, Rejected
|
||||||
from anonstream.stream import get_stream_title, get_stream_uptime
|
from anonstream.stream import get_stream_title, get_stream_uptime, get_stream_viewership
|
||||||
from anonstream.user import add_state, pop_state, try_change_appearance, verify, deverify, BadCaptcha
|
from anonstream.user import add_state, pop_state, try_change_appearance, verify, deverify, BadCaptcha
|
||||||
from anonstream.routes.wrappers import with_user_from, render_template_with_etag
|
from anonstream.routes.wrappers import with_user_from, render_template_with_etag
|
||||||
from anonstream.helpers.chat import get_scrollback
|
from anonstream.helpers.chat import get_scrollback
|
||||||
from anonstream.helpers.user import get_default_name
|
from anonstream.helpers.user import get_default_name, Presence
|
||||||
from anonstream.utils.chat import generate_nonce
|
from anonstream.utils.chat import generate_nonce
|
||||||
from anonstream.utils.user import concatenate_for_notice
|
from anonstream.utils.user import concatenate_for_notice
|
||||||
|
|
||||||
|
@ -19,8 +19,9 @@ async def nojs_info(user):
|
||||||
return await render_template(
|
return await render_template(
|
||||||
'nojs_info.html',
|
'nojs_info.html',
|
||||||
user=user,
|
user=user,
|
||||||
title=await get_stream_title(),
|
viewership=get_stream_viewership(),
|
||||||
uptime=get_stream_uptime(),
|
uptime=get_stream_uptime(),
|
||||||
|
title=await get_stream_title(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@current_app.route('/chat/messages.html')
|
@current_app.route('/chat/messages.html')
|
||||||
|
|
|
@ -6,7 +6,9 @@ const jsmarkup_style_color = '<style id="style-color"></style>'
|
||||||
const jsmarkup_style_tripcode_display = '<style id="style-tripcode-display"></style>'
|
const jsmarkup_style_tripcode_display = '<style id="style-tripcode-display"></style>'
|
||||||
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
|
const jsmarkup_style_tripcode_colors = '<style id="style-tripcode-colors"></style>'
|
||||||
const jsmarkup_info = '<div id="info_js" data-js="true"></div>';
|
const jsmarkup_info = '<div id="info_js" data-js="true"></div>';
|
||||||
const jsmarkup_info_uptime = '<aside id="info_js__uptime"></aside>';
|
const jsmarkup_info_float = '<aside id="info_js__float"></aside>';
|
||||||
|
const jsmarkup_info_float_viewership = '<div id="info_js__float__viewership"></div>';
|
||||||
|
const jsmarkup_info_float_uptime = '<div id="info_js__float__uptime"></div>';
|
||||||
const jsmarkup_info_title = '<header id="info_js__title"></header>';
|
const jsmarkup_info_title = '<header id="info_js__title"></header>';
|
||||||
const jsmarkup_chat_messages = '<ol id="chat-messages_js" data-js="true"></ol>';
|
const jsmarkup_chat_messages = '<ol id="chat-messages_js" data-js="true"></ol>';
|
||||||
const jsmarkup_chat_form = `\
|
const jsmarkup_chat_form = `\
|
||||||
|
@ -23,7 +25,7 @@ const jsmarkup_chat_form = `\
|
||||||
<input id="chat-form_js__submit" type="submit" value="Chat" accesskey="p" disabled>
|
<input id="chat-form_js__submit" type="submit" value="Chat" accesskey="p" disabled>
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
||||||
const insert_jsmarkup = () => {
|
const insert_jsmarkup = () => {jsmarkup_info_float_viewership
|
||||||
if (document.getElementById("style-color") === null) {
|
if (document.getElementById("style-color") === null) {
|
||||||
const parent = document.head;
|
const parent = document.head;
|
||||||
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
|
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
|
||||||
|
@ -40,9 +42,17 @@ const insert_jsmarkup = () => {
|
||||||
const parent = document.getElementById("info");
|
const parent = document.getElementById("info");
|
||||||
parent.insertAdjacentHTML("beforeend", jsmarkup_info);
|
parent.insertAdjacentHTML("beforeend", jsmarkup_info);
|
||||||
}
|
}
|
||||||
if (document.getElementById("info_js__uptime") === null) {
|
if (document.getElementById("info_js__float") === null) {
|
||||||
const parent = document.getElementById("info_js");
|
const parent = document.getElementById("info_js");
|
||||||
parent.insertAdjacentHTML("beforeend", jsmarkup_info_uptime);
|
parent.insertAdjacentHTML("beforeend", jsmarkup_info_float);
|
||||||
|
}
|
||||||
|
if (document.getElementById("info_js__float__viewership") === null) {
|
||||||
|
const parent = document.getElementById("info_js__float");
|
||||||
|
parent.insertAdjacentHTML("beforeend", jsmarkup_info_float_viewership);
|
||||||
|
}
|
||||||
|
if (document.getElementById("info_js__float__uptime") === null) {
|
||||||
|
const parent = document.getElementById("info_js__float");
|
||||||
|
parent.insertAdjacentHTML("beforeend", jsmarkup_info_float_uptime);
|
||||||
}
|
}
|
||||||
if (document.getElementById("info_js__title") === null) {
|
if (document.getElementById("info_js__title") === null) {
|
||||||
const parent = document.getElementById("info_js");
|
const parent = document.getElementById("info_js");
|
||||||
|
@ -65,7 +75,8 @@ const stylesheet_tripcode_colors = document.styleSheets[3];
|
||||||
|
|
||||||
/* create websocket */
|
/* create websocket */
|
||||||
const info_title = document.getElementById("info_js__title");
|
const info_title = document.getElementById("info_js__title");
|
||||||
const info_uptime = document.getElementById("info_js__uptime");
|
const info_viewership = document.getElementById("info_js__float__viewership");
|
||||||
|
const info_uptime = document.getElementById("info_js__float__uptime");
|
||||||
const chat_messages = document.getElementById("chat-messages_js");
|
const chat_messages = document.getElementById("chat-messages_js");
|
||||||
|
|
||||||
const create_chat_message = (object) => {
|
const create_chat_message = (object) => {
|
||||||
|
@ -352,6 +363,10 @@ const update_uptime = () => {
|
||||||
}
|
}
|
||||||
setInterval(update_uptime, 1000); // always update uptime
|
setInterval(update_uptime, 1000); // always update uptime
|
||||||
|
|
||||||
|
const set_viewership = (n) => {
|
||||||
|
info_viewership.innerText = n === null ? "" : `${n} viewers`;
|
||||||
|
}
|
||||||
|
|
||||||
const on_websocket_message = (event) => {
|
const on_websocket_message = (event) => {
|
||||||
//console.log("websocket message", event);
|
//console.log("websocket message", event);
|
||||||
const receipt = JSON.parse(event.data);
|
const receipt = JSON.parse(event.data);
|
||||||
|
@ -363,13 +378,23 @@ const on_websocket_message = (event) => {
|
||||||
case "init":
|
case "init":
|
||||||
console.log("ws init", receipt);
|
console.log("ws init", receipt);
|
||||||
|
|
||||||
|
// set title
|
||||||
set_title(receipt.title);
|
set_title(receipt.title);
|
||||||
|
|
||||||
|
// set viewership
|
||||||
|
set_viewership(receipt.viewership);
|
||||||
|
|
||||||
|
// set uptime
|
||||||
set_frozen_uptime(receipt.uptime);
|
set_frozen_uptime(receipt.uptime);
|
||||||
update_uptime();
|
update_uptime();
|
||||||
|
|
||||||
|
// chat form nonce
|
||||||
chat_form_nonce.value = receipt.nonce;
|
chat_form_nonce.value = receipt.nonce;
|
||||||
|
|
||||||
|
// chat form captcha digest
|
||||||
receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest);
|
receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest);
|
||||||
|
|
||||||
|
// remove messages the server isn't acknowledging the existance of
|
||||||
const seqs = new Set(receipt.messages.map((message) => {return message.seq;}));
|
const seqs = new Set(receipt.messages.map((message) => {return message.seq;}));
|
||||||
const to_delete = [];
|
const to_delete = [];
|
||||||
for (const chat_message of chat_messages.children) {
|
for (const chat_message of chat_messages.children) {
|
||||||
|
@ -382,13 +407,17 @@ const on_websocket_message = (event) => {
|
||||||
chat_message.remove();
|
chat_message.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// settings
|
||||||
default_name = receipt.default;
|
default_name = receipt.default;
|
||||||
max_chat_scrollback = receipt.scrollback;
|
max_chat_scrollback = receipt.scrollback;
|
||||||
|
|
||||||
|
// appearances
|
||||||
users = receipt.users;
|
users = receipt.users;
|
||||||
update_user_names();
|
update_user_names();
|
||||||
update_user_colors();
|
update_user_colors();
|
||||||
update_user_tripcodes();
|
update_user_tripcodes();
|
||||||
|
|
||||||
|
// insert new messages
|
||||||
const last = chat_messages.children.length == 0 ? null : chat_messages.children[chat_messages.children.length - 1];
|
const last = chat_messages.children.length == 0 ? null : chat_messages.children[chat_messages.children.length - 1];
|
||||||
const last_seq = last === null ? null : parseInt(last.dataset.seq);
|
const last_seq = last === null ? null : parseInt(last.dataset.seq);
|
||||||
for (const message of receipt.messages) {
|
for (const message of receipt.messages) {
|
||||||
|
@ -408,6 +437,11 @@ const on_websocket_message = (event) => {
|
||||||
set_frozen_uptime(receipt.uptime);
|
set_frozen_uptime(receipt.uptime);
|
||||||
update_uptime();
|
update_uptime();
|
||||||
}
|
}
|
||||||
|
if (receipt.viewership === 0 && frozen_uptime === null) {
|
||||||
|
set_viewership(null);
|
||||||
|
} else if (receipt.viewership !== undefined) {
|
||||||
|
set_viewership(receipt.viewership);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ack":
|
case "ack":
|
||||||
|
|
|
@ -61,12 +61,18 @@ noscript {
|
||||||
}
|
}
|
||||||
#info_js {
|
#info_js {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1ch 1.5ch;
|
padding: 0.75ch 1.25ch;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
#info_js__uptime {
|
#info_js__float {
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-gap: 2.5ch;
|
||||||
|
}
|
||||||
|
#info_js__float__uptime {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
#info_js__title > h1 {
|
#info_js__title > h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -4,9 +4,11 @@ import aiofiles
|
||||||
from quart import current_app
|
from quart import current_app
|
||||||
|
|
||||||
from anonstream.segments import get_playlist, Offline
|
from anonstream.segments import get_playlist, Offline
|
||||||
from anonstream.wrappers import ttl_cache_async
|
from anonstream.wrappers import ttl_cache_async, with_timestamp
|
||||||
|
from anonstream.helpers.user import is_watching
|
||||||
|
|
||||||
CONFIG = current_app.config
|
CONFIG = current_app.config
|
||||||
|
USERS = current_app.users
|
||||||
|
|
||||||
@ttl_cache_async(CONFIG['STREAM_TITLE_CACHE_LIFETIME'])
|
@ttl_cache_async(CONFIG['STREAM_TITLE_CACHE_LIFETIME'])
|
||||||
async def get_stream_title():
|
async def get_stream_title():
|
||||||
|
@ -32,6 +34,14 @@ def get_stream_uptime(rounded=True):
|
||||||
uptime = round(uptime, 2) if rounded else uptime
|
uptime = round(uptime, 2) if rounded else uptime
|
||||||
return uptime
|
return uptime
|
||||||
|
|
||||||
|
@with_timestamp
|
||||||
|
def get_stream_viewership(timestamp):
|
||||||
|
return sum(map(lambda user: is_watching(timestamp, user), USERS))
|
||||||
|
|
||||||
|
def get_stream_viewership_or_none(uptime):
|
||||||
|
viewership = get_stream_viewership()
|
||||||
|
return uptime and viewership
|
||||||
|
|
||||||
def is_online():
|
def is_online():
|
||||||
try:
|
try:
|
||||||
get_playlist()
|
get_playlist()
|
||||||
|
|
|
@ -5,7 +5,7 @@ from functools import wraps
|
||||||
from quart import current_app
|
from quart import current_app
|
||||||
|
|
||||||
from anonstream.broadcast import broadcast, broadcast_users_update
|
from anonstream.broadcast import broadcast, broadcast_users_update
|
||||||
from anonstream.stream import is_online, get_stream_title, get_stream_uptime
|
from anonstream.stream import is_online, get_stream_title, get_stream_uptime, get_stream_viewership_or_none
|
||||||
from anonstream.wrappers import with_timestamp
|
from anonstream.wrappers import with_timestamp
|
||||||
from anonstream.helpers.user import is_visible
|
from anonstream.helpers.user import is_visible
|
||||||
|
|
||||||
|
@ -98,8 +98,12 @@ async def t_broadcast_users_update(iteration):
|
||||||
@with_period(CONFIG['TASK_PERIOD_BROADCAST_STREAM_INFO_UPDATE'])
|
@with_period(CONFIG['TASK_PERIOD_BROADCAST_STREAM_INFO_UPDATE'])
|
||||||
async def t_broadcast_stream_info_update(iteration):
|
async def t_broadcast_stream_info_update(iteration):
|
||||||
if iteration == 0:
|
if iteration == 0:
|
||||||
current_app.stream_title = await get_stream_title()
|
title = await get_stream_title()
|
||||||
current_app.stream_uptime = get_stream_uptime()
|
uptime = get_stream_uptime()
|
||||||
|
viewership = get_stream_viewership_or_none(uptime)
|
||||||
|
current_app.stream_title = title
|
||||||
|
current_app.stream_uptime = uptime
|
||||||
|
current_app.stream_viewership = viewership
|
||||||
else:
|
else:
|
||||||
payload = {}
|
payload = {}
|
||||||
|
|
||||||
|
@ -109,7 +113,7 @@ async def t_broadcast_stream_info_update(iteration):
|
||||||
current_app.stream_title = title
|
current_app.stream_title = title
|
||||||
payload['title'] = title
|
payload['title'] = title
|
||||||
|
|
||||||
# Check if the stream uptime has changed differently than expected
|
# Check if the stream uptime has changed more or less than expected
|
||||||
if current_app.stream_uptime is None:
|
if current_app.stream_uptime is None:
|
||||||
expected_uptime = None
|
expected_uptime = None
|
||||||
else:
|
else:
|
||||||
|
@ -126,6 +130,12 @@ async def t_broadcast_stream_info_update(iteration):
|
||||||
elif abs(uptime - expected_uptime) >= 0.0625:
|
elif abs(uptime - expected_uptime) >= 0.0625:
|
||||||
payload['uptime'] = uptime
|
payload['uptime'] = uptime
|
||||||
|
|
||||||
|
# Check if viewership has changed
|
||||||
|
viewership = get_stream_viewership_or_none(uptime)
|
||||||
|
if current_app.stream_viewership != viewership:
|
||||||
|
current_app.stream_viewership = viewership
|
||||||
|
payload['viewership'] = viewership
|
||||||
|
|
||||||
if payload:
|
if payload:
|
||||||
broadcast(USERS, payload={'type': 'info', **payload})
|
broadcast(USERS, payload={'type': 'info', **payload})
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,23 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="refresh" content="12">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin: 1ch 1.5ch;
|
margin: 0.75ch 1.25ch;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
#uptime {
|
#float {
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-gap: 2.5ch;
|
||||||
|
}
|
||||||
|
#float__uptime {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
#title > h1 {
|
#title > h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -22,7 +29,12 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<aside id="uptime">{{ uptime }}</aside>
|
{% if uptime is not none %}
|
||||||
|
<aside id="float">
|
||||||
|
<div id="float__viewership">{{ viewership }} viewers</div>
|
||||||
|
<div id="float__uptime">{{ uptime }}</div>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
<header id="title"><h1>{{ title }}</h1></header>
|
<header id="title"><h1>{{ title }}</h1></header>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import json
|
||||||
|
|
||||||
from quart import current_app, websocket
|
from quart import current_app, websocket
|
||||||
|
|
||||||
from anonstream.stream import get_stream_title, get_stream_uptime
|
from anonstream.stream import get_stream_title, get_stream_uptime, get_stream_viewership_or_none
|
||||||
from anonstream.captcha import get_random_captcha_digest_for
|
from anonstream.captcha import get_random_captcha_digest_for
|
||||||
from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected
|
from anonstream.chat import get_all_messages_for_websocket, add_chat_message, Rejected
|
||||||
from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha
|
from anonstream.user import get_all_users_for_websocket, see, verify, deverify, BadCaptcha
|
||||||
|
@ -13,11 +13,14 @@ from anonstream.utils.websocket import parse_websocket_data, Malformed
|
||||||
CONFIG = current_app.config
|
CONFIG = current_app.config
|
||||||
|
|
||||||
async def websocket_outbound(queue, user):
|
async def websocket_outbound(queue, user):
|
||||||
|
uptime = get_stream_uptime()
|
||||||
|
viewership = get_stream_viewership_or_none(uptime)
|
||||||
payload = {
|
payload = {
|
||||||
'type': 'init',
|
'type': 'init',
|
||||||
'nonce': generate_nonce(),
|
'nonce': generate_nonce(),
|
||||||
'title': await get_stream_title(),
|
'title': await get_stream_title(),
|
||||||
'uptime': get_stream_uptime(),
|
'uptime': uptime,
|
||||||
|
'viewership': viewership,
|
||||||
'messages': get_all_messages_for_websocket(),
|
'messages': get_all_messages_for_websocket(),
|
||||||
'users': get_all_users_for_websocket(),
|
'users': get_all_users_for_websocket(),
|
||||||
'default': {
|
'default': {
|
||||||
|
|
読み込み中…
新しいイシューから参照