diff --git a/metrics/redis_metrics.py b/metrics/redis_metrics.py index b35d96a..bcb61b2 100644 --- a/metrics/redis_metrics.py +++ b/metrics/redis_metrics.py @@ -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: diff --git a/metrics/routes.py b/metrics/routes.py index c8f2ec0..3842778 100644 --- a/metrics/routes.py +++ b/metrics/routes.py @@ -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} diff --git a/resources/css/metrics.css b/resources/css/metrics.css index 94d3150..39ba3c9 100644 --- a/resources/css/metrics.css +++ b/resources/css/metrics.css @@ -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; } diff --git a/resources/html/metrics.html b/resources/html/metrics.html index d0e6795..8028dd3 100644 --- a/resources/html/metrics.html +++ b/resources/html/metrics.html @@ -48,7 +48,7 @@
-

Torrent Count By Sources

+

Top 20 Torrents Sources

@@ -56,30 +56,27 @@
-

Debrid Cache Statistics

-
-
-
-
-
Total Cached Torrents Count (Not Unique)
-

-
-
-
-
-
-
-
-
Total Cache Memory Usage
-

-
-
-
-
+

Top 20 Torrents Uploaders

+
+ +
+
+ +
+

Top 20 Uploaders This Week

+
+
+ +
+
+
+ +
+

Debrid Cache Statistics

-
Cache Distribution
+
Cached Torrents by Service
diff --git a/resources/js/metrics.js b/resources/js/metrics.js index 54b6159..6a4b511 100644 --- a/resources/js/metrics.js +++ b/resources/js/metrics.js @@ -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 = ` + + ${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()} + + `; + + 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(); };