Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trackarr",
"version": "0.5.1",
"version": "0.5.2",
"type": "module",
"private": true,
"scripts": {
Expand Down
18 changes: 13 additions & 5 deletions server/api/admin/stats.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ export default defineEventHandler(async (event) => {
.from(schema.torrents);
const totalTorrents = torrentsCountResult[0]?.count || 0;

// Get total peers and seeders from Redis using SCAN for safety
// Get total unique peers and seeders from Redis using SCAN for safety
// Note: ioredis with keyPrefix - SCAN returns full keys with prefix,
// but we need to strip the prefix before passing to other commands
// We use Sets to count unique peers by ip:port (a peer seeding multiple torrents counts as 1)
const keyPrefix = process.env.REDIS_KEY_PREFIX || 'ot:';
let totalPeers = 0;
let totalSeeders = 0;
const uniquePeers = new Set<string>();
const uniqueSeeders = new Set<string>();
let cursor = '0';
let scannedKeys = 0;
try {
do {
const [nextCursor, keys] = await redis.scan(
Expand All @@ -30,6 +32,7 @@ export default defineEventHandler(async (event) => {
100
);
cursor = nextCursor;
scannedKeys += keys.length;
for (const fullKey of keys) {
// Strip the prefix from the key returned by SCAN to avoid double-prefixing
const key = fullKey.startsWith(keyPrefix)
Expand All @@ -39,8 +42,10 @@ export default defineEventHandler(async (event) => {
for (const json of Object.values(peersData)) {
try {
const peer = JSON.parse(json as string);
totalPeers++;
if (peer.isSeeder) totalSeeders++;
// Use ip:port as unique identifier for a peer
const peerKey = `${peer.ip}:${peer.port}`;
uniquePeers.add(peerKey);
if (peer.isSeeder) uniqueSeeders.add(peerKey);
} catch (e) {
// Ignore invalid JSON
}
Expand All @@ -51,6 +56,9 @@ export default defineEventHandler(async (event) => {
console.error('[Stats] Failed to fetch peer count from Redis:', err);
}

const totalPeers = uniquePeers.size;
const totalSeeders = uniqueSeeders.size;

// Try to get tracker, may fail if native modules aren't built
let tracker = null;
let protocols = { http: false, udp: false, ws: false };
Expand Down
54 changes: 34 additions & 20 deletions server/api/admin/stats/history.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ export default defineEventHandler(async (event) => {
);
const dbSize = Number(dbSizeResult[0]?.pg_database_size) || 0;

// Peers & Seeders (SCAN)
let peersCount = 0;
let seedersCount = 0;
// Peers & Seeders (SCAN) - count unique peers by ip:port
const uniquePeers = new Set<string>();
const uniqueSeeders = new Set<string>();
let cursor = '0';
do {
const [nextCursor, keys] = await redis.scan(
Expand All @@ -69,33 +69,47 @@ export default defineEventHandler(async (event) => {
for (const json of Object.values(peersData)) {
try {
const peer = JSON.parse(json as string);
peersCount++;
if (peer.isSeeder) seedersCount++;
const peerKey = `${peer.ip}:${peer.port}`;
uniquePeers.add(peerKey);
if (peer.isSeeder) uniqueSeeders.add(peerKey);
} catch (e) {}
}
}
} while (cursor !== '0');
const peersCount = uniquePeers.size;
const seedersCount = uniqueSeeders.size;

// Append 'live' data point
// Check if we should append: only if history is empty OR last point is older than 5 mins
// Always append a 'live' data point to show current real-time stats
// This ensures the chart always reflects the current state
const livePoint = {
id: 'live',
usersCount,
torrentsCount,
peersCount,
seedersCount,
redisMemoryUsage,
dbSize,
createdAt: new Date(),
} as any;

// If the last historical point is very recent (< 1 min), replace it with live data
// Otherwise append as a new point
const lastPoint = history[history.length - 1];
const now = Date.now();
const fiveMinutes = 5 * 60 * 1000;
const oneMinute = 60 * 1000;

if (
!lastPoint ||
now - new Date(lastPoint.createdAt).getTime() > fiveMinutes
lastPoint &&
now - new Date(lastPoint.createdAt).getTime() < oneMinute
) {
history.push({
id: 'live',
usersCount,
torrentsCount,
peersCount,
seedersCount,
redisMemoryUsage,
dbSize,
createdAt: new Date(),
} as any);
// Replace the last point with live data
history[history.length - 1] = {
...lastPoint,
...livePoint,
id: lastPoint.id,
};
} else {
history.push(livePoint);
}
} catch (err) {
console.error('[Stats History] Failed to fetch live stats:', err);
Expand Down
13 changes: 8 additions & 5 deletions server/plugins/stats-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ export default defineNitroPlugin((nitroApp) => {
.from(schema.torrents);
const torrentsCount = torrentsCountResult[0]?.count || 0;

// 3. Peers & Seeders Count (from Redis)
// 3. Peers & Seeders Count (from Redis) - count unique peers by ip:port
// Note: ioredis with keyPrefix - SCAN returns full keys with prefix,
// but we need to strip the prefix before passing to other commands
const keyPrefix = process.env.REDIS_KEY_PREFIX || 'ot:';
let peersCount = 0;
let seedersCount = 0;
const uniquePeers = new Set<string>();
const uniqueSeeders = new Set<string>();
let cursor = '0';
do {
const [nextCursor, keys] = await redis.scan(
Expand All @@ -52,12 +52,15 @@ export default defineNitroPlugin((nitroApp) => {
for (const json of Object.values(peersData)) {
try {
const peer = JSON.parse(json as string);
peersCount++;
if (peer.isSeeder) seedersCount++;
const peerKey = `${peer.ip}:${peer.port}`;
uniquePeers.add(peerKey);
if (peer.isSeeder) uniqueSeeders.add(peerKey);
} catch (e) {}
}
}
} while (cursor !== '0');
const peersCount = uniquePeers.size;
const seedersCount = uniqueSeeders.size;

// 4. Redis Memory Usage
const info = await redis.info('memory');
Expand Down