Add charts for top torrent uploaders and weekly top uploader data & improve debrid cache & torrent source charts

Introduced new visualizations to display the top 20 torrent uploaders and their weekly activity. Enhanced the styling for metrics sections and updated backend logic to support these additions, ensuring cleaner and more focused data retrieval.
This commit is contained in:
mhdzumair
2025-01-09 23:46:18 +05:30
parent 458fc0d09c
commit 361b6ba74a
5 changed files with 351 additions and 148 deletions
+12 -32
View File
@@ -110,14 +110,10 @@ async def get_redis_metrics() -> Dict[str, Any]:
async def get_debrid_cache_metrics() -> Dict[str, Any]:
"""
Get detailed metrics about debrid cache usage.
Get simplified metrics about debrid cache usage, focusing only on cached torrents per service.
"""
metrics = {
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
"total_memory_usage": 0,
"total_cached_torrents": 0,
"services": {},
}
metrics = {"timestamp": datetime.now(tz=timezone.utc).isoformat(), "services": {}}
debrid_services = [
"alldebrid",
"debridlink",
@@ -132,37 +128,21 @@ async def get_debrid_cache_metrics() -> Dict[str, Any]:
]
try:
for service in debrid_services:
cache_key = f"debrid_cache:{service}"
# Get various metrics for each service
cache_size = await REDIS_ASYNC_CLIENT.hlen(cache_key)
memory_usage = await REDIS_ASYNC_CLIENT.memory_usage(cache_key) or 0
metrics["services"][service] = {
"cached_torrents": cache_size,
"memory_usage": memory_usage,
}
if cache_size > 0: # Only include services with cached torrents
metrics["services"][service] = {"cached_torrents": cache_size}
# Update totals
metrics["total_cached_torrents"] += cache_size
metrics["total_memory_usage"] += memory_usage
# Add human readable total memory
metrics["total_memory_usage_human"] = humanize.naturalsize(
metrics["total_memory_usage"]
)
# Calculate which service has most cached torrents
if metrics["services"]:
most_cached = max(
metrics["services"].items(), key=lambda x: x[1]["cached_torrents"]
# Sort services by cache size
metrics["services"] = dict(
sorted(
metrics["services"].items(),
key=lambda x: x[1]["cached_torrents"],
reverse=True,
)
metrics["most_used_service"] = {
"name": most_cached[0],
"cached_count": most_cached[1]["cached_torrents"],
}
)
return metrics
except Exception as e:
+64
View File
@@ -1,4 +1,5 @@
import asyncio
from datetime import datetime, timezone, timedelta
import humanize
from fastapi import APIRouter, Request, Response
@@ -65,6 +66,7 @@ async def get_torrents_by_sources(response: Response):
[
{"$group": {"_id": "$source", "count": {"$sum": 1}}},
{"$sort": {"count": -1}},
{"$limit": 20}, # Limit to top 20 sources
]
).to_list()
torrent_sources = [
@@ -151,3 +153,65 @@ async def debrid_cache_metrics():
Returns statistics about cache size, memory usage, and usage patterns per service.
"""
return await get_debrid_cache_metrics()
@metrics_router.get("/torrents/uploaders", tags=["metrics"])
async def get_torrents_by_uploaders(response: Response):
response.headers.update(const.NO_CACHE_HEADERS)
results = await TorrentStreams.aggregate(
[
{"$match": {"uploader": {"$ne": None}}},
{"$group": {"_id": "$uploader", "count": {"$sum": 1}}},
{"$sort": {"count": -1}},
{"$limit": 20}, # Limit to top 20 uploaders
]
).to_list()
uploader_stats = [
{"name": stat["_id"] if stat["_id"] else "Unknown", "count": stat["count"]}
for stat in results
]
return uploader_stats
@metrics_router.get("/torrents/uploaders/weekly", tags=["metrics"])
async def get_weekly_top_uploaders(response: Response):
response.headers.update(const.NO_CACHE_HEADERS)
# Calculate the start of the current week (Monday)
today = datetime.now(tz=timezone.utc)
start_of_week = today - timedelta(days=today.weekday())
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
results = await TorrentStreams.aggregate(
[
{
"$match": {
"uploader": {"$ne": None},
"created_at": {"$gte": start_of_week},
}
},
{
"$group": {
"_id": "$uploader",
"count": {"$sum": 1},
"latest_upload": {"$max": "$created_at"},
}
},
{"$sort": {"count": -1}},
{"$limit": 20},
]
).to_list()
uploaders_stats = [
{
"name": stat["_id"],
"count": stat["count"],
"latest_upload": (
stat["latest_upload"].isoformat() if stat["latest_upload"] else None
),
}
for stat in results
]
return {"week_start": start_of_week.isoformat(), "uploaders": uploaders_stats}
+59 -17
View File
@@ -13,6 +13,14 @@ body, html {
overflow-y: auto;
}
h4 {
color: #ffffff;
font-weight: 600;
margin-bottom: 1.5rem;
border-bottom: 2px solid rgba(74, 71, 163, 0.5);
padding-bottom: 0.5rem;
}
.config-container {
margin: 2% auto;
padding: 2%;
@@ -38,7 +46,37 @@ body, html {
}
.chart-section {
margin-bottom: 2rem;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.chart-section:hover {
transform: translateY(-5px);
}
.date-info {
background: rgba(129,87,208,0.6);
padding: 8px 16px;
border-radius: 4px;
text-align: center;
font-size: 0.9rem;
}
.chart-wrapper {
position: relative;
width: 100%;
height: 400px;
margin: 20px 0;
}
.chart-wrapper canvas {
position: absolute;
width: 100% !important;
height: 100% !important;
}
.chart-container {
@@ -49,19 +87,6 @@ body, html {
position: relative;
}
.chart-wrapper {
position: relative;
width: 100%;
height: 0;
padding-bottom: 50%;
}
.chart-wrapper canvas {
position: absolute;
width: 100%;
height: 100%;
}
.logo {
max-width: 300px;
display: block;
@@ -69,9 +94,14 @@ body, html {
}
.card {
background: #4a47a3;
position: relative;
text-align: center;
background: linear-gradient(145deg, #4a47a3, #2d2a6a);
border: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card-text {
@@ -178,6 +208,18 @@ body, html {
@media (max-width: 768px) {
.chart-wrapper {
height: 300px;
}
.chart-section {
padding: 15px;
margin-bottom: 20px;
}
.card-text {
font-size: 1.2rem;
}
.config-container {
padding: 10px;
}
+19 -22
View File
@@ -48,7 +48,7 @@
</div>
<div class="chart-section">
<h4 class="text-left">Torrent Count By Sources</h4>
<h4 class="text-left">Top 20 Torrents Sources</h4>
<div class="chart-wrapper">
<canvas id="torrentSourcesChart"></canvas>
<div class="skeleton-loader" id="torrentSourcesSkeleton"></div>
@@ -56,30 +56,27 @@
</div>
<div class="chart-section">
<h4 class="text-left">Debrid Cache Statistics</h4>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card text-white">
<div class="card-body">
<h5 class="card-title">Total Cached Torrents Count (Not Unique)</h5>
<p id="totalCachedTorrentsValue" class="card-text large-number"></p>
<div class="skeleton-loader" id="debridTotalsSkeleton"></div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card text-white">
<div class="card-body">
<h5 class="card-title">Total Cache Memory Usage</h5>
<p id="totalCacheMemoryValue" class="card-text large-number"></p>
<div class="skeleton-loader" id="debridMemorySkeleton"></div>
</div>
</div>
</div>
<h4 class="text-left">Top 20 Torrents Uploaders</h4>
<div class="chart-wrapper">
<canvas id="torrentUploadersChart"></canvas>
<div class="skeleton-loader" id="torrentUploadersSkeleton"></div>
</div>
</div>
<div class="chart-section">
<h4 class="text-left">Top 20 Uploaders This Week</h4>
<div class="date-info mb-3" id="weekDateRange"></div>
<div class="chart-wrapper">
<canvas id="weeklyUploadersChart"></canvas>
<div class="skeleton-loader" id="weeklyUploadersSkeleton"></div>
</div>
</div>
<div class="chart-section">
<h4 class="text-left">Debrid Cache Statistics</h4>
<div class="card text-white">
<div class="card-body">
<h5 class="card-title">Cache Distribution</h5>
<h5 class="card-title">Cached Torrents by Service</h5>
<div class="chart-wrapper">
<canvas id="debridCacheChart"></canvas>
<div class="skeleton-loader" id="debridChartSkeleton"></div>
+197 -77
View File
@@ -230,110 +230,228 @@ document.addEventListener('DOMContentLoaded', function () {
});
};
const renderDebridCacheMetrics = async () => {
const data = await fetchData('/metrics/debrid-cache');
const renderTorrentUploadersChart = async () => {
const data = await fetchData('/metrics/torrents/uploaders');
removeLoadingElement('torrentUploadersSkeleton');
const ctx = document.getElementById('torrentUploadersChart').getContext('2d');
// Remove loading skeletons
removeLoadingElement('debridTotalsSkeleton');
removeLoadingElement('debridMemorySkeleton');
removeLoadingElement('debridChartSkeleton');
// Update total values
const totalCachedTorrents = document.getElementById('totalCachedTorrentsValue');
totalCachedTorrents.textContent = data.total_cached_torrents.toLocaleString();
const totalMemoryUsage = document.getElementById('totalCacheMemoryValue');
totalMemoryUsage.textContent = data.total_memory_usage_human;
// Create the stacked bar chart for service comparison
const ctx = document.getElementById('debridCacheChart').getContext('2d');
// Prepare data for the chart
const services = Object.keys(data.services);
const cachedTorrents = services.map(service => data.services[service].cached_torrents);
const memoryUsages = services.map(service => data.services[service].memory_usage / (1024 * 1024)); // Convert to MB
const sortedData = data.sort((a, b) => b.count - a.count);
new Chart(ctx, {
type: 'bar',
data: {
labels: services,
datasets: [
{
label: 'Cached Torrents',
data: cachedTorrents,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
yAxisID: 'y'
},
{
label: 'Memory Usage (MB)',
data: memoryUsages,
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1,
yAxisID: 'y1'
}
]
labels: sortedData.map(item => item.name),
datasets: [{
data: sortedData.map(item => item.count),
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.raw.toLocaleString()} Torrents`;
}
}
},
datalabels: {
color: '#fff',
anchor: 'end',
align: 'end',
formatter: (value) => value.toLocaleString(),
font: {
weight: 'bold'
}
}
},
scales: {
x: {
ticks: {
color: '#fff'
}
},
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Cached Torrents',
color: '#fff'
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#fff'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
y: {
grid: {
display: false
},
ticks: {
color: '#fff'
}
}
}
}
});
};
const renderWeeklyUploadersChart = async () => {
const data = await fetchData('/metrics/torrents/uploaders/weekly');
removeLoadingElement('weeklyUploadersSkeleton');
// Update date range display
const weekStart = new Date(data.week_start);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
const dateRangeElement = document.getElementById('weekDateRange');
dateRangeElement.innerHTML = `
<span class="text-white">
${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}
</span>
`;
const ctx = document.getElementById('weeklyUploadersChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: data.uploaders.map(uploader => uploader.name),
datasets: [{
data: data.uploaders.map(uploader => uploader.count),
backgroundColor: 'rgba(122,76,204,0.6)',
borderColor: 'rgba(129,87,208,0.6)',
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
const uploader = data.uploaders[context.dataIndex];
const lastUpload = new Date(uploader.latest_upload);
return [
`Torrents: ${context.raw.toLocaleString()}`,
`Last Upload: ${lastUpload.toLocaleString()}`
];
}
}
},
datalabels: {
color: '#fff',
anchor: 'end',
align: 'end',
formatter: (value) => value.toLocaleString(),
font: {
weight: 'bold'
}
}
},
scales: {
x: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#fff',
callback: function (value) {
return value.toLocaleString();
}
}
},
y: {
grid: {
display: false
},
ticks: {
color: '#fff',
font: {
size: 12
}
}
}
}
}
});
};
const renderDebridCacheMetrics = async () => {
const data = await fetchData('/metrics/debrid-cache');
removeLoadingElement('debridChartSkeleton');
// Create the bar chart for service comparison
const ctx = document.getElementById('debridCacheChart').getContext('2d');
// Prepare data for the chart
const services = Object.keys(data.services);
const cachedTorrents = services.map(service => data.services[service].cached_torrents);
new Chart(ctx, {
type: 'bar',
data: {
labels: services,
datasets: [{
label: 'Cached Torrents',
data: cachedTorrents,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return `Cached Torrents: ${context.raw.toLocaleString()}`;
}
}
},
datalabels: {
color: '#fff',
anchor: 'end',
align: 'end',
formatter: (value) => value.toLocaleString(),
font: {
weight: 'bold'
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Memory Usage (MB)',
text: 'Number of Cached Torrents',
color: '#fff'
},
ticks: {
color: '#fff'
},
grid: {
drawOnChartArea: false
}
}
},
plugins: {
legend: {
labels: {
color: '#fff'
color: 'rgba(255, 255, 255, 0.1)'
}
},
tooltip: {
callbacks: {
label: function (context) {
if (context.datasetIndex === 0) {
return `Cached Torrents: ${context.raw.toLocaleString()}`;
} else {
return `Memory Usage: ${context.raw.toFixed(2)} MB`;
}
}
x: {
ticks: {
color: '#fff'
},
grid: {
display: false
}
}
}
@@ -346,6 +464,8 @@ document.addEventListener('DOMContentLoaded', function () {
renderMetadataCountsChart();
renderTotalTorrentsCount();
renderTorrentSourcesChart();
renderTorrentUploadersChart();
renderWeeklyUploadersChart();
renderDebridCacheMetrics();
};