mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-16 05:36:52 +01:00
Merge pull request #9787 from TheMelmacian/feature/language_filters
New filters for audio and subtitle languages
This commit is contained in:
@@ -88,6 +88,7 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
|
||||
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||
private readonly IMediaStreamRepository _mediaStreamRepository;
|
||||
|
||||
/// <summary>
|
||||
/// The _root folder sync lock.
|
||||
@@ -130,6 +131,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="peopleRepository">The people repository.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||
/// <param name="mediaStreamRepository">The media stream repository.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -152,7 +154,8 @@ namespace Emby.Server.Implementations.Library
|
||||
IDirectoryService directoryService,
|
||||
IPeopleRepository peopleRepository,
|
||||
IPathManager pathManager,
|
||||
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
|
||||
DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
|
||||
IMediaStreamRepository mediaStreamRepository)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = loggerFactory.CreateLogger<LibraryManager>();
|
||||
@@ -182,6 +185,8 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
_mediaStreamRepository = mediaStreamRepository;
|
||||
|
||||
RecordConfigurationValues(_configurationManager.Configuration);
|
||||
}
|
||||
|
||||
@@ -3845,5 +3850,11 @@ namespace Emby.Server.Implementations.Library
|
||||
SetTopParentOrAncestorIds(query);
|
||||
return _itemRepository.GetQueryFiltersLegacy(query);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
|
||||
{
|
||||
return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -24,16 +26,19 @@ public class FilterController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FilterController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public FilterController(ILibraryManager libraryManager, IUserManager userManager, ILocalizationManager localization)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -183,6 +188,36 @@ public class FilterController : BaseJellyfinApiController
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
if (includeItemTypes.Contains(BaseItemKind.Movie) || includeItemTypes.Contains(BaseItemKind.Series))
|
||||
{
|
||||
filters.AudioLanguages = _libraryManager
|
||||
.GetMediaStreamLanguages(MediaStreamType.Audio)
|
||||
.Select(language =>
|
||||
{
|
||||
var culture = _localization.FindLanguageInfo(language);
|
||||
return new NameValuePair
|
||||
{
|
||||
Name = culture is null ? language : $"{culture.DisplayName} ({language})",
|
||||
Value = language
|
||||
};
|
||||
})
|
||||
.OrderBy(l => l.Name)
|
||||
.ToArray();
|
||||
filters.SubtitleLanguages = _libraryManager
|
||||
.GetMediaStreamLanguages(MediaStreamType.Subtitle)
|
||||
.Select(language =>
|
||||
{
|
||||
var culture = _localization.FindLanguageInfo(language);
|
||||
return new NameValuePair
|
||||
{
|
||||
Name = culture is null ? language : $"{culture.DisplayName} ({language})",
|
||||
Value = language
|
||||
};
|
||||
})
|
||||
.OrderBy(l => l.Name)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ public class ItemsController : BaseJellyfinApiController
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
|
||||
/// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitle language. This allows multiple, comma delimited values.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
@@ -247,6 +249,8 @@ public class ItemsController : BaseJellyfinApiController
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
@@ -399,6 +403,8 @@ public class ItemsController : BaseJellyfinApiController
|
||||
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
|
||||
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
|
||||
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
|
||||
AudioLanguages = audioLanguages,
|
||||
SubtitleLanguages = subtitleLanguages,
|
||||
};
|
||||
|
||||
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
|
||||
@@ -406,6 +412,33 @@ public class ItemsController : BaseJellyfinApiController
|
||||
query.CollapseBoxSetItems = false;
|
||||
}
|
||||
|
||||
if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
|
||||
{
|
||||
if (query.HasSubtitles.Value)
|
||||
{
|
||||
// if we check for specific subtitles we don't need a separate check for subtitle existence
|
||||
query.HasSubtitles = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we search for items without subtitles, we don't need to check for subtitles of a specific language
|
||||
query.SubtitleLanguages = [];
|
||||
}
|
||||
}
|
||||
|
||||
// for filter values that rely on media streams, we need to include alternative and linked versions
|
||||
if (query.HasSubtitles.HasValue
|
||||
|| query.SubtitleLanguages.Count > 0
|
||||
|| query.AudioLanguages.Count > 0
|
||||
|| query.Is3D.HasValue
|
||||
|| query.IsHD.HasValue
|
||||
|| query.Is4K.HasValue
|
||||
|| query.VideoTypes.Length > 0
|
||||
)
|
||||
{
|
||||
query.IncludeOwnedItems = true;
|
||||
}
|
||||
|
||||
query.ApplyFilters(filters);
|
||||
|
||||
// Filter by Series Status
|
||||
@@ -785,6 +818,8 @@ public class ItemsController : BaseJellyfinApiController
|
||||
nameLessThan,
|
||||
studioIds,
|
||||
genreIds,
|
||||
[],
|
||||
[],
|
||||
enableTotalRecordCount,
|
||||
enableImages).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -115,6 +115,8 @@ public class TrailersController : BaseJellyfinApiController
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
|
||||
/// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitale language. This allows multiple, comma delimited values.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
|
||||
@@ -203,6 +205,8 @@ public class TrailersController : BaseJellyfinApiController
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
@@ -294,6 +298,8 @@ public class TrailersController : BaseJellyfinApiController
|
||||
nameLessThan,
|
||||
studioIds,
|
||||
genreIds,
|
||||
audioLanguages,
|
||||
subtitleLanguages,
|
||||
enableTotalRecordCount,
|
||||
enableImages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -824,6 +824,26 @@ public sealed partial class BaseItemRepository
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.SubtitleLanguages.Count > 0)
|
||||
{
|
||||
var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, filter.SubtitleLanguages));
|
||||
baseQuery = baseQuery
|
||||
.Where(e =>
|
||||
(!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
|
||||
&& (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))))
|
||||
|| (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
|
||||
}
|
||||
|
||||
if (filter.AudioLanguages.Count > 0)
|
||||
{
|
||||
var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Audio, filter.AudioLanguages));
|
||||
baseQuery = baseQuery
|
||||
.Where(e =>
|
||||
(!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
|
||||
&& (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))))
|
||||
|| (e.IsFolder && foldersWithAudio.Contains(e.Id)));
|
||||
}
|
||||
|
||||
if (filter.HasChapterImages.HasValue)
|
||||
{
|
||||
var hasChapterImages = filter.HasChapterImages.Value;
|
||||
@@ -1068,8 +1088,12 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (filter.VideoTypes.Length > 0)
|
||||
{
|
||||
// Dvds and Blu-rays can either be stored in a folder structure or as an iso file
|
||||
// => to find all matches we need to check both: VideoType and IsoType
|
||||
// alternatively, we could provide specific IsoType filters
|
||||
var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
|
||||
Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f));
|
||||
var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
|
||||
Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoTypeBs.Any(f => e.Data!.Contains(f));
|
||||
baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,17 @@ public class MediaStreamRepository : IMediaStreamRepository
|
||||
return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
return context.MediaStreamInfos
|
||||
.Where(e => e.StreamType == (MediaStreamTypeEntity)mediaStreamType)
|
||||
.Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private string? GetPathToSave(string? path)
|
||||
{
|
||||
if (path is null)
|
||||
|
||||
@@ -58,6 +58,8 @@ namespace MediaBrowser.Controller.Entities
|
||||
VideoTypes = [];
|
||||
Years = [];
|
||||
SkipDeserialization = false;
|
||||
AudioLanguages = [];
|
||||
SubtitleLanguages = [];
|
||||
}
|
||||
|
||||
public InternalItemsQuery(User? user)
|
||||
@@ -387,6 +389,10 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public bool IncludeExtras { get; set; }
|
||||
|
||||
public IReadOnlyList<string> AudioLanguages { get; set; }
|
||||
|
||||
public IReadOnlyList<string> SubtitleLanguages { get; set; }
|
||||
|
||||
public void SetUser(User user)
|
||||
{
|
||||
var maxRating = user.MaxParentalRatingScore;
|
||||
|
||||
@@ -784,5 +784,12 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <param name="query">The query filter.</param>
|
||||
/// <returns>Aggregated filter values.</returns>
|
||||
QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all language codes of the provided stream type.
|
||||
/// </summary>
|
||||
/// <param name="mediaStreamType">The stream type.</param>
|
||||
/// <returns>List of language codes.</returns>
|
||||
IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ public interface IMediaStreamRepository
|
||||
/// <returns>IEnumerable{MediaStream}.</returns>
|
||||
IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all language codes of the provided stream type.
|
||||
/// </summary>
|
||||
/// <param name="mediaStreamType">The type of the media stream.</param>
|
||||
/// <returns>IEnumerable{string}.</returns>
|
||||
IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the media streams.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Model.Querying
|
||||
@@ -12,10 +13,16 @@ namespace MediaBrowser.Model.Querying
|
||||
{
|
||||
Tags = Array.Empty<string>();
|
||||
Genres = Array.Empty<NameGuidPair>();
|
||||
AudioLanguages = Array.Empty<NameValuePair>();
|
||||
SubtitleLanguages = Array.Empty<NameValuePair>();
|
||||
}
|
||||
|
||||
public NameGuidPair[] Genres { get; set; }
|
||||
public IReadOnlyList<NameGuidPair> Genres { get; set; }
|
||||
|
||||
public string[] Tags { get; set; }
|
||||
public IReadOnlyList<string> Tags { get; set; }
|
||||
|
||||
public IReadOnlyList<NameValuePair> AudioLanguages { get; set; }
|
||||
|
||||
public IReadOnlyList<NameValuePair> SubtitleLanguages { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,9 @@ public static class DescendantQueryHelper
|
||||
private static HashSet<Guid> GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
|
||||
{
|
||||
var query = context.MediaStreamInfos
|
||||
.Where(ms => ms.StreamType == criteria.StreamType && ms.Language == criteria.Language);
|
||||
.Where(ms => ms.StreamType == criteria.StreamType
|
||||
&& (criteria.Language.Contains(ms.Language)
|
||||
|| (criteria.Language.Contains("und") && string.IsNullOrEmpty(ms.Language)))); // und = undetermined
|
||||
|
||||
if (criteria.IsExternal.HasValue)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace Jellyfin.Database.Implementations.MatchCriteria;
|
||||
@@ -6,9 +9,23 @@ namespace Jellyfin.Database.Implementations.MatchCriteria;
|
||||
/// Matches folders containing descendants with a specific media stream type and language.
|
||||
/// </summary>
|
||||
/// <param name="StreamType">The type of media stream to match (Audio, Subtitle, etc.).</param>
|
||||
/// <param name="Language">The language to match.</param>
|
||||
/// <param name="Language">List of languages to match.</param>
|
||||
/// <param name="IsExternal">If not null, filters by internal (false) or external (true) streams. Only applicable to subtitles.</param>
|
||||
public sealed record HasMediaStreamType(
|
||||
MediaStreamTypeEntity StreamType,
|
||||
string Language,
|
||||
bool? IsExternal = null) : FolderMatchCriteria;
|
||||
IReadOnlyCollection<string> Language,
|
||||
bool? IsExternal = null) : FolderMatchCriteria
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HasMediaStreamType"/> class.
|
||||
/// </summary>
|
||||
/// <param name="StreamType">The type of media stream to match (Audio, Subtitle, etc.).</param>
|
||||
/// <param name="Language">The language to match.</param>
|
||||
/// <param name="IsExternal">If not null, filters by internal (false) or external (true) streams. Only applicable to subtitles.</param>
|
||||
public HasMediaStreamType(
|
||||
MediaStreamTypeEntity StreamType,
|
||||
string Language,
|
||||
bool? IsExternal = null) : this(StreamType, [Language], IsExternal)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user