diff options
Diffstat (limited to 'Emby.Server.Implementations')
28 files changed, 447 insertions, 167 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index e74755ec32..c69bcfef78 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.AppBase private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) { var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); - if (otherMarkers != null) + if (otherMarkers is not null) { throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); } diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 31ae82d6a3..676bb7f816 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -50,6 +50,8 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask _logger.LogDebug("Cleaning {Number} items with dead parents", numItems); + IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2)); + foreach (var itemId in itemIds) { cancellationToken.ThrowIfCancellationRequested(); @@ -95,9 +97,10 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask numComplete++; double percent = numComplete; percent /= numItems; - progress.Report(percent * 100); + subProgress.Report(percent * 100); } + subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50)); var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { @@ -105,7 +108,9 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask await using (transaction.ConfigureAwait(false)) { await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(50); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0db1606ea5..c5dc3b054c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1051,30 +1051,15 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = hasArtist.Artists - // .Except(foundArtists, new DistinctNameComparer()) + dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) + .Where(e => e.Value.Length > 0) .Select(i => { - // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrEmpty(i)) - { - return null; - } - - var artist = _libraryManager.GetArtist(i, new DtoOptions(false) - { - EnableImages = false - }); - if (artist is not null) + return new NameGuidPair { - return new NameGuidPair - { - Name = artist.Name, - Id = artist.Id - }; - } - - return null; + Name = i.Key, + Id = i.Value.First().Id + }; }).Where(i => i is not null).ToArray(); } diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f9538fbad6..ca0744a17d 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -37,6 +37,11 @@ namespace Emby.Server.Implementations.Library return false; } + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) + { + return true; + } + // Don't ignore top level folders if (fileInfo.IsDirectory && (parent is AggregateFolder || (parent?.IsTopParent ?? false))) @@ -44,11 +49,6 @@ namespace Emby.Server.Implementations.Library return false; } - if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) - { - return true; - } - if (parent is null) { return false; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 401ca73b80..bafe3ad436 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -50,6 +50,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule return false; } + // Fast path in case the ignore files isn't a symlink and is empty + if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0 + && dirIgnoreFile.Length == 0) + { + return true; + } + // ignore the directory only if the .ignore file is empty // evaluate individual files otherwise return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 25ddade829..fe3a1ce611 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -48,6 +48,8 @@ namespace Emby.Server.Implementations.Library "**/.wd_tv", "**/lost+found/**", "**/lost+found", + "**/subs/**", + "**/subs", // Trickplay files "**/*.trickplay", diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 58a971f62a..ef497726e2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -327,6 +327,45 @@ namespace Emby.Server.Implementations.Library DeleteItem(item, options, parent, notifyParentItem); } + public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items) + { + var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray(); + + foreach (var (item, internalPaths, pathsToDelete) in pathMaps) + { + foreach (var metadataPath in internalPaths) + { + if (!Directory.Exists(metadataPath)) + { + continue; + } + + _logger.LogDebug( + "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + metadataPath, + item.Id); + + try + { + Directory.Delete(metadataPath, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath); + } + } + + foreach (var fileSystemInfo in pathsToDelete) + { + DeleteItemPath(item, false, fileSystemInfo); + } + } + + _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]); + } + public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) { ArgumentNullException.ThrowIfNull(item); @@ -403,59 +442,7 @@ namespace Emby.Server.Implementations.Library foreach (var fileSystemInfo in item.GetDeletePaths()) { - if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) - { - try - { - _logger.LogInformation( - "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - - if (fileSystemInfo.IsDirectory) - { - Directory.Delete(fileSystemInfo.FullName, true); - } - else - { - File.Delete(fileSystemInfo.FullName); - } - } - catch (DirectoryNotFoundException) - { - _logger.LogInformation( - "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (FileNotFoundException) - { - _logger.LogInformation( - "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", - item.GetType().Name, - item.Name ?? "Unknown name", - fileSystemInfo.FullName, - item.Id); - } - catch (IOException) - { - if (isRequiredForDelete) - { - throw; - } - } - catch (UnauthorizedAccessException) - { - if (isRequiredForDelete) - { - throw; - } - } - } + DeleteItemPath(item, isRequiredForDelete, fileSystemInfo); isRequiredForDelete = false; } @@ -463,17 +450,73 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - _itemRepository.DeleteItem(item.Id); + _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]); _cache.TryRemove(item.Id, out _); foreach (var child in children) { - _itemRepository.DeleteItem(child.Id); _cache.TryRemove(child.Id, out _); } ReportItemRemoved(item, parent); } + private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo) + { + if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName)) + { + try + { + _logger.LogInformation( + "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + + if (fileSystemInfo.IsDirectory) + { + Directory.Delete(fileSystemInfo.FullName, true); + } + else + { + File.Delete(fileSystemInfo.FullName); + } + } + catch (DirectoryNotFoundException) + { + _logger.LogInformation( + "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (FileNotFoundException) + { + _logger.LogInformation( + "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + } + catch (IOException) + { + if (isRequiredForDelete) + { + throw; + } + } + catch (UnauthorizedAccessException) + { + if (isRequiredForDelete) + { + throw; + } + } + } + } + private bool IsInternalItem(BaseItem item) { if (!item.IsFileProtocol) @@ -485,7 +528,7 @@ namespace Emby.Server.Implementations.Library { Genre => _configurationManager.ApplicationPaths.GenrePath, MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, - MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath, Person => _configurationManager.ApplicationPaths.PeoplePath, Studio => _configurationManager.ApplicationPaths.StudioPath, Year => _configurationManager.ApplicationPaths.YearPath, @@ -826,6 +869,7 @@ namespace Emby.Server.Implementations.Library if (!folder.ParentId.Equals(rootFolder.Id)) { + rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); folder.ParentId = rootFolder.Id; folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); } @@ -989,6 +1033,11 @@ namespace Emby.Server.Implementations.Library return GetArtist(name, new DtoOptions(true)); } + public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names) + { + return _itemRepository.FindArtists(names); + } + public MusicArtist GetArtist(string name, DtoOptions options) { return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options); @@ -1090,6 +1139,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { + RootFolder.Children = null; await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further @@ -1100,9 +1150,12 @@ namespace Emby.Server.Implementations.Library allowRemoveRoot: removeRoot, cancellationToken: cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); + var rootFolder = GetUserRootFolder(); + rootFolder.Children = null; + + await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().ValidateChildren( + await rootFolder.ValidateChildren( new Progress<double>(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, @@ -1110,18 +1163,24 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var child in GetUserRootFolder().Children.OfType<Folder>()) + var toDelete = new List<Guid>(); + foreach (var child in rootFolder.Children!.OfType<Folder>()) { // If the user has somehow deleted the collection directory, remove the metadata from the database. if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) { - _itemRepository.DeleteItem(collectionFolder.Id); + toDelete.Add(collectionFolder.Id); } else { await child.RefreshMetadata(cancellationToken).ConfigureAwait(false); } } + + if (toDelete.Count > 0) + { + _itemRepository.DeleteItem(toDelete.ToArray()); + } } private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken) @@ -2027,6 +2086,12 @@ namespace Emby.Server.Implementations.Library } } + if (!File.Exists(image.Path)) + { + _logger.LogWarning("Image not found at {ImagePath}", image.Path); + continue; + } + ImageDimensions size; try { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 1e3b8ea760..750346169f 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -657,7 +657,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); + _logger.LogDebug(ex, "Error parsing cached media info."); } finally { diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 28cf695007..e0c8ae371b 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item - .GetRecursiveChildren(user, new InternalItemsQuery(user) - { - IncludeItemTypes = [BaseItemKind.Audio], - DtoOptions = dtoOptions - }) + .GetRecursiveChildren( + user, + new InternalItemsQuery(user) + { + IncludeItemTypes = [BaseItemKind.Audio], + DtoOptions = dtoOptions + }, + out _) .Cast<Audio>() .SelectMany(i => i.Genres) .Concat(item.Genres) diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index b2ceee97d8..333c8c34bf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -405,6 +405,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies if (child.IsDirectory) { + if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)) + { + continue; + } + if (IsDvdDirectory(child.FullName, filename, directoryService)) { var movie = new T diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index be1d96bf0b..72c8d7a9d2 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -80,6 +80,7 @@ namespace Emby.Server.Implementations.Library var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); _cache.AddOrUpdate(cacheKey, userData); + item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata UserDataSaved?.Invoke(this, new UserDataSaveEventArgs { @@ -159,7 +160,7 @@ namespace Emby.Server.Implementations.Library }; } - private UserItemData Map(UserData dto) + private static UserItemData Map(UserData dto) { return new UserItemData() { @@ -237,7 +238,10 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public UserItemData? GetUserData(User user, BaseItem item) { - return GetUserData(user, item.Id, item.GetUserDataKeys()); + return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData() + { + Key = item.GetUserDataKeys()[0], + }; } /// <inheritdoc /> @@ -304,7 +308,7 @@ namespace Emby.Server.Implementations.Library // ignore progress during the beginning positionTicks = 0; } - else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks) + else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond)) { // mark as completed close to the end positionTicks = 0; diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index b7fd24fa5c..f9a6f0d19e 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -1,5 +1,5 @@ using System; -using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; @@ -55,6 +55,8 @@ public class PeopleValidator var numPeople = people.Count; + IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2)); + _logger.LogDebug("Will refresh {Amount} people", numPeople); foreach (var person in people) @@ -92,7 +94,7 @@ public class PeopleValidator double percent = numComplete; percent /= numPeople; - progress.Report(100 * percent); + subProgress.Report(100 * percent); } var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery @@ -102,17 +104,13 @@ public class PeopleValidator IsLocked = false }); - foreach (var item in deadEntities) - { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50)); - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + var i = 0; + foreach (var item in deadEntities.Chunk(500)) + { + _libraryManager.DeleteItemsUnsafeFast(item); + subProgress.Report(100f / deadEntities.Count * (i++ * 100)); } progress.Report(100); diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index dec491d08b..29847048cb 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,16 +1,16 @@ { "Sync": "Сінхранізаваць", - "Playlists": "Спісы прайгравання", - "Latest": "Апошні", + "Playlists": "Плэй-лісты", + "Latest": "Апошняе", "LabelIpAddressValue": "IP-адрас: {0}", - "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", + "ItemAddedWithName": "{0} даданы ў бібліятэку", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены", - "NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана", + "NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "PluginInstalledWithName": "{0} быў усталяваны", "UserCreatedWithName": "Карыстальнік {0} быў створаны", "Albums": "Альбомы", - "Application": "Прыкладанне", - "AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны", + "Application": "Праграма", + "AuthenticationSucceededWithUserName": "{0} паспяхова аўтарызаваны", "Channels": "Каналы", "ChapterNameValue": "Раздзел {0}", "Collections": "Калекцыі", @@ -29,18 +29,18 @@ "HeaderAlbumArtists": "Выканаўцы альбома", "LabelRunningTimeValue": "Працягласць: {0}", "HomeVideos": "Хатнія відэа", - "ItemRemovedWithName": "{0} быў выдалены з бібліятэкі", - "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}", + "ItemRemovedWithName": "{0} выдалены з бібліятэкі", + "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да версіі {0}", "Movies": "Фільмы", "Music": "Музыка", "MusicVideos": "Музычныя кліпы", - "NameInstallFailed": "Устаноўка {0} не атрымалася", + "NameInstallFailed": "Усталяванне {0} не атрымалася", "NameSeasonNumber": "Сезон {0}", - "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання", + "NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне праграмы", "NotificationOptionPluginInstalled": "Плагін усталяваны", - "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана", + "NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна ўсталявана", "NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера", - "Photos": "Фатаграфіі", + "Photos": "Фотаздымкі", "Plugin": "Плагін", "PluginUninstalledWithName": "{0} быў выдалены", "PluginUpdatedWithName": "{0} быў абноўлены", @@ -54,16 +54,16 @@ "Artists": "Выканаўцы", "UserOfflineFromDevice": "{0} адлучыўся ад {1}", "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", - "TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.", + "TaskCleanActivityLogDescription": "Выдаляе запісы старэйшыя за зададзены ўзрост ў журнале актыўнасці.", "TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.", "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.", - "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.", + "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.", "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.", - "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.", - "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.", + "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.", + "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.", "TaskKeyframeExtractor": "Экстрактар ключавых кадраў", - "TasksApplicationCategory": "Прыкладанне", - "AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}", + "TasksApplicationCategory": "Праграма", + "AppDeviceValues": "Праграма: {0}, Прылада: {1}", "Books": "Кнігі", "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}", "DeviceOfflineWithName": "{0} адлучыўся", @@ -74,7 +74,7 @@ "HeaderFavoriteArtists": "Абраныя выканаўцы", "HearingImpaired": "Са слабым слыхам", "Inherit": "Атрымаць у спадчыну", - "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена", + "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера (секцыя {0}) абноўлена", "MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена", "MixedContent": "Змешаны змест", "NameSeasonUnknown": "Невядомы сезон", @@ -92,48 +92,48 @@ "NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена", "ScheduledTaskFailedWithName": "{0} не атрымалася", "ScheduledTaskStartedWithName": "{0} пачалося", - "ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць", + "ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску", "Shows": "Шоу", "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}", - "TvShows": "ТБ-шоу", + "TvShows": "Тэлепраграма", "Undefined": "Нявызначана", "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", "UserOnlineFromDevice": "{0} падключаны з {1}", "UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}", - "UserStartedPlayingItemWithValues": "{0} грае {1} на {2}", + "UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}", "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", "ValueSpecialEpisodeName": "Спецэпізод - {0}", "VersionNumber": "Версія {0}", "TasksMaintenanceCategory": "Абслугоўванне", - "TasksLibraryCategory": "Медыятэка", + "TasksLibraryCategory": "Бібліятэка", "TasksChannelsCategory": "Інтэрнэт-каналы", "TaskCleanActivityLog": "Ачысціць журнал актыўнасці", "TaskCleanCache": "Ачысціць кэш", "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", - "TaskRefreshChapterImages": "Выняць выявы раздзелаў", - "TaskRefreshLibrary": "Сканіраваць медыятэку", - "TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", - "TaskCleanLogs": "Ачысціць часопіс", - "TaskRefreshPeople": "Абнавіць людзей", + "TaskRefreshChapterImages": "Вынуць выявы раздзелаў", + "TaskRefreshLibrary": "Сканаваць бібліятэку", + "TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", + "TaskCleanLogs": "Ачысціць журнал", + "TaskRefreshPeople": "Абнавіць выканаўцаў", "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", "TaskUpdatePlugins": "Абнавіць плагіны", "TaskCleanTranscode": "Ачысціць каталог перакадзіравання", "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskRefreshChannels": "Абнавіць каналы", - "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры", - "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.", - "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay", - "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.", - "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання", - "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.", - "TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.", + "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры", + "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.", + "TaskRefreshTrickplayImages": "Стварыць выявы Trickplay", + "TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.", + "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты", + "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.", + "TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.", "TaskAudioNormalization": "Нармалізацыя гуку", "TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.", "TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.", - "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень", - "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень", + "TaskDownloadMissingLyrics": "Спампаваць адсутныя тэксты песняў", + "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 20f38de62f..52a26c1af2 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -137,5 +137,6 @@ "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.", "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.", - "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario" + "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario", + "CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días." } diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 8cdd06b7c4..f98a5e5b2c 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -125,5 +125,11 @@ "Undefined": "Sin definir", "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.", "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.", - "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad." + "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.", + "NotificationOptionApplicationUpdateAvailable": "actualización disponible", + "TaskDownloadMissingLyrics": "Descargue letras desaparecidas", + "TaskDownloadMissingLyricsDescription": "Decarga letras para canciones", + "TaskMoveTrickplayImages": "Mover localización de foto vista previa", + "NotificationOptionApplicationUpdateInstalled": "Aplicación actualización disponible", + "CleanupUserDataTask": "Tarea de limpieza de los datos del usuario" } diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index b95d07d5cf..f847d83d14 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -12,10 +12,10 @@ "DeviceOfflineWithName": "{0} wurde getrennt", "DeviceOnlineWithName": "{0} ist verbunden", "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", - "Favorites": "Favoriten", + "Favorites": "Favorite", "Folders": "Ordner", "Genres": "Genre", - "HeaderAlbumArtists": "Album-Künstler", + "HeaderAlbumArtists": "Album-Künschtler", "HeaderContinueWatching": "weiter schauen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Künstler", diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json index 4fcba99e90..f927d3173a 100644 --- a/Emby.Server.Implementations/Localization/Core/ht.json +++ b/Emby.Server.Implementations/Localization/Core/ht.json @@ -1,3 +1,62 @@ { - "Books": "liv" + "Books": "Liv", + "TasksLibraryCategory": "Libreri", + "Albums": "Albòm yo", + "Artists": "Atis yo", + "Application": "Aplikasyon", + "Channels": "Kanal yo", + "ChapterNameValue": "Chapit {0}", + "Default": "Defo", + "DeviceOnlineWithName": "{0} konekte", + "DeviceOfflineWithName": "{0} dekonekte", + "External": "Extèn", + "Collections": "Koleksyon yo", + "Favorites": "Pi Renmen", + "Folders": "Dosye", + "Genres": "Jan yo", + "Forced": "Fòse", + "HeaderAlbumArtists": "Albòm Atis", + "HeaderContinueWatching": "Kontinye Kade", + "HeaderFavoriteAlbums": "Albòm Pi Renmen", + "HeaderFavoriteArtists": "Atis Pi Renmen", + "HeaderFavoriteEpisodes": "Epizòd Pi Renmen", + "HeaderFavoriteShows": "Emisyon Pi Renmen", + "HeaderFavoriteSongs": "Mizik Pi Renmen", + "HeaderLiveTV": "Televizyon an Direk", + "HeaderNextUp": "Pwochen an", + "HomeVideos": "Videyo Lakay", + "Latest": "Pi Resan", + "MessageApplicationUpdated": "Sèvè Jellyfin met a jou", + "MessageApplicationUpdatedTo": "Sèvè Jellyfin met a jou sou {0}", + "Movies": "Fim", + "MixedContent": "Kontni Melanje", + "Music": "Mizik", + "MusicVideos": "Videyo Mizik", + "NameInstallFailed": "{0} enstalasyon fe fayit", + "NameSeasonNumber": "Sezon {0}", + "NameSeasonUnknown": "Sezon Enkoni", + "NotificationOptionCameraImageUploaded": "Imaj Kamera telechaje", + "NotificationOptionInstallationFailed": "Enstalasyon echwe", + "Photos": "Foto", + "PluginInstalledWithName": "{0} te enstale", + "PluginUninstalledWithName": "{0} te dezenstale", + "PluginUpdatedWithName": "{0} te mi a jou", + "ScheduledTaskFailedWithName": "{0} echwe", + "ScheduledTaskStartedWithName": "{0} komanse", + "Songs": "Mizik yo", + "Shows": "Emisyon yo", + "System": "Sistèm", + "TvShows": "Emisyon Tele", + "User": "Itilizatè", + "UserCreatedWithName": "Itilizatè {0} kreye", + "UserDeletedWithName": "Itilizatè {0} a efase", + "UserDownloadingItemWithValues": "{0} ap telechaje {1}", + "UserOfflineFromDevice": "{0} dekonekte de {1}", + "UserStartedPlayingItemWithValues": "{0} ap jwe {1} sou {2}", + "UserStoppedPlayingItemWithValues": "{0} fin jwe {1} sou {2}", + "UserPasswordChangedWithName": "Modpas la chanje pou Itilizatè {0}", + "ValueSpecialEpisodeName": "Spesyal - {0}", + "VersionNumber": "Vesyon {0}", + "TasksApplicationCategory": "Aplikasyon", + "TasksMaintenanceCategory": "Antretyen" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 8baa63d89f..e73c56cb90 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skann mediasegment", "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.", - "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." + "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.", + "CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.", + "CleanupUserDataTask": "Oppgave for opprydding av brukerdata" } diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index f7d1b112e1..9076b9c878 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -43,5 +43,75 @@ "NameInstallFailed": "Ye couldn't bring {0} aboard yer ship", "MessageApplicationUpdatedTo": "Yer Map of the Seas has been scribbled with {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Yer Map Drawer has been rescribbled to {0}", - "MessageServerConfigurationUpdated": "Yer Map drawer has been rescribbled" + "MessageServerConfigurationUpdated": "Yer Map drawer has been rescribbled", + "Inherit": "Carry on what be passed along", + "Latest": "Newfangled", + "Movies": "Moving pictures", + "NewVersionIsAvailable": "A fresh build o’ Jellyfin Server be waitin’ fer ye to fetch.", + "NotificationOptionPluginInstalled": "Plugin nailed down", + "NotificationOptionVideoPlayback": "Video playback be underway", + "ScheduledTaskFailedWithName": "{0} ran aground", + "StartupEmbyServerIsLoading": "Jellyfin Server be preparin’ the ship. Try yer luck again soon.", + "UserOfflineFromDevice": "{0} severed ties with {1}", + "UserDownloadingItemWithValues": "{0} be haulin’ in {1}", + "UserStartedPlayingItemWithValues": "{0} be playin’ {1} aboard {2}", + "ValueHasBeenAddedToLibrary": "{0} be stashed in yer treasure trove", + "TaskCleanCacheDescription": "Wipes away cache cargo no longer called fer.", + "TaskCleanLogsDescription": "Clears the logbook o’ entries older than {0} days.", + "TaskRefreshPeopleDescription": "Refreshes the charts fer actors an’ directors in yer Treasure Trove.", + "UserLockedOutWithName": "Matey {0} be denied boarding", + "TaskAudioNormalization": "Steadyin’ the shanties", + "TaskAudioNormalizationDescription": "Scans files fer shanty steadiyin’ data.", + "HeaderRecordingGroups": "Loggin' Groups", + "MusicVideos": "Shanty films", + "Playlists": "Lists o’ plunder", + "Plugin": "Extra sail", + "NotificationOptionVideoPlaybackStopped": "Video playback dropped anchor", + "NameSeasonNumber": "Saga {0}", + "NameSeasonUnknown": "Saga be Lost", + "NotificationOptionApplicationUpdateAvailable": "A fresh build awaits", + "NotificationOptionApplicationUpdateInstalled": "App upgrade be aboard", + "NotificationOptionAudioPlayback": "Audio playback be rollin", + "NotificationOptionAudioPlaybackStopped": "Audio playback dropped anchor", + "NotificationOptionCameraImageUploaded": "Spyglass shot be hoisted", + "NotificationOptionInstallationFailed": "Install be wrecked", + "NotificationOptionNewLibraryContent": "Fresh plunder ready to claim", + "NotificationOptionPluginError": "Plugin ran aground", + "NotificationOptionPluginUninstalled": "Plugin cast overboard", + "NotificationOptionPluginUpdateInstalled": "Plugin patched ‘n ready", + "NotificationOptionServerRestartRequired": "Server be due fer a restart", + "NotificationOptionTaskFailed": "Set chore went overboard", + "TaskRefreshLibraryDescription": "Searches the Treasure Trove fer new plunder ‘n updates the charts.", + "PluginInstalledWithName": "{0} nailed down", + "TaskCleanLogs": "Swab the Log Hold", + "TaskRefreshPeople": "Freshen the Mateys", + "PluginUninstalledWithName": "{0} sent t’ Davy Jones", + "PluginUpdatedWithName": "{0} patched ‘n ready", + "ProviderValue": "Supplier o’ goods: {0}", + "ScheduledTaskStartedWithName": "{0} set sail", + "ServerNameNeedsToBeRestarted": "{0} be cravin’ a restart", + "Shows": "Sagas", + "SubtitleDownloadFailureFromForItem": "Subtitles be sunk fetchin’ from {0} fer {1}", + "Sync": "Match the tides", + "System": "The ship’s works", + "TvShows": "TV Sagas", + "Undefined": "Uncharted", + "User": "Matey", + "UserCreatedWithName": "Matey {0} joined the crew", + "UserDeletedWithName": "Matey {0} cast overboard", + "UserOnlineFromDevice": "{0} be aboard ship from {1}", + "UserPasswordChangedWithName": "New passphrase set fer Matey {0}", + "UserPolicyUpdatedWithName": "Ship rules be changed fer {0}", + "UserStoppedPlayingItemWithValues": "{0} be done playin’ {1} on {2", + "ValueSpecialEpisodeName": "Special Tale – {0}", + "VersionNumber": "Edition {0}", + "TasksMaintenanceCategory": "Hull patchin’", + "TasksLibraryCategory": "Treasure Trove", + "TasksApplicationCategory": "Ship", + "TaskCleanActivityLog": "Clear the Ship’s Log", + "TaskCleanActivityLogDescription": "Purges ship’s logs older than the chosen time.", + "TaskCleanCache": "Sweep the Cache Chest", + "TaskRefreshChapterImages": "Claim chapter portraits", + "TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.", + "TaskRefreshLibrary": "Scan the Treasure Trove" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 84be91a872..1470a538c2 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} - неудачна", "ScheduledTaskStartedWithName": "{0} - запущена", "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}", - "Shows": "Телешоу", + "Shows": "Сериалы", "Songs": "Композиции", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index af40b5e5a9..76a136cf56 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -126,5 +126,16 @@ "HearingImpaired": "ослабљен слух", "TaskAudioNormalization": "Нормализација звука", "TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте", - "TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука." + "TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука.", + "TaskRefreshTrickplayImages": "Направи сличице за визуелно премотавање", + "TaskRefreshTrickplayImagesDescription": "Прављење сличица које помажу код визуелног премотавања видео-снимака.", + "TaskDownloadMissingLyrics": "Преузми стихове који недостају", + "TaskCleanCollectionsAndPlaylistsDescription": "Уклања ставке које више не постоје из колекција и плејлиста.", + "TaskExtractMediaSegments": "Скенирај сегменте медија", + "TaskExtractMediaSegmentsDescription": "Извлачи или добавља сегменте медија у додацима који раде са MediaSegment-ом.", + "TaskMoveTrickplayImagesDescription": "Премешта постојеће сличице за визуелно премотавање сходно подешавањима библиотеке.", + "CleanupUserDataTask": "Задатак чишћења корисничких података", + "CleanupUserDataTaskDescription": "Чисти све корисничке податке (напредак гледања, ознаке за омиљено...) медија који нису доступни 90 дана или дуже.", + "TaskMoveTrickplayImages": "Промени локацију сличица за визуелно премотавање", + "TaskDownloadMissingLyricsDescription": "Преузми стихове песама" } diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index 5e65bae26f..d5a7e866b8 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -402,8 +402,8 @@ sog|||Sogdian|sogdien som||so|Somali|somali son|||Songhai languages|songhai, langues sot||st|Sotho, Southern|sotho du Sud -spa||es-419|Spanish; Latin|espagnol; Latin spa||es|Spanish; Castilian|espagnol; castillan +spa||es-419|Spanish; Latin|espagnol; Latin sqi|alb|sq|Albanian|albanais srd||sc|Sardinian|sarde srn|||Sranan Tongo|sranan tongo diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 1ce363de5c..c9d76df0bf 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -314,7 +314,7 @@ namespace Emby.Server.Implementations.Playlists return; } - var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1; + var newPriorItemIndex = Math.Max(newIndex - 1, 0); var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId; var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId)); var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index bf8ffaf479..92d7a3907a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -61,7 +61,7 @@ public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks + IntervalTicks = TimeSpan.FromHours(6).Ticks }; } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 18162ad2fc..6e4e5c7808 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -1,10 +1,14 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; +using Microsoft.EntityFrameworkCore; namespace Emby.Server.Implementations.ScheduledTasks.Tasks; @@ -15,16 +19,19 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask { private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; + private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory; /// <summary> /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class. /// </summary> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization) + /// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param> + public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory) { _libraryManager = libraryManager; _localization = localization; + _dbContextFactory = dbContextFactory; } /// <inheritdoc /> @@ -62,8 +69,61 @@ public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask } /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - return _libraryManager.ValidatePeopleAsync(progress, cancellationToken); + IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2)); + await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false); + + subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50)); + var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var dupQuery = context.Peoples + .GroupBy(e => new { e.Name, e.PersonType }) + .Where(e => e.Count() > 1) + .Select(e => e.Select(f => f.Id).ToArray()); + + var total = dupQuery.Count(); + + const int PartitionSize = 100; + var iterator = 0; + int itemCounter; + var buffer = ArrayPool<Guid[]>.Shared.Rent(PartitionSize)!; + try + { + do + { + itemCounter = 0; + await foreach (var item in dupQuery + .Take(PartitionSize) + .AsAsyncEnumerable() + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + buffer[itemCounter++] = item; + } + + for (int i = 0; i < itemCounter; i++) + { + var item = buffer[i]; + var reference = item[0]; + var dups = item[1..]; + await context.PeopleBaseItemMap.WhereOneOrMany(dups, e => e.PeopleId) + .ExecuteUpdateAsync(e => e.SetProperty(f => f.PeopleId, reference), cancellationToken) + .ConfigureAwait(false); + await context.Peoples.Where(e => dups.Contains(e.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + subProgress.Report(100f / total * ((iterator * PartitionSize) + i)); + } + + iterator++; + } while (itemCounter == PartitionSize && !cancellationToken.IsCancellationRequested); + } + finally + { + ArrayPool<Guid[]>.Shared.Return(buffer); + } + + subProgress.Report(100); + } } } diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 01c1e596f9..86d08ed27b 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -6,7 +6,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -54,7 +53,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private int GetValue(BaseItem x) { - return x.IsFavoriteOrLiked(User) ? 0 : 1; + return x.IsFavoriteOrLiked(User, userItemData: null) ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 6f206c8772..9faa02f1fd 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private int GetValue(BaseItem x) { - return x.IsPlayed(User) ? 0 : 1; + return x.IsPlayed(User, userItemData: null) ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index fd1326327b..6f177c4637 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private int GetValue(BaseItem x) { - return x.IsUnplayed(User) ? 0 : 1; + return x.IsUnplayed(User, userItemData: null) ? 0 : 1; } } } |
