move telemetry to somewhere else
This commit is contained in:
parent
5880f2faa8
commit
0dfca92cdf
@ -41,508 +41,6 @@ module.exports = function (app, config, renderTemplate) {
|
|||||||
|
|
||||||
f.body.pipe(res);
|
f.body.pipe(res);
|
||||||
});
|
});
|
||||||
const telemetryConfig = { telemetry: true }
|
|
||||||
|
|
||||||
const path = require("path")
|
|
||||||
|
|
||||||
const statsFile = path.join(__dirname, "stats.json")
|
|
||||||
|
|
||||||
if (!fs.existsSync(statsFile)) {
|
|
||||||
fs.writeFileSync(
|
|
||||||
statsFile,
|
|
||||||
JSON.stringify({ videos: {}, browsers: {}, os: {}, users: {} }, null, 2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseUA(ua) {
|
|
||||||
let browser = "unknown"
|
|
||||||
let os = "unknown"
|
|
||||||
|
|
||||||
if (/firefox/i.test(ua)) browser = "firefox"
|
|
||||||
else if (/chrome|chromium|crios/i.test(ua)) browser = "chrome"
|
|
||||||
else if (/safari/i.test(ua)) browser = "safari"
|
|
||||||
else if (/edge/i.test(ua)) browser = "edge"
|
|
||||||
|
|
||||||
if (/windows/i.test(ua)) os = "windows"
|
|
||||||
else if (/android/i.test(ua)) os = "android"
|
|
||||||
else if (/mac os|macintosh/i.test(ua)) os = "macos"
|
|
||||||
else if (/linux/i.test(ua)) os = "gnu-linux"
|
|
||||||
else if (/iphone|ipad|ios/i.test(ua)) os = "ios"
|
|
||||||
|
|
||||||
return { browser, os }
|
|
||||||
}
|
|
||||||
|
|
||||||
app.post("/api/stats", (req, res) => {
|
|
||||||
if (!telemetryConfig.telemetry) return res.status(200).json({ ok: true })
|
|
||||||
|
|
||||||
const { videoId, userId } = req.body
|
|
||||||
if (!videoId) return res.status(400).json({ error: "missing videoId" })
|
|
||||||
if (!userId) return res.status(400).json({ error: "missing userId" })
|
|
||||||
|
|
||||||
const ua = req.headers["user-agent"] || ""
|
|
||||||
const { browser, os } = parseUA(ua)
|
|
||||||
|
|
||||||
const raw = fs.readFileSync(statsFile, "utf8")
|
|
||||||
const data = JSON.parse(raw)
|
|
||||||
if (!data.users) data.users = {}
|
|
||||||
|
|
||||||
if (!data.videos[videoId]) data.videos[videoId] = 0
|
|
||||||
data.videos[videoId]++
|
|
||||||
|
|
||||||
if (!data.browsers[browser]) data.browsers[browser] = 0
|
|
||||||
data.browsers[browser]++
|
|
||||||
|
|
||||||
if (!data.os[os]) data.os[os] = 0
|
|
||||||
data.os[os]++
|
|
||||||
|
|
||||||
if (!data.users[userId]) data.users[userId] = true
|
|
||||||
|
|
||||||
fs.writeFileSync(statsFile, JSON.stringify(data, null, 2))
|
|
||||||
res.json({ ok: true })
|
|
||||||
})
|
|
||||||
app.get("/api/stats/optout", (req, res) => {
|
|
||||||
res.send(`<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Poke – Opt out of stats</title>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: "PokeTube Flex";
|
|
||||||
src: url("https://p.poketube.fun/https://cdn.glitch.global/43b6691a-c8db-41d4-921c-8cf6aa0d9108/robotoflex.ttf?v=16683434286881");
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: 1% 800%;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
:root{color-scheme:dark}
|
|
||||||
body{color:#fff}
|
|
||||||
body {
|
|
||||||
background:#1c1b22;
|
|
||||||
margin:0;
|
|
||||||
}
|
|
||||||
:visited{color:#00c0ff}
|
|
||||||
a{color:#0ab7f0}
|
|
||||||
.app{max-width:1000px;margin:0 auto;padding:24px}
|
|
||||||
p{
|
|
||||||
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans", sans-serif;
|
|
||||||
line-height:1.6;
|
|
||||||
}
|
|
||||||
ul{
|
|
||||||
font-family:"poketube flex";
|
|
||||||
font-weight:500;
|
|
||||||
font-stretch:extra-expanded;
|
|
||||||
padding-left:1.2rem;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-family:"poketube flex", sans-serif;
|
|
||||||
font-weight:700;
|
|
||||||
font-stretch:extra-expanded;
|
|
||||||
margin-top:1.5rem;
|
|
||||||
margin-bottom:.3rem;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family:"poketube flex", sans-serif;
|
|
||||||
font-weight:1000;
|
|
||||||
font-stretch:ultra-expanded;
|
|
||||||
margin-top:0;
|
|
||||||
margin-bottom:.3rem;
|
|
||||||
}
|
|
||||||
.note{color:#bbb;font-size:.95rem}
|
|
||||||
.btn{
|
|
||||||
display:inline-block;
|
|
||||||
margin-top:1rem;
|
|
||||||
padding:.5rem 1rem;
|
|
||||||
border-radius:999px;
|
|
||||||
border:1px solid #2a2a35;
|
|
||||||
background:#252432;
|
|
||||||
color:#fff;
|
|
||||||
font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans", sans-serif;
|
|
||||||
text-decoration:none;
|
|
||||||
font-size:.95rem;
|
|
||||||
}
|
|
||||||
.btn:hover{
|
|
||||||
background:#2f2e3d;
|
|
||||||
}
|
|
||||||
.status{
|
|
||||||
margin-top:.5rem;
|
|
||||||
font-size:.95rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="app">
|
|
||||||
<h1>Stats opt-out</h1>
|
|
||||||
<p>
|
|
||||||
This page lets you turn off <strong>anonymous usage stats</strong> for this browser.
|
|
||||||
Poke will remember this choice using <code>localStorage</code> only (no cookies).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="note">
|
|
||||||
Anonymous stats help us understand which videos are popular and which platforms people use —
|
|
||||||
without collecting personal data. You can read the full details here:
|
|
||||||
<a href="/policies/privacy#stats">Privacy Policy</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a href="#" id="optout-btn" class="btn">Opt out of anonymous stats</a>
|
|
||||||
<div id="status" class="status note"></div>
|
|
||||||
|
|
||||||
<p class="note" style="margin-top:1.5rem;">
|
|
||||||
• To see the stats UI (if enabled on this instance), visit
|
|
||||||
<code><a href="/api/stats?view=human">/api/stats?view=human</a></code>.<br>
|
|
||||||
• For raw JSON, use <code><a href="/api/stats?view=json">/api/stats?view=json</a></code>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var KEY = "poke_stats_optout";
|
|
||||||
var btn = document.getElementById("optout-btn");
|
|
||||||
var status = document.getElementById("status");
|
|
||||||
|
|
||||||
function updateStatus() {
|
|
||||||
try {
|
|
||||||
var v = localStorage.getItem(KEY);
|
|
||||||
if (v === "1") {
|
|
||||||
status.textContent = "Anonymous stats are currently DISABLED in this browser.";
|
|
||||||
btn.textContent = "Re-enable anonymous stats";
|
|
||||||
} else {
|
|
||||||
status.textContent = "Anonymous stats are currently ENABLED in this browser.";
|
|
||||||
btn.textContent = "Opt out of anonymous stats";
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
status.textContent = "Your browser blocked localStorage, so we cannot store your opt-out choice.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.addEventListener("click", function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
try {
|
|
||||||
var v = localStorage.getItem(KEY);
|
|
||||||
if (v === "1") {
|
|
||||||
// was opted out -> turn stats back on
|
|
||||||
localStorage.removeItem(KEY);
|
|
||||||
} else {
|
|
||||||
// was enabled -> opt out
|
|
||||||
localStorage.setItem(KEY, "1");
|
|
||||||
}
|
|
||||||
updateStatus();
|
|
||||||
} catch (e) {
|
|
||||||
status.textContent = "Could not save opt-out preference (localStorage error).";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updateStatus();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/api/stats", (req, res) => {
|
|
||||||
const view = (req.query.view || "").toString()
|
|
||||||
|
|
||||||
// JSON view – explicit: /api/stats?view=json
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human view – /api/stats?view=human (just stats UI)
|
|
||||||
if (view === "human") {
|
|
||||||
const telemetryOn = telemetryConfig.telemetry
|
|
||||||
|
|
||||||
return res.send(`<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Improving Poke – Stats</title>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: "PokeTube Flex";
|
|
||||||
src: url("https://p.poketube.fun/https://cdn.glitch.global/43b6691a-c8db-41d4-921c-8cf6aa0d9108/robotoflex.ttf?v=16683434286881");
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: 1% 800%;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
:root{color-scheme:dark}
|
|
||||||
body{color:#fff}
|
|
||||||
body {
|
|
||||||
background:#1c1b22;
|
|
||||||
margin:0;
|
|
||||||
}
|
|
||||||
img{float:right;margin:.3em 0 1em 2em}
|
|
||||||
:visited{color:#00c0ff}
|
|
||||||
a{color:#0ab7f0}
|
|
||||||
.app{max-width:1000px;margin:0 auto;padding:24px}
|
|
||||||
p{
|
|
||||||
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans", sans-serif;
|
|
||||||
line-height:1.6;
|
|
||||||
}
|
|
||||||
ul{
|
|
||||||
font-family:"poketube flex";
|
|
||||||
font-weight:500;
|
|
||||||
font-stretch:extra-expanded;
|
|
||||||
padding-left:1.2rem;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-family:"poketube flex", sans-serif;
|
|
||||||
font-weight:700;
|
|
||||||
font-stretch:extra-expanded;
|
|
||||||
margin-top:1.5rem;
|
|
||||||
margin-bottom:.3rem;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family:"poketube flex", sans-serif;
|
|
||||||
font-weight:1000;
|
|
||||||
font-stretch:ultra-expanded;
|
|
||||||
margin-top:0;
|
|
||||||
margin-bottom:.3rem;
|
|
||||||
}
|
|
||||||
.toc{margin:1rem 0 2rem}
|
|
||||||
.toc li{margin:.25rem 0}
|
|
||||||
pre.license{
|
|
||||||
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
|
||||||
background:#111;padding:14px 16px;border-radius:12px;overflow-x:auto;line-height:1.45;border:1px solid #222
|
|
||||||
}
|
|
||||||
hr{border:0;border-top:1px solid #222;margin:28px 0}
|
|
||||||
.note{color:#bbb;font-size:.95rem}
|
|
||||||
|
|
||||||
/* extra tiny helpers */
|
|
||||||
.stats-list li{margin:.15rem 0;}
|
|
||||||
.muted{opacity:.8;font-size:.95rem;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="app">
|
|
||||||
<h1>Anonymous stats</h1>
|
|
||||||
<p class="note">
|
|
||||||
These stats are aggregated locally on this Poke instance. For what is collected (and what is not),
|
|
||||||
see <a href="/policies/privacy#stats">privacy policy</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Current anonymous stats</h2>
|
|
||||||
<p id="stats-note" class="note">Loading…</p>
|
|
||||||
<ul id="stats-list" class="stats-list"></ul>
|
|
||||||
|
|
||||||
<h2>Top videos (local-only)</h2>
|
|
||||||
<p class="note">Up to 10 most watched videos on this instance.</p>
|
|
||||||
<ul id="top-videos" class="stats-list"></ul>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h2>API usage</h2>
|
|
||||||
<p class="note">
|
|
||||||
• Human view (this page): <code><a href="/api/stats?view=human">/api/stats?view=human</a></code><br>
|
|
||||||
• JSON view (for scripts/tools): <code><a href="/api/stats?view=json">/api/stats?view=json</a></code><br>
|
|
||||||
• Opt out for this browser: <code><a href="/api/stats/optout">/api/stats/optout</a></code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const TELEMETRY_ON = ${telemetryOn ? "true" : "false"};
|
|
||||||
const OPT_KEY = "poke_stats_optout";
|
|
||||||
|
|
||||||
const statsNote = document.getElementById("stats-note");
|
|
||||||
const statsList = document.getElementById("stats-list");
|
|
||||||
const topVideos = document.getElementById("top-videos");
|
|
||||||
|
|
||||||
if (!TELEMETRY_ON) {
|
|
||||||
statsNote.textContent =
|
|
||||||
"Anonymous usage statistics are disabled on this instance. No stats are being collected.";
|
|
||||||
statsList.innerHTML = "";
|
|
||||||
topVideos.innerHTML = "<li>No data (telemetry disabled).</li>";
|
|
||||||
} else {
|
|
||||||
// respect per-browser opt-out for the human UI
|
|
||||||
var optedOut = false;
|
|
||||||
try {
|
|
||||||
optedOut = localStorage.getItem(OPT_KEY) === "1";
|
|
||||||
} catch (e) {
|
|
||||||
// ignore localStorage errors; treat as not opted out
|
|
||||||
}
|
|
||||||
|
|
||||||
if (optedOut) {
|
|
||||||
statsNote.textContent =
|
|
||||||
"You have opted out of anonymous stats in this browser. Poke will not load stats for you here.";
|
|
||||||
statsList.innerHTML = "";
|
|
||||||
topVideos.innerHTML = "<li>Opt-out active (no stats loaded).</li>";
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
|
|
||||||
var videoCount = Object.keys(videos).length;
|
|
||||||
|
|
||||||
statsNote.textContent = "";
|
|
||||||
statsList.innerHTML = "";
|
|
||||||
|
|
||||||
var summaryItems = [
|
|
||||||
"Anonymous users (unique local IDs): " + totalUsers,
|
|
||||||
"Videos with recorded views: " + videoCount,
|
|
||||||
"Browser types seen: " + Object.keys(browsers).length,
|
|
||||||
"OS families seen: " + Object.keys(os).length
|
|
||||||
];
|
|
||||||
|
|
||||||
summaryItems.forEach(function (text) {
|
|
||||||
var li = document.createElement("li");
|
|
||||||
li.textContent = text;
|
|
||||||
statsList.appendChild(li);
|
|
||||||
});
|
|
||||||
|
|
||||||
var videoKeys = Object.keys(videos);
|
|
||||||
if (videoKeys.length === 0) {
|
|
||||||
topVideos.innerHTML = "<li>No stats recorded yet.</li>";
|
|
||||||
} else {
|
|
||||||
topVideos.innerHTML = "";
|
|
||||||
videoKeys.forEach(function (id) {
|
|
||||||
var li = document.createElement("li");
|
|
||||||
var a = document.createElement("a");
|
|
||||||
a.href = "/watch?v=" + encodeURIComponent(id);
|
|
||||||
a.textContent = id;
|
|
||||||
li.appendChild(a);
|
|
||||||
li.appendChild(document.createTextNode(" – " + videos[id] + " views"));
|
|
||||||
topVideos.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
statsNote.textContent =
|
|
||||||
"Could not load stats (maybe they are disabled or there was an error).";
|
|
||||||
statsList.innerHTML = "";
|
|
||||||
topVideos.innerHTML = "<li>Error loading data.</li>";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// any other view value (including "/api/stats" with no ?view) -> landing page HTML
|
|
||||||
return res.send(`<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Improving Poke</title>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<style>
|
|
||||||
@font-face {
|
|
||||||
font-family: "PokeTube Flex";
|
|
||||||
src: url("https://p.poketube.fun/https://cdn.glitch.global/43b6691a-c8db-41d4-921c-8cf6aa0d9108/robotoflex.ttf?v=16683434286881");
|
|
||||||
font-style: normal;
|
|
||||||
font-stretch: 1% 800%;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
:root{color-scheme:dark}
|
|
||||||
body{color:#fff}
|
|
||||||
body {
|
|
||||||
background:#1c1b22;
|
|
||||||
margin:0;
|
|
||||||
}
|
|
||||||
img{float:right;margin:.3em 0 1em 2em}
|
|
||||||
:visited{color:#00c0ff}
|
|
||||||
a{color:#0ab7f0}
|
|
||||||
.app{max-width:1000px;margin:0 auto;padding:24px}
|
|
||||||
p{
|
|
||||||
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans", sans-serif;
|
|
||||||
line-height:1.6;
|
|
||||||
}
|
|
||||||
ul{
|
|
||||||
font-family:"poketube flex";
|
|
||||||
font-weight:500;
|
|
||||||
font-stretch:extra-expanded;
|
|
||||||
padding-left:1.2rem;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-family:"poketube flex", sans-serif;
|
|
||||||
font-weight:700;
|
|
||||||
font-stretch:extra-expanded;
|
|
||||||
margin-top:1.5rem;
|
|
||||||
margin-bottom:.3rem;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-family:"poketube flex", sans-serif;
|
|
||||||
font-weight:1000;
|
|
||||||
font-stretch:ultra-expanded;
|
|
||||||
margin-top:0;
|
|
||||||
margin-bottom:.3rem;
|
|
||||||
}
|
|
||||||
.toc{margin:1rem 0 2rem}
|
|
||||||
.toc li{margin:.25rem 0}
|
|
||||||
pre.license{
|
|
||||||
font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
|
||||||
background:#111;padding:14px 16px;border-radius:12px;overflow-x:auto;line-height:1.45;border:1px solid #222
|
|
||||||
}
|
|
||||||
hr{border:0;border-top:1px solid #222;margin:28px 0}
|
|
||||||
.note{color:#bbb;font-size:.95rem}
|
|
||||||
|
|
||||||
/* extra tiny helpers */
|
|
||||||
.stats-list li{margin:.15rem 0;}
|
|
||||||
.muted{opacity:.8;font-size:.95rem;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="app">
|
|
||||||
<img src="/css/logo-poke.svg" alt="Poke logo">
|
|
||||||
<h1>Improving Poke</h1>
|
|
||||||
<h2>Private by design</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
At <a href="/">Poke</a>, we do not collect or share any personal information.
|
|
||||||
That's our privacy promise in a nutshell.
|
|
||||||
To improve Poke we use a completely anonymous, local-only way to figure out how the site is being used.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Any anonymous stats recorded by this instance come from the <code>/api/stats</code> system.
|
|
||||||
You can read exactly what is measured (and what is <em>not</em>) in our privacy policy:
|
|
||||||
<a href="/policies/privacy#stats">here</a>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h2>API usage</h2>
|
|
||||||
<p class="note">
|
|
||||||
• Human view (stats UI): <code><a href="/api/stats?view=human">/api/stats?view=human</a></code><br>
|
|
||||||
• JSON view (for scripts/tools): <code><a href="/api/stats?view=json">/api/stats?view=json</a></code><br>
|
|
||||||
• Opt out for this browser: <code><a href="/api/stats/optout">/api/stats/optout</a></code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.get("/avatars/:v", async function (req, res) {
|
app.get("/avatars/:v", async function (req, res) {
|
||||||
var url = `https://yt3.ggpht.com/${req.params.v}`;
|
var url = `https://yt3.ggpht.com/${req.params.v}`;
|
||||||
@ -613,11 +111,9 @@ app.get("/api/geo", async (req, res) => {
|
|||||||
app.get("/api/nominatim/search", async (req, res) => {
|
app.get("/api/nominatim/search", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL("https://nominatim.openstreetmap.org/search");
|
const url = new URL("https://nominatim.openstreetmap.org/search");
|
||||||
// Forward all query params (format, q, limit, etc.)
|
|
||||||
for (const [key, value] of Object.entries(req.query)) {
|
for (const [key, value] of Object.entries(req.query)) {
|
||||||
url.searchParams.set(key, value);
|
url.searchParams.set(key, value);
|
||||||
}
|
}
|
||||||
// Force JSON output if not specified
|
|
||||||
if (!url.searchParams.has("format")) url.searchParams.set("format", "json");
|
if (!url.searchParams.has("format")) url.searchParams.set("format", "json");
|
||||||
|
|
||||||
const r = await fetch(url.toString(), {
|
const r = await fetch(url.toString(), {
|
||||||
@ -630,7 +126,6 @@ app.get("/api/nominatim/search", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Proxy for reverse geocoding
|
|
||||||
app.get("/api/nominatim/reverse", async (req, res) => {
|
app.get("/api/nominatim/reverse", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL("https://nominatim.openstreetmap.org/reverse");
|
const url = new URL("https://nominatim.openstreetmap.org/reverse");
|
||||||
@ -680,15 +175,6 @@ app.get("/api/nominatim/reverse", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
let url = `${config.videourl}/companion/api/v1/captions/${id}?label=${l}`;
|
let url = `${config.videourl}/companion/api/v1/captions/${id}?label=${l}`;
|
||||||
|
|
||||||
/*
|
|
||||||
let f = await fetch(url, {
|
|
||||||
headers: headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = await f.text();
|
|
||||||
|
|
||||||
res.send(body);
|
|
||||||
*/
|
|
||||||
res.send("j");
|
res.send("j");
|
||||||
|
|
||||||
} catch {}
|
} catch {}
|
||||||
@ -742,23 +228,15 @@ app.get("/api/weather", async (req, res) => {
|
|||||||
const dislikePercentage = total > 0 ? ((dislikes / total) * 100).toFixed(2) : 0;
|
const dislikePercentage = total > 0 ? ((dislikes / total) * 100).toFixed(2) : 0;
|
||||||
|
|
||||||
const getLikePercentageColor = (percentage) => {
|
const getLikePercentageColor = (percentage) => {
|
||||||
if (percentage >= 80) {
|
if (percentage >= 80) return "green";
|
||||||
return "green";
|
else if (percentage >= 50) return "orange";
|
||||||
} else if (percentage >= 50) {
|
else return "red";
|
||||||
return "orange";
|
|
||||||
} else {
|
|
||||||
return "red";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDislikePercentageColor = (percentage) => {
|
const getDislikePercentageColor = (percentage) => {
|
||||||
if (percentage >= 50) {
|
if (percentage >= 50) return "red";
|
||||||
return "red";
|
else if (percentage >= 20) return "orange";
|
||||||
} else if (percentage >= 20) {
|
else return "green";
|
||||||
return "orange";
|
|
||||||
} else {
|
|
||||||
return "green";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const likeColor = getLikePercentageColor(likePercentage);
|
const likeColor = getLikePercentageColor(likePercentage);
|
||||||
@ -770,24 +248,19 @@ app.get("/api/weather", async (req, res) => {
|
|||||||
).toFixed(2);
|
).toFixed(2);
|
||||||
|
|
||||||
const getUserScoreLabel = (score) => {
|
const getUserScoreLabel = (score) => {
|
||||||
if (score >= 98) {
|
if (score >= 98) return "Masterpiece Video";
|
||||||
return "Masterpiece Video";
|
else if (score >= 80) return "Overwhelmingly Positive";
|
||||||
} else if (score >= 80) {
|
else if (score >= 60) return "Positive";
|
||||||
return "Overwhelmingly Positive";
|
else if (score >= 40) return "Mixed";
|
||||||
} else if (score >= 60) {
|
else if (score >= 20) return "Negative";
|
||||||
return "Positive";
|
else return "Overwhelmingly Negative";
|
||||||
} else if (score >= 40) {
|
|
||||||
return "Mixed";
|
|
||||||
} else if (score >= 20) {
|
|
||||||
return "Negative";
|
|
||||||
} else {
|
|
||||||
return "Overwhelmingly Negative";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const userScoreLabel = getUserScoreLabel(userScore);
|
const userScoreLabel = getUserScoreLabel(userScore);
|
||||||
const userScoreColor =
|
const userScoreColor =
|
||||||
userScore >= 80 ? "green" : userScore >= 50 ? "orange" : "red";
|
userScore >= 80 ? "green" :
|
||||||
|
userScore >= 50 ? "orange" :
|
||||||
|
"red";
|
||||||
|
|
||||||
const respon = {
|
const respon = {
|
||||||
like_count: likes,
|
like_count: likes,
|
||||||
@ -825,7 +298,7 @@ app.get("/api/weather", async (req, res) => {
|
|||||||
|
|
||||||
let f = await modules.fetch(url, {
|
let f = await modules.fetch(url, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: headers, // Add headers to the fetch request
|
headers: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
f.body.pipe(res);
|
f.body.pipe(res);
|
||||||
@ -861,6 +334,7 @@ app.get("/api/weather", async (req, res) => {
|
|||||||
app.get("/api/v1", async (req, res) => {
|
app.get("/api/v1", async (req, res) => {
|
||||||
res.redirect("https://invid-api.poketube.fun/api/v1/stats");
|
res.redirect("https://invid-api.poketube.fun/api/v1/stats");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/version.json", async (req, res) => {
|
app.get("/api/version.json", async (req, res) => {
|
||||||
let latestCommitHash = null;
|
let latestCommitHash = null;
|
||||||
|
|
||||||
@ -909,14 +383,14 @@ app.get("/api/version.json", async (req, res) => {
|
|||||||
const roundedTotalGB = totalMemoryGB.toFixed(2);
|
const roundedTotalGB = totalMemoryGB.toFixed(2);
|
||||||
const roundedFreeGB = freeMemoryGB.toFixed(2);
|
const roundedFreeGB = freeMemoryGB.toFixed(2);
|
||||||
|
|
||||||
const loadavg = os.loadavg(); // [1m, 5m, 15m]
|
const loadavg = os.loadavg();
|
||||||
const uptimeSeconds = Math.floor(os.uptime());
|
const uptimeSeconds = Math.floor(os.uptime());
|
||||||
|
|
||||||
let platform = os.platform(); // 'linux', 'darwin', 'win32', etc.
|
let platform = os.platform();
|
||||||
if (platform === 'linux') platform = 'gnu/linux';
|
if (platform === 'linux') platform = 'gnu/linux';
|
||||||
|
|
||||||
const kernelRelease = os.release(); // e.g., '6.8.0-40-generic'
|
const kernelRelease = os.release();
|
||||||
const arch = os.arch(); // e.g., 'x64', 'arm64'
|
const arch = os.arch();
|
||||||
const hostname = os.hostname();
|
const hostname = os.hostname();
|
||||||
const cpuModel = cpus[0]?.model || "Unknown CPU";
|
const cpuModel = cpus[0]?.model || "Unknown CPU";
|
||||||
const cpuCount = cpus.length;
|
const cpuCount = cpus.length;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user