From 2b7f64116309c7a33611334c1d08745c6c50d537 Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Sun, 10 May 2026 11:10:56 +0200 Subject: feat: language filters for subtitles and audio --- MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index fa82ea8663..e520ffd179 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -58,6 +58,8 @@ namespace MediaBrowser.Controller.Entities VideoTypes = []; Years = []; SkipDeserialization = false; + AudioLanguages = []; + SubtitleLanguages = []; } public InternalItemsQuery(User? user) @@ -385,6 +387,10 @@ namespace MediaBrowser.Controller.Entities public bool IncludeExtras { get; set; } + public IReadOnlyList AudioLanguages { get; set; } + + public IReadOnlyList SubtitleLanguages { get; set; } + public void SetUser(User user) { var maxRating = user.MaxParentalRatingScore; -- cgit v1.2.3 From 39049a726e1a88e8acf1d8cc5c217bc8d86be9ae Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Tue, 12 May 2026 01:47:07 +0200 Subject: move language filters from QueryFiltersLegacy to QueryFilters --- .../Library/LibraryManager.cs | 13 +++++++- Jellyfin.Api/Controllers/FilterController.cs | 37 +++++++++++++++++++++- Jellyfin.Api/Controllers/ItemsController.cs | 2 +- .../Item/BaseItemRepository.Querying.cs | 36 +-------------------- .../Item/BaseItemRepository.TranslateQuery.cs | 1 + .../Item/MediaStreamRepository.cs | 11 +++++++ MediaBrowser.Controller/Library/ILibraryManager.cs | 7 ++++ .../Persistence/IMediaStreamRepository.cs | 7 ++++ MediaBrowser.Model/Querying/QueryFilters.cs | 6 ++++ MediaBrowser.Model/Querying/QueryFiltersLegacy.cs | 6 ---- 10 files changed, 82 insertions(+), 44 deletions(-) (limited to 'MediaBrowser.Controller') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 11f1496086..15d51cf35f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -87,6 +87,7 @@ namespace Emby.Server.Implementations.Library private readonly IPathManager _pathManager; private readonly FastConcurrentLru _cache; private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule; + private readonly IMediaStreamRepository _mediaStreamRepository; /// /// The _root folder sync lock. @@ -129,6 +130,7 @@ namespace Emby.Server.Implementations.Library /// The people repository. /// The path manager. /// The .ignore rule handler. + /// The media stream repository. public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -151,7 +153,8 @@ namespace Emby.Server.Implementations.Library IDirectoryService directoryService, IPeopleRepository peopleRepository, IPathManager pathManager, - DotIgnoreIgnoreRule dotIgnoreIgnoreRule) + DotIgnoreIgnoreRule dotIgnoreIgnoreRule, + IMediaStreamRepository mediaStreamRepository) { _appHost = appHost; _logger = loggerFactory.CreateLogger(); @@ -181,6 +184,8 @@ namespace Emby.Server.Implementations.Library _configurationManager.ConfigurationUpdated += ConfigurationUpdated; + _mediaStreamRepository = mediaStreamRepository; + RecordConfigurationValues(_configurationManager.Configuration); } @@ -3800,5 +3805,11 @@ namespace Emby.Server.Implementations.Library SetTopParentOrAncestorIds(query); return _itemRepository.GetQueryFiltersLegacy(query); } + + /// + public IReadOnlyList GetMediaStreamLanguages(MediaStreamType mediaStreamType) + { + return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType); + } } } diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 2f53784db1..740423ef04 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -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; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - public FilterController(ILibraryManager libraryManager, IUserManager userManager) + /// Instance of the interface. + public FilterController(ILibraryManager libraryManager, IUserManager userManager, ILocalizationManager localization) { _libraryManager = libraryManager; _userManager = userManager; + _localization = localization; } /// @@ -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 != null ? $"{culture.DisplayName} ({language})" : 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 != null ? $"{culture.DisplayName} ({language})" : language, + Value = language + }; + }) + .OrderBy(l => l.Name) + .ToArray(); + } + return filters; } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index a813109c96..8eca2787b1 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -421,7 +421,7 @@ public class ItemsController : BaseJellyfinApiController } else { - // if we want to know if an item has no subtitles we don't need to check for subtitles of a specific language + // if we search for items without subtitles, we don't need to check for subtitles of a specific language query.SubtitleLanguages = []; } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs index 71b46b3cb5..dc16c3b1b3 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs @@ -535,46 +535,12 @@ public sealed partial class BaseItemRepository .OrderBy(g => g) .ToArray(); - // At the moment language filters are only available for video types (Movie and Series libraries). - // They are fetched directly from the MediaStreamInfos table and only filtered by StreamType. - // This is the fastest and most perfomant way to get the list of available languages, - // but the filter values can include language tags that are not linked to any item in the current library. - var subtitleLanguages = IncludesVideoTypes(filter) - ? context.MediaStreamInfos - .Where(s => s.StreamType == MediaStreamTypeEntity.Subtitle) - .Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined - .Distinct() - .OrderBy(l => l) - .ToArray() - : []; - - var audioLanguages = IncludesVideoTypes(filter) - ? context.MediaStreamInfos - .Where(s => s.StreamType == MediaStreamTypeEntity.Audio) - .Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined - .Distinct() - .OrderBy(l => l) - .ToArray() - : []; - return new QueryFiltersLegacy { Years = years, OfficialRatings = officialRatings, Tags = tags, - Genres = genres, - SubtitleLanguages = subtitleLanguages, - AudioLanguages = audioLanguages + Genres = genres }; } - - private bool IncludesVideoTypes(InternalItemsQuery filter) - { - return filter.IncludeItemTypes.Contains(BaseItemKind.Movie) - || filter.IncludeItemTypes.Contains(BaseItemKind.Video) - || filter.IncludeItemTypes.Contains(BaseItemKind.Series) - || filter.IncludeItemTypes.Contains(BaseItemKind.Season) - || filter.IncludeItemTypes.Contains(BaseItemKind.Episode) - || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer); - } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index b58e7fffe3..3d1aafd72e 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -1078,6 +1078,7 @@ public sealed partial class BaseItemRepository { // 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(); var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray(); Expression> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoTypeBs.Any(f => e.Data!.Contains(f)); diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index dd0446f49a..7fa33c8639 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -55,6 +55,17 @@ public class MediaStreamRepository : IMediaStreamRepository return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray(); } + /// + public IReadOnlyList 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) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index f5e3d7034e..f4c2196400 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -784,5 +784,12 @@ namespace MediaBrowser.Controller.Library /// The query filter. /// Aggregated filter values. QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query); + + /// + /// Gets a list of all language codes of the provided stream type. + /// + /// The stream type. + /// List of language codes. + IReadOnlyList GetMediaStreamLanguages(MediaStreamType mediaStreamType); } } diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs index 665129eafd..de04ff021d 100644 --- a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs +++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs @@ -21,6 +21,13 @@ public interface IMediaStreamRepository /// IEnumerable{MediaStream}. IReadOnlyList GetMediaStreams(MediaStreamQuery filter); + /// + /// Gets all language codes of the provided stream type. + /// + /// The type of the media stream. + /// IEnumerable{string}. + IReadOnlyList GetMediaStreamLanguages(MediaStreamType mediaStreamType); + /// /// Saves the media streams. /// diff --git a/MediaBrowser.Model/Querying/QueryFilters.cs b/MediaBrowser.Model/Querying/QueryFilters.cs index 73b27a7b06..b877af71c6 100644 --- a/MediaBrowser.Model/Querying/QueryFilters.cs +++ b/MediaBrowser.Model/Querying/QueryFilters.cs @@ -12,10 +12,16 @@ namespace MediaBrowser.Model.Querying { Tags = Array.Empty(); Genres = Array.Empty(); + AudioLanguages = Array.Empty(); + SubtitleLanguages = Array.Empty(); } public NameGuidPair[] Genres { get; set; } public string[] Tags { get; set; } + + public NameValuePair[] AudioLanguages { get; set; } + + public NameValuePair[] SubtitleLanguages { get; set; } } } diff --git a/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs b/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs index aa1ca85cad..fcb450ed30 100644 --- a/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs +++ b/MediaBrowser.Model/Querying/QueryFiltersLegacy.cs @@ -13,8 +13,6 @@ namespace MediaBrowser.Model.Querying Tags = Array.Empty(); OfficialRatings = Array.Empty(); Years = Array.Empty(); - AudioLanguages = Array.Empty(); - SubtitleLanguages = Array.Empty(); } public string[] Genres { get; set; } @@ -24,9 +22,5 @@ namespace MediaBrowser.Model.Querying public string[] OfficialRatings { get; set; } public int[] Years { get; set; } - - public string[] AudioLanguages { get; set; } - - public string[] SubtitleLanguages { get; set; } } } -- cgit v1.2.3