mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-07 12:30:56 +02:00
parent
e8e31def9b
commit
5c5b53d341
19 changed files with 153 additions and 13 deletions
|
|
@ -84,6 +84,12 @@
|
|||
flex: 0 0 180px;
|
||||
}
|
||||
|
||||
.episodeFileQualities {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 220px;
|
||||
}
|
||||
|
||||
.seasonCount,
|
||||
.certification {
|
||||
composes: cell;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface CssExports {
|
|||
'certification': string;
|
||||
'checkInput': string;
|
||||
'episodeCount': string;
|
||||
'episodeFileQualities': string;
|
||||
'episodeProgress': string;
|
||||
'genres': string;
|
||||
'latestSeason': string;
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
totalEpisodeCount = 0,
|
||||
sizeOnDisk = 0,
|
||||
releaseGroups = [],
|
||||
episodeFileQualities = [],
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
|
|
@ -447,6 +448,25 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (name === 'episodeFileQualities') {
|
||||
const joinedQualities = episodeFileQualities
|
||||
.map((q) => q.name)
|
||||
.join(', ');
|
||||
const truncatedQualities =
|
||||
episodeFileQualities.length > 3
|
||||
? `${episodeFileQualities
|
||||
.slice(0, 3)
|
||||
.map((q) => q.name)
|
||||
.join(', ')}...`
|
||||
: joinedQualities;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<span title={joinedQualities}>{truncatedQualities}</span>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@
|
|||
flex: 0 0 180px;
|
||||
}
|
||||
|
||||
.episodeFileQualities {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 220px;
|
||||
}
|
||||
|
||||
.seasonCount,
|
||||
.certification {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface CssExports {
|
|||
'bannerGrow': string;
|
||||
'certification': string;
|
||||
'episodeCount': string;
|
||||
'episodeFileQualities': string;
|
||||
'episodeProgress': string;
|
||||
'genres': string;
|
||||
'latestSeason': string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import Quality from 'Quality/Quality';
|
||||
|
||||
export type SeriesType = 'anime' | 'daily' | 'standard';
|
||||
export type SeriesMonitor =
|
||||
|
|
@ -34,6 +35,7 @@ export interface Statistics {
|
|||
percentOfEpisodes: number;
|
||||
previousAiring?: Date;
|
||||
releaseGroups: string[];
|
||||
episodeFileQualities: Quality[];
|
||||
sizeOnDisk: number;
|
||||
totalEpisodeCount: number;
|
||||
monitoredEpisodeCount: number;
|
||||
|
|
|
|||
|
|
@ -221,6 +221,12 @@ const { useOptions, useOption, setOptions, setOption, setSort, getOptions } =
|
|||
isSortable: false,
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'episodeFileQualities',
|
||||
label: () => translate('EpisodeFileQualities'),
|
||||
isSortable: false,
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: () => translate('Tags'),
|
||||
|
|
|
|||
|
|
@ -254,6 +254,18 @@ const FILTER_PREDICATES = {
|
|||
return predicate(releaseGroups, filterValue);
|
||||
},
|
||||
|
||||
episodeFileQualities: (
|
||||
item: Series,
|
||||
filterValue: number[],
|
||||
type: FilterType
|
||||
) => {
|
||||
const episodeFileQualities = (
|
||||
item.statistics?.episodeFileQualities ?? []
|
||||
).map((q) => q.id);
|
||||
const predicate = getFilterTypePredicate(type);
|
||||
return predicate(episodeFileQualities, filterValue);
|
||||
},
|
||||
|
||||
seasonCount: (item: Series, filterValue: number, type: FilterType) => {
|
||||
const predicate = getFilterTypePredicate(type);
|
||||
const seasonCount = item.statistics?.seasonCount ?? 0;
|
||||
|
|
@ -521,6 +533,12 @@ export const FILTER_BUILDER: FilterBuilderProp<Series>[] = [
|
|||
label: () => translate('ReleaseGroups'),
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
},
|
||||
{
|
||||
name: 'episodeFileQualities',
|
||||
label: () => translate('EpisodeFileQualities'),
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
valueType: filterBuilderValueTypes.QUALITY,
|
||||
},
|
||||
{
|
||||
name: 'ratings',
|
||||
label: () => translate('Rating'),
|
||||
|
|
|
|||
|
|
@ -668,6 +668,7 @@
|
|||
"EpisodeFileDeleted": "Episode File Deleted",
|
||||
"EpisodeFileDeletedTooltip": "Episode file deleted",
|
||||
"EpisodeFileMissingTooltip": "Episode file missing",
|
||||
"EpisodeFileQualities": "Episode File Qualities",
|
||||
"EpisodeFileRenamed": "Episode File Renamed",
|
||||
"EpisodeFileRenamedTooltip": "Episode file renamed",
|
||||
"EpisodeFilesLoadError": "Unable to load episode files",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
|
|
@ -21,6 +22,7 @@ public class SeasonStatistics : ResultSet
|
|||
public int MonitoredEpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
public string ReleaseGroupsString { get; set; }
|
||||
public string EpisodeFileQualitiesString { get; set; }
|
||||
|
||||
public DateTime? NextAiring
|
||||
{
|
||||
|
|
@ -110,5 +112,23 @@ public List<string> ReleaseGroups
|
|||
return releasegroups;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Quality> EpisodeFileQualities
|
||||
{
|
||||
get
|
||||
{
|
||||
if (EpisodeFileQualitiesString.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new List<Quality>();
|
||||
}
|
||||
|
||||
return EpisodeFileQualitiesString
|
||||
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(int.Parse)
|
||||
.Distinct()
|
||||
.Select(Quality.FindById)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
|
|
@ -16,6 +17,7 @@ public class SeriesStatistics : ResultSet
|
|||
public int MonitoredEpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
public List<string> ReleaseGroups { get; set; }
|
||||
public List<Quality> EpisodeFileQualities { get; set; }
|
||||
public List<SeasonStatistics> SeasonStatistics { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ private List<SeasonStatistics> MapResults(List<SeasonStatistics> episodesResult,
|
|||
|
||||
e.SizeOnDisk = file?.SizeOnDisk ?? 0;
|
||||
e.ReleaseGroupsString = file?.ReleaseGroupsString;
|
||||
e.EpisodeFileQualitiesString = file?.EpisodeFileQualitiesString;
|
||||
});
|
||||
|
||||
return episodesResult;
|
||||
|
|
@ -96,7 +97,8 @@ private SqlBuilder EpisodeFilesBuilder()
|
|||
.Select(@"""SeriesId"",
|
||||
""SeasonNumber"",
|
||||
SUM(COALESCE(""Size"", 0)) AS SizeOnDisk,
|
||||
GROUP_CONCAT(""ReleaseGroup"", '|') AS ReleaseGroupsString")
|
||||
GROUP_CONCAT(""ReleaseGroup"", '|') AS ReleaseGroupsString,
|
||||
GROUP_CONCAT(JSON_EXTRACT(""Quality"", '$.quality'), '|') AS EpisodeFileQualitiesString")
|
||||
.GroupBy<EpisodeFile>(x => x.SeriesId)
|
||||
.GroupBy<EpisodeFile>(x => x.SeasonNumber);
|
||||
}
|
||||
|
|
@ -105,7 +107,8 @@ private SqlBuilder EpisodeFilesBuilder()
|
|||
.Select(@"""SeriesId"",
|
||||
""SeasonNumber"",
|
||||
SUM(COALESCE(""Size"", 0)) AS SizeOnDisk,
|
||||
string_agg(""ReleaseGroup"", '|') AS ReleaseGroupsString")
|
||||
string_agg(""ReleaseGroup"", '|') AS ReleaseGroupsString,
|
||||
string_agg(""Quality""::json->>'quality', '|') AS EpisodeFileQualitiesString")
|
||||
.GroupBy<EpisodeFile>(x => x.SeriesId)
|
||||
.GroupBy<EpisodeFile>(x => x.SeasonNumber);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,50 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.SeriesStats
|
||||
{
|
||||
public interface ISeriesStatisticsService
|
||||
{
|
||||
List<SeriesStatistics> SeriesStatistics();
|
||||
SeriesStatistics SeriesStatistics(int seriesId);
|
||||
SeriesStatistics SeriesStatistics(int seriesId, int qualityProfileId);
|
||||
}
|
||||
|
||||
public class SeriesStatisticsService : ISeriesStatisticsService
|
||||
{
|
||||
private readonly ISeriesStatisticsRepository _seriesStatisticsRepository;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IQualityProfileService _qualityProfileService;
|
||||
|
||||
public SeriesStatisticsService(ISeriesStatisticsRepository seriesStatisticsRepository)
|
||||
public SeriesStatisticsService(ISeriesStatisticsRepository seriesStatisticsRepository,
|
||||
ISeriesService seriesService,
|
||||
IQualityProfileService qualityProfileService)
|
||||
{
|
||||
_seriesStatisticsRepository = seriesStatisticsRepository;
|
||||
_seriesService = seriesService;
|
||||
_qualityProfileService = qualityProfileService;
|
||||
}
|
||||
|
||||
public List<SeriesStatistics> SeriesStatistics()
|
||||
{
|
||||
var seasonStatistics = _seriesStatisticsRepository.SeriesStatistics();
|
||||
var seriesProfiles = _seriesService.GetAllSeriesQualityProfiles();
|
||||
var profiles = _qualityProfileService.All().ToDictionary(p => p.Id);
|
||||
|
||||
return seasonStatistics.GroupBy(s => s.SeriesId).Select(s => MapSeriesStatistics(s.ToList())).ToList();
|
||||
return seasonStatistics
|
||||
.GroupBy(s => s.SeriesId)
|
||||
.Select(s =>
|
||||
{
|
||||
var profileId = seriesProfiles.GetValueOrDefault(s.Key);
|
||||
profiles.TryGetValue(profileId, out var profile);
|
||||
return MapSeriesStatistics(s.ToList(), profile);
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public SeriesStatistics SeriesStatistics(int seriesId)
|
||||
public SeriesStatistics SeriesStatistics(int seriesId, int qualityProfileId)
|
||||
{
|
||||
var stats = _seriesStatisticsRepository.SeriesStatistics(seriesId);
|
||||
|
||||
|
|
@ -34,10 +53,12 @@ public SeriesStatistics SeriesStatistics(int seriesId)
|
|||
return new SeriesStatistics();
|
||||
}
|
||||
|
||||
return MapSeriesStatistics(stats);
|
||||
var profile = _qualityProfileService.Get(qualityProfileId);
|
||||
|
||||
return MapSeriesStatistics(stats, profile);
|
||||
}
|
||||
|
||||
private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics)
|
||||
private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics, QualityProfile profile)
|
||||
{
|
||||
var seriesStatistics = new SeriesStatistics
|
||||
{
|
||||
|
|
@ -48,7 +69,8 @@ private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatis
|
|||
TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount),
|
||||
MonitoredEpisodeCount = seasonStatistics.Sum(s => s.MonitoredEpisodeCount),
|
||||
SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk),
|
||||
ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList()
|
||||
ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList(),
|
||||
EpisodeFileQualities = SortQualities(seasonStatistics.SelectMany(s => s.EpisodeFileQualities).Distinct().ToList(), profile)
|
||||
};
|
||||
|
||||
var nextAiring = seasonStatistics.Where(s => s.NextAiring != null).MinBy(s => s.NextAiring);
|
||||
|
|
@ -61,5 +83,15 @@ private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatis
|
|||
|
||||
return seriesStatistics;
|
||||
}
|
||||
|
||||
private static List<Quality> SortQualities(List<Quality> qualities, QualityProfile profile)
|
||||
{
|
||||
if (profile == null)
|
||||
{
|
||||
return qualities;
|
||||
}
|
||||
|
||||
return qualities.OrderBy(q => profile.GetIndex(q.Id).Index).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public interface ISeriesRepository : IBasicRepository<Series>
|
|||
List<int> AllSeriesTvdbIds();
|
||||
Dictionary<int, string> AllSeriesPaths();
|
||||
Dictionary<int, List<int>> AllSeriesTags();
|
||||
Dictionary<int, int> AllSeriesQualityProfiles();
|
||||
}
|
||||
|
||||
public class SeriesRepository : BasicRepository<Series>, ISeriesRepository
|
||||
|
|
@ -111,6 +112,15 @@ public Dictionary<int, List<int>> AllSeriesTags()
|
|||
}
|
||||
}
|
||||
|
||||
public Dictionary<int, int> AllSeriesQualityProfiles()
|
||||
{
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
var strSql = "SELECT \"Id\" AS Key, \"QualityProfileId\" AS Value FROM \"Series\"";
|
||||
return conn.Query<KeyValuePair<int, int>>(strSql).ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private Series ReturnSingleSeriesOrThrow(List<Series> series)
|
||||
{
|
||||
if (series.Count == 0)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public interface ISeriesService
|
|||
Dictionary<int, string> GetAllSeriesPaths();
|
||||
Dictionary<int, List<int>> GetAllSeriesTags();
|
||||
List<Series> AllForTag(int tagId);
|
||||
Dictionary<int, int> GetAllSeriesQualityProfiles();
|
||||
Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true);
|
||||
List<Series> UpdateSeries(List<Series> series, bool useExistingRelativeFolder);
|
||||
bool SeriesPathExists(string folder);
|
||||
|
|
@ -186,6 +187,11 @@ public Dictionary<int, List<int>> GetAllSeriesTags()
|
|||
return _seriesRepository.AllSeriesTags();
|
||||
}
|
||||
|
||||
public Dictionary<int, int> GetAllSeriesQualityProfiles()
|
||||
{
|
||||
return _seriesRepository.AllSeriesQualityProfiles();
|
||||
}
|
||||
|
||||
public List<Series> AllForTag(int tagId)
|
||||
{
|
||||
return GetAllSeries().Where(s => s.Tags.Contains(tagId))
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ private void MapCoversToLocal(params SeriesResource[] series)
|
|||
|
||||
private void FetchAndLinkSeriesStatistics(SeriesResource resource)
|
||||
{
|
||||
LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id));
|
||||
LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id, resource.QualityProfileId));
|
||||
}
|
||||
|
||||
private void LinkSeriesStatistics(List<SeriesResource> resources, Dictionary<int, SeriesStatistics> seriesStatistics)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.SeriesStats;
|
||||
|
||||
namespace Sonarr.Api.V5.Series;
|
||||
|
|
@ -12,6 +13,7 @@ public class SeasonStatisticsResource
|
|||
public int MonitoredEpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
public List<string>? ReleaseGroups { get; set; }
|
||||
public List<Quality>? EpisodeFileQualities { get; set; }
|
||||
|
||||
public decimal PercentOfEpisodes
|
||||
{
|
||||
|
|
@ -40,7 +42,8 @@ public static SeasonStatisticsResource ToResource(this SeasonStatistics model)
|
|||
TotalEpisodeCount = model.TotalEpisodeCount,
|
||||
MonitoredEpisodeCount = model.MonitoredEpisodeCount,
|
||||
SizeOnDisk = model.SizeOnDisk,
|
||||
ReleaseGroups = model.ReleaseGroups
|
||||
ReleaseGroups = model.ReleaseGroups,
|
||||
EpisodeFileQualities = model.EpisodeFileQualities
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ private void MapCoversToLocal(params SeriesResource[] series)
|
|||
|
||||
private void FetchAndLinkSeriesStatistics(SeriesResource resource)
|
||||
{
|
||||
LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id));
|
||||
LinkSeriesStatistics(resource, _seriesStatisticsService.SeriesStatistics(resource.Id, resource.QualityProfileId));
|
||||
}
|
||||
|
||||
private void LinkSeriesStatistics(List<SeriesResource> resources, Dictionary<int, SeriesStatistics> seriesStatistics)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.SeriesStats;
|
||||
|
||||
namespace Sonarr.Api.V5.Series;
|
||||
|
|
@ -11,6 +12,7 @@ public class SeriesStatisticsResource
|
|||
public int MonitoredEpisodeCount { get; set; }
|
||||
public long SizeOnDisk { get; set; }
|
||||
public List<string>? ReleaseGroups { get; set; }
|
||||
public List<Quality>? EpisodeFileQualities { get; set; }
|
||||
|
||||
public decimal PercentOfEpisodes
|
||||
{
|
||||
|
|
@ -38,7 +40,8 @@ public static SeriesStatisticsResource ToResource(this SeriesStatistics model, L
|
|||
TotalEpisodeCount = model.TotalEpisodeCount,
|
||||
MonitoredEpisodeCount = model.MonitoredEpisodeCount,
|
||||
SizeOnDisk = model.SizeOnDisk,
|
||||
ReleaseGroups = model.ReleaseGroups
|
||||
ReleaseGroups = model.ReleaseGroups,
|
||||
EpisodeFileQualities = model.EpisodeFileQualities
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue