diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e7e506..78e00dca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix adding pad filler to content that is less than one minute in duration - Generate unique identifier for virtual HDHomeRun tuner by @raknam - This allows a single Plex server to connect to multiple ETV instances +- Include *all* language codes from media library in preferred audio and subtitle language options + - Language codes where an English name cannot be found will be at the bottom of the list ### Changed - Remove some unnecessary API calls related to media server scanning and paging diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs index 0ad8c330..49498308 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs @@ -39,7 +39,6 @@ public class CreateChannelHandler( private static async Task> Validate(TvContext dbContext, CreateChannel request) => (ValidateName(request), await ValidateNumber(dbContext, request), await FFmpegProfileMustExist(dbContext, request), - ValidatePreferredAudioLanguage(request), ValidatePreferredSubtitleLanguage(request), await WatermarkMustExist(dbContext, request), await FillerPresetMustExist(dbContext, request)) @@ -48,7 +47,6 @@ public class CreateChannelHandler( name, number, ffmpegProfileId, - preferredAudioLanguageCode, preferredSubtitleLanguageCode, watermarkId, fillerPresetId) => @@ -76,7 +74,7 @@ public class CreateChannelHandler( ProgressMode = request.ProgressMode, StreamingMode = request.StreamingMode, Artwork = artwork, - PreferredAudioLanguageCode = preferredAudioLanguageCode, + PreferredAudioLanguageCode = request.PreferredAudioLanguageCode, PreferredAudioTitle = request.PreferredAudioTitle, PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode, SubtitleMode = request.SubtitleMode, @@ -101,13 +99,6 @@ public class CreateChannelHandler( createChannel.NotEmpty(c => c.Name) .Bind(_ => createChannel.NotLongerThan(50)(c => c.Name)); - private static Validation ValidatePreferredAudioLanguage(CreateChannel createChannel) => - Optional(createChannel.PreferredAudioLanguageCode ?? string.Empty) - .Filter( - lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any( - ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase))) - .ToValidation("Preferred audio language code is invalid"); - private static Validation ValidatePreferredSubtitleLanguage(CreateChannel createChannel) => Optional(createChannel.PreferredSubtitleLanguageCode ?? string.Empty) .Filter( diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs index e7005b5e..b8996325 100644 --- a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs @@ -93,9 +93,8 @@ public class UpdateChannelHandler( private static async Task> Validate(TvContext dbContext, UpdateChannel request) => (await ChannelMustExist(dbContext, request), ValidateName(request), - await ValidateNumber(dbContext, request), - ValidatePreferredAudioLanguage(request)) - .Apply((channelToUpdate, _, _, _) => channelToUpdate); + await ValidateNumber(dbContext, request)) + .Apply((channelToUpdate, _, _) => channelToUpdate); private static Task> ChannelMustExist( TvContext dbContext, @@ -130,11 +129,4 @@ public class UpdateChannelHandler( return BaseError.New("Channel number must be unique"); } - - private static Validation ValidatePreferredAudioLanguage(UpdateChannel updateChannel) => - Optional(updateChannel.PreferredAudioLanguageCode ?? string.Empty) - .Filter( - lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any( - ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase))) - .ToValidation("Preferred audio language code is invalid"); } diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs index df624a7c..395a55aa 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs @@ -1,20 +1,18 @@ using System.Globalization; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Metadata; namespace ErsatzTV.Application.MediaItems; -public class GetAllLanguageCodesHandler : IRequestHandler> +public class GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) + : IRequestHandler> { - private readonly IMediaItemRepository _mediaItemRepository; - - public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) => - _mediaItemRepository = mediaItemRepository; - public async Task> Handle( GetAllLanguageCodes request, CancellationToken cancellationToken) { - List cultures = await _mediaItemRepository.GetAllLanguageCodeCultures(); - return cultures.Map(c => new LanguageCodeViewModel(c.ThreeLetterISOLanguageName, c.EnglishName)).ToList(); + List languageCodes = await mediaItemRepository.GetAllLanguageCodesAndNames(); + return languageCodes.Map(c => new LanguageCodeViewModel(c.Code, c.Name)).ToList(); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs index 7bc50780..199dc177 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaItemRepository.cs @@ -1,13 +1,14 @@ using System.Collections.Immutable; using System.Globalization; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Metadata; namespace ErsatzTV.Core.Interfaces.Repositories; public interface IMediaItemRepository { Task> GetAllKnownCultures(); - Task> GetAllLanguageCodeCultures(); + Task> GetAllLanguageCodesAndNames(); Task> FlagFileNotFound(LibraryPath libraryPath, string path); Task FlagNormal(MediaItem mediaItem); Task> DeleteItems(List mediaItemIds); diff --git a/ErsatzTV.Core/Metadata/LanguageCodeAndName.cs b/ErsatzTV.Core/Metadata/LanguageCodeAndName.cs new file mode 100644 index 00000000..beec3e3f --- /dev/null +++ b/ErsatzTV.Core/Metadata/LanguageCodeAndName.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Metadata; + +public record LanguageCodeAndName(string Code, string Name); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs index 16a1500b..6bdb5772 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs @@ -5,6 +5,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Metadata; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -41,27 +42,38 @@ public class MediaItemRepository : IMediaItemRepository return result.ToList(); } - public async Task> GetAllLanguageCodeCultures() + public async Task> GetAllLanguageCodesAndNames() { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); - var result = new System.Collections.Generic.HashSet(); + var result = new System.Collections.Generic.HashSet(); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); List mediaCodes = await GetAllLanguageCodes(); + var unseenCodes = new System.Collections.Generic.HashSet(mediaCodes); foreach (string mediaCode in mediaCodes) { foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode)) { Option maybeCulture = allCultures.Find( c => string.Equals(code, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)); + foreach (CultureInfo culture in maybeCulture) { - result.Add(culture); + unseenCodes.Remove(mediaCode); + unseenCodes.Remove(code); + result.Add(new LanguageCodeAndName(culture.ThreeLetterISOLanguageName, culture.EnglishName)); } } } + // every language code from the db must appear in the results + // entries that have no culture info (and thus english name) will just use the code twice + foreach (string mediaCode in unseenCodes.Where(c => !string.IsNullOrWhiteSpace(c))) + { + result.Add(new LanguageCodeAndName(mediaCode, mediaCode)); + } + return result.ToList(); } diff --git a/ErsatzTV/Validators/ChannelEditViewModelValidator.cs b/ErsatzTV/Validators/ChannelEditViewModelValidator.cs index 581a7acf..74df18b9 100644 --- a/ErsatzTV/Validators/ChannelEditViewModelValidator.cs +++ b/ErsatzTV/Validators/ChannelEditViewModelValidator.cs @@ -15,16 +15,5 @@ public class ChannelEditViewModelValidator : AbstractValidator x.Name).NotEmpty(); RuleFor(x => x.Group).NotEmpty(); RuleFor(x => x.FFmpegProfileId).GreaterThan(0); - - RuleFor(x => x.PreferredAudioLanguageCode) - .Must( - languageCode => CultureInfo.GetCultures(CultureTypes.NeutralCultures) - .Any( - ci => string.Equals( - ci.ThreeLetterISOLanguageName, - languageCode, - StringComparison.OrdinalIgnoreCase))) - .When(vm => !string.IsNullOrWhiteSpace(vm.PreferredAudioLanguageCode)) - .WithMessage("Preferred audio language code is invalid"); } }