diff --git a/anonstream/__init__.py b/anonstream/__init__.py
index 2fcbd73..30bf3f5 100644
--- a/anonstream/__init__.py
+++ b/anonstream/__init__.py
@@ -89,8 +89,9 @@ def create_app(config_file):
# State for tasks
app.users_update_buffer = set()
- app.stream_uptime = None
app.stream_title = None
+ app.stream_uptime = None
+ app.stream_viewership = None
# Background tasks' asyncio.sleep tasks, cancelled on shutdown
app.background_sleep = set()
diff --git a/anonstream/helpers/user.py b/anonstream/helpers/user.py
index 871026b..a1df64e 100644
--- a/anonstream/helpers/user.py
+++ b/anonstream/helpers/user.py
@@ -76,6 +76,9 @@ def get_presence(timestamp, user):
return Presence.ABSENT
+def is_watching(timestamp, user):
+ return get_presence(timestamp, user) == Presence.WATCHING
+
def is_listed(timestamp, user):
return (
get_presence(timestamp, user)
diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py
index 5f70556..b412a8b 100644
--- a/anonstream/routes/nojs.py
+++ b/anonstream/routes/nojs.py
@@ -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.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.routes.wrappers import with_user_from, render_template_with_etag
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.user import concatenate_for_notice
@@ -19,8 +19,9 @@ async def nojs_info(user):
return await render_template(
'nojs_info.html',
user=user,
- title=await get_stream_title(),
+ viewership=get_stream_viewership(),
uptime=get_stream_uptime(),
+ title=await get_stream_title(),
)
@current_app.route('/chat/messages.html')
diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js
index 649edc4..12769ea 100644
--- a/anonstream/static/anonstream.js
+++ b/anonstream/static/anonstream.js
@@ -6,7 +6,9 @@ const jsmarkup_style_color = ''
const jsmarkup_style_tripcode_display = ''
const jsmarkup_style_tripcode_colors = ''
const jsmarkup_info = '
';
-const jsmarkup_info_uptime = '';
+const jsmarkup_info_float = '';
+const jsmarkup_info_float_viewership = '';
+const jsmarkup_info_float_uptime = '';
const jsmarkup_info_title = '';
const jsmarkup_chat_messages = '
';
const jsmarkup_chat_form = `\
@@ -23,7 +25,7 @@ const jsmarkup_chat_form = `\
`;
-const insert_jsmarkup = () => {
+const insert_jsmarkup = () => {jsmarkup_info_float_viewership
if (document.getElementById("style-color") === null) {
const parent = document.head;
parent.insertAdjacentHTML("beforeend", jsmarkup_style_color);
@@ -40,9 +42,17 @@ const insert_jsmarkup = () => {
const parent = document.getElementById("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");
- 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) {
const parent = document.getElementById("info_js");
@@ -65,7 +75,8 @@ const stylesheet_tripcode_colors = document.styleSheets[3];
/* create websocket */
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 create_chat_message = (object) => {
@@ -352,6 +363,10 @@ const 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) => {
//console.log("websocket message", event);
const receipt = JSON.parse(event.data);
@@ -363,13 +378,23 @@ const on_websocket_message = (event) => {
case "init":
console.log("ws init", receipt);
+ // set title
set_title(receipt.title);
+
+ // set viewership
+ set_viewership(receipt.viewership);
+
+ // set uptime
set_frozen_uptime(receipt.uptime);
update_uptime();
+ // chat form nonce
chat_form_nonce.value = receipt.nonce;
+
+ // chat form captcha 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 to_delete = [];
for (const chat_message of chat_messages.children) {
@@ -382,13 +407,17 @@ const on_websocket_message = (event) => {
chat_message.remove();
}
+ // settings
default_name = receipt.default;
max_chat_scrollback = receipt.scrollback;
+
+ // appearances
users = receipt.users;
update_user_names();
update_user_colors();
update_user_tripcodes();
+ // insert new messages
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);
for (const message of receipt.messages) {
@@ -408,6 +437,11 @@ const on_websocket_message = (event) => {
set_frozen_uptime(receipt.uptime);
update_uptime();
}
+ if (receipt.viewership === 0 && frozen_uptime === null) {
+ set_viewership(null);
+ } else if (receipt.viewership !== undefined) {
+ set_viewership(receipt.viewership);
+ }
break;
case "ack":
diff --git a/anonstream/static/style.css b/anonstream/static/style.css
index 227890b..07bf2fe 100644
--- a/anonstream/static/style.css
+++ b/anonstream/static/style.css
@@ -61,12 +61,18 @@ noscript {
}
#info_js {
overflow-y: auto;
- padding: 1ch 1.5ch;
+ padding: 0.75ch 1.25ch;
height: 100%;
}
-#info_js__uptime {
+#info_js__float {
float: right;
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 {
margin: 0;
diff --git a/anonstream/stream.py b/anonstream/stream.py
index 07472f7..e3eeebe 100644
--- a/anonstream/stream.py
+++ b/anonstream/stream.py
@@ -4,9 +4,11 @@ import aiofiles
from quart import current_app
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
+USERS = current_app.users
@ttl_cache_async(CONFIG['STREAM_TITLE_CACHE_LIFETIME'])
async def get_stream_title():
@@ -32,6 +34,14 @@ def get_stream_uptime(rounded=True):
uptime = round(uptime, 2) if rounded else 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():
try:
get_playlist()
diff --git a/anonstream/tasks.py b/anonstream/tasks.py
index 002fd4a..d97af48 100644
--- a/anonstream/tasks.py
+++ b/anonstream/tasks.py
@@ -5,7 +5,7 @@ from functools import wraps
from quart import current_app
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.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'])
async def t_broadcast_stream_info_update(iteration):
if iteration == 0:
- current_app.stream_title = await get_stream_title()
- current_app.stream_uptime = get_stream_uptime()
+ title = await get_stream_title()
+ 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:
payload = {}
@@ -109,7 +113,7 @@ async def t_broadcast_stream_info_update(iteration):
current_app.stream_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:
expected_uptime = None
else:
@@ -126,6 +130,12 @@ async def t_broadcast_stream_info_update(iteration):
elif abs(uptime - expected_uptime) >= 0.0625:
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:
broadcast(USERS, payload={'type': 'info', **payload})
diff --git a/anonstream/templates/nojs_info.html b/anonstream/templates/nojs_info.html
index b9785a7..d026e07 100644
--- a/anonstream/templates/nojs_info.html
+++ b/anonstream/templates/nojs_info.html
@@ -2,16 +2,23 @@
+
-
+ {% if uptime is not none %}
+
+ {% endif %}
diff --git a/anonstream/websocket.py b/anonstream/websocket.py
index 26b1340..3b340f7 100644
--- a/anonstream/websocket.py
+++ b/anonstream/websocket.py
@@ -3,7 +3,7 @@ import json
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.chat import get_all_messages_for_websocket, add_chat_message, Rejected
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
async def websocket_outbound(queue, user):
+ uptime = get_stream_uptime()
+ viewership = get_stream_viewership_or_none(uptime)
payload = {
'type': 'init',
'nonce': generate_nonce(),
'title': await get_stream_title(),
- 'uptime': get_stream_uptime(),
+ 'uptime': uptime,
+ 'viewership': viewership,
'messages': get_all_messages_for_websocket(),
'users': get_all_users_for_websocket(),
'default': {