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
+36 -17
View File
@@ -186,15 +186,22 @@ async def get_torrents_by_uploaders(response: Response):
return uploader_stats
@metrics_router.get("/torrents/uploaders/weekly", tags=["metrics"])
async def get_weekly_top_uploaders(response: Response):
@metrics_router.get("/torrents/uploaders/weekly/{week_date}", tags=["metrics"])
async def get_weekly_top_uploaders(week_date: str, 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())
try:
# Parse the input date
selected_date = datetime.strptime(week_date, "%Y-%m-%d")
selected_date = selected_date.replace(tzinfo=timezone.utc)
# Calculate the start of the week (Monday) for the selected date
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)
# Calculate end of week
end_of_week = start_of_week + timedelta(days=7)
# Get raw aggregation results
raw_results = await TorrentStreams.aggregate(
[
@@ -202,8 +209,12 @@ async def get_weekly_top_uploaders(response: Response):
"$match": {
"source": "Contribution Stream",
"$or": [
{"created_at": {"$gte": start_of_week}},
{"uploaded_at": {"$gte": start_of_week}},
{
"uploaded_at": {
"$gte": start_of_week,
"$lt": end_of_week,
}
},
],
"is_blocked": {"$ne": True},
}
@@ -218,7 +229,7 @@ async def get_weekly_top_uploaders(response: Response):
]
).to_list()
# Process and clean the results
# Process and clean the results (keeping existing logic)
processed_results = []
anonymous_total = 0
anonymous_latest = None
@@ -228,11 +239,9 @@ async def get_weekly_top_uploaders(response: Response):
count = stat["count"]
latest_upload = stat["latest_upload"]
# Skip invalid entries
if count <= 0:
continue
# Combine all anonymous-like entries
if (
uploader_name is None
or uploader_name.strip() == ""
@@ -249,29 +258,39 @@ async def get_weekly_top_uploaders(response: Response):
"name": uploader_name.strip(),
"count": count,
"latest_upload": (
latest_upload.strftime("%Y-%m-%d") if latest_upload else None
latest_upload.strftime("%Y-%m-%d")
if latest_upload
else None
),
}
)
# Add anonymous entry if we have any anonymous uploads
if anonymous_total > 0:
processed_results.append(
{
"name": "Anonymous",
"count": anonymous_total,
"latest_upload": (
anonymous_latest.strftime("%Y-%m-%d") if anonymous_latest else None
anonymous_latest.strftime("%Y-%m-%d")
if anonymous_latest
else None
),
}
)
# Sort by count and limit to top 20
final_results = sorted(processed_results, key=lambda x: x["count"], reverse=True)[
:20
]
final_results = sorted(
processed_results, key=lambda x: x["count"], reverse=True
)[:20]
return {
"week_start": start_of_week.strftime("%Y-%m-%d"),
"week_end": end_of_week.strftime("%Y-%m-%d"),
"uploaders": final_results,
}
except ValueError as e:
return {
"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);
}
.date-info {
background: rgba(129,87,208,0.6);
.week-selector {
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;
border-radius: 4px;
text-align: center;
border-radius: 6px;
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 {
@@ -229,6 +303,17 @@ h4 {
padding: 10px;
}
.week-selector {
flex-direction: column;
gap: 8px;
}
.week-selector button,
.week-selector input[type="date"] {
width: 100%;
min-width: unset;
}
h3 {
font-size: 1.2rem;
}
+6 -1
View File
@@ -64,7 +64,12 @@
</div>
<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="chart-wrapper">
<canvas id="weeklyUploadersChart"></canvas>
+205 -258
View File
@@ -1,17 +1,29 @@
document.addEventListener('DOMContentLoaded', function () {
const fetchData = async (url) => {
let weeklyUploadersChart = null;
const formatDateToYYYYMMDD = (date) => {
return date.toISOString().split('T')[0];
};
const getStartOfWeek = (date) => {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
return new Date(d.setDate(diff));
};
const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
};
const removeLoadingElement = (spinnerId) => {
const removeLoadingElement = (spinnerId) => {
const spinner = document.getElementById(spinnerId);
if (spinner) {
spinner.remove();
}
};
};
const renderSchedulerDetails = async () => {
const renderSchedulerDetails = async () => {
const data = await fetchData('/metrics/scrapy-schedulers');
removeLoadingElement('schedulerChartsSkeleton');
const container = document.getElementById('schedulerChartsContainer');
@@ -63,9 +75,7 @@ document.addEventListener('DOMContentLoaded', function () {
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
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);
@@ -113,129 +123,91 @@ document.addEventListener('DOMContentLoaded', function () {
if (disabledWithoutStats.length > 0) {
createSchedulerSection(disabledWithoutStats, 'Disabled Schedulers without Stats');
}
};
};
const renderMetadataCountsChart = async () => {
const renderMetadataCountsChart = async () => {
const data = await fetchData('/metrics/metadata');
removeLoadingElement('metadataCountsSkeleton');
const ctx = document.getElementById('metadataCountsChart').getContext('2d');
new Chart(ctx, {
type: 'pie',
data: {
labels: ['Movies', 'Series', 'TV Channels'],
datasets: [{
type: 'pie', data: {
labels: ['Movies', 'Series', 'TV Channels'], datasets: [{
label: 'Metadata Counts',
data: [data.movies, data.series, data.tv_channels],
backgroundColor: [
'rgba(153, 102, 255, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(75, 192, 192, 0.6)'
],
borderColor: [
'rgba(153, 102, 255, 1)',
'rgba(54, 162, 235, 1)',
'rgba(75, 192, 192, 1)'
],
backgroundColor: ['rgba(153, 102, 255, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(75, 192, 192, 0.6)'],
borderColor: ['rgba(153, 102, 255, 1)', 'rgba(54, 162, 235, 1)', 'rgba(75, 192, 192, 1)'],
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
}, options: {
responsive: true, maintainAspectRatio: false, plugins: {
legend: {
display: false
},
datalabels: {
}, datalabels: {
formatter: (value, context) => {
return context.chart.data.labels[context.dataIndex] + ': ' + value.toLocaleString();
},
color: '#fff',
anchor: 'center',
align: 'center',
offset: 0,
borderRadius: 4,
backgroundColor: (context) => {
}, color: '#fff', anchor: 'center', align: 'center', offset: 0, borderRadius: 4, backgroundColor: (context) => {
return context.dataset.backgroundColor;
},
font: {
}, font: {
weight: 'bold'
}
}
}
}
});
};
};
const renderTotalTorrentsCount = async () => {
const renderTotalTorrentsCount = async () => {
const data = await fetchData('/metrics/torrents');
removeLoadingElement('totalTorrentsSkeleton');
const totalTorrentsValue = document.getElementById('totalTorrentsValue');
totalTorrentsValue.textContent = `${data.total_torrents.toLocaleString()} (${data.total_torrents_readable})`;
};
};
const renderTorrentSourcesChart = async () => {
const renderTorrentSourcesChart = async () => {
const data = await fetchData('/metrics/torrents/sources');
removeLoadingElement('torrentSourcesSkeleton');
const ctx = document.getElementById('torrentSourcesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
type: 'bar', data: {
datasets: data.map(source => {
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: {
}, options: {
grouped: false, responsive: true, maintainAspectRatio: false, scales: {
y: {
beginAtZero: true,
display: false
},
x: {
beginAtZero: true, display: false
}, x: {
ticks: {
color: '#fff',
}
}
},
layout: {
}, layout: {
padding: {
top: 50 // Add padding to prevent label cutoff
}
},
plugins: {
}, plugins: {
legend: {
display: false
},
tooltip: {
}, tooltip: {
callbacks: {
label: function (context) {
return `${context.raw.y.toLocaleString()} Torrents`;
}
}
},
datalabels: {
align: 'end',
anchor: 'end',
rotation: 0,
color: '#fff',
formatter: (value, context) => {
}, datalabels: {
align: 'end', anchor: 'end', rotation: 0, color: '#fff', formatter: (value, context) => {
return value.y.toLocaleString();
},
font: {
}, font: {
weight: 'bold'
},
}
}
}
});
};
};
const renderTorrentUploadersChart = async () => {
const renderTorrentUploadersChart = async () => {
const data = await fetchData('/metrics/torrents/uploaders');
removeLoadingElement('torrentUploadersSkeleton');
const ctx = document.getElementById('torrentUploadersChart').getContext('2d');
@@ -243,175 +215,49 @@ document.addEventListener('DOMContentLoaded', function () {
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
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: {
}, options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: {
padding: {
right: 50 // Add padding to prevent label cutoff
}
},
plugins: {
}, plugins: {
legend: {
display: false
},
tooltip: {
}, tooltip: {
callbacks: {
label: function (context) {
return `${context.raw.toLocaleString()} Torrents`;
}
}
},
datalabels: {
color: '#fff',
anchor: 'end',
align: 'end',
formatter: (value) => value.toLocaleString(),
font: {
}, datalabels: {
color: '#fff', anchor: 'end', align: 'end', formatter: (value) => value.toLocaleString(), font: {
weight: 'bold'
}
}
},
scales: {
}, scales: {
x: {
beginAtZero: true,
grid: {
beginAtZero: true, grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
}, ticks: {
color: '#fff'
}
},
y: {
}, y: {
grid: {
display: false
},
ticks: {
}, 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(147, 112, 219, 0.6)',
borderColor: 'rgba(147, 112, 219, 1)',
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
right: 50
}
},
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}`,
`Last Upload: ${lastUpload.toLocaleDateString()}`
];
}
}
},
datalabels: {
color: '#fff',
anchor: 'end',
align: 'right',
formatter: (value) => value,
font: {
weight: 'bold',
size: 12
},
padding: {
right: 6
}
}
},
scales: {
x: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#fff',
callback: function (value) {
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 renderDebridCacheMetrics = async () => {
const data = await fetchData('/metrics/debrid-cache');
removeLoadingElement('debridChartSkeleton');
@@ -423,68 +269,170 @@ document.addEventListener('DOMContentLoaded', function () {
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
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: {
}, options: {
responsive: true, maintainAspectRatio: false, plugins: {
legend: {
display: false
},
tooltip: {
}, tooltip: {
callbacks: {
label: function (context) {
return `Cached Torrents: ${context.raw.toLocaleString()}`;
}
}
},
datalabels: {
color: '#fff',
anchor: 'end',
align: 'end',
formatter: (value) => value.toLocaleString(),
font: {
}, datalabels: {
color: '#fff', anchor: 'end', align: 'end', formatter: (value) => value.toLocaleString(), font: {
weight: 'bold'
}
}
},
scales: {
}, scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Number of Cached Torrents',
beginAtZero: true, title: {
display: true, text: 'Number of Cached Torrents', color: '#fff'
}, ticks: {
color: '#fff'
},
ticks: {
color: '#fff'
},
grid: {
}, grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
x: {
}, x: {
ticks: {
color: '#fff'
},
grid: {
}, grid: {
display: false
}
}
}
}
});
};
};
const renderWeeklyUploadersChart = async (selectedDate = null) => {
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
const weekStart = new Date(data.week_start);
const weekEnd = new Date(data.week_end);
const dateRangeElement = document.getElementById('weekDateRange');
dateRangeElement.innerHTML = `
<span class="text-white">
${weekStart.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}
</span>
`;
// Update week selector value
document.getElementById('weekSelector').value = formattedDate;
const ctx = document.getElementById('weeklyUploadersChart').getContext('2d');
weeklyUploadersChart = new Chart(ctx, {
type: 'bar', data: {
labels: data.uploaders.map(uploader => uploader.name), datasets: [{
data: data.uploaders.map(uploader => uploader.count), backgroundColor: 'rgba(147, 112, 219, 0.6)', borderColor: 'rgba(147, 112, 219, 1)', borderWidth: 1
}]
}, options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false, layout: {
padding: {
right: 50
}
}, 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}`, `Last Upload: ${lastUpload.toLocaleDateString()}`];
}
}
}, datalabels: {
color: '#fff', anchor: 'end', align: 'right', formatter: (value) => value, font: {
weight: 'bold', size: 12
}, padding: {
right: 6
}
}
}, 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'
}
}
}
}
});
} 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 = () => {
renderSchedulerDetails();
@@ -492,7 +440,6 @@ document.addEventListener('DOMContentLoaded', function () {
renderTotalTorrentsCount();
renderTorrentSourcesChart();
renderTorrentUploadersChart();
renderWeeklyUploadersChart();
renderDebridCacheMetrics();
};