mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
feat: add week selector for weekly top uploaders metrics
This commit is contained in:
+36
-17
@@ -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)}"}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user