From 0b45929ccc8f136f374fe8279ac41f8b554655b4 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 20 Feb 2021 00:47:49 -0500 Subject: [PATCH] User Agent Stats --- frontend/src/Components/Chart/BarChart.js | 8 +- .../src/Components/Chart/DoughnutChart.js | 4 +- frontend/src/Indexer/Stats/Stats.js | 119 ++++++++++++------ .../src/Store/Actions/indexerStatsActions.js | 4 +- frontend/src/Styles/Variables/colors.js | 7 +- .../IndexerStats/IndexerStatistics.cs | 14 +++ .../IndexerStatisticsRepository.cs | 25 +++- .../IndexerStats/IndexerStatisticsService.cs | 8 ++ .../Indexers/IndexerStatsModule.cs | 15 ++- .../Indexers/IndexerStatsResource.cs | 32 +---- 10 files changed, 157 insertions(+), 79 deletions(-) diff --git a/frontend/src/Components/Chart/BarChart.js b/frontend/src/Components/Chart/BarChart.js index b31de923b..c95e5f2a5 100644 --- a/frontend/src/Components/Chart/BarChart.js +++ b/frontend/src/Components/Chart/BarChart.js @@ -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: '' }; diff --git a/frontend/src/Components/Chart/DoughnutChart.js b/frontend/src/Components/Chart/DoughnutChart.js index d86fdfc40..722ee695a 100644 --- a/frontend/src/Components/Chart/DoughnutChart.js +++ b/frontend/src/Components/Chart/DoughnutChart.js @@ -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 }] } }); diff --git a/frontend/src/Indexer/Stats/Stats.js b/frontend/src/Indexer/Stats/Stats.js index f88acb50c..777078e82 100644 --- a/frontend/src/Indexer/Stats/Stats.js +++ b/frontend/src/Indexer/Stats/Stats.js @@ -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,60 +46,106 @@ 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 ( - { - isFetching && !isPopulated && - - } - - { - !isFetching && !!error && -
- {getErrorMessage(error, 'Failed to load indexer stats from API')} -
- } - - { - isLoaded && -
-
- -
-
- + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
+ {getErrorMessage(error, 'Failed to load indexer stats from API')}
-
- + } + + { + isLoaded && +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
-
- } + } +
); } Stats.propTypes = { - items: PropTypes.arrayOf(PropTypes.object).isRequired, + item: PropTypes.object.isRequired, isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, diff --git a/frontend/src/Store/Actions/indexerStatsActions.js b/frontend/src/Store/Actions/indexerStatsActions.js index 2e5e9e9ad..ef2a7ccb0 100644 --- a/frontend/src/Store/Actions/indexerStatsActions.js +++ b/frontend/src/Store/Actions/indexerStatsActions.js @@ -14,13 +14,13 @@ export const defaultState = { isFetching: false, isPopulated: false, error: null, - items: [], + item: {}, details: { isFetching: false, isPopulated: false, error: null, - items: [] + item: [] } }; diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js index fa87b81e9..d51f65316 100644 --- a/frontend/src/Styles/Variables/colors.js +++ b/frontend/src/Styles/Variables/colors.js @@ -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'] }; diff --git a/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs b/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs index f8376adb1..481123e2d 100644 --- a/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs +++ b/src/NzbDrone.Core/IndexerStats/IndexerStatistics.cs @@ -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; } + } } diff --git a/src/NzbDrone.Core/IndexerStats/IndexerStatisticsRepository.cs b/src/NzbDrone.Core/IndexerStats/IndexerStatisticsRepository.cs index 1508a39f1..7bb2b73db 100644 --- a/src/NzbDrone.Core/IndexerStats/IndexerStatisticsRepository.cs +++ b/src/NzbDrone.Core/IndexerStats/IndexerStatisticsRepository.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.IndexerStats public interface IIndexerStatisticsRepository { List IndexerStatistics(); + List UserAgentStatistics(); } public class IndexerStatisticsRepository : IIndexerStatisticsRepository @@ -26,13 +27,13 @@ namespace NzbDrone.Core.IndexerStats public List IndexerStatistics() { var time = DateTime.UtcNow; - return Query(Builder()); + return Query(IndexerBuilder()); } - public List IndexerStatistics(int indexerId) + public List UserAgentStatistics() { var time = DateTime.UtcNow; - return Query(Builder().Where(x => x.Id == indexerId)); + return UserAgentQuery(UserAgentBuilder()); } private List Query(SqlBuilder builder) @@ -45,7 +46,17 @@ namespace NzbDrone.Core.IndexerStats } } - private SqlBuilder Builder() => new SqlBuilder() + private List UserAgentQuery(SqlBuilder builder) + { + var sql = builder.AddTemplate(_selectTemplate).LogQuery(); + + using (var conn = _database.OpenConnection()) + { + return conn.Query(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((t, r) => t.IndexerId == r.Id) .GroupBy(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"); } } diff --git a/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs b/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs index 504d95d8e..bb7594a19 100644 --- a/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs +++ b/src/NzbDrone.Core/IndexerStats/IndexerStatisticsService.cs @@ -6,6 +6,7 @@ namespace NzbDrone.Core.IndexerStats public interface IIndexerStatisticsService { List IndexerStatistics(); + List UserAgentStatistics(); } public class IndexerStatisticsService : IIndexerStatisticsService @@ -23,5 +24,12 @@ namespace NzbDrone.Core.IndexerStats return seasonStatistics.ToList(); } + + public List UserAgentStatistics() + { + var seasonStatistics = _indexerStatisticsRepository.UserAgentStatistics(); + + return seasonStatistics.ToList(); + } } } diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerStatsModule.cs b/src/Prowlarr.Api.V1/Indexers/IndexerStatsModule.cs index 72e5fbf2d..f79dfa0f4 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerStatsModule.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerStatsModule.cs @@ -16,12 +16,21 @@ namespace Prowlarr.Api.V1.Indexers { _indexerStatisticsService = indexerStatisticsService; - GetResourceAll = GetAll; + Get("/", x => + { + return GetAll(); + }); } - private List GetAll() + private IndexerStatsResource GetAll() { - return _indexerStatisticsService.IndexerStatistics().ToResource(); + var indexerResource = new IndexerStatsResource + { + Indexers = _indexerStatisticsService.IndexerStatistics(), + UserAgents = _indexerStatisticsService.UserAgentStatistics(), + }; + + return indexerResource; } } } diff --git a/src/Prowlarr.Api.V1/Indexers/IndexerStatsResource.cs b/src/Prowlarr.Api.V1/Indexers/IndexerStatsResource.cs index 867f176b8..613108335 100644 --- a/src/Prowlarr.Api.V1/Indexers/IndexerStatsResource.cs +++ b/src/Prowlarr.Api.V1/Indexers/IndexerStatsResource.cs @@ -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 ToResource(this IEnumerable models) - { - return models.Select(ToResource).ToList(); - } + public List Indexers { get; set; } + public List UserAgents { get; set; } } }