New: Filter series by episode release types

This commit is contained in:
Bogdan 2026-04-21 19:45:38 +03:00 committed by Mark McDowall
parent 53eb9d8545
commit 39f043a96c
18 changed files with 130 additions and 0 deletions

View file

@ -22,6 +22,7 @@ import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValue from './QualityFilterBuilderRowValue';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
import ReleaseTypeFilterBuilderRowValue from './ReleaseTypeFilterBuilderRowValue';
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
@ -113,6 +114,9 @@ function getRowValueConnector<T>(
case filterBuilderValueTypes.MONITORED_STATUS:
return MonitoredStatusFilterBuilderRowValue;
case filterBuilderValueTypes.RELEASE_TYPES:
return ReleaseTypeFilterBuilderRowValue;
case filterBuilderValueTypes.SERIES:
return SeriesFilterBuilderRowValue;

View file

@ -0,0 +1,45 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue, {
FilterBuilderRowValueProps,
} from './FilterBuilderRowValue';
const releaseTypeList = [
{
id: 'unknown',
get name() {
return translate('Unknown');
},
},
{
id: 'singleEpisode',
get name() {
return translate('SingleEpisode');
},
},
{
id: 'multiEpisode',
get name() {
return translate('MultiEpisode');
},
},
{
id: 'seasonPack',
get name() {
return translate('SeasonPack');
},
},
];
type ReleaseTypeFilterBuilderRowValueProps<T> = Omit<
FilterBuilderRowValueProps<T, string, string>,
'tagList'
>;
function ReleaseTypeFilterBuilderRowValue<T>(
props: ReleaseTypeFilterBuilderRowValueProps<T>
) {
return <FilterBuilderRowValue tagList={releaseTypeList} {...props} />;
}
export default ReleaseTypeFilterBuilderRowValue;

View file

@ -10,6 +10,7 @@ export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const QUEUE_STATUS = 'queueStatus';
export const MONITORED_STATUS = 'monitoredStatus';
export const RELEASE_TYPES = 'releaseTypes';
export const SERIES = 'series';
export const SERIES_STATUS = 'seriesStatus';
export const SERIES_TYPES = 'seriesType';
@ -28,6 +29,7 @@ export type FilterBuildValueType =
| 'qualityProfile'
| 'queueStatus'
| 'monitoredStatus'
| 'releaseTypes'
| 'series'
| 'seriesStatus'
| 'seriesType'

View file

@ -75,6 +75,7 @@
}
.releaseGroups,
.releaseTypes,
.nextAiring,
.previousAiring,
.added,

View file

@ -27,6 +27,7 @@ interface CssExports {
'qualityProfileId': string;
'ratings': string;
'releaseGroups': string;
'releaseTypes': string;
'seasonCount': string;
'seasonFolder': string;
'seriesType': string;

View file

@ -13,6 +13,7 @@ import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
import getReleaseTypeName from 'Episode/getReleaseTypeName';
import { icons } from 'Helpers/Props';
import useCountryName from 'Internationalization/useCountryName';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
@ -149,6 +150,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
totalEpisodeCount = 0,
sizeOnDisk = 0,
releaseGroups = [],
releaseTypes = [],
episodeFileQualities = [],
} = statistics;
@ -448,6 +450,25 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
);
}
if (name === 'releaseTypes') {
const joinedReleaseTypes = releaseTypes
.map(getReleaseTypeName)
.join(', ');
const truncatedReleaseTypes =
releaseTypes.length > 3
? `${releaseTypes
.slice(0, 3)
.map(getReleaseTypeName)
.join(', ')}...`
: joinedReleaseTypes;
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<span title={joinedReleaseTypes}>{truncatedReleaseTypes}</span>
</VirtualTableRowCell>
);
}
if (name === 'episodeFileQualities') {
const joinedQualities = episodeFileQualities
.map((q) => q.name)

View file

@ -39,6 +39,7 @@
}
.releaseGroups,
.releaseTypes,
.nextAiring,
.previousAiring,
.added,

View file

@ -22,6 +22,7 @@ interface CssExports {
'qualityProfileId': string;
'ratings': string;
'releaseGroups': string;
'releaseTypes': string;
'seasonCount': string;
'seasonFolder': string;
'seriesType': string;

View file

@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import ReleaseType from 'InteractiveImport/ReleaseType';
import Language from 'Language/Language';
import Quality from 'Quality/Quality';
@ -35,6 +36,7 @@ export interface Statistics {
percentOfEpisodes: number;
previousAiring?: Date;
releaseGroups: string[];
releaseTypes: ReleaseType[];
episodeFileQualities: Quality[];
sizeOnDisk: number;
totalEpisodeCount: number;

View file

@ -221,6 +221,12 @@ const { useOptions, useOption, setOptions, setOption, setSort, getOptions } =
isSortable: false,
isVisible: false,
},
{
name: 'releaseTypes',
label: () => translate('ReleaseTypes'),
isSortable: false,
isVisible: false,
},
{
name: 'episodeFileQualities',
label: () => translate('EpisodeFileQualities'),

View file

@ -254,6 +254,12 @@ const FILTER_PREDICATES = {
return predicate(releaseGroups, filterValue);
},
releaseTypes: (item: Series, filterValue: string[], type: FilterType) => {
const releaseTypes = item.statistics?.releaseTypes ?? [];
const predicate = getFilterTypePredicate(type);
return predicate(releaseTypes, filterValue);
},
episodeFileQualities: (
item: Series,
filterValue: number[],
@ -533,6 +539,12 @@ export const FILTER_BUILDER: FilterBuilderProp<Series>[] = [
label: () => translate('ReleaseGroups'),
type: filterBuilderTypes.ARRAY,
},
{
name: 'releaseTypes',
label: () => translate('ReleaseTypes'),
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.RELEASE_TYPES,
},
{
name: 'episodeFileQualities',
label: () => translate('EpisodeFileQualities'),

View file

@ -1776,6 +1776,7 @@
"ReleaseSource": "Release Source",
"ReleaseTitle": "Release Title",
"ReleaseType": "Release Type",
"ReleaseTypes": "Release Types",
"Reload": "Reload",
"RemotePath": "Remote Path",
"RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {downloadClientName} places downloads in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.",

View file

@ -4,6 +4,7 @@
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.SeriesStats
@ -22,6 +23,7 @@ public class SeasonStatistics : ResultSet
public int MonitoredEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public string ReleaseGroupsString { get; set; }
public string ReleaseTypesString { get; set; }
public string EpisodeFileQualitiesString { get; set; }
public DateTime? NextAiring
@ -113,6 +115,25 @@ public List<string> ReleaseGroups
}
}
public List<ReleaseType> ReleaseTypes
{
get
{
if (ReleaseTypesString.IsNullOrWhiteSpace())
{
return [];
}
return ReleaseTypesString
.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(int.Parse)
.Distinct()
.Where(type => Enum.IsDefined(typeof(ReleaseType), type))
.Select(type => (ReleaseType)type)
.ToList();
}
}
public List<Quality> EpisodeFileQualities
{
get

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.SeriesStats
@ -17,6 +18,7 @@ public class SeriesStatistics : ResultSet
public int MonitoredEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public List<string> ReleaseGroups { get; set; }
public List<ReleaseType> ReleaseTypes { get; set; }
public List<Quality> EpisodeFileQualities { get; set; }
public List<SeasonStatistics> SeasonStatistics { get; set; }
}

View file

@ -49,6 +49,7 @@ private List<SeasonStatistics> MapResults(List<SeasonStatistics> episodesResult,
e.SizeOnDisk = file?.SizeOnDisk ?? 0;
e.ReleaseGroupsString = file?.ReleaseGroupsString;
e.ReleaseTypesString = file?.ReleaseTypesString;
e.EpisodeFileQualitiesString = file?.EpisodeFileQualitiesString;
});
@ -98,6 +99,7 @@ private SqlBuilder EpisodeFilesBuilder()
""SeasonNumber"",
SUM(COALESCE(""Size"", 0)) AS SizeOnDisk,
GROUP_CONCAT(""ReleaseGroup"", '|') AS ReleaseGroupsString,
GROUP_CONCAT(""ReleaseType"", '|') AS ReleaseTypesString,
GROUP_CONCAT(JSON_EXTRACT(""Quality"", '$.quality'), '|') AS EpisodeFileQualitiesString")
.GroupBy<EpisodeFile>(x => x.SeriesId)
.GroupBy<EpisodeFile>(x => x.SeasonNumber);
@ -108,6 +110,7 @@ private SqlBuilder EpisodeFilesBuilder()
""SeasonNumber"",
SUM(COALESCE(""Size"", 0)) AS SizeOnDisk,
string_agg(""ReleaseGroup"", '|') AS ReleaseGroupsString,
string_agg(""ReleaseType""::text, '|') AS ReleaseTypesString,
string_agg(""Quality""::json->>'quality', '|') AS EpisodeFileQualitiesString")
.GroupBy<EpisodeFile>(x => x.SeriesId)
.GroupBy<EpisodeFile>(x => x.SeasonNumber);

View file

@ -70,6 +70,7 @@ private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatis
MonitoredEpisodeCount = seasonStatistics.Sum(s => s.MonitoredEpisodeCount),
SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk),
ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList(),
ReleaseTypes = seasonStatistics.SelectMany(s => s.ReleaseTypes).Distinct().OrderBy(s => s).ToList(),
EpisodeFileQualities = SortQualities(seasonStatistics.SelectMany(s => s.EpisodeFileQualities).Distinct().ToList(), profile)
};

View file

@ -1,3 +1,4 @@
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.SeriesStats;
@ -13,6 +14,7 @@ public class SeasonStatisticsResource
public int MonitoredEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public List<string>? ReleaseGroups { get; set; }
public List<ReleaseType>? ReleaseTypes { get; set; }
public List<Quality>? EpisodeFileQualities { get; set; }
public decimal PercentOfEpisodes
@ -43,6 +45,7 @@ public static SeasonStatisticsResource ToResource(this SeasonStatistics model)
MonitoredEpisodeCount = model.MonitoredEpisodeCount,
SizeOnDisk = model.SizeOnDisk,
ReleaseGroups = model.ReleaseGroups,
ReleaseTypes = model.ReleaseTypes,
EpisodeFileQualities = model.EpisodeFileQualities
};
}

View file

@ -1,3 +1,4 @@
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.SeriesStats;
@ -12,6 +13,7 @@ public class SeriesStatisticsResource
public int MonitoredEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public List<string>? ReleaseGroups { get; set; }
public List<ReleaseType>? ReleaseTypes { get; set; }
public List<Quality>? EpisodeFileQualities { get; set; }
public decimal PercentOfEpisodes
@ -41,6 +43,7 @@ public static SeriesStatisticsResource ToResource(this SeriesStatistics model, L
MonitoredEpisodeCount = model.MonitoredEpisodeCount,
SizeOnDisk = model.SizeOnDisk,
ReleaseGroups = model.ReleaseGroups,
ReleaseTypes = model.ReleaseTypes,
EpisodeFileQualities = model.EpisodeFileQualities
};
}