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 Chart from 'chart.js';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import colors from 'Styles/Variables/colors';
|
||||||
|
|
||||||
class BarChart extends Component {
|
class BarChart extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -10,7 +11,7 @@ class BarChart extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.myChart = new Chart(this.canvasRef.current, {
|
this.myChart = new Chart(this.canvasRef.current, {
|
||||||
type: 'bar',
|
type: this.props.horizontal ? 'horizontalBar' : 'bar',
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false
|
maintainAspectRatio: false
|
||||||
},
|
},
|
||||||
@@ -18,7 +19,8 @@ class BarChart extends Component {
|
|||||||
labels: this.props.data.map((d) => d.label),
|
labels: this.props.data.map((d) => d.label),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: this.props.title,
|
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 = {
|
BarChart.propTypes = {
|
||||||
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
data: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
horizontal: PropTypes.bool,
|
||||||
title: PropTypes.string.isRequired
|
title: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
BarChart.defaultProps = {
|
BarChart.defaultProps = {
|
||||||
data: [],
|
data: [],
|
||||||
|
horizontal: false,
|
||||||
title: ''
|
title: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import Chart from 'chart.js';
|
import Chart from 'chart.js';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import colors from 'Styles/Variables/colors';
|
||||||
|
|
||||||
class DoughnutChart extends Component {
|
class DoughnutChart extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -25,7 +26,8 @@ class DoughnutChart extends Component {
|
|||||||
labels: this.props.data.map((d) => d.label),
|
labels: this.props.data.map((d) => d.label),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: this.props.title,
|
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 DoughnutChart from 'Components/Chart/DoughnutChart';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import styles from './Stats.css';
|
import styles from './Stats.css';
|
||||||
@@ -45,60 +46,106 @@ function getNumberGrabsData(indexerStats) {
|
|||||||
return data;
|
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) {
|
function Stats(props) {
|
||||||
const {
|
const {
|
||||||
items,
|
item,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error
|
error
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isLoaded = !!(!error && isPopulated && items.length);
|
const isLoaded = !!(!error && isPopulated);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<PageToolbar />
|
<PageToolbar />
|
||||||
{
|
<PageContentBody>
|
||||||
isFetching && !isPopulated &&
|
{
|
||||||
<LoadingIndicator />
|
isFetching && !isPopulated &&
|
||||||
}
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<div className={styles.errorMessage}>
|
<div className={styles.errorMessage}>
|
||||||
{getErrorMessage(error, 'Failed to load indexer stats from API')}
|
{getErrorMessage(error, 'Failed to load indexer stats from API')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isLoaded &&
|
isLoaded &&
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.fullWidthChart}>
|
<div className={styles.fullWidthChart}>
|
||||||
<BarChart
|
<BarChart
|
||||||
data={getAverageResponseTimeData(items)}
|
data={getAverageResponseTimeData(item.indexers)}
|
||||||
title='Average Response Times (ms)'
|
title='Average Response Times (ms)'
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.halfWidthChart}>
|
||||||
|
<DoughnutChart
|
||||||
|
data={getTotalRequestsData(item.indexers)}
|
||||||
|
title='Total Indexer Queries'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.halfWidthChart}>
|
||||||
|
<BarChart
|
||||||
|
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>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
}
|
||||||
<DoughnutChart
|
</PageContentBody>
|
||||||
data={getTotalRequestsData(items)}
|
|
||||||
title='Total Indexer Queries'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.halfWidthChart}>
|
|
||||||
<BarChart
|
|
||||||
data={getNumberGrabsData(items)}
|
|
||||||
title='Total Indexer Grabs'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stats.propTypes = {
|
Stats.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
|
@@ -14,13 +14,13 @@ export const defaultState = {
|
|||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: null,
|
error: null,
|
||||||
items: [],
|
item: {},
|
||||||
|
|
||||||
details: {
|
details: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: null,
|
error: null,
|
||||||
items: []
|
item: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -181,5 +181,10 @@ module.exports = {
|
|||||||
//
|
//
|
||||||
// Table
|
// 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 NumberOfQueries { get; set; }
|
||||||
public int NumberOfGrabs { 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
|
public interface IIndexerStatisticsRepository
|
||||||
{
|
{
|
||||||
List<IndexerStatistics> IndexerStatistics();
|
List<IndexerStatistics> IndexerStatistics();
|
||||||
|
List<UserAgentStatistics> UserAgentStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IndexerStatisticsRepository : IIndexerStatisticsRepository
|
public class IndexerStatisticsRepository : IIndexerStatisticsRepository
|
||||||
@@ -26,13 +27,13 @@ namespace NzbDrone.Core.IndexerStats
|
|||||||
public List<IndexerStatistics> IndexerStatistics()
|
public List<IndexerStatistics> IndexerStatistics()
|
||||||
{
|
{
|
||||||
var time = DateTime.UtcNow;
|
var time = DateTime.UtcNow;
|
||||||
return Query(Builder());
|
return Query(IndexerBuilder());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<IndexerStatistics> IndexerStatistics(int indexerId)
|
public List<UserAgentStatistics> UserAgentStatistics()
|
||||||
{
|
{
|
||||||
var time = DateTime.UtcNow;
|
var time = DateTime.UtcNow;
|
||||||
return Query(Builder().Where<IndexerDefinition>(x => x.Id == indexerId));
|
return UserAgentQuery(UserAgentBuilder());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<IndexerStatistics> Query(SqlBuilder builder)
|
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,
|
.Select(@"Indexers.Id AS IndexerId,
|
||||||
Indexers.Name AS IndexerName,
|
Indexers.Name AS IndexerName,
|
||||||
SUM(CASE WHEN EventType == 2 then 1 else 0 end) AS NumberOfQueries,
|
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")
|
AVG(json_extract(History.Data,'$.elapsedTime')) AS AverageResponseTime")
|
||||||
.Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id)
|
.Join<History.History, IndexerDefinition>((t, r) => t.IndexerId == r.Id)
|
||||||
.GroupBy<IndexerDefinition>(x => x.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
|
public interface IIndexerStatisticsService
|
||||||
{
|
{
|
||||||
List<IndexerStatistics> IndexerStatistics();
|
List<IndexerStatistics> IndexerStatistics();
|
||||||
|
List<UserAgentStatistics> UserAgentStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IndexerStatisticsService : IIndexerStatisticsService
|
public class IndexerStatisticsService : IIndexerStatisticsService
|
||||||
@@ -23,5 +24,12 @@ namespace NzbDrone.Core.IndexerStats
|
|||||||
|
|
||||||
return seasonStatistics.ToList();
|
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;
|
_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 class IndexerStatsResource : RestResource
|
||||||
{
|
{
|
||||||
public int IndexerId { get; set; }
|
public List<IndexerStatistics> Indexers { get; set; }
|
||||||
public string IndexerName { get; set; }
|
public List<UserAgentStatistics> UserAgents { 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user