Add viewer count
This commit is contained in:
parent
1218e601ae
commit
77d5f16fd5
5 changed files with 474 additions and 0 deletions
|
|
@ -208,6 +208,34 @@
|
||||||
openLink.href = url;
|
openLink.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Viewer Count URL generator
|
||||||
|
function updateViewerCountUrl() {
|
||||||
|
const baseUrl = document.getElementById('viewercount-base-url');
|
||||||
|
if (!baseUrl) return;
|
||||||
|
|
||||||
|
const theme = document.getElementById('viewercount-theme').value;
|
||||||
|
const fontsize = document.getElementById('viewercount-fontsize').value;
|
||||||
|
const hidelabel = document.getElementById('viewercount-hidelabel').value;
|
||||||
|
const livedot = document.getElementById('viewercount-livedot').value;
|
||||||
|
const urlInput = document.getElementById('viewercount-url');
|
||||||
|
const openLink = document.getElementById('viewercount-open');
|
||||||
|
|
||||||
|
let url = baseUrl.value;
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (theme !== 'dark') params.push(`theme=${theme}`);
|
||||||
|
if (fontsize !== 'medium') params.push(`fontsize=${fontsize}`);
|
||||||
|
if (hidelabel === 'true') params.push(`hidelabel=true`);
|
||||||
|
if (livedot === 'true') params.push(`livedot=true`);
|
||||||
|
|
||||||
|
if (params.length > 0) {
|
||||||
|
url += '?' + params.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
urlInput.value = url;
|
||||||
|
openLink.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
function copyUrl(inputId) {
|
function copyUrl(inputId) {
|
||||||
const input = document.getElementById(inputId);
|
const input = document.getElementById(inputId);
|
||||||
input.select();
|
input.select();
|
||||||
|
|
@ -222,6 +250,7 @@
|
||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
updateLiveChatUrl();
|
updateLiveChatUrl();
|
||||||
|
updateViewerCountUrl();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
139
app/assets/web/widgets/viewercount/app.js
Normal file
139
app/assets/web/widgets/viewercount/app.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
class ViewerCountWidget {
|
||||||
|
constructor() {
|
||||||
|
this.countElement = document.getElementById('count-value');
|
||||||
|
this.viewerCount = document.getElementById('viewer-count');
|
||||||
|
this.statusMessage = document.getElementById('status-message');
|
||||||
|
this.iconElement = document.getElementById('count-icon');
|
||||||
|
this.labelElement = document.getElementById('count-label');
|
||||||
|
|
||||||
|
this.currentCount = 0;
|
||||||
|
this.refreshInterval = 30000; // 30 seconds
|
||||||
|
this.intervalId = null;
|
||||||
|
|
||||||
|
// Parse URL parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Theme: dark (default), light, minimal
|
||||||
|
const theme = urlParams.get('theme') || 'dark';
|
||||||
|
document.body.classList.add(`theme-${theme}`);
|
||||||
|
|
||||||
|
// Font size: small, medium (default), large, xlarge
|
||||||
|
const fontSize = urlParams.get('fontsize') || 'medium';
|
||||||
|
document.body.classList.add(`font-${fontSize}`);
|
||||||
|
|
||||||
|
// Hide label option
|
||||||
|
const hideLabel = urlParams.get('hidelabel');
|
||||||
|
if (hideLabel === 'true' || hideLabel === '1') {
|
||||||
|
document.body.classList.add('hide-label');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show live dot
|
||||||
|
const showDot = urlParams.get('livedot');
|
||||||
|
if (showDot === 'true' || showDot === '1') {
|
||||||
|
document.body.classList.add('show-live-dot');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom refresh interval (in seconds)
|
||||||
|
const interval = parseInt(urlParams.get('interval'));
|
||||||
|
if (interval && interval >= 10) {
|
||||||
|
this.refreshInterval = interval * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchViewerCount();
|
||||||
|
this.intervalId = setInterval(() => this.fetchViewerCount(), this.refreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchViewerCount() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/viewercount');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.updateDisplay(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch viewer count:', error);
|
||||||
|
this.showError('Failed to load');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay(data) {
|
||||||
|
const { twitch, youtube, total } = data;
|
||||||
|
|
||||||
|
// Determine which platform(s) have data
|
||||||
|
const hasTwitch = twitch !== null;
|
||||||
|
const hasYoutube = youtube !== null;
|
||||||
|
|
||||||
|
// Set platform class for icon color based on what's configured/available
|
||||||
|
document.body.classList.remove('platform-twitch', 'platform-youtube', 'platform-combined');
|
||||||
|
if (hasTwitch && hasYoutube) {
|
||||||
|
document.body.classList.add('platform-combined');
|
||||||
|
this.setIcon('eye');
|
||||||
|
} else if (hasTwitch) {
|
||||||
|
document.body.classList.add('platform-twitch');
|
||||||
|
this.setIcon('twitch');
|
||||||
|
} else if (hasYoutube) {
|
||||||
|
document.body.classList.add('platform-youtube');
|
||||||
|
this.setIcon('youtube');
|
||||||
|
} else {
|
||||||
|
// No platform configured, use generic eye icon
|
||||||
|
document.body.classList.add('platform-combined');
|
||||||
|
this.setIcon('eye');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update count with animation
|
||||||
|
const newCount = total || 0;
|
||||||
|
if (newCount !== this.currentCount) {
|
||||||
|
this.viewerCount.classList.add('updating');
|
||||||
|
setTimeout(() => this.viewerCount.classList.remove('updating'), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentCount = newCount;
|
||||||
|
this.countElement.textContent = this.formatNumber(newCount);
|
||||||
|
|
||||||
|
// Update label
|
||||||
|
this.labelElement.textContent = newCount === 1 ? 'viewer' : 'viewers';
|
||||||
|
|
||||||
|
// Show the widget, hide status
|
||||||
|
this.viewerCount.classList.remove('hidden');
|
||||||
|
this.statusMessage.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatNumber(num) {
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (num >= 10000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
} else if (num >= 1000) {
|
||||||
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
return num.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
twitch: `<svg viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/></svg>`,
|
||||||
|
youtube: `<svg viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>`,
|
||||||
|
eye: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>`
|
||||||
|
};
|
||||||
|
this.iconElement.innerHTML = icons[type] || icons.eye;
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.viewerCount.classList.add('hidden');
|
||||||
|
this.statusMessage.textContent = message;
|
||||||
|
this.statusMessage.classList.remove('hidden');
|
||||||
|
this.statusMessage.classList.add('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new ViewerCountWidget();
|
||||||
|
});
|
||||||
|
|
||||||
21
app/assets/web/widgets/viewercount/index.html
Normal file
21
app/assets/web/widgets/viewercount/index.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Viewer Count</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="viewer-container">
|
||||||
|
<div id="viewer-count" class="hidden">
|
||||||
|
<span id="count-icon" class="icon"></span>
|
||||||
|
<span id="count-value">0</span>
|
||||||
|
<span id="count-label">viewers</span>
|
||||||
|
</div>
|
||||||
|
<div id="status-message" class="status">Loading...</div>
|
||||||
|
</div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
161
app/assets/web/widgets/viewercount/style.css
Normal file
161
app/assets/web/widgets/viewercount/style.css
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer-count.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme: Dark (default) */
|
||||||
|
body.theme-dark #viewer-count {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme: Light */
|
||||||
|
body.theme-light #viewer-count {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: #1a1a1a;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme: Minimal (no background) */
|
||||||
|
body.theme-minimal #viewer-count {
|
||||||
|
background: transparent;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-minimal.light-text #viewer-count {
|
||||||
|
color: #1a1a1a;
|
||||||
|
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon */
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Platform-specific colors */
|
||||||
|
body.platform-twitch .icon svg {
|
||||||
|
fill: #9146FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.platform-youtube .icon svg {
|
||||||
|
fill: #FF0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.platform-combined .icon svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Count value */
|
||||||
|
#count-value {
|
||||||
|
font-size: var(--count-font-size, 24px);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 2ch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label */
|
||||||
|
#count-label {
|
||||||
|
font-size: calc(var(--count-font-size, 24px) * 0.6);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.hide-label #count-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font sizes */
|
||||||
|
body.font-small {
|
||||||
|
--count-font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.font-medium {
|
||||||
|
--count-font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.font-large {
|
||||||
|
--count-font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.font-xlarge {
|
||||||
|
--count-font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewer-count.updating #count-value {
|
||||||
|
animation: pulse 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live indicator dot */
|
||||||
|
body.show-live-dot #viewer-count::before {
|
||||||
|
content: '';
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #e91916;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 4px;
|
||||||
|
animation: live-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes live-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status message */
|
||||||
|
.status {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
124
app/webserver.py
124
app/webserver.py
|
|
@ -13,6 +13,7 @@ from app.state import AppState
|
||||||
WIDGETS = [
|
WIDGETS = [
|
||||||
{"slug": "nowplaying", "label": "Now Playing"},
|
{"slug": "nowplaying", "label": "Now Playing"},
|
||||||
{"slug": "livechat", "label": "Live Chat"},
|
{"slug": "livechat", "label": "Live Chat"},
|
||||||
|
{"slug": "viewercount", "label": "Viewer Count"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,6 +77,53 @@ async def handle_root(request: web.Request) -> web.Response:
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
"""
|
"""
|
||||||
|
elif slug == "viewercount":
|
||||||
|
# Viewer Count widget with options
|
||||||
|
item_html = f"""
|
||||||
|
<li class="widget-item">
|
||||||
|
<div class="widget-header">
|
||||||
|
<a id="viewercount-open" class="widget-name" href="{url}" target="_blank">{label}</a>
|
||||||
|
</div>
|
||||||
|
<div class="widget-url-row">
|
||||||
|
<input type="hidden" id="viewercount-base-url" value="{url}">
|
||||||
|
<input type="text" id="viewercount-url" value="{url}" readonly>
|
||||||
|
<button class="copy-btn" onclick="copyUrl('viewercount-url')">Copy</button>
|
||||||
|
</div>
|
||||||
|
<div class="widget-options">
|
||||||
|
<div class="option-group">
|
||||||
|
<label>Theme</label>
|
||||||
|
<select id="viewercount-theme" onchange="updateViewerCountUrl()">
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="minimal">Minimal (no bg)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="option-group">
|
||||||
|
<label>Font Size</label>
|
||||||
|
<select id="viewercount-fontsize" onchange="updateViewerCountUrl()">
|
||||||
|
<option value="small">Small</option>
|
||||||
|
<option value="medium" selected>Medium</option>
|
||||||
|
<option value="large">Large</option>
|
||||||
|
<option value="xlarge">Extra Large</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="option-group">
|
||||||
|
<label>Label</label>
|
||||||
|
<select id="viewercount-hidelabel" onchange="updateViewerCountUrl()">
|
||||||
|
<option value="false">Show</option>
|
||||||
|
<option value="true">Hide</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="option-group">
|
||||||
|
<label>Live Dot</label>
|
||||||
|
<select id="viewercount-livedot" onchange="updateViewerCountUrl()">
|
||||||
|
<option value="false">Hide</option>
|
||||||
|
<option value="true">Show</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
"""
|
||||||
else:
|
else:
|
||||||
# Standard widget without options
|
# Standard widget without options
|
||||||
item_html = f"""
|
item_html = f"""
|
||||||
|
|
@ -237,6 +285,81 @@ async def handle_open_config_dir(request: web.Request) -> web.Response:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_viewer_count(request: web.Request) -> web.Response:
|
||||||
|
"""Get viewer count from Twitch and/or YouTube."""
|
||||||
|
import aiohttp
|
||||||
|
from app.chat_models import Platform
|
||||||
|
|
||||||
|
state: AppState = request.app["state"]
|
||||||
|
app_config = load_config()
|
||||||
|
chat_config = state.chat_config
|
||||||
|
|
||||||
|
twitch_count: int | None = None
|
||||||
|
youtube_count: int | None = None
|
||||||
|
|
||||||
|
# Fetch Twitch viewer count
|
||||||
|
if chat_config.twitch_channel:
|
||||||
|
try:
|
||||||
|
twitch_tokens = await state.get_auth_tokens(Platform.TWITCH)
|
||||||
|
if twitch_tokens and app_config.twitch_oauth.client_id:
|
||||||
|
headers = {
|
||||||
|
"Client-ID": app_config.twitch_oauth.client_id,
|
||||||
|
"Authorization": f"Bearer {twitch_tokens.access_token}",
|
||||||
|
}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
url = f"https://api.twitch.tv/helix/streams?user_login={chat_config.twitch_channel}"
|
||||||
|
async with session.get(url, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
streams = data.get("data", [])
|
||||||
|
if streams:
|
||||||
|
twitch_count = streams[0].get("viewer_count", 0)
|
||||||
|
else:
|
||||||
|
# Channel configured but not live
|
||||||
|
twitch_count = 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching Twitch viewer count: {e}")
|
||||||
|
|
||||||
|
# Fetch YouTube viewer count
|
||||||
|
if chat_config.youtube_video_id:
|
||||||
|
try:
|
||||||
|
youtube_tokens = await state.get_auth_tokens(Platform.YOUTUBE)
|
||||||
|
if youtube_tokens:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
url = (
|
||||||
|
f"https://www.googleapis.com/youtube/v3/videos"
|
||||||
|
f"?part=liveStreamingDetails&id={chat_config.youtube_video_id}"
|
||||||
|
)
|
||||||
|
headers = {"Authorization": f"Bearer {youtube_tokens.access_token}"}
|
||||||
|
async with session.get(url, headers=headers) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
data = await resp.json()
|
||||||
|
items = data.get("items", [])
|
||||||
|
if items:
|
||||||
|
live_details = items[0].get("liveStreamingDetails", {})
|
||||||
|
concurrent = live_details.get("concurrentViewers")
|
||||||
|
if concurrent is not None:
|
||||||
|
youtube_count = int(concurrent)
|
||||||
|
else:
|
||||||
|
# Video exists but not live
|
||||||
|
youtube_count = 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching YouTube viewer count: {e}")
|
||||||
|
|
||||||
|
# Calculate total
|
||||||
|
total = 0
|
||||||
|
if twitch_count is not None:
|
||||||
|
total += twitch_count
|
||||||
|
if youtube_count is not None:
|
||||||
|
total += youtube_count
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
"twitch": twitch_count,
|
||||||
|
"youtube": youtube_count,
|
||||||
|
"total": total,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
async def handle_ws(request: web.Request) -> web.WebSocketResponse:
|
async def handle_ws(request: web.Request) -> web.WebSocketResponse:
|
||||||
state: AppState = request.app["state"]
|
state: AppState = request.app["state"]
|
||||||
ws = web.WebSocketResponse(heartbeat=30)
|
ws = web.WebSocketResponse(heartbeat=30)
|
||||||
|
|
@ -287,6 +410,7 @@ def make_app(state: AppState) -> web.Application:
|
||||||
app.router.add_get("/api/oauth/status", handle_oauth_status)
|
app.router.add_get("/api/oauth/status", handle_oauth_status)
|
||||||
app.router.add_get("/api/auth/status", handle_auth_status)
|
app.router.add_get("/api/auth/status", handle_auth_status)
|
||||||
app.router.add_post("/api/config/open-directory", handle_open_config_dir)
|
app.router.add_post("/api/config/open-directory", handle_open_config_dir)
|
||||||
|
app.router.add_get("/api/viewercount", handle_viewer_count)
|
||||||
app.router.add_get("/ws", handle_ws)
|
app.router.add_get("/ws", handle_ws)
|
||||||
|
|
||||||
# Register OAuth routes
|
# Register OAuth routes
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue