mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
User Agent Stats
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Chart from 'chart.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import colors from 'Styles/Variables/colors';
|
||||
|
||||
class BarChart extends Component {
|
||||
constructor(props) {
|
||||
@@ -10,7 +11,7 @@ class BarChart extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.myChart = new Chart(this.canvasRef.current, {
|
||||
type: 'bar',
|
||||
type: this.props.horizontal ? 'horizontalBar' : 'bar',
|
||||
options: {
|
||||
maintainAspectRatio: false
|
||||
},
|
||||
@@ -18,7 +19,8 @@ class BarChart extends Component {
|
||||
labels: this.props.data.map((d) => d.label),
|
||||
datasets: [{
|
||||
label: this.props.title,
|
||||
data: this.props.data.map((d) => d.value)
|
||||
data: this.props.data.map((d) => d.value),
|
||||
backgroundColor: colors.chartColors
|
||||
}]
|
||||
}
|
||||
});
|
||||
@@ -39,11 +41,13 @@ class BarChart extends Component {
|
||||
|
||||
BarChart.propTypes = {
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
horizontal: PropTypes.bool,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
BarChart.defaultProps = {
|
||||
data: [],
|
||||
horizontal: false,
|
||||
title: ''
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import Chart from 'chart.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import colors from 'Styles/Variables/colors';
|
||||
|
||||
class DoughnutChart extends Component {
|
||||
constructor(props) {
|
||||
@@ -25,7 +26,8 @@ class DoughnutChart extends Component {
|
||||
labels: this.props.data.map((d) => d.label),
|
||||
datasets: [{
|
||||
label: this.props.title,
|
||||
data: this.props.data.map((d) => d.value)
|
||||
data: this.props.data.map((d) => d.value),
|
||||
backgroundColor: colors.chartColors
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
@@ -4,6 +4,7 @@ import BarChart from 'Components/Chart/BarChart';
|
||||
import DoughnutChart from 'Components/Chart/DoughnutChart';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import styles from './Stats.css';
|
||||
@@ -45,19 +46,50 @@ function getNumberGrabsData(indexerStats) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function getUserAgentGrabsData(indexerStats) {
|
||||
const data = indexerStats.map((indexer) => {
|
||||
return {
|
||||
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
||||
value: indexer.numberOfGrabs
|
||||
};
|
||||
});
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getUserAgentQueryData(indexerStats) {
|
||||
const data = indexerStats.map((indexer) => {
|
||||
return {
|
||||
label: indexer.userAgent ? indexer.userAgent : 'Other',
|
||||
value: indexer.numberOfQueries
|
||||
};
|
||||
});
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function Stats(props) {
|
||||
const {
|
||||
items,
|
||||
item,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = props;
|
||||
|
||||
const isLoaded = !!(!error && isPopulated && items.length);
|
||||
const isLoaded = !!(!error && isPopulated);
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar />
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -75,30 +107,45 @@ function Stats(props) {
|
||||
<div>
|
||||
<div className={styles.fullWidthChart}>
|
||||
<BarChart
|
||||
data={getAverageResponseTimeData(items)}
|
||||
data={getAverageResponseTimeData(item.indexers)}
|
||||
title='Average Response Times (ms)'
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<DoughnutChart
|
||||
data={getTotalRequestsData(items)}
|
||||
data={getTotalRequestsData(item.indexers)}
|
||||
title='Total Indexer Queries'
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getNumberGrabsData(items)}
|
||||
data={getNumberGrabsData(item.indexers)}
|
||||
title='Total Indexer Grabs'
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getUserAgentQueryData(item.userAgents)}
|
||||
title='Total User Agent Queries'
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.halfWidthChart}>
|
||||
<BarChart
|
||||
data={getUserAgentGrabsData(item.userAgents)}
|
||||
title='Total User Agent Grabs'
|
||||
horizontal={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
Stats.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
|
@@ -14,13 +14,13 @@ export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: [],
|
||||
item: {},
|
||||
|
||||
details: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
item: []
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -181,5 +181,10 @@ module.exports = {
|
||||
//
|
||||
// Table
|
||||
|
||||
tableRowHoverBackgroundColor: '#fafbfc'
|
||||
tableRowHoverBackgroundColor: '#fafbfc',
|
||||
|
||||
//
|
||||
// Charts
|
||||
|
||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
||||
};
|
||||
|
@@ -10,4 +10,18 @@ namespace NzbDrone.Core.IndexerStats
|
||||
public int NumberOfQueries { get; set; }
|
||||
public int NumberOfGrabs { get; set; }
|
||||
}
|
||||
|
||||
public class UserAgentStatistics : ResultSet
|
||||
{
|
||||
public string UserAgent { get; set; }
|
||||
public int NumberOfQueries { get; set; }
|
||||
public int NumberOfGrabs { get; set; }
|
||||
}
|
||||
|
||||
public class HostStatistics : ResultSet
|
||||
{
|
||||
public string Host { get; set; }
|
||||
public int NumberOfQueries { get; set; }
|
||||
public int NumberOfGrabs { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.IndexerStats
|
||||
public interface IIndexerStatisticsRepository
|
||||
{
|
||||
List<IndexerStatistics> IndexerStatistics();
|
||||
List<UserAgentStatistics> UserAgentStatistics();
|
||||
}
|
||||
|
||||
public class IndexerStatisticsRepository : IIndexerStatisticsRepository
|
||||
@@ -26,13 +27,13 @@ namespace NzbDrone.Core.IndexerStats
|
||||
public List<IndexerStatistics> IndexerStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return Query(Builder());
|
||||
return Query(IndexerBuilder());
|
||||
}
|
||||
|
||||
public List<IndexerStatistics> IndexerStatistics(int indexerId)
|
||||
public List<UserAgentStatistics> UserAgentStatistics()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
return Query(Builder().Where<IndexerDefinition>(x => x.Id == indexerId));
|
||||
return UserAgentQuery(UserAgentBuilder());
|
||||
}
|
||||
|
||||
private List<IndexerStatistics> Query(SqlBuilder builder)
|
||||
@@ -45,7 +46,17 @@ namespace NzbDrone.Core.IndexerStats
|
||||
}
|
||||
}
|
||||
|
||||
private SqlBuilder Builder() => new SqlBuilder()
|
||||
private List<UserAgentStatistics> UserAgentQuery(SqlBuilder builder)
|
||||
{
|
||||
var sql = builder.AddTemplate(_selectTemplate).LogQuery();
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.Query<UserAgentStatistics>(sql.RawSql, sql.Parameters).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private SqlBuilder IndexerBuilder() => new SqlBuilder()
|
||||
.Select(@"Indexers.Id AS IndexerId,
|
||||
Indexers.Name AS IndexerName,
|
||||
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
|
||||
@@ -53,5 +64,11 @@ namespace NzbDrone.Core.IndexerStats
|
||||
AVG(json_extract(History.Data,'$.elapsedTime')) AS AverageResponseTime")
|
||||
.Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id)
|
||||
.GroupBy<IndexerDefinition>(x => x.Id);
|
||||
|
||||
private SqlBuilder UserAgentBuilder() => new SqlBuilder()
|
||||
.Select(@"json_extract(History.Data,'$.source') AS UserAgent,
|
||||
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
|
||||
SUM(CASE WHEN EventType == 1 then 1 else 0 end) AS NumberOfGrabs")
|
||||
.GroupBy("UserAgent");
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ namespace NzbDrone.Core.IndexerStats
|
||||
public interface IIndexerStatisticsService
|
||||
{
|
||||
List<IndexerStatistics> IndexerStatistics();
|
||||
List<UserAgentStatistics> UserAgentStatistics();
|
||||
}
|
||||
|
||||
public class IndexerStatisticsService : IIndexerStatisticsService
|
||||
@@ -23,5 +24,12 @@ namespace NzbDrone.Core.IndexerStats
|
||||
|
||||
return seasonStatistics.ToList();
|
||||
}
|
||||
|
||||
public List<UserAgentStatistics> UserAgentStatistics()
|
||||
{
|
||||
var seasonStatistics = _indexerStatisticsRepository.UserAgentStatistics();
|
||||
|
||||
return seasonStatistics.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,12 +16,21 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
_indexerStatisticsService = indexerStatisticsService;
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
Get("/", x =>
|
||||
{
|
||||
return GetAll();
|
||||
});
|
||||
}
|
||||
|
||||
private List<IndexerStatsResource> GetAll()
|
||||
private IndexerStatsResource GetAll()
|
||||
{
|
||||
return _indexerStatisticsService.IndexerStatistics().ToResource();
|
||||
var indexerResource = new IndexerStatsResource
|
||||
{
|
||||
Indexers = _indexerStatisticsService.IndexerStatistics(),
|
||||
UserAgents = _indexerStatisticsService.UserAgentStatistics(),
|
||||
};
|
||||
|
||||
return indexerResource;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,35 +7,7 @@ namespace Prowlarr.Api.V1.Indexers
|
||||
{
|
||||
public class IndexerStatsResource : RestResource
|
||||
{
|
||||
public int IndexerId { get; set; }
|
||||
public string IndexerName { get; set; }
|
||||
public int NumberOfQueries { get; set; }
|
||||
public int AverageResponseTime { get; set; }
|
||||
public int NumberOfGrabs { get; set; }
|
||||
}
|
||||
|
||||
public static class IndexerStatsResourceMapper
|
||||
{
|
||||
public static IndexerStatsResource ToResource(this IndexerStatistics model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new IndexerStatsResource
|
||||
{
|
||||
IndexerId = model.IndexerId,
|
||||
IndexerName = model.IndexerName,
|
||||
NumberOfQueries = model.NumberOfQueries,
|
||||
AverageResponseTime = model.AverageResponseTime,
|
||||
NumberOfGrabs = model.NumberOfGrabs
|
||||
};
|
||||
}
|
||||
|
||||
public static List<IndexerStatsResource> ToResource(this IEnumerable<IndexerStatistics> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
public List<IndexerStatistics> Indexers { get; set; }
|
||||
public List<UserAgentStatistics> UserAgents { get; set; }
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user