Update src/libpoketube/init/pages-api.js
This commit is contained in:
parent
29b0a6e6e5
commit
67138a72f9
@ -101,184 +101,287 @@ if (!data.users) data.users = {}
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.get("/api/stats", (req, res) => {
|
||||
if (!telemetryConfig.telemetry) {
|
||||
const view = (req.query.view || "").toString()
|
||||
if (!view) {
|
||||
return res.send(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Poke – Stats</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
background: #05070a;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
p { line-height: 1.6; margin: 0.4rem 0; }
|
||||
code {
|
||||
background: #111827;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
a { color: #7dd3fc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.box {
|
||||
margin-top: 1.2rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 10px;
|
||||
background: #0b1120;
|
||||
border: 1px solid #1f2937;
|
||||
}
|
||||
.small { font-size: 0.9rem; opacity: 0.85; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<h1>Poke stats</h1>
|
||||
<p class="small">How Poke improves itself while keeping your browsing private.</p>
|
||||
|
||||
<div class="box">
|
||||
<p>
|
||||
This page shows information about Poke’s <strong>anonymous, local-only</strong> usage statistics feature
|
||||
(<code>/api/stats</code>).
|
||||
</p>
|
||||
<p>
|
||||
For full details on what is collected (and what is <em>not</em>), see the privacy policy section:
|
||||
<a href="/policies/privacy#stats">/policies/privacy#stats</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<p><strong>Developers:</strong></p>
|
||||
<p class="small">
|
||||
• To get the aggregated stats as JSON, call:<br>
|
||||
<code>GET /api/stats?view=json</code><br><br>
|
||||
• The response includes:<br>
|
||||
<code>videos</code> (top 10 most-watched), <code>browsers</code>, <code>os</code>, and <code>totalUsers</code>.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`)
|
||||
}
|
||||
|
||||
return res.json({ videos: {}, browsers: {}, os: {}, totalUsers: 0 })
|
||||
}
|
||||
|
||||
app.get("/api/stats", (req, res) => {
|
||||
const view = (req.query.view || "").toString()
|
||||
|
||||
// if no ?view= param, show info page instead of raw JSON
|
||||
if (!view) {
|
||||
return res.send(`<!DOCTYPE html>
|
||||
// JSON view (for programmatic access)
|
||||
if (view === "json") {
|
||||
if (!telemetryConfig.telemetry) {
|
||||
return res.json({ videos: {}, browsers: {}, os: {}, totalUsers: 0 })
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(statsFile, "utf8")
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
if (!data.videos) data.videos = {}
|
||||
if (!data.browsers) data.browsers = {}
|
||||
if (!data.os) data.os = {}
|
||||
if (!data.users) data.users = {}
|
||||
|
||||
const sortedVideos = Object.entries(data.videos)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
|
||||
const topVideos = Object.fromEntries(sortedVideos)
|
||||
|
||||
return res.json({
|
||||
videos: topVideos,
|
||||
browsers: data.browsers,
|
||||
os: data.os,
|
||||
totalUsers: Object.keys(data.users).length
|
||||
})
|
||||
}
|
||||
|
||||
const telemetryOn = telemetryConfig.telemetry
|
||||
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Poke – Stats</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
padding: 16px;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #05070a;
|
||||
color: #e5e5e5;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 720px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
p { line-height: 1.6; margin: 0.4rem 0; }
|
||||
code {
|
||||
background: #111827;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.92rem;
|
||||
p {
|
||||
margin: 4px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
a { color: #7dd3fc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.box {
|
||||
margin-top: 1.2rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 10px;
|
||||
small {
|
||||
opacity: 0.8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
a {
|
||||
color: #7dd3fc;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.card {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #0b0f19;
|
||||
border: 1px solid #1f2933;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
th, td {
|
||||
padding: 4px 6px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #111827;
|
||||
word-break: break-all;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
background: #020617;
|
||||
}
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
background: #0b1120;
|
||||
border: 1px solid #1f2937;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.muted {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.col {
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 12px; }
|
||||
}
|
||||
.small { font-size: 0.9rem; opacity: 0.85; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<h1>Poke stats</h1>
|
||||
<p class="small">How Poke improves itself while keeping your browsing private.</p>
|
||||
<p>
|
||||
<small>
|
||||
At Poke, we do not collect or share any personal information.
|
||||
To improve Poke we use a completely anonymous, local-only way to figure out how the site is being used.
|
||||
</small>
|
||||
</p>
|
||||
|
||||
<div class="box">
|
||||
<p>
|
||||
This page shows information about Poke’s <strong>anonymous, local-only</strong> usage statistics feature
|
||||
(<code>/api/stats</code>).
|
||||
</p>
|
||||
<p>
|
||||
For full details on what is collected (and what is <em>not</em>), see the privacy policy section:
|
||||
<div class="card">
|
||||
<h2>Status</h2>
|
||||
<p id="status-text"></p>
|
||||
<p class="muted">
|
||||
For full details on what is collected (and what is <em>not</em>), see:
|
||||
<a href="/policies/privacy#stats">/policies/privacy#stats</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<p><strong>Developers:</strong></p>
|
||||
<p class="small">
|
||||
• To get the aggregated stats as JSON, call:<br>
|
||||
<code>GET /api/stats?view=json</code><br><br>
|
||||
• The response includes:<br>
|
||||
<code>videos</code> (top 10 most-watched), <code>browsers</code>, <code>os</code>, and <code>totalUsers</code>.
|
||||
<div class="card">
|
||||
<h2>Overview</h2>
|
||||
<p id="overview-text" class="muted">Loading stats…</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="card col">
|
||||
<h2>Top videos</h2>
|
||||
<table id="videos-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Video ID</th>
|
||||
<th>Views</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="2" class="muted">Loading…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card col">
|
||||
<h2>Platforms</h2>
|
||||
<p class="muted">Browsers</p>
|
||||
<ul id="browsers-list" class="muted" style="padding-left:18px;margin:4px 0 8px 0;">
|
||||
<li>Loading…</li>
|
||||
</ul>
|
||||
<p class="muted">Operating systems</p>
|
||||
<ul id="os-list" class="muted" style="padding-left:18px;margin:4px 0;">
|
||||
<li>Loading…</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>API usage</h2>
|
||||
<p class="muted">
|
||||
• Human view (this page): <code>/api/stats</code><br>
|
||||
• JSON view (for scripts/tools): <code>/api/stats?view=json</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const TELEMETRY_ON = ${telemetryOn ? "true" : "false"};
|
||||
|
||||
const statusEl = document.getElementById("status-text");
|
||||
const overviewEl = document.getElementById("overview-text");
|
||||
const videosTableBody = document.querySelector("#videos-table tbody");
|
||||
const browsersList = document.getElementById("browsers-list");
|
||||
const osList = document.getElementById("os-list");
|
||||
|
||||
if (!TELEMETRY_ON) {
|
||||
statusEl.textContent = "Anonymous usage statistics are disabled on this instance. No stats are being collected.";
|
||||
overviewEl.textContent = "There is no stats data to show because telemetry is turned off.";
|
||||
videosTableBody.innerHTML = '<tr><td colspan="2" class="muted">No data (telemetry disabled).</td></tr>';
|
||||
browsersList.innerHTML = '<li>No data (telemetry disabled).</li>';
|
||||
osList.innerHTML = '<li>No data (telemetry disabled).</li>';
|
||||
} else {
|
||||
statusEl.textContent = "Anonymous usage statistics are enabled. Data is kept local and never shared with third parties.";
|
||||
|
||||
fetch("/api/stats?view=json")
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
var videos = data.videos || {};
|
||||
var browsers = data.browsers || {};
|
||||
var os = data.os || {};
|
||||
var totalUsers = data.totalUsers || 0;
|
||||
|
||||
overviewEl.textContent =
|
||||
"Poke has seen " + Object.keys(videos).length +
|
||||
" tracked videos, from " + totalUsers +
|
||||
" anonymous users (unique local IDs).";
|
||||
|
||||
// videos table
|
||||
var videoKeys = Object.keys(videos);
|
||||
if (videoKeys.length === 0) {
|
||||
videosTableBody.innerHTML = '<tr><td colspan="2" class="muted">No stats recorded yet.</td></tr>';
|
||||
} else {
|
||||
videosTableBody.innerHTML = "";
|
||||
videoKeys.forEach(function (id) {
|
||||
var views = videos[id];
|
||||
var tr = document.createElement("tr");
|
||||
|
||||
var tdId = document.createElement("td");
|
||||
var link = document.createElement("a");
|
||||
link.href = "/watch?v=" + encodeURIComponent(id);
|
||||
link.textContent = id;
|
||||
tdId.appendChild(link);
|
||||
|
||||
var tdViews = document.createElement("td");
|
||||
tdViews.textContent = views;
|
||||
|
||||
tr.appendChild(tdId);
|
||||
tr.appendChild(tdViews);
|
||||
videosTableBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// browsers list
|
||||
var browserKeys = Object.keys(browsers);
|
||||
if (browserKeys.length === 0) {
|
||||
browsersList.innerHTML = "<li>No data yet.</li>";
|
||||
} else {
|
||||
browsersList.innerHTML = "";
|
||||
browserKeys.forEach(function (name) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = name + " – " + browsers[name] + " hits";
|
||||
browsersList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// os list
|
||||
var osKeys = Object.keys(os);
|
||||
if (osKeys.length === 0) {
|
||||
osList.innerHTML = "<li>No data yet.</li>";
|
||||
} else {
|
||||
osList.innerHTML = "";
|
||||
osKeys.forEach(function (name) {
|
||||
var li = document.createElement("li");
|
||||
li.textContent = name + " – " + os[name] + " hits";
|
||||
osList.appendChild(li);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
overviewEl.textContent = "Could not load stats (maybe they are disabled or there was an error).";
|
||||
videosTableBody.innerHTML = '<tr><td colspan="2" class="muted">Error loading data.</td></tr>';
|
||||
browsersList.innerHTML = '<li>Error loading data.</li>';
|
||||
osList.innerHTML = '<li>Error loading data.</li>';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`)
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(statsFile, "utf8")
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
// ensure structure exists
|
||||
if (!data.videos) data.videos = {}
|
||||
if (!data.browsers) data.browsers = {}
|
||||
if (!data.os) data.os = {}
|
||||
if (!data.users) data.users = {}
|
||||
|
||||
const sortedVideos = Object.entries(data.videos)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
|
||||
const topVideos = Object.fromEntries(sortedVideos)
|
||||
|
||||
res.json({
|
||||
videos: topVideos,
|
||||
browsers: data.browsers,
|
||||
os: data.os,
|
||||
totalUsers: Object.keys(data.users).length
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
app.get("/avatars/:v", async function (req, res) {
|
||||
var url = `https://yt3.ggpht.com/${req.params.v}`;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user