diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.css b/frontend/src/Series/Index/Table/SeriesIndexRow.css
index e18a9ed74..4b4706a01 100644
--- a/frontend/src/Series/Index/Table/SeriesIndexRow.css
+++ b/frontend/src/Series/Index/Table/SeriesIndexRow.css
@@ -84,6 +84,12 @@
flex: 0 0 180px;
}
+.episodeFileQualities {
+ composes: cell;
+
+ flex: 0 0 220px;
+}
+
.seasonCount,
.certification {
composes: cell;
diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts b/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts
index 635b5a616..93da75bbf 100644
--- a/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts
+++ b/frontend/src/Series/Index/Table/SeriesIndexRow.css.d.ts
@@ -11,6 +11,7 @@ interface CssExports {
'certification': string;
'checkInput': string;
'episodeCount': string;
+ 'episodeFileQualities': string;
'episodeProgress': string;
'genres': string;
'latestSeason': string;
diff --git a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx
index b8d83211b..6a997af97 100644
--- a/frontend/src/Series/Index/Table/SeriesIndexRow.tsx
+++ b/frontend/src/Series/Index/Table/SeriesIndexRow.tsx
@@ -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 (
+
+ {truncatedQualities}
+
+ );
+ }
+
if (name === 'tags') {
return (
diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css
index a37cb5081..0fac2d75f 100644
--- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css
+++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css
@@ -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';
diff --git a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts
index 1b566399d..63fd01bbe 100644
--- a/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts
+++ b/frontend/src/Series/Index/Table/SeriesIndexTableHeader.css.d.ts
@@ -8,6 +8,7 @@ interface CssExports {
'bannerGrow': string;
'certification': string;
'episodeCount': string;
+ 'episodeFileQualities': string;
'episodeProgress': string;
'genres': string;
'latestSeason': string;
diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts
index 39d1ea1ff..47917ed2d 100644
--- a/frontend/src/Series/Series.ts
+++ b/frontend/src/Series/Series.ts
@@ -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;
diff --git a/frontend/src/Series/seriesOptionsStore.ts b/frontend/src/Series/seriesOptionsStore.ts
index b71c1cbb9..51a37e3e0 100644
--- a/frontend/src/Series/seriesOptionsStore.ts
+++ b/frontend/src/Series/seriesOptionsStore.ts
@@ -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'),
diff --git a/frontend/src/Series/useSeries.ts b/frontend/src/Series/useSeries.ts
index b41428f2f..6e62301bb 100644
--- a/frontend/src/Series/useSeries.ts
+++ b/frontend/src/Series/useSeries.ts
@@ -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[] = [
label: () => translate('ReleaseGroups'),
type: filterBuilderTypes.ARRAY,
},
+ {
+ name: 'episodeFileQualities',
+ label: () => translate('EpisodeFileQualities'),
+ type: filterBuilderTypes.ARRAY,
+ valueType: filterBuilderValueTypes.QUALITY,
+ },
{
name: 'ratings',
label: () => translate('Rating'),
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 0c58bee40..c6d64ac6d 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -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",
diff --git a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs
index cf615a731..bcca6a4d8 100644
--- a/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs
+++ b/src/NzbDrone.Core/SeriesStats/SeasonStatistics.cs
@@ -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 ReleaseGroups
return releasegroups;
}
}
+
+ public List EpisodeFileQualities
+ {
+ get
+ {
+ if (EpisodeFileQualitiesString.IsNullOrWhiteSpace())
+ {
+ return new List();
+ }
+
+ return EpisodeFileQualitiesString
+ .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(int.Parse)
+ .Distinct()
+ .Select(Quality.FindById)
+ .ToList();
+ }
+ }
}
}
diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs
index e3f10d24f..41f637ee6 100644
--- a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs
+++ b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs
@@ -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 ReleaseGroups { get; set; }
+ public List EpisodeFileQualities { get; set; }
public List SeasonStatistics { get; set; }
}
}
diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs
index 68b4f91ff..c7098117d 100644
--- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs
+++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs
@@ -49,6 +49,7 @@ private List MapResults(List 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(x => x.SeriesId)
.GroupBy(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(x => x.SeriesId)
.GroupBy(x => x.SeasonNumber);
}
diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs
index 6e7b75d85..62d9ba9fa 100644
--- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs
+++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsService.cs
@@ -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(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()
{
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)
+ private SeriesStatistics MapSeriesStatistics(List seasonStatistics, QualityProfile profile)
{
var seriesStatistics = new SeriesStatistics
{
@@ -48,7 +69,8 @@ private SeriesStatistics MapSeriesStatistics(List 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 seasonStatis
return seriesStatistics;
}
+
+ private static List SortQualities(List qualities, QualityProfile profile)
+ {
+ if (profile == null)
+ {
+ return qualities;
+ }
+
+ return qualities.OrderBy(q => profile.GetIndex(q.Id).Index).ToList();
+ }
}
}
diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs
index f83545cc8..a4f6e6365 100644
--- a/src/NzbDrone.Core/Tv/SeriesRepository.cs
+++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs
@@ -19,6 +19,7 @@ public interface ISeriesRepository : IBasicRepository
List AllSeriesTvdbIds();
Dictionary AllSeriesPaths();
Dictionary> AllSeriesTags();
+ Dictionary AllSeriesQualityProfiles();
}
public class SeriesRepository : BasicRepository, ISeriesRepository
@@ -111,6 +112,15 @@ public Dictionary> AllSeriesTags()
}
}
+ public Dictionary AllSeriesQualityProfiles()
+ {
+ using (var conn = _database.OpenConnection())
+ {
+ var strSql = "SELECT \"Id\" AS Key, \"QualityProfileId\" AS Value FROM \"Series\"";
+ return conn.Query>(strSql).ToDictionary(x => x.Key, x => x.Value);
+ }
+ }
+
private Series ReturnSingleSeriesOrThrow(List series)
{
if (series.Count == 0)
diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs
index 29cb6fac5..2430cb759 100644
--- a/src/NzbDrone.Core/Tv/SeriesService.cs
+++ b/src/NzbDrone.Core/Tv/SeriesService.cs
@@ -28,6 +28,7 @@ public interface ISeriesService
Dictionary GetAllSeriesPaths();
Dictionary> GetAllSeriesTags();
List AllForTag(int tagId);
+ Dictionary GetAllSeriesQualityProfiles();
Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true);
List UpdateSeries(List series, bool useExistingRelativeFolder);
bool SeriesPathExists(string folder);
@@ -186,6 +187,11 @@ public Dictionary> GetAllSeriesTags()
return _seriesRepository.AllSeriesTags();
}
+ public Dictionary GetAllSeriesQualityProfiles()
+ {
+ return _seriesRepository.AllSeriesQualityProfiles();
+ }
+
public List AllForTag(int tagId)
{
return GetAllSeries().Where(s => s.Tags.Contains(tagId))
diff --git a/src/Sonarr.Api.V3/Series/SeriesController.cs b/src/Sonarr.Api.V3/Series/SeriesController.cs
index 341373167..8f155b9ff 100644
--- a/src/Sonarr.Api.V3/Series/SeriesController.cs
+++ b/src/Sonarr.Api.V3/Series/SeriesController.cs
@@ -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 resources, Dictionary seriesStatistics)
diff --git a/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs b/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs
index c338cfbcb..d7cb364f4 100644
--- a/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs
+++ b/src/Sonarr.Api.V5/Series/SeasonStatisticsResource.cs
@@ -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? ReleaseGroups { get; set; }
+ public List? 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
};
}
}
diff --git a/src/Sonarr.Api.V5/Series/SeriesController.cs b/src/Sonarr.Api.V5/Series/SeriesController.cs
index 992d4c99d..cdadfe76a 100644
--- a/src/Sonarr.Api.V5/Series/SeriesController.cs
+++ b/src/Sonarr.Api.V5/Series/SeriesController.cs
@@ -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 resources, Dictionary seriesStatistics)
diff --git a/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs b/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs
index d04bed7a6..b9f58ff71 100644
--- a/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs
+++ b/src/Sonarr.Api.V5/Series/SeriesStatisticsResource.cs
@@ -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? ReleaseGroups { get; set; }
+ public List? 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
};
}
}