feat: add week selector for weekly top uploaders metrics

This commit is contained in:
mhdzumair
2025-02-20 23:27:01 +05:30
parent ec20feca52
commit 185f015cb7
4 changed files with 541 additions and 485 deletions
+91 -72
View File
@@ -186,92 +186,111 @@ async def get_torrents_by_uploaders(response: Response):
return uploader_stats return uploader_stats
@metrics_router.get("/torrents/uploaders/weekly", tags=["metrics"]) @metrics_router.get("/torrents/uploaders/weekly/{week_date}", tags=["metrics"])
async def get_weekly_top_uploaders(response: Response): async def get_weekly_top_uploaders(week_date: str, response: Response):
response.headers.update(const.NO_CACHE_HEADERS) response.headers.update(const.NO_CACHE_HEADERS)
# Calculate the start of the current week (Monday) try:
today = datetime.now(tz=timezone.utc) # Parse the input date
start_of_week = today - timedelta(days=today.weekday()) selected_date = datetime.strptime(week_date, "%Y-%m-%d")
start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0) selected_date = selected_date.replace(tzinfo=timezone.utc)
# Get raw aggregation results # Calculate the start of the week (Monday) for the selected date
raw_results = await TorrentStreams.aggregate( start_of_week = selected_date - timedelta(days=selected_date.weekday())
[ start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0)
{
"$match": {
"source": "Contribution Stream",
"$or": [
{"created_at": {"$gte": start_of_week}},
{"uploaded_at": {"$gte": start_of_week}},
],
"is_blocked": {"$ne": True},
}
},
{
"$group": {
"_id": "$uploader",
"count": {"$sum": 1},
"latest_upload": {"$max": "$created_at"},
}
},
]
).to_list()
# Process and clean the results # Calculate end of week
processed_results = [] end_of_week = start_of_week + timedelta(days=7)
anonymous_total = 0
anonymous_latest = None
for stat in raw_results: # Get raw aggregation results
uploader_name = stat["_id"] raw_results = await TorrentStreams.aggregate(
count = stat["count"] [
latest_upload = stat["latest_upload"] {
"$match": {
"source": "Contribution Stream",
"$or": [
{
"uploaded_at": {
"$gte": start_of_week,
"$lt": end_of_week,
}
},
],
"is_blocked": {"$ne": True},
}
},
{
"$group": {
"_id": "$uploader",
"count": {"$sum": 1},
"latest_upload": {"$max": "$created_at"},
}
},
]
).to_list()
# Skip invalid entries # Process and clean the results (keeping existing logic)
if count <= 0: processed_results = []
continue anonymous_total = 0
anonymous_latest = None
# Combine all anonymous-like entries for stat in raw_results:
if ( uploader_name = stat["_id"]
uploader_name is None count = stat["count"]
or uploader_name.strip() == "" latest_upload = stat["latest_upload"]
or uploader_name.lower() == "anonymous"
): if count <= 0:
anonymous_total += count continue
if anonymous_latest is None or (
latest_upload and latest_upload > anonymous_latest if (
uploader_name is None
or uploader_name.strip() == ""
or uploader_name.lower() == "anonymous"
): ):
anonymous_latest = latest_upload anonymous_total += count
else: if anonymous_latest is None or (
latest_upload and latest_upload > anonymous_latest
):
anonymous_latest = latest_upload
else:
processed_results.append(
{
"name": uploader_name.strip(),
"count": count,
"latest_upload": (
latest_upload.strftime("%Y-%m-%d")
if latest_upload
else None
),
}
)
if anonymous_total > 0:
processed_results.append( processed_results.append(
{ {
"name": uploader_name.strip(), "name": "Anonymous",
"count": count, "count": anonymous_total,
"latest_upload": ( "latest_upload": (
latest_upload.strftime("%Y-%m-%d") if latest_upload else None anonymous_latest.strftime("%Y-%m-%d")
if anonymous_latest
else None
), ),
} }
) )
# Add anonymous entry if we have any anonymous uploads final_results = sorted(
if anonymous_total > 0: processed_results, key=lambda x: x["count"], reverse=True
processed_results.append( )[:20]
{
"name": "Anonymous",
"count": anonymous_total,
"latest_upload": (
anonymous_latest.strftime("%Y-%m-%d") if anonymous_latest else None
),
}
)
# Sort by count and limit to top 20 return {
final_results = sorted(processed_results, key=lambda x: x["count"], reverse=True)[ "week_start": start_of_week.strftime("%Y-%m-%d"),
:20 "week_end": end_of_week.strftime("%Y-%m-%d"),
] "uploaders": final_results,
}
return { except ValueError as e:
"week_start": start_of_week.strftime("%Y-%m-%d"), return {
"uploaders": final_results, "error": f"Invalid date format. Please use YYYY-MM-DD format. Details: {str(e)}"
} }
except Exception as e:
return {"error": f"An error occurred: {str(e)}"}
+89 -4
View File
@@ -58,12 +58,86 @@ h4 {
transform: translateY(-5px); transform: translateY(-5px);
} }
.date-info { .week-selector {
background: rgba(129,87,208,0.6); display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin: 20px 0;
padding: 10px;
background: rgba(74, 71, 163, 0.2);
border-radius: 8px;
}
.week-selector button {
background: #4a47a3;
color: white;
border: none;
padding: 8px 16px; padding: 8px 16px;
border-radius: 4px; border-radius: 6px;
text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
min-width: 130px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.week-selector button:hover {
background: #5b58b4;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.week-selector button:active {
transform: translateY(0);
}
.week-selector input[type="date"] {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 7px 14px;
border-radius: 6px;
font-size: 0.9rem;
min-width: 150px;
cursor: pointer;
transition: all 0.3s ease;
}
.week-selector input[type="date"]:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.week-selector input[type="date"]:focus {
outline: none;
border-color: #4a47a3;
background: rgba(255, 255, 255, 0.2);
}
/* Style the calendar icon */
.week-selector input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
opacity: 0.7;
cursor: pointer;
}
.week-selector input[type="date"]::-webkit-calendar-picker-indicator:hover {
opacity: 1;
}
.date-info {
background: rgba(74, 71, 163, 0.4);
color: white;
padding: 10px 20px;
border-radius: 6px;
text-align: center;
font-size: 1rem;
margin: 15px 0;
border: 1px solid rgba(255, 255, 255, 0.1);
} }
.chart-wrapper { .chart-wrapper {
@@ -229,6 +303,17 @@ h4 {
padding: 10px; padding: 10px;
} }
.week-selector {
flex-direction: column;
gap: 8px;
}
.week-selector button,
.week-selector input[type="date"] {
width: 100%;
min-width: unset;
}
h3 { h3 {
font-size: 1.2rem; font-size: 1.2rem;
} }
+6 -1
View File
@@ -64,7 +64,12 @@
</div> </div>
<div class="chart-section"> <div class="chart-section">
<h4 class="text-left">Top 20 Contribution Streams Uploaders This Week</h4> <h4 class="text-left">Top 20 Contribution Streams Uploaders</h4>
<div class="week-selector mb-3">
<button class="btn btn-outline-light btn-sm" id="prevWeek">Previous Week</button>
<input type="date" id="weekSelector" class="form-control mx-2">
<button class="btn btn-outline-light btn-sm" id="nextWeek">Next Week</button>
</div>
<div class="date-info mb-3" id="weekDateRange"></div> <div class="date-info mb-3" id="weekDateRange"></div>
<div class="chart-wrapper"> <div class="chart-wrapper">
<canvas id="weeklyUploadersChart"></canvas> <canvas id="weeklyUploadersChart"></canvas>
+355 -408
View File
@@ -1,84 +1,94 @@
document.addEventListener('DOMContentLoaded', function () { let weeklyUploadersChart = null;
const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
const removeLoadingElement = (spinnerId) => { const formatDateToYYYYMMDD = (date) => {
const spinner = document.getElementById(spinnerId); return date.toISOString().split('T')[0];
if (spinner) { };
spinner.remove();
}
};
const renderSchedulerDetails = async () => { const getStartOfWeek = (date) => {
const data = await fetchData('/metrics/scrapy-schedulers'); const d = new Date(date);
removeLoadingElement('schedulerChartsSkeleton'); const day = d.getDay();
const container = document.getElementById('schedulerChartsContainer'); const diff = d.getDate() - day + (day === 0 ? -6 : 1);
container.innerHTML = ''; // Clear loading skeleton return new Date(d.setDate(diff));
};
const enabledWithStats = []; const fetchData = async (url) => {
const enabledWithoutStats = []; const response = await fetch(url);
const disabledWithStats = []; return response.json();
const disabledWithoutStats = []; };
data.forEach(scheduler => { const removeLoadingElement = (spinnerId) => {
if (scheduler.is_scheduler_disabled) { const spinner = document.getElementById(spinnerId);
if (scheduler.last_run_state) { if (spinner) {
disabledWithStats.push(scheduler); spinner.remove();
} else { }
disabledWithoutStats.push(scheduler); };
}
const renderSchedulerDetails = async () => {
const data = await fetchData('/metrics/scrapy-schedulers');
removeLoadingElement('schedulerChartsSkeleton');
const container = document.getElementById('schedulerChartsContainer');
container.innerHTML = ''; // Clear loading skeleton
const enabledWithStats = [];
const enabledWithoutStats = [];
const disabledWithStats = [];
const disabledWithoutStats = [];
data.forEach(scheduler => {
if (scheduler.is_scheduler_disabled) {
if (scheduler.last_run_state) {
disabledWithStats.push(scheduler);
} else { } else {
if (scheduler.last_run_state) { disabledWithoutStats.push(scheduler);
enabledWithStats.push(scheduler); }
} else {
if (scheduler.last_run_state) {
enabledWithStats.push(scheduler);
} else {
enabledWithoutStats.push(scheduler);
}
}
});
const createSchedulerSection = (schedulers, sectionTitle) => {
const section = document.createElement('div');
section.className = 'scheduler-section';
const sectionTitleElem = document.createElement('h5');
sectionTitleElem.textContent = sectionTitle;
section.appendChild(sectionTitleElem);
let row;
schedulers.forEach((scheduler, index) => {
if (index % 3 === 0) {
row = document.createElement('div');
row.className = 'row';
section.appendChild(row);
}
const schedulerCard = document.createElement('div');
schedulerCard.className = 'col-lg-4 col-md-6 col-sm-12 mb-4';
let cardClass = 'card-scheduler ';
if (scheduler.is_scheduler_disabled) {
cardClass += 'disabled';
} else if (scheduler.last_run_state === null) {
cardClass += 'enabled';
} else {
const logCounts = {
info: scheduler.last_run_state.log_count_info, warning: scheduler.last_run_state.log_count_warning, error: scheduler.last_run_state.log_count_error
};
const maxLogCount = Math.max(logCounts.info, logCounts.warning, logCounts.error);
if (maxLogCount === logCounts.error) {
cardClass += 'log-error';
} else if (maxLogCount === logCounts.warning) {
cardClass += 'log-warning';
} else { } else {
enabledWithoutStats.push(scheduler); cardClass += 'log-info';
} }
} }
});
const createSchedulerSection = (schedulers, sectionTitle) => { const lastRunState = scheduler.last_run_state ? `
const section = document.createElement('div');
section.className = 'scheduler-section';
const sectionTitleElem = document.createElement('h5');
sectionTitleElem.textContent = sectionTitle;
section.appendChild(sectionTitleElem);
let row;
schedulers.forEach((scheduler, index) => {
if (index % 3 === 0) {
row = document.createElement('div');
row.className = 'row';
section.appendChild(row);
}
const schedulerCard = document.createElement('div');
schedulerCard.className = 'col-lg-4 col-md-6 col-sm-12 mb-4';
let cardClass = 'card-scheduler ';
if (scheduler.is_scheduler_disabled) {
cardClass += 'disabled';
} else if (scheduler.last_run_state === null) {
cardClass += 'enabled';
} else {
const logCounts = {
info: scheduler.last_run_state.log_count_info,
warning: scheduler.last_run_state.log_count_warning,
error: scheduler.last_run_state.log_count_error
};
const maxLogCount = Math.max(logCounts.info, logCounts.warning, logCounts.error);
if (maxLogCount === logCounts.error) {
cardClass += 'log-error';
} else if (maxLogCount === logCounts.warning) {
cardClass += 'log-warning';
} else {
cardClass += 'log-info';
}
}
const lastRunState = scheduler.last_run_state ? `
<p>Items Scraped: ${scheduler.last_run_state.item_scraped_count}</p> <p>Items Scraped: ${scheduler.last_run_state.item_scraped_count}</p>
<p>Items Dropped: ${scheduler.last_run_state.item_dropped_count}</p> <p>Items Dropped: ${scheduler.last_run_state.item_dropped_count}</p>
<p class="text-info">Info: ${scheduler.last_run_state.log_count_info}</p> <p class="text-info">Info: ${scheduler.last_run_state.log_count_info}</p>
@@ -86,7 +96,7 @@ document.addEventListener('DOMContentLoaded', function () {
<p class="text-danger">Error: ${scheduler.last_run_state.log_count_error}</p> <p class="text-danger">Error: ${scheduler.last_run_state.log_count_error}</p>
` : ''; ` : '';
const cardContent = ` const cardContent = `
<div class="${cardClass}"> <div class="${cardClass}">
<h5>${scheduler.name}</h5> <h5>${scheduler.name}</h5>
<p>Last Run: ${scheduler.time_since_last_run}</p> <p>Last Run: ${scheduler.time_since_last_run}</p>
@@ -94,397 +104,335 @@ document.addEventListener('DOMContentLoaded', function () {
${lastRunState} ${lastRunState}
</div> </div>
`; `;
schedulerCard.innerHTML = cardContent; schedulerCard.innerHTML = cardContent;
row.appendChild(schedulerCard); row.appendChild(schedulerCard);
}); });
container.appendChild(section); container.appendChild(section);
};
if (enabledWithStats.length > 0) {
createSchedulerSection(enabledWithStats, 'Enabled Schedulers with Stats');
}
if (enabledWithoutStats.length > 0) {
createSchedulerSection(enabledWithoutStats, 'Enabled Schedulers without Stats');
}
if (disabledWithStats.length > 0) {
createSchedulerSection(disabledWithStats, 'Disabled Schedulers with Stats');
}
if (disabledWithoutStats.length > 0) {
createSchedulerSection(disabledWithoutStats, 'Disabled Schedulers without Stats');
}
}; };
const renderMetadataCountsChart = async () => { if (enabledWithStats.length > 0) {
const data = await fetchData('/metrics/metadata'); createSchedulerSection(enabledWithStats, 'Enabled Schedulers with Stats');
removeLoadingElement('metadataCountsSkeleton'); }
const ctx = document.getElementById('metadataCountsChart').getContext('2d'); if (enabledWithoutStats.length > 0) {
new Chart(ctx, { createSchedulerSection(enabledWithoutStats, 'Enabled Schedulers without Stats');
type: 'pie', }
data: { if (disabledWithStats.length > 0) {
labels: ['Movies', 'Series', 'TV Channels'], createSchedulerSection(disabledWithStats, 'Disabled Schedulers with Stats');
datasets: [{ }
label: 'Metadata Counts', if (disabledWithoutStats.length > 0) {
data: [data.movies, data.series, data.tv_channels], createSchedulerSection(disabledWithoutStats, 'Disabled Schedulers without Stats');
backgroundColor: [ }
'rgba(153, 102, 255, 0.6)', };
'rgba(54, 162, 235, 0.6)',
'rgba(75, 192, 192, 0.6)' const renderMetadataCountsChart = async () => {
], const data = await fetchData('/metrics/metadata');
borderColor: [ removeLoadingElement('metadataCountsSkeleton');
'rgba(153, 102, 255, 1)', const ctx = document.getElementById('metadataCountsChart').getContext('2d');
'rgba(54, 162, 235, 1)', new Chart(ctx, {
'rgba(75, 192, 192, 1)' type: 'pie', data: {
], labels: ['Movies', 'Series', 'TV Channels'], datasets: [{
}] label: 'Metadata Counts',
}, data: [data.movies, data.series, data.tv_channels],
options: { backgroundColor: ['rgba(153, 102, 255, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(75, 192, 192, 0.6)'],
responsive: true, borderColor: ['rgba(153, 102, 255, 1)', 'rgba(54, 162, 235, 1)', 'rgba(75, 192, 192, 1)'],
maintainAspectRatio: false, }]
plugins: { }, options: {
legend: { responsive: true, maintainAspectRatio: false, plugins: {
display: false legend: {
}, display: false
datalabels: { }, datalabels: {
formatter: (value, context) => { formatter: (value, context) => {
return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString(); return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString();
}, }, color: '#fff', anchor: 'center', align: 'center', offset: 0, borderRadius: 4, backgroundColor: (context) => {
color: '#fff', return context.dataset.backgroundColor;
anchor: 'center', }, font: {
align: 'center', weight: 'bold'
offset: 0,
borderRadius: 4,
backgroundColor: (context) => {
return context.dataset.backgroundColor;
},
font: {
weight: 'bold'
}
} }
} }
} }
}); }
}; });
};
const renderTotalTorrentsCount = async () => { const renderTotalTorrentsCount = async () => {
const data = await fetchData('/metrics/torrents'); const data = await fetchData('/metrics/torrents');
removeLoadingElement('totalTorrentsSkeleton'); removeLoadingElement('totalTorrentsSkeleton');
const totalTorrentsValue = document.getElementById('totalTorrentsValue'); const totalTorrentsValue = document.getElementById('totalTorrentsValue');
totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`; totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`;
}; };
const renderTorrentSourcesChart = async () => { const renderTorrentSourcesChart = async () => {
const data = await fetchData('/metrics/torrents/sources'); const data = await fetchData('/metrics/torrents/sources');
removeLoadingElement('torrentSourcesSkeleton'); removeLoadingElement('torrentSourcesSkeleton');
const ctx = document.getElementById('torrentSourcesChart').getContext('2d'); const ctx = document.getElementById('torrentSourcesChart').getContext('2d');
new Chart(ctx, { new Chart(ctx, {
type: 'bar', type: 'bar', data: {
data: { datasets: data.map(source => {
datasets: data.map(source => { return {
return { label: source.name, data: [{x: source.name, y: source.count}],
label: source.name, };
data: [{x: source.name, y: source.count}], }),
}; }, options: {
}), grouped: false, responsive: true, maintainAspectRatio: false, scales: {
}, y: {
options: { beginAtZero: true, display: false
grouped: false, }, x: {
responsive: true, ticks: {
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
display: false
},
x: {
ticks: {
color: '#fff',
}
}
},
layout: {
padding: {
top: 50 // Add padding to prevent label cutoff
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.raw.y.toLocaleString()} Torrents`;
}
}
},
datalabels: {
align: 'end',
anchor: 'end',
rotation: 0,
color: '#fff', color: '#fff',
formatter: (value, context) => { }
return value.y.toLocaleString(); }
}, }, layout: {
font: { padding: {
weight: 'bold' top: 50 // Add padding to prevent label cutoff
}, }
}, plugins: {
legend: {
display: false
}, tooltip: {
callbacks: {
label: function (context) {
return `${context.raw.y.toLocaleString()} Torrents`;
}
}
}, datalabels: {
align: 'end', anchor: 'end', rotation: 0, color: '#fff', formatter: (value, context) => {
return value.y.toLocaleString();
}, font: {
weight: 'bold'
},
}
}
}
});
};
const renderTorrentUploadersChart = async () => {
const data = await fetchData('/metrics/torrents/uploaders');
removeLoadingElement('torrentUploadersSkeleton');
const ctx = document.getElementById('torrentUploadersChart').getContext('2d');
const sortedData = data.sort((a, b) => b.count - a.count);
new Chart(ctx, {
type: 'bar', data: {
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, layout: {
padding: {
right: 50 // Add padding to prevent label cutoff
}
}, 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: {
beginAtZero: true, grid: {
color: 'rgba(255, 255, 255, 0.1)'
}, ticks: {
color: '#fff'
}
}, y: {
grid: {
display: false
}, ticks: {
color: '#fff'
} }
} }
} }
}); }
}; });
};
const renderTorrentUploadersChart = async () => { const renderDebridCacheMetrics = async () => {
const data = await fetchData('/metrics/torrents/uploaders'); const data = await fetchData('/metrics/debrid-cache');
removeLoadingElement('torrentUploadersSkeleton'); removeLoadingElement('debridChartSkeleton');
const ctx = document.getElementById('torrentUploadersChart').getContext('2d');
const sortedData = data.sort((a, b) => b.count - a.count); // Create the bar chart for service comparison
const ctx = document.getElementById('debridCacheChart').getContext('2d');
new Chart(ctx, { // Prepare data for the chart
type: 'bar', const services = Object.keys(data.services);
data: { const cachedTorrents = services.map(service => data.services[service].cached_torrents);
labels: sortedData.map(item => item.name),
datasets: [{ new Chart(ctx, {
data: sortedData.map(item => item.count), type: 'bar', data: {
backgroundColor: 'rgba(75, 192, 192, 0.6)', labels: services, datasets: [{
borderColor: 'rgba(75, 192, 192, 1)', label: 'Cached Torrents', data: cachedTorrents, backgroundColor: 'rgba(54, 162, 235, 0.6)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1
borderWidth: 1 }]
}] }, options: {
}, responsive: true, maintainAspectRatio: false, plugins: {
options: { legend: {
indexAxis: 'y', display: false
responsive: true, }, tooltip: {
maintainAspectRatio: false, callbacks: {
layout: { label: function (context) {
padding: { return `Cached Torrents: ${context.raw.toLocaleString()}`;
right: 50 // Add padding to prevent label cutoff }
} }
}, }, datalabels: {
plugins: { color: '#fff', anchor: 'end', align: 'end', formatter: (value) => value.toLocaleString(), font: {
legend: { weight: 'bold'
}
}
}, scales: {
y: {
beginAtZero: true, title: {
display: true, text: 'Number of Cached Torrents', color: '#fff'
}, ticks: {
color: '#fff'
}, grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
}, x: {
ticks: {
color: '#fff'
}, grid: {
display: false 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: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#fff'
}
},
y: {
grid: {
display: false
},
ticks: {
color: '#fff'
}
} }
} }
} }
}); }
}; });
};
const renderWeeklyUploadersChart = async () => {
const data = await fetchData('/metrics/torrents/uploaders/weekly'); const renderWeeklyUploadersChart = async (selectedDate = null) => {
removeLoadingElement('weeklyUploadersSkeleton'); if (!selectedDate) {
selectedDate = new Date();
}
const startOfWeek = getStartOfWeek(selectedDate);
const formattedDate = formatDateToYYYYMMDD(startOfWeek);
try {
// Show loading state
document.getElementById('weeklyUploadersSkeleton').style.display = 'block';
if (weeklyUploadersChart) {
weeklyUploadersChart.destroy();
}
const data = await fetchData(`/metrics/torrents/uploaders/weekly/${formattedDate}`);
if (data.error) {
console.error('Error fetching data:', data.error);
return;
}
// Update date range display // Update date range display
const weekStart = new Date(data.week_start); const weekStart = new Date(data.week_start);
const weekEnd = new Date(weekStart); const weekEnd = new Date(data.week_end);
weekEnd.setDate(weekEnd.getDate() + 6);
const dateRangeElement = document.getElementById('weekDateRange'); const dateRangeElement = document.getElementById('weekDateRange');
dateRangeElement.innerHTML = ` dateRangeElement.innerHTML = `
<span class="text-white"> <span class="text-white">
${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()} ${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}
</span> </span>
`; `;
// Update week selector value
document.getElementById('weekSelector').value = formattedDate;
const ctx = document.getElementById('weeklyUploadersChart').getContext('2d'); const ctx = document.getElementById('weeklyUploadersChart').getContext('2d');
new Chart(ctx, { weeklyUploadersChart = new Chart(ctx, {
type: 'bar', type: 'bar', data: {
data: { labels: data.uploaders.map(uploader => uploader.name), datasets: [{
labels: data.uploaders.map(uploader => uploader.name), data: data.uploaders.map(uploader => uploader.count), backgroundColor: 'rgba(147, 112, 219, 0.6)', borderColor: 'rgba(147, 112, 219, 1)', borderWidth: 1
datasets: [{
data: data.uploaders.map(uploader => uploader.count),
backgroundColor: 'rgba(147, 112, 219, 0.6)',
borderColor: 'rgba(147, 112, 219, 1)',
borderWidth: 1
}] }]
}, }, options: {
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: { padding: {
right: 50 right: 50
} }
}, }, plugins: {
plugins: {
legend: { legend: {
display: false display: false
}, }, tooltip: {
tooltip: {
callbacks: { callbacks: {
label: function (context) { label: function (context) {
const uploader = data.uploaders[context.dataIndex]; const uploader = data.uploaders[context.dataIndex];
const lastUpload = new Date(uploader.latest_upload); const lastUpload = new Date(uploader.latest_upload);
return [ return [`Torrents: ${context.raw}`, `Last Upload: ${lastUpload.toLocaleDateString()}`];
`Torrents: ${context.raw}`,
`Last Upload: ${lastUpload.toLocaleDateString()}`
];
} }
} }
}, }, datalabels: {
datalabels: { color: '#fff', anchor: 'end', align: 'right', formatter: (value) => value, font: {
color: '#fff', weight: 'bold', size: 12
anchor: 'end', }, padding: {
align: 'right',
formatter: (value) => value,
font: {
weight: 'bold',
size: 12
},
padding: {
right: 6 right: 6
} }
} }
}, }, scales: {
scales: {
x: { x: {
beginAtZero: true, beginAtZero: true, grid: {
grid: {
color: 'rgba(255, 255, 255, 0.1)' color: 'rgba(255, 255, 255, 0.1)'
}, }, ticks: {
ticks: { color: '#fff', callback: function (value) {
color: '#fff',
callback: function (value) {
return value.toLocaleString(); return value.toLocaleString();
},
padding: 10 // Add padding to axis ticks
},
// Ensure the axis extends a bit beyond the max value
suggestedMax: function (context) {
const max = Math.max(...context.chart.data.datasets[0].data);
return max * 1.1; // Extend 10% beyond the max value
}
},
y: {
grid: {
display: false,
borderWidth: 0
},
ticks: {
color: '#fff',
font: {
size: 12
},
padding: 10
}
}
}
}
});
};
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()}`;
} }
} }
}, }, y: {
datalabels: {
color: '#fff',
anchor: 'end',
align: 'end',
formatter: (value) => value.toLocaleString(),
font: {
weight: 'bold'
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Number of Cached Torrents',
color: '#fff'
},
ticks: {
color: '#fff'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
x: {
ticks: {
color: '#fff'
},
grid: { grid: {
display: false display: false
}, ticks: {
color: '#fff'
} }
} }
} }
} }
}); });
}; } finally {
// Hide loading state
document.getElementById('weeklyUploadersSkeleton').style.display = 'none';
}
};
document.addEventListener('DOMContentLoaded', function () {
const weekSelector = document.getElementById('weekSelector');
const prevWeekBtn = document.getElementById('prevWeek');
const nextWeekBtn = document.getElementById('nextWeek');
// Set initial date to start of current week
const currentDate = new Date();
const startOfWeek = getStartOfWeek(currentDate);
weekSelector.value = formatDateToYYYYMMDD(startOfWeek);
// Event listeners for navigation
weekSelector.addEventListener('change', (e) => {
renderWeeklyUploadersChart(new Date(e.target.value));
});
prevWeekBtn.addEventListener('click', () => {
const currentDate = new Date(weekSelector.value);
const prevWeek = new Date(currentDate.setDate(currentDate.getDate() - 7));
renderWeeklyUploadersChart(prevWeek);
});
nextWeekBtn.addEventListener('click', () => {
const currentDate = new Date(weekSelector.value);
const nextWeek = new Date(currentDate.setDate(currentDate.getDate() + 7));
renderWeeklyUploadersChart(nextWeek);
});
// Initial render
renderWeeklyUploadersChart();
});
document.addEventListener('DOMContentLoaded', function () {
const initCharts = () => { const initCharts = () => {
renderSchedulerDetails(); renderSchedulerDetails();
@@ -492,7 +440,6 @@ document.addEventListener('DOMContentLoaded', function () {
renderTotalTorrentsCount(); renderTotalTorrentsCount();
renderTorrentSourcesChart(); renderTorrentSourcesChart();
renderTorrentUploadersChart(); renderTorrentUploadersChart();
renderWeeklyUploadersChart();
renderDebridCacheMetrics(); renderDebridCacheMetrics();
}; };