diff options
Diffstat (limited to 'Emby.Server.Implementations')
215 files changed, 7074 insertions, 3918 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index dc845b2d7..e74755ec3 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.AppBase @@ -30,80 +33,91 @@ namespace Emby.Server.Implementations.AppBase ConfigurationDirectoryPath = configurationDirectoryPath; CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } - /// <summary> - /// Gets the path to the program data folder. - /// </summary> - /// <value>The program data path.</value> + /// <inheritdoc/> public string ProgramDataPath { get; } /// <inheritdoc/> public string WebPath { get; } - /// <summary> - /// Gets the path to the system folder. - /// </summary> - /// <value>The path to the system folder.</value> + /// <inheritdoc/> public string ProgramSystemPath { get; } = AppContext.BaseDirectory; - /// <summary> - /// Gets the folder path to the data directory. - /// </summary> - /// <value>The data directory.</value> + /// <inheritdoc/> public string DataPath { get; } /// <inheritdoc /> public string VirtualDataPath => "%AppDataPath%"; - /// <summary> - /// Gets the image cache path. - /// </summary> - /// <value>The image cache path.</value> + /// <inheritdoc/> public string ImageCachePath => Path.Combine(CachePath, "images"); - /// <summary> - /// Gets the path to the plugin directory. - /// </summary> - /// <value>The plugins path.</value> + /// <inheritdoc/> public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); - /// <summary> - /// Gets the path to the plugin configurations directory. - /// </summary> - /// <value>The plugin configurations path.</value> + /// <inheritdoc/> public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); - /// <summary> - /// Gets the path to the log directory. - /// </summary> - /// <value>The log directory path.</value> + /// <inheritdoc/> public string LogDirectoryPath { get; } - /// <summary> - /// Gets the path to the application configuration root directory. - /// </summary> - /// <value>The configuration directory path.</value> + /// <inheritdoc/> public string ConfigurationDirectoryPath { get; } - /// <summary> - /// Gets the path to the system configuration file. - /// </summary> - /// <value>The system configuration file path.</value> + /// <inheritdoc/> public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); - /// <summary> - /// Gets or sets the folder path to the cache directory. - /// </summary> - /// <value>The cache directory.</value> + /// <inheritdoc/> public string CachePath { get; set; } - /// <summary> - /// Gets the folder path to the temp directory within the cache folder. - /// </summary> - /// <value>The temp directory.</value> + /// <inheritdoc/> public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); + + /// <inheritdoc /> + public string TrickplayPath => Path.Combine(DataPath, "trickplay"); + + /// <inheritdoc /> + public string BackupPath => Path.Combine(DataPath, "backups"); + + /// <inheritdoc /> + public virtual void MakeSanityCheckOrThrow() + { + CreateAndCheckMarker(ConfigurationDirectoryPath, "config"); + CreateAndCheckMarker(LogDirectoryPath, "log"); + CreateAndCheckMarker(PluginsPath, "plugin"); + CreateAndCheckMarker(ProgramDataPath, "data"); + CreateAndCheckMarker(CachePath, "cache"); + CreateAndCheckMarker(DataPath, "data"); + } + + /// <inheritdoc /> + public void CreateAndCheckMarker(string path, string markerName, bool recursive = false) + { + Directory.CreateDirectory(path); + + CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive); + } + + private IEnumerable<string> GetMarkers(string path, bool recursive = false) + { + return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + } + + private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) + { + var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); + if (otherMarkers != null) + { + throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); + } + + var markerPath = Path.Combine(path, markerName); + if (!File.Exists(markerPath)) + { + FileHelper.CreateEmpty(markerPath); + } + } } } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 9bc3a0204..81ef0e5f9 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -227,6 +227,7 @@ namespace Emby.Server.Implementations.AppBase Logger.LogInformation("Setting cache path: {Path}", cachePath); ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath; + CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache"); } /// <summary> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 29967c6df..cbb0f6c56 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Emby.Naming.Common; using Emby.Photos; +using Emby.Server.Implementations.Chapters; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; @@ -39,9 +40,10 @@ using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; using Jellyfin.Networking.Udp; -using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.FullSystemBackup; using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Implementations.MediaSegments; +using Jellyfin.Server.Implementations.SystemBackupService; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -57,10 +59,14 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LibraryTaskScheduler; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -91,7 +97,6 @@ using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -266,6 +271,8 @@ namespace Emby.Server.Implementations ? Environment.MachineName : ConfigurationManager.Configuration.ServerName; + public string RestoreBackupPath { get; set; } + public string ExpandVirtualPath(string path) { if (path is null) @@ -470,6 +477,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IApplicationHost>(this); serviceCollection.AddSingleton<IPluginManager>(_pluginManager); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); + serviceCollection.AddSingleton<IBackupService, BackupService>(); serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); @@ -504,10 +512,13 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>(); serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>(); serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>(); + serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>(); serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>(); serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); serviceCollection.AddSingleton<EncodingHelper>(); + serviceCollection.AddSingleton<IPathManager, PathManager>(); + serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); @@ -542,6 +553,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); + serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); @@ -549,13 +561,14 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); + serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IAuthService, AuthService>(); serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>(); serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>(); + serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); @@ -572,20 +585,10 @@ namespace Emby.Server.Implementations /// <summary> /// Create services registered with the service container that need to be initialized at application startup. /// </summary> + /// <param name="startupConfig">The configuration used to initialise the application.</param> /// <returns>A task representing the service initialization operation.</returns> - public async Task InitializeServices() + public async Task InitializeServices(IConfiguration startupConfig) { - var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false); - await using (jellyfinDb.ConfigureAwait(false)) - { - if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) - { - Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)"); - await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false); - Logger.LogInformation("EFCore migrations applied successfully"); - } - } - var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); await localizationManager.LoadAll().ConfigureAwait(false); @@ -633,24 +636,26 @@ namespace Emby.Server.Implementations private void SetStaticProperties() { // For now there's no real way to inject these properly - BaseItem.Logger = Resolve<ILogger<BaseItem>>(); + BaseItem.ChapterManager = Resolve<IChapterManager>(); + BaseItem.ChannelManager = Resolve<IChannelManager>(); BaseItem.ConfigurationManager = ConfigurationManager; + BaseItem.FileSystem = Resolve<IFileSystem>(); + BaseItem.ItemRepository = Resolve<IItemRepository>(); BaseItem.LibraryManager = Resolve<ILibraryManager>(); - BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); - BaseItem.ItemRepository = Resolve<IItemRepository>(); - BaseItem.ChapterRepository = Resolve<IChapterRepository>(); - BaseItem.FileSystem = Resolve<IFileSystem>(); - BaseItem.UserDataManager = Resolve<IUserDataManager>(); - BaseItem.ChannelManager = Resolve<IChannelManager>(); - Video.RecordingsManager = Resolve<IRecordingsManager>(); - Folder.UserViewManager = Resolve<IUserViewManager>(); - UserView.TVSeriesManager = Resolve<ITVSeriesManager>(); - UserView.CollectionManager = Resolve<ICollectionManager>(); - BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>(); + BaseItem.Logger = Resolve<ILogger<BaseItem>>(); BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>(); + BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>(); + BaseItem.ProviderManager = Resolve<IProviderManager>(); + BaseItem.UserDataManager = Resolve<IUserDataManager>(); CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.ApplicationHost = this; + Folder.UserViewManager = Resolve<IUserViewManager>(); + Folder.CollectionManager = Resolve<ICollectionManager>(); + Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>(); + Episode.MediaEncoder = Resolve<IMediaEncoder>(); + UserView.TVSeriesManager = Resolve<ITVSeriesManager>(); + Video.RecordingsManager = Resolve<IRecordingsManager>(); } /// <summary> diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs new file mode 100644 index 000000000..b4daa2a14 --- /dev/null +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Chapters; + +/// <summary> +/// The chapter manager. +/// </summary> +public class ChapterManager : IChapterManager +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger<ChapterManager> _logger; + private readonly IMediaEncoder _encoder; + private readonly IChapterRepository _chapterRepository; + private readonly ILibraryManager _libraryManager; + private readonly IPathManager _pathManager; + + /// <summary> + /// The first chapter ticks. + /// </summary> + private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; + + /// <summary> + /// Initializes a new instance of the <see cref="ChapterManager"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger{ChapterManager}"/>.</param> + /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param> + /// <param name="encoder">The <see cref="IMediaEncoder"/>.</param> + /// <param name="chapterRepository">The <see cref="IChapterRepository"/>.</param> + /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param> + /// <param name="pathManager">The <see cref="IPathManager"/>.</param> + public ChapterManager( + ILogger<ChapterManager> logger, + IFileSystem fileSystem, + IMediaEncoder encoder, + IChapterRepository chapterRepository, + ILibraryManager libraryManager, + IPathManager pathManager) + { + _logger = logger; + _fileSystem = fileSystem; + _encoder = encoder; + _chapterRepository = chapterRepository; + _libraryManager = libraryManager; + _pathManager = pathManager; + } + + /// <summary> + /// Determines whether [is eligible for chapter image extraction] [the specified video]. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="libraryOptions">The library options for the video.</param> + /// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns> + private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) + { + if (video.IsPlaceHolder) + { + return false; + } + + if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + // Can't extract images if there are no video streams + return video.DefaultVideoStreamIndex.HasValue; + } + + private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters) + { + if (chapters.Count < 2) + { + return 0; + } + + long sum = 0; + for (int i = 1; i < chapters.Count; i++) + { + sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; + } + + return sum / chapters.Count; + } + + /// <inheritdoc /> + public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) + { + if (chapters.Count == 0) + { + return true; + } + + var libraryOptions = _libraryManager.GetLibraryOptions(video); + + if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) + { + extractImages = false; + } + + var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); + var threshold = TimeSpan.FromSeconds(1).Ticks; + if (averageChapterDuration < threshold) + { + _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); + extractImages = false; + } + + var success = true; + var changesMade = false; + + var runtimeTicks = video.RunTimeTicks ?? 0; + + var currentImages = GetSavedChapterImages(video, directoryService); + + foreach (var chapter in chapters) + { + if (chapter.StartPositionTicks >= runtimeTicks) + { + _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name); + break; + } + + var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks); + + if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) + { + if (extractImages) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // Add some time for the first chapter to make sure we don't end up with a black image + var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); + + var inputPath = video.Path; + var directoryPath = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + var container = video.Container; + var mediaSource = new MediaSourceInfo + { + VideoType = video.VideoType, + IsoType = video.IsoType, + Protocol = video.PathProtocol ?? MediaProtocol.File, + }; + + _logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath); + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + File.Copy(tempFile, path, true); + + try + { + _fileSystem.DeleteFile(tempFile); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); + } + + chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); + changesMade = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)); + success = false; + break; + } + } + else if (!string.IsNullOrEmpty(chapter.ImagePath)) + { + chapter.ImagePath = null; + changesMade = true; + } + } + else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) + { + chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); + changesMade = true; + } + else if (libraryOptions?.EnableChapterImageExtraction != true) + { + // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image + chapter.ImagePath = null; + changesMade = true; + } + } + + if (saveChapters && changesMade) + { + _chapterRepository.SaveChapters(video.Id, chapters); + } + + DeleteDeadImages(currentImages, chapters); + + return success; + } + + /// <inheritdoc /> + public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters) + { + _chapterRepository.SaveChapters(video.Id, chapters); + } + + /// <inheritdoc /> + public ChapterInfo? GetChapter(Guid baseItemId, int index) + { + return _chapterRepository.GetChapter(baseItemId, index); + } + + /// <inheritdoc /> + public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId) + { + return _chapterRepository.GetChapters(baseItemId); + } + + /// <inheritdoc /> + public void DeleteChapterImages(Video video) + { + var path = _pathManager.GetChapterImageFolderPath(video); + try + { + if (Directory.Exists(path)) + { + _logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id); + Directory.Delete(path, true); + } + } + catch (Exception ex) + { + _logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex); + } + + _chapterRepository.DeleteChapters(video.Id); + } + + private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService) + { + var path = _pathManager.GetChapterImageFolderPath(video); + if (!Directory.Exists(path)) + { + return []; + } + + try + { + return directoryService.GetFilePaths(path); + } + catch (IOException) + { + return []; + } + } + + private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters) + { + var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)); + var deadImages = images + .Except(existingImages, StringComparer.OrdinalIgnoreCase) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var image in deadImages) + { + _logger.LogDebug("Deleting dead chapter image {Path}", image); + + try + { + _fileSystem.DeleteFile(image!); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting {Path}.", image); + } + } + } +} diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index e414792ba..0eb387ffd 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; @@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections var libraryOptions = new LibraryOptions { - PathInfos = new[] { new MediaPathInfo(path) }, + PathInfos = [new MediaPathInfo(path)], EnableRealtimeMonitor = false, SaveLocalMetadata = true }; @@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections try { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); var collection = new BoxSet { Name = name, Path = path, IsLocked = options.IsLocked, ProviderIds = options.ProviderIds, - DateCreated = DateTime.UtcNow + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc }; parentFolder.AddChild(collection); @@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections { if (_libraryManager.GetItemById(collectionId) is not BoxSet collection) { - throw new ArgumentException("No collection exists with the supplied Id"); + throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId); } List<BaseItem>? itemList = null; @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections if (item is null) { - throw new ArgumentException("No item exists with the supplied Id"); + throw new ArgumentException("No item exists with the supplied Id " + id); } if (!currentLinkedChildrenIds.Contains(id)) diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 7ea863d76..31ae82d6a 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,84 +1,114 @@ #pragma warning disable CS1591 using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Data +namespace Emby.Server.Implementations.Data; + +public class CleanDatabaseScheduledTask : ILibraryPostScanTask { - public class CleanDatabaseScheduledTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger<CleanDatabaseScheduledTask> _logger; + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; + private readonly IPathManager _pathManager; + + public CleanDatabaseScheduledTask( + ILibraryManager libraryManager, + ILogger<CleanDatabaseScheduledTask> logger, + IDbContextFactory<JellyfinDbContext> dbProvider, + IPathManager pathManager) { - private readonly ILibraryManager _libraryManager; - private readonly ILogger<CleanDatabaseScheduledTask> _logger; - private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; - - public CleanDatabaseScheduledTask( - ILibraryManager libraryManager, - ILogger<CleanDatabaseScheduledTask> logger, - IDbContextFactory<JellyfinDbContext> dbProvider) - { - _libraryManager = libraryManager; - _logger = logger; - _dbProvider = dbProvider; - } + _libraryManager = libraryManager; + _logger = logger; + _dbProvider = dbProvider; + _pathManager = pathManager; + } - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); - } + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); + } - private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress) + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery - { - HasDeadParentId = true - }); + HasDeadParentId = true + }); - var numComplete = 0; - var numItems = itemIds.Count + 1; + var numComplete = 0; + var numItems = itemIds.Count + 1; - _logger.LogDebug("Cleaning {0} items with dead parent links", numItems); + _logger.LogDebug("Cleaning {Number} items with dead parents", numItems); - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var itemId in itemIds) + { + cancellationToken.ThrowIfCancellationRequested(); - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId); + if (item is not null) + { + _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - if (item is not null) + foreach (var mediaSource in item.GetMediaSources(false)) { - _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); + // Delete extracted data + var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id); + if (mediaSourceItem is null) + { + continue; + } - _libraryManager.DeleteItem(item, new DeleteOptions + var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem); + foreach (var folder in extractedDataFolders) { - DeleteFileLocation = false - }); + if (Directory.Exists(folder)) + { + try + { + Directory.Delete(folder, true); + } + catch (Exception e) + { + _logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message); + } + } + } } - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); + // Delete item + _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false + }); } - var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); + } + + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) { - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using (transaction.ConfigureAwait(false)) - { - await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } - - progress.Report(100); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 356d1e437..cf886ae82 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; @@ -17,7 +17,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; @@ -41,7 +40,6 @@ namespace Emby.Server.Implementations.Dto private readonly ILogger<DtoService> _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; - private readonly IItemRepository _itemRepo; private readonly IImageProcessor _imageProcessor; private readonly IProviderManager _providerManager; @@ -52,13 +50,12 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly ITrickplayManager _trickplayManager; - private readonly IChapterRepository _chapterRepository; + private readonly IChapterManager _chapterManager; public DtoService( ILogger<DtoService> logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, - IItemRepository itemRepo, IImageProcessor imageProcessor, IProviderManager providerManager, IRecordingsManager recordingsManager, @@ -66,12 +63,11 @@ namespace Emby.Server.Implementations.Dto IMediaSourceManager mediaSourceManager, Lazy<ILiveTvManager> livetvManagerFactory, ITrickplayManager trickplayManager, - IChapterRepository chapterRepository) + IChapterManager chapterManager) { _logger = logger; _libraryManager = libraryManager; _userDataRepository = userDataRepository; - _itemRepo = itemRepo; _imageProcessor = imageProcessor; _providerManager = providerManager; _recordingsManager = recordingsManager; @@ -79,7 +75,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _trickplayManager = trickplayManager; - _chapterRepository = chapterRepository; + _chapterManager = chapterManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -99,11 +95,11 @@ namespace Emby.Server.Implementations.Dto if (item is LiveTvChannel tvChannel) { - (channelTuples ??= new()).Add((dto, tvChannel)); + (channelTuples ??= []).Add((dto, tvChannel)); } else if (item is LiveTvProgram) { - (programTuples ??= new()).Add((item, dto)); + (programTuples ??= []).Add((item, dto)); } if (item is IItemByName byName) @@ -590,12 +586,12 @@ namespace Emby.Server.Implementations.Dto if (dto.ImageBlurHashes is not null) { // Only add BlurHash for the person's image. - baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); + baseItemPerson.ImageBlurHashes = []; foreach (var (imageType, blurHash) in dto.ImageBlurHashes) { if (blurHash is not null) { - baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>(); + baseItemPerson.ImageBlurHashes[imageType] = []; foreach (var (imageId, blurHashValue) in blurHash) { if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase)) @@ -674,11 +670,11 @@ namespace Emby.Server.Implementations.Dto if (!string.IsNullOrEmpty(image.BlurHash)) { - dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>(); + dto.ImageBlurHashes ??= []; if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value)) { - value = new Dictionary<string, string>(); + value = []; dto.ImageBlurHashes[image.Type] = value; } @@ -709,7 +705,7 @@ namespace Emby.Server.Implementations.Dto if (hashes.Count > 0) { - dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>(); + dto.ImageBlurHashes ??= []; dto.ImageBlurHashes[imageType] = hashes; } @@ -756,7 +752,7 @@ namespace Emby.Server.Implementations.Dto dto.AspectRatio = hasAspectRatio.AspectRatio; } - dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); + dto.ImageBlurHashes = []; var backdropLimit = options.GetImageLimit(ImageType.Backdrop); if (backdropLimit > 0) @@ -772,7 +768,7 @@ namespace Emby.Server.Implementations.Dto if (options.EnableImages) { - dto.ImageTags = new Dictionary<ImageType, string>(); + dto.ImageTags = []; // Prevent implicitly captured closure var currentItem = item; @@ -1064,12 +1060,17 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Chapters)) { - dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList(); + dto.Chapters = _chapterManager.GetChapters(item.Id).ToList(); } if (options.ContainsField(ItemFields.Trickplay)) { - dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); + var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); + dto.Trickplay = trickplay.ToDictionary( + mediaStream => mediaStream.Key, + mediaStream => mediaStream.Value.ToDictionary( + width => width.Key, + width => new TrickplayInfoDto(width.Value))); } dto.ExtraType = video.ExtraType; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 70dd5eb9a..15843730e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -18,9 +18,11 @@ <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" /> <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" /> + <ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" /> </ItemGroup> <ItemGroup> + <PackageReference Include="BitFaster.Caching" /> <PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> @@ -63,9 +65,13 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="Ignore" /> + </ItemGroup> + + <ItemGroup> <EmbeddedResource Include="Localization\iso6392.txt" /> <EmbeddedResource Include="Localization\countries.json" /> <EmbeddedResource Include="Localization\Core\*.json" /> - <EmbeddedResource Include="Localization\Ratings\*.csv" /> + <EmbeddedResource Include="Localization\Ratings\*.json" /> </ItemGroup> </Project> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index fb0a55135..933cfc8cb 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -5,8 +5,8 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 1d04f3da3..8a79cdebc 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,7 +1,8 @@ #pragma warning disable CS1591 using System.Threading.Tasks; -using Jellyfin.Data.Enums; +using Jellyfin.Data; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index a720c86fb..373b0994a 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer RemoteEndPoint = remoteEndPoint; _jsonOptions = JsonDefaults.Options; - LastActivityDate = DateTime.Now; + LastActivityDate = DateTime.UtcNow; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 7378cf885..f63408403 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List<string> paths) { IEnumerable<BaseItem> itemsToRefresh = paths - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Select(GetAffectedBaseItem) .Where(item => item is not null) .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 6af2a553d..d87ad729e 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO .Where(IsLibraryMonitorEnabled) .OfType<Folder>() .SelectMany(f => f.PhysicalLocations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Order(); foreach (var path in paths) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 66b7839f7..c9630b894 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -160,12 +160,13 @@ namespace Emby.Server.Implementations.IO { // Cross device move requires a copy Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) + var sourceDir = new DirectoryInfo(source); + foreach (var file in sourceDir.EnumerateFiles()) { - File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true); + file.CopyTo(Path.Combine(destination, file.Name), true); } - Directory.Delete(source, true); + sourceDir.Delete(true); } } @@ -541,8 +542,8 @@ namespace Emby.Server.Implementations.IO return DriveInfo.GetDrives() .Where( d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) - && d.IsReady - && d.TotalSize != 0) + && d.IsReady + && d.TotalSize != 0) .Select(d => new FileSystemMetadata { Name = d.Name, @@ -560,11 +561,23 @@ namespace Emby.Server.Implementations.IO /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) { - return GetFiles(path, null, false, recursive); + return GetFiles(path, "*", recursive); } /// <inheritdoc /> - public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false) + { + return GetFiles(path, searchPattern, null, false, recursive); + } + + /// <inheritdoc /> + public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive) + { + return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive); + } + + /// <inheritdoc /> + public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -572,10 +585,12 @@ namespace Emby.Server.Implementations.IO // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1) { - return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions)); + searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0]; + + return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions)); } - var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions); + var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions); if (extensions is not null && extensions.Count > 0) { diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 8b2869149..4874eca8e 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images protected IImageProcessor ImageProcessor { get; set; } protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; } - = new ImageType[] { ImageType.Primary }; + = [ImageType.Primary]; /// <inheritdoc /> public string Name => "Dynamic Image Provider"; - protected virtual int MaxImageAgeDays => 7; - public int Order => 0; protected virtual bool Supports(BaseItem item) => true; @@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) { - var age = DateTime.UtcNow - image.DateModified; - return age.TotalDays > MaxImageAgeDays; + var path = image.Path; + if (!string.IsNullOrEmpty(path)) + { + var modificationDate = FileSystem.GetLastWriteTimeUtc(path); + return image.DateModified != modificationDate; + } + + return false; } protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType) diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index f9c10ba09..0d63b3af7 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 34c722e41..273d356a3 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index c9b41f819..706de60a9 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs index 31f053f06..c472623e6 100644 --- a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index b01fd93a7..f29a0b3ad 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library { if (parent is not null) { - // Ignore extras folders but allow it at the collection level + // Ignore extras for unsupported types if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename) && parent is not AggregateFolder && parent is not UserRootFolder) @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library { if (parent is not null) { - // Don't resolve these into audio files + // Don't resolve theme songs if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) && AudioFileParser.IsAudioFile(filename, _namingOptions)) { diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs new file mode 100644 index 000000000..401ca73b8 --- /dev/null +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Library; + +/// <summary> +/// Resolver rule class for ignoring files via .ignore. +/// </summary> +public class DotIgnoreIgnoreRule : IResolverIgnoreRule +{ + private static FileInfo? FindIgnoreFile(DirectoryInfo directory) + { + var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); + if (ignoreFile.Exists) + { + return ignoreFile; + } + + var parentDir = directory.Parent; + if (parentDir is null) + { + return null; + } + + return FindIgnoreFile(parentDir); + } + + /// <inheritdoc /> + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) + { + return IsIgnored(fileInfo, parent); + } + + /// <summary> + /// Checks whether or not the file is ignored. + /// </summary> + /// <param name="fileInfo">The file information.</param> + /// <param name="parent">The parent BaseItem.</param> + /// <returns>True if the file should be ignored.</returns> + public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) + { + if (fileInfo.IsDirectory) + { + var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); + if (dirIgnoreFile is null) + { + return false; + } + + // ignore the directory only if the .ignore file is empty + // evaluate individual files otherwise + return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); + } + + var parentDirPath = Path.GetDirectoryName(fileInfo.FullName); + if (string.IsNullOrEmpty(parentDirPath)) + { + return false; + } + + var folder = new DirectoryInfo(parentDirPath); + var ignoreFile = FindIgnoreFile(folder); + if (ignoreFile is null) + { + return false; + } + + string ignoreFileString = GetFileContent(ignoreFile); + + if (string.IsNullOrWhiteSpace(ignoreFileString)) + { + // Ignore directory if we just have the file + return true; + } + + // If file has content, base ignoring off the content .gitignore-style rules + var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var ignore = new Ignore.Ignore(); + ignore.Add(ignoreRules); + + return ignore.IsIgnored(fileInfo.FullName); + } + + private static string GetFileContent(FileInfo dirIgnoreFile) + { + using (var reader = dirIgnoreFile.OpenText()) + { + return reader.ReadToEnd(); + } + } +} diff --git a/Emby.Server.Implementations/Library/ExternalDataManager.cs b/Emby.Server.Implementations/Library/ExternalDataManager.cs new file mode 100644 index 000000000..d3cfa1d25 --- /dev/null +++ b/Emby.Server.Implementations/Library/ExternalDataManager.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.MediaSegments; +using MediaBrowser.Controller.Trickplay; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library; + +/// <summary> +/// IExternalDataManager implementation. +/// </summary> +public class ExternalDataManager : IExternalDataManager +{ + private readonly IKeyframeManager _keyframeManager; + private readonly IMediaSegmentManager _mediaSegmentManager; + private readonly IPathManager _pathManager; + private readonly ITrickplayManager _trickplayManager; + private readonly ILogger<ExternalDataManager> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="ExternalDataManager"/> class. + /// </summary> + /// <param name="keyframeManager">The keyframe manager.</param> + /// <param name="mediaSegmentManager">The media segment manager.</param> + /// <param name="pathManager">The path manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + /// <param name="logger">The logger.</param> + public ExternalDataManager( + IKeyframeManager keyframeManager, + IMediaSegmentManager mediaSegmentManager, + IPathManager pathManager, + ITrickplayManager trickplayManager, + ILogger<ExternalDataManager> logger) + { + _keyframeManager = keyframeManager; + _mediaSegmentManager = mediaSegmentManager; + _pathManager = pathManager; + _trickplayManager = trickplayManager; + _logger = logger; + } + + /// <inheritdoc/> + public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken) + { + var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList(); + var itemId = item.Id; + if (validPaths.Count > 0) + { + foreach (var path in validPaths) + { + try + { + Directory.Delete(path, true); + } + catch (Exception ex) + { + _logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex); + } + } + } + + await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false); + await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false); + await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index bb45dd87e..25ddade82 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using DotNet.Globbing; namespace Emby.Server.Implementations.Library diff --git a/Emby.Server.Implementations/Library/KeyframeManager.cs b/Emby.Server.Implementations/Library/KeyframeManager.cs new file mode 100644 index 000000000..18f4ce047 --- /dev/null +++ b/Emby.Server.Implementations/Library/KeyframeManager.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Persistence; + +namespace Emby.Server.Implementations.Library; + +/// <summary> +/// Manager for Keyframe data. +/// </summary> +public class KeyframeManager : IKeyframeManager +{ + private readonly IKeyframeRepository _repository; + + /// <summary> + /// Initializes a new instance of the <see cref="KeyframeManager"/> class. + /// </summary> + /// <param name="repository">The keyframe repository.</param> + public KeyframeManager(IKeyframeRepository repository) + { + _repository = repository; + } + + /// <inheritdoc /> + public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId) + { + return _repository.GetKeyframeData(itemId); + } + + /// <inheritdoc /> + public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken) + { + await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false); + } + + /// <inheritdoc /> + public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken) + { + await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c483f3c61..df71868b6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2,7 +2,6 @@ #pragma warning disable CA5394 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,6 +10,7 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using BitFaster.Caching.Lru; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Server.Implementations.Library.Resolvers; @@ -18,8 +18,10 @@ using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks.Tasks; using Emby.Server.Implementations.Sorting; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -32,10 +34,12 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; @@ -62,14 +66,13 @@ namespace Emby.Server.Implementations.Library private const string ShortcutFileExtension = ".mblink"; private readonly ILogger<LibraryManager> _logger; - private readonly ConcurrentDictionary<Guid, BaseItem> _cache; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataRepository; + private readonly IUserDataManager _userDataManager; private readonly IServerConfigurationManager _configurationManager; private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory; private readonly Lazy<IProviderManager> _providerManagerFactory; - private readonly Lazy<IUserViewManager> _userviewManagerFactory; + private readonly Lazy<IUserViewManager> _userViewManagerFactory; private readonly IServerApplicationHost _appHost; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; @@ -78,6 +81,8 @@ namespace Emby.Server.Implementations.Library private readonly NamingOptions _namingOptions; private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; + private readonly IPathManager _pathManager; + private readonly FastConcurrentLru<Guid, BaseItem> _cache; /// <summary> /// The _root folder sync lock. @@ -103,56 +108,61 @@ namespace Emby.Server.Implementations.Library /// <param name="taskManager">The task manager.</param> /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> - /// <param name="userDataRepository">The user data repository.</param> + /// <param name="userDataManager">The user data manager.</param> /// <param name="libraryMonitorFactory">The library monitor.</param> /// <param name="fileSystem">The file system.</param> /// <param name="providerManagerFactory">The provider manager.</param> - /// <param name="userviewManagerFactory">The userview manager.</param> + /// <param name="userViewManagerFactory">The user view manager.</param> /// <param name="mediaEncoder">The media encoder.</param> /// <param name="itemRepository">The item repository.</param> /// <param name="imageProcessor">The image processor.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="directoryService">The directory service.</param> - /// <param name="peopleRepository">The People Repository.</param> + /// <param name="peopleRepository">The people repository.</param> + /// <param name="pathManager">The path manager.</param> public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, - IUserDataManager userDataRepository, + IUserDataManager userDataManager, Lazy<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Lazy<IProviderManager> providerManagerFactory, - Lazy<IUserViewManager> userviewManagerFactory, + Lazy<IUserViewManager> userViewManagerFactory, IMediaEncoder mediaEncoder, IItemRepository itemRepository, IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService, - IPeopleRepository peopleRepository) + IPeopleRepository peopleRepository, + IPathManager pathManager) { _appHost = appHost; _logger = loggerFactory.CreateLogger<LibraryManager>(); _taskManager = taskManager; _userManager = userManager; _configurationManager = configurationManager; - _userDataRepository = userDataRepository; + _userDataManager = userDataManager; _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; - _userviewManagerFactory = userviewManagerFactory; + _userViewManagerFactory = userViewManagerFactory; _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; _imageProcessor = imageProcessor; - _cache = new ConcurrentDictionary<Guid, BaseItem>(); + + _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize); + _namingOptions = namingOptions; _peopleRepository = peopleRepository; + _pathManager = pathManager; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; - RecordConfigurationValues(configurationManager.Configuration); + RecordConfigurationValues(_configurationManager.Configuration); } /// <summary> @@ -194,39 +204,39 @@ namespace Emby.Server.Implementations.Library private IProviderManager ProviderManager => _providerManagerFactory.Value; - private IUserViewManager UserViewManager => _userviewManagerFactory.Value; + private IUserViewManager UserViewManager => _userViewManagerFactory.Value; /// <summary> /// Gets or sets the postscan tasks. /// </summary> /// <value>The postscan tasks.</value> - private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>(); + private ILibraryPostScanTask[] PostScanTasks { get; set; } = []; /// <summary> /// Gets or sets the intro providers. /// </summary> /// <value>The intro providers.</value> - private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>(); + private IIntroProvider[] IntroProviders { get; set; } = []; /// <summary> /// Gets or sets the list of entity resolution ignore rules. /// </summary> /// <value>The entity resolution ignore rules.</value> - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>(); + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = []; /// <summary> /// Gets or sets the list of currently registered entity resolvers. /// </summary> /// <value>The entity resolvers enumerable.</value> - private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>(); + private IItemResolver[] EntityResolvers { get; set; } = []; - private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>(); + private IMultiItemResolver[] MultiItemResolvers { get; set; } = []; /// <summary> /// Gets or sets the comparers. /// </summary> /// <value>The comparers.</value> - private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>(); + private IBaseItemComparer[] Comparers { get; set; } = []; public bool IsScanRunning { get; private set; } @@ -237,20 +247,20 @@ namespace Emby.Server.Implementations.Library /// <param name="resolvers">The resolvers.</param> /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> - /// <param name="postscanTasks">The post scan tasks.</param> + /// <param name="postScanTasks">The post scan tasks.</param> public void AddParts( IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, IEnumerable<IBaseItemComparer> itemComparers, - IEnumerable<ILibraryPostScanTask> postscanTasks) + IEnumerable<ILibraryPostScanTask> postScanTasks) { EntityResolutionIgnoreRules = rules.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray(); IntroProviders = introProviders.ToArray(); Comparers = itemComparers.ToArray(); - PostscanTasks = postscanTasks.ToArray(); + PostScanTasks = postScanTasks.ToArray(); } /// <summary> @@ -300,7 +310,7 @@ namespace Emby.Server.Implementations.Library } } - _cache[item.Id] = item; + _cache.AddOrUpdate(item.Id, item); } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -359,7 +369,7 @@ namespace Emby.Server.Implementations.Library var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) - : Array.Empty<BaseItem>(); + : []; foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -385,7 +395,7 @@ namespace Emby.Server.Implementations.Library } } - if (options.DeleteFileLocation && item.IsFileProtocol) + if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item)) { // Assume only the first is required // Add this flag to GetDeletePaths if required in the future @@ -454,24 +464,85 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); _itemRepository.DeleteItem(item.Id); + _cache.TryRemove(item.Id, out _); foreach (var child in children) { _itemRepository.DeleteItem(child.Id); + _cache.TryRemove(child.Id, out _); } - _cache.TryRemove(item.Id, out _); - ReportItemRemoved(item, parent); } - private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children) + private bool IsInternalItem(BaseItem item) + { + if (!item.IsFileProtocol) + { + return false; + } + + var pathToCheck = item switch + { + Genre => _configurationManager.ApplicationPaths.GenrePath, + MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath, + MusicGenre => _configurationManager.ApplicationPaths.GenrePath, + Person => _configurationManager.ApplicationPaths.PeoplePath, + Studio => _configurationManager.ApplicationPaths.StudioPath, + Year => _configurationManager.ApplicationPaths.YearPath, + _ => null + }; + + var itemPath = item.Path; + if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath)) + { + var cleanPath = _fileSystem.GetValidFilename(itemPath); + var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck); + + return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal); + } + + return false; + } + + private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children) + { + var list = GetInternalMetadataPaths(item); + foreach (var child in children) + { + list.AddRange(GetInternalMetadataPaths(child)); + } + + return list; + } + + private List<string> GetInternalMetadataPaths(BaseItem item) { var list = new List<string> { item.GetInternalMetadataPath() }; - list.AddRange(children.Select(i => i.GetInternalMetadataPath())); + if (item is Video video) + { + // Trickplay + list.Add(_pathManager.GetTrickplayDirectory(video)); + + // Subtitles and attachments + foreach (var mediaSource in item.GetMediaSources(false)) + { + var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id); + if (subtitleFolder is not null) + { + list.Add(subtitleFolder); + } + + var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + if (attachmentFolder is not null) + { + list.Add(attachmentFolder); + } + } + } return list; } @@ -592,7 +663,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); - files = Array.Empty<FileSystemMetadata>(); + files = []; } else { @@ -600,7 +671,7 @@ namespace Emby.Server.Implementations.Library } } - // Need to remove subpaths that may have been resolved from shortcuts + // Need to remove sub-paths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action if (isPhysicalRoot) { @@ -610,10 +681,11 @@ namespace Emby.Server.Implementations.Library args.FileSystemChildren = files; } - // Check to see if we should resolve based on our contents - if (args.IsDirectory && !ShouldResolvePathContents(args)) + // Filter content based on ignore rules + if (args.IsDirectory) { - return null; + var filtered = args.GetActualFileSystemChildren().ToArray(); + args.FileSystemChildren = filtered ?? []; } return ResolveItem(args, resolvers); @@ -628,10 +700,10 @@ namespace Emby.Server.Implementations.Library var list = originalList.Where(i => i.IsDirectory) .Select(i => Path.TrimEndingDirectorySeparator(i.FullName)) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .ToList(); - var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) + var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) .ToList(); foreach (var dupe in dupes) @@ -639,22 +711,11 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Found duplicate path: {0}", dupe); } - var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList(); + var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList(); newList.AddRange(originalList.Where(i => !i.IsDirectory)); return newList; } - /// <summary> - /// Determines whether a path should be ignored based on its contents - called after the contents have been read. - /// </summary> - /// <param name="args">The args.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - private static bool ShouldResolvePathContents(ItemResolveArgs args) - { - // Ignore any folders containing a file called .ignore - return !args.ContainsFileSystemEntryByName(".ignore"); - } - public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); @@ -729,8 +790,6 @@ namespace Emby.Server.Implementations.Library { var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath; - Directory.CreateDirectory(rootFolderPath); - var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong")) .DeepCopy<Folder, AggregateFolder>(); @@ -745,11 +804,12 @@ namespace Emby.Server.Implementations.Library // Add in the plug-in folders var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists"); - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); Folder folder = new PlaylistsFolder { - Path = path + Path = path, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, }; if (folder.Id.IsEmpty()) @@ -835,7 +895,7 @@ namespace Emby.Server.Implementations.Library { Path = path, IsFolder = isFolder, - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)], Limit = 1, DtoOptions = new DtoOptions(true) }; @@ -941,7 +1001,7 @@ namespace Emby.Server.Implementations.Library { var existing = GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, + IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, DtoOptions = options }).Cast<MusicArtist>() @@ -960,12 +1020,13 @@ namespace Emby.Server.Implementations.Library var item = GetItemById(id) as T; if (item is null) { + var info = Directory.CreateDirectory(path); item = new T { Name = name, Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Path = path }; @@ -1091,7 +1152,7 @@ namespace Emby.Server.Implementations.Library /// <returns>Task.</returns> private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken) { - var tasks = PostscanTasks.ToList(); + var tasks = PostScanTasks.ToList(); var numComplete = 0; var numTasks = tasks.Count; @@ -1214,7 +1275,7 @@ namespace Emby.Server.Implementations.Library private CollectionTypeOptions? GetCollectionType(string path) { - var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false); + var files = _fileSystem.GetFilePaths(path, [".collection"], true, false); foreach (ReadOnlySpan<char> file in files) { if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res)) @@ -1234,7 +1295,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_cache.TryGetValue(id, out BaseItem? item)) + if (_cache.TryGet(id, out var item)) { return item; } @@ -1251,7 +1312,7 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public T? GetItemById<T>(Guid id) - where T : BaseItem + where T : BaseItem { var item = GetItemById(id); if (item is T typedItem) @@ -1285,7 +1346,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1316,7 +1377,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1343,6 +1404,36 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemList(query); } + public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, CollectionType collectionType) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetLatestItemList(query, collectionType); + } + + public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff); + } + public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) { if (query.User is not null) @@ -1447,7 +1538,7 @@ namespace Emby.Server.Implementations.Library // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); - query.AncestorIds = Array.Empty<Guid>(); + query.AncestorIds = []; // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) @@ -1474,7 +1565,7 @@ namespace Emby.Server.Implementations.Library var parent = GetItemById(query.ParentId); if (parent is not null) { - SetTopParentIdsOrAncestors(query, new[] { parent }); + SetTopParentIdsOrAncestors(query, [parent]); } } @@ -1504,7 +1595,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } else @@ -1515,7 +1606,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.AncestorIds.Length == 0) { - query.AncestorIds = new[] { Guid.NewGuid() }; + query.AncestorIds = [Guid.NewGuid()]; } } @@ -1544,7 +1635,7 @@ namespace Emby.Server.Implementations.Library // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) { - query.TopParentIds = new[] { Guid.NewGuid() }; + query.TopParentIds = [Guid.NewGuid()]; } } } @@ -1555,7 +1646,7 @@ namespace Emby.Server.Implementations.Library { if (view.ViewType == CollectionType.livetv) { - return new[] { view.Id }; + return [view.Id]; } // Translate view into folders @@ -1567,7 +1658,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty<Guid>(); + return []; } if (!view.ParentId.IsEmpty()) @@ -1578,7 +1669,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty<Guid>(); + return []; } // Handle grouping @@ -1593,7 +1684,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return Array.Empty<Guid>(); + return []; } if (item is CollectionFolder collectionFolder) @@ -1604,10 +1695,10 @@ namespace Emby.Server.Implementations.Library var topParent = item.GetTopParent(); if (topParent is not null) { - return new[] { topParent.Id }; + return [topParent.Id]; } - return Array.Empty<Guid>(); + return []; } /// <summary> @@ -1651,7 +1742,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return Enumerable.Empty<IntroInfo>(); + return []; } } @@ -1800,7 +1891,7 @@ namespace Emby.Server.Implementations.Library userComparer.User = user; userComparer.UserManager = _userManager; - userComparer.UserDataRepository = _userDataRepository; + userComparer.UserDataManager = _userDataManager; return userComparer; } @@ -1811,7 +1902,7 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public void CreateItem(BaseItem item, BaseItem? parent) { - CreateItems(new[] { item }, parent, CancellationToken.None); + CreateItems([item], parent, CancellationToken.None); } /// <inheritdoc /> @@ -1863,7 +1954,7 @@ namespace Emby.Server.Implementations.Library try { - return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified; + return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeconds > 1; } catch (Exception ex) { @@ -1890,6 +1981,8 @@ namespace Emby.Server.Implementations.Library return; } + var anyChange = false; + foreach (var img in outdated) { var image = img; @@ -1921,6 +2014,7 @@ namespace Emby.Server.Implementations.Library try { size = _imageProcessor.GetImageDimensions(item, image); + anyChange = image.Width != size.Width || image.Height != size.Height; image.Width = size.Width; image.Height = size.Height; } @@ -1928,23 +2022,29 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); size = default; + anyChange = image.Width != size.Width || image.Height != size.Height; image.Width = 0; image.Height = 0; } try { - image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size); + var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size); + anyChange = anyChange || !blurhash.Equals(image.BlurHash, StringComparison.Ordinal); + image.BlurHash = blurhash; } catch (Exception ex) { _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path); + anyChange = anyChange || !string.IsNullOrEmpty(image.BlurHash); image.BlurHash = string.Empty; } try { - image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path); + var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path); + anyChange = anyChange || modifiedDate != image.DateModified; + image.DateModified = modifiedDate; } catch (Exception ex) { @@ -1952,20 +2052,28 @@ namespace Emby.Server.Implementations.Library } } - _itemRepository.SaveImages(item); + if (anyChange) + { + _itemRepository.SaveImages(item); + } + RegisterItem(item); } /// <inheritdoc /> public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - _itemRepository.SaveItems(items, cancellationToken); - foreach (var item in items) { + item.DateLastSaved = DateTime.UtcNow; await RunMetadataSavers(item, updateReason).ConfigureAwait(false); + + // Modify again, so saved value is after write time of externally saved metadata + item.DateLastSaved = DateTime.UtcNow; } + _itemRepository.SaveItems(items, cancellationToken); + if (ItemUpdated is not null) { foreach (var item in items) @@ -1997,7 +2105,7 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) - => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); + => UpdateItemsAsync([item], parent, updateReason, cancellationToken); public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { @@ -2006,8 +2114,6 @@ namespace Emby.Server.Implementations.Library await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false); } - item.DateLastSaved = DateTime.UtcNow; - await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); } @@ -2226,13 +2332,13 @@ namespace Emby.Server.Implementations.Library if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, ForcedSortName = sortName @@ -2274,13 +2380,13 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, ForcedSortName = sortName, @@ -2293,12 +2399,13 @@ namespace Emby.Server.Implementations.Library isNew = true; } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2338,31 +2445,31 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; - CreateItem(item, null); isNew = true; } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2408,20 +2515,19 @@ namespace Emby.Server.Implementations.Library if (item is null) { - Directory.CreateDirectory(path); - + var info = Directory.CreateDirectory(path); item = new UserView { Path = path, Id = id, - DateCreated = DateTime.UtcNow, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, Name = name, ViewType = viewType, - ForcedSortName = sortName + ForcedSortName = sortName, + DisplayParentId = parentId }; - item.DisplayParentId = parentId; - CreateItem(item, null); isNew = true; @@ -2433,12 +2539,13 @@ namespace Emby.Server.Implementations.Library item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); } - var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; + var lastRefreshedUtc = item.DateLastRefreshed; + var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval; if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); - refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; + refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc; } if (refresh) @@ -2478,8 +2585,11 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> - public int? GetSeasonNumberFromPath(string path) - => SeasonPathParser.Parse(path, true, true).SeasonNumber; + public int? GetSeasonNumberFromPath(string path, Guid? parentId) + { + var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null; + return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber; + } /// <inheritdoc /> public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) @@ -2496,7 +2606,6 @@ namespace Emby.Server.Implementations.Library var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; - // TODO nullable - what are we trying to do there with empty episodeInfo? EpisodeInfo? episodeInfo = null; if (episode.IsFileProtocol) { @@ -2514,44 +2623,12 @@ namespace Emby.Server.Implementations.Library } } - episodeInfo ??= new EpisodeInfo(episode.Path); - - try - { - var libraryOptions = GetLibraryOptions(episode); - if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase)) - { - // Read from metadata - var mediaInfo = _mediaEncoder.GetMediaInfo( - new MediaInfoRequest - { - MediaSource = episode.GetMediaSources(false)[0], - MediaType = DlnaProfileType.Video - }, - CancellationToken.None).GetAwaiter().GetResult(); - if (mediaInfo.ParentIndexNumber > 0) - { - episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber; - } - - if (mediaInfo.IndexNumber > 0) - { - episodeInfo.EpisodeNumber = mediaInfo.IndexNumber; - } - - if (!string.IsNullOrEmpty(mediaInfo.ShowName)) - { - episodeInfo.SeriesName = mediaInfo.ShowName; - } - } - } - catch (Exception ex) + var changed = false; + if (episodeInfo is null) { - _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path); + return changed; } - var changed = false; - if (episodeInfo.IsByDate) { if (episode.IndexNumber.HasValue) @@ -2630,15 +2707,6 @@ namespace Emby.Server.Implementations.Library { episode.ParentIndexNumber = season.IndexNumber; } - else - { - /* - Anime series don't generally have a season in their file name, however, - TVDb needs a season to correctly get the metadata. - Hence, a null season needs to be filled with something. */ - // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified - episode.ParentIndexNumber = 1; - } if (episode.ParentIndexNumber.HasValue) { @@ -2663,27 +2731,33 @@ namespace Emby.Server.Implementations.Library public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions); + // Apply .ignore rules + var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList(); + var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath); if (ownerVideoInfo is null) { yield break; } - var count = fileSystemChildren.Count; + var count = filtered.Count; for (var i = 0; i < count; i++) { - var current = fileSystemChildren[i]; + var current = filtered[i]; if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false); - foreach (var file in filesInSubFolder) + var filesInSubFolderList = filesInSubFolder.ToList(); + + bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1; + + foreach (var file in filesInSubFolderList) { if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType)) { continue; } - var extra = GetExtra(file, extraType.Value); + var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder); if (extra is not null) { yield return extra; @@ -2692,7 +2766,7 @@ namespace Emby.Server.Implementations.Library } else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType)) { - var extra = GetExtra(current, extraType.Value); + var extra = GetExtra(current, extraType.Value, false); if (extra is not null) { yield return extra; @@ -2700,7 +2774,7 @@ namespace Emby.Server.Implementations.Library } } - BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType) + BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder) { var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType)); if (extra is not Video && extra is not Audio) @@ -2723,6 +2797,7 @@ namespace Emby.Server.Implementations.Library extra.ParentId = Guid.Empty; extra.OwnerId = owner.Id; + extra.IsInMixedFolder = isInMixedFolder; return extra; } } @@ -2853,7 +2928,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - name = _fileSystem.GetValidFilename(name); + name = _fileSystem.GetValidFilename(name.Trim()); var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; @@ -2887,7 +2962,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values? - await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false); + FileHelper.CreateEmpty(path); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); @@ -2930,19 +3005,28 @@ namespace Emby.Server.Implementations.Library if (personEntity is null) { - var path = Person.GetPath(person.Name); - personEntity = new Person() + try { - Name = person.Name, - Id = GetItemByNameId<Person>(path), - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - Path = path - }; - - personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); - saveEntity = true; - createEntity = true; + var path = Person.GetPath(person.Name); + var info = Directory.CreateDirectory(path); + personEntity = new Person() + { + Name = person.Name, + Id = GetItemByNameId<Person>(path), + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc, + Path = path + }; + + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); + saveEntity = true; + createEntity = true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to create person {Name}", person.Name); + continue; + } } foreach (var id in person.ProviderIds) @@ -2976,6 +3060,8 @@ namespace Emby.Server.Implementations.Library } await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + personEntity.DateLastSaved = DateTime.UtcNow; + CreateItems([personEntity], null, CancellationToken.None); } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 5795c47cc..ab30971e2 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -13,8 +13,10 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; @@ -425,6 +427,7 @@ namespace Emby.Server.Implementations.Library if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index)) { source.DefaultAudioStreamIndex = index; + source.DefaultAudioIndexSource = AudioIndexSource.User; return; } } @@ -432,6 +435,15 @@ namespace Emby.Server.Implementations.Library var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); + if (user.PlayDefaultAudioTrack) + { + source.DefaultAudioIndexSource |= AudioIndexSource.Default; + } + + if (preferredAudio.Count > 0) + { + source.DefaultAudioIndexSource |= AudioIndexSource.Language; + } } public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) @@ -669,17 +681,17 @@ namespace Emby.Server.Implementations.Library mediaInfo = await _mediaEncoder.GetMediaInfo( new MediaInfoRequest - { - MediaSource = mediaSource, - MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, - ExtractChapters = false - }, + { + MediaSource = mediaSource, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false + }, cancellationToken).ConfigureAwait(false); if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - FileStream createStream = File.Create(cacheFilePath); + FileStream createStream = AsyncFile.Create(cacheFilePath); await using (createStream.ConfigureAwait(false)) { await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); @@ -782,9 +794,13 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(id); - // TODO probably shouldn't throw here but it is kept for "backwards compatibility" - var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); - return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider)); + var info = GetLiveStreamInfo(id); + if (info is null) + { + return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null)); + } + + return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider)); } public ILiveStream GetLiveStreamInfo(string id) diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index ea223e3ec..631179ffc 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Entities; @@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library return null; } + // Sort in the following order: Default > No tag > Forced var sortedStreams = streams .Where(i => i.Type == MediaStreamType.Subtitle) .OrderByDescending(x => x.IsExternal) - .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(x => x.IsForced) .ThenByDescending(x => x.IsDefault) - .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language)) + .ThenByDescending(x => x.IsForced) .ToList(); MediaStream? stream = null; + if (mode == SubtitlePlaybackMode.Default) { - // Load subtitles according to external, forced and default flags. - stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Load subtitles according to external, default and forced flags. + stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced); } else if (mode == SubtitlePlaybackMode.Smart) { // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages. - // If no subtitles of preferred language available, use default behaviour. + // If no subtitles of preferred language available, use none. + // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages)); } else { - // Respect forced flag. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour. - stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour. + stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ?? + BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Only load subtitles that are flagged forced. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } return stream?.Index; @@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic - filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault) + // Load subtitles according to external, default, and forced flags. + filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced) .ToList(); } else if (mode == SubtitlePlaybackMode.Smart) { // Prefer smart logic over embedded metadata + // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) + filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) .ToList(); } + else + { + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); + } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior. + filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); } - // Load forced subs if we have found no suitable full subtitles - var iterStreams = filteredStreams is null || filteredStreams.Count == 0 - ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - : filteredStreams; + // If filteredStreams is null, initialize it as an empty list to avoid null reference errors + filteredStreams ??= new List<MediaStream>(); - foreach (var stream in iterStreams) + foreach (var stream in filteredStreams) { stream.Score = GetStreamScore(stream, preferredLanguages); } } + private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages) + { + // If preferredLanguages is empty, treat it as "any language" (wildcard) + return preferredLanguages.Count == 0 || + preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsLanguageUndefined(string language) + { + // Check for null, empty, or known placeholders + return string.IsNullOrEmpty(language) || + language.Equals("und", StringComparison.OrdinalIgnoreCase) || + language.Equals("unknown", StringComparison.OrdinalIgnoreCase) || + language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) || + language.Equals("mul", StringComparison.OrdinalIgnoreCase) || + language.Equals("zxx", StringComparison.OrdinalIgnoreCase); + } + + private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages) + { + return sortedStreams + .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language))) + .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ThenByDescending(s => IsLanguageUndefined(s.Language)) + .ToList(); + } + internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences) { var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 71c69ec50..28cf69500 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -4,8 +4,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs new file mode 100644 index 000000000..a9b7a1274 --- /dev/null +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; + +namespace Emby.Server.Implementations.Library; + +/// <summary> +/// IPathManager implementation. +/// </summary> +public class PathManager : IPathManager +{ + private readonly IServerConfigurationManager _config; + private readonly IApplicationPaths _appPaths; + + /// <summary> + /// Initializes a new instance of the <see cref="PathManager"/> class. + /// </summary> + /// <param name="config">The server configuration manager.</param> + /// <param name="appPaths">The application paths.</param> + public PathManager( + IServerConfigurationManager config, + IApplicationPaths appPaths) + { + _config = config; + _appPaths = appPaths; + } + + private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); + + private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); + + /// <inheritdoc /> + public string GetAttachmentPath(string mediaSourceId, string fileName) + { + return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName); + } + + /// <inheritdoc /> + public string GetAttachmentFolderPath(string mediaSourceId) + { + var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return Path.Join(AttachmentCachePath, id[..2], id); + } + + /// <inheritdoc /> + public string GetSubtitleFolderPath(string mediaSourceId) + { + var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return Path.Join(SubtitleCachePath, id[..2], id); + } + + /// <inheritdoc /> + public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension) + { + return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension); + } + + /// <inheritdoc /> + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false) + { + var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay")) + : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id); + } + + /// <inheritdoc/> + public string GetChapterImageFolderPath(BaseItem item) + { + return Path.Combine(item.GetInternalMetadataPath(), "chapters"); + } + + /// <inheritdoc/> + public string GetChapterImagePath(BaseItem item, long chapterPositionTicks) + { + var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; + + return Path.Combine(GetChapterImageFolderPath(item), filename); + } + + /// <inheritdoc/> + public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item) + { + var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture); + return [ + GetAttachmentFolderPath(mediaSourceId), + GetSubtitleFolderPath(mediaSourceId), + GetTrickplayDirectory(item, false), + GetTrickplayDirectory(item, true), + GetChapterImageFolderPath(item) + ]; + } +} diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs index c9e3a4daf..06aa772bd 100644 --- a/Emby.Server.Implementations/Library/ResolverHelper.cs +++ b/Emby.Server.Implementations/Library/ResolverHelper.cs @@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library if (config.UseFileCreationTimeForDateAdded) { - // directoryService.getFile may return null - if (info is not null) + var fileCreationDate = info?.CreationTimeUtc; + if (fileCreationDate is not null) { - var dateCreated = info.CreationTimeUtc; - - if (dateCreated.Equals(DateTime.MinValue)) + var dateCreated = fileCreationDate; + if (dateCreated == DateTime.MinValue) { dateCreated = DateTime.UtcNow; } - item.DateCreated = dateCreated; + item.DateCreated = dateCreated.Value; } } else { item.DateCreated = DateTime.UtcNow; } + + if (info is not null && !info.IsDirectory) + { + item.Size = info.Length; + } + + var fileModificationDate = info?.LastWriteTimeUtc; + if (fileModificationDate.HasValue) + { + item.DateModified = fileModificationDate.Value; + } } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index b4791b945..b9f9f2972 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers _ => _videoResolvers }; - public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType) + public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "") { - var extraResult = GetExtraInfo(path, _namingOptions); + var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot); if (extraResult.ExtraType is null) { extraType = null; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 4debe722b..b2ceee97d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } var videoInfos = files - .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName)) + .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath)) .Where(f => f is not null) .ToList(); - var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName); + var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath); var result = new MultiItemResolverResult { @@ -456,12 +456,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { var videoPath = result.Items[0].Path; var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); + var hasOtherSubfolders = multiDiscFolders.Count > 0; - if (!hasPhotos) + if (!hasPhotos && !hasOtherSubfolders) { var movie = (T)result.Items[0]; movie.IsInMixedFolder = false; - movie.Name = Path.GetFileName(movie.ContainingFolderPath); + if (collectionType == CollectionType.movies || collectionType is null) + { + movie.Name = Path.GetFileName(movie.ContainingFolderPath); + } + return movie; } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index abf2d0115..6cb63a28a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; - var seasonParserResult = SeasonPathParser.Parse(path, true, true); + var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true); var season = new Season { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index fb48d7bf1..c81a0adb8 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (child.IsDirectory) { - if (IsSeasonFolder(child.FullName, isTvContentType)) + if (IsSeasonFolder(child.FullName, path, isTvContentType)) { _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; @@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// Determines whether [is season folder] [the specified path]. /// </summary> /// <param name="path">The path.</param> + /// <param name="parentPath">The parentpath.</param> /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param> /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns> - private static bool IsSeasonFolder(string path, bool isTvContentType) + private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType) { - var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 3ac1d0219..9d81b835c 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 76e564d53..71ce3b601 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -4,13 +4,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library; @@ -77,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask CollapseBoxSetItems = false, Recursive = true, DtoOptions = new DtoOptions(false), - ImageTypes = new[] { imageType }, + ImageTypes = [imageType], Limit = 30, // TODO max parental rating configurable - MaxParentalRating = 10, - OrderBy = new[] - { + MaxParentalRating = new(10, null), + OrderBy = + [ (ItemSortBy.Random, SortOrder.Ascending) - }, - IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } + ], + IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series] }); } } diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a41ef888b..be1d96bf0 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -1,14 +1,13 @@ #pragma warning disable RS0030 // Do not use banned APIs using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; -using Jellyfin.Extensions; -using Jellyfin.Server.Implementations; +using BitFaster.Caching.Lru; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library /// </summary> public class UserDataManager : IUserDataManager { - private readonly ConcurrentDictionary<string, UserItemData> _userData = - new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); - private readonly IServerConfigurationManager _config; private readonly IDbContextFactory<JellyfinDbContext> _repository; + private readonly FastConcurrentLru<string, UserItemData> _cache; /// <summary> /// Initializes a new instance of the <see cref="UserDataManager"/> class. @@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library { _config = config; _repository = repository; + _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); - _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData); + _cache.AddOrUpdate(cacheKey, userData); UserDataSaved?.Invoke(this, new UserDataSaveEventArgs { @@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library { var cacheKey = GetCacheKey(user.InternalId, itemId); - if (_userData.TryGetValue(cacheKey, out var data)) + if (_cache.TryGet(cacheKey, out var data)) { return data; } @@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library }; } - return _userData.GetOrAdd(cacheKey, data); + return _cache.GetOrAdd(cacheKey, _ => data); } private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys) diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index d42a0e7d2..87214c273 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -6,8 +6,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -370,6 +372,21 @@ namespace Emby.Server.Implementations.Library MediaTypes = mediaTypes }; + if (request.GroupItems) + { + if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows)) + { + query.Limit = limit; + return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows); + } + + if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music)) + { + query.Limit = limit; + return _libraryManager.GetLatestItemList(query, parents, CollectionType.music); + } + } + return _libraryManager.GetItemList(query, parents); } } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs index d51f9aaa7..a31d5ecca 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class ArtistsPostScanTask. +/// </summary> +public class ArtistsPostScanTask : ILibraryPostScanTask { /// <summary> - /// Class ArtistsPostScanTask. + /// The _library manager. /// </summary> - public class ArtistsPostScanTask : ILibraryPostScanTask - { - /// <summary> - /// The _library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - private readonly ILogger<ArtistsValidator> _logger; - private readonly IItemRepository _itemRepo; + private readonly ILibraryManager _libraryManager; + private readonly ILogger<ArtistsValidator> _logger; + private readonly IItemRepository _itemRepo; - /// <summary> - /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public ArtistsPostScanTask( - ILibraryManager libraryManager, - ILogger<ArtistsValidator> logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public ArtistsPostScanTask( + ILibraryManager libraryManager, + ILogger<ArtistsValidator> logger, + IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7591e8391..7cc851b73 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class ArtistsValidator. +/// </summary> +public class ArtistsValidator { /// <summary> - /// Class ArtistsValidator. + /// The library manager. /// </summary> - public class ArtistsValidator - { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager; - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<ArtistsValidator> _logger; - private readonly IItemRepository _itemRepo; + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<ArtistsValidator> _logger; + private readonly IItemRepository _itemRepo; - /// <summary> - /// Initializes a new instance of the <see cref="ArtistsValidator" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="ArtistsValidator" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetAllArtistNames(); + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetAllArtistNames(); - var numComplete = 0; - var count = names.Count; + var numComplete = 0; + var count = names.Count; - foreach (var name in names) + foreach (var name in names) + { + try { - try - { - var item = _libraryManager.GetArtist(name); - - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {ArtistName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; + var item = _libraryManager.GetArtist(name); - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); } - - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + catch (OperationCanceledException) { - IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, - IsDeadArtist = true, - IsLocked = false - }).Cast<MusicArtist>().ToList(); - - foreach (var item in deadEntities) + // Don't clutter the log + throw; + } + catch (Exception ex) { - if (!item.IsAccessedByName) - { - continue; - } + _logger.LogError(ex, "Error refreshing {ArtistName}", name); + } - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + progress.Report(percent); + } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist], + IsDeadArtist = true, + IsLocked = false + }).Cast<MusicArtist>().ToList(); + + foreach (var item in deadEntities) + { + if (!item.IsAccessedByName) + { + continue; } - progress.Report(100); + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index 89f64ee4f..e62c638ed 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -4,153 +4,150 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class CollectionPostScanTask. +/// </summary> +public class CollectionPostScanTask : ILibraryPostScanTask { + private readonly ILibraryManager _libraryManager; + private readonly ICollectionManager _collectionManager; + private readonly ILogger<CollectionPostScanTask> _logger; + /// <summary> - /// Class CollectionPostScanTask. + /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class. /// </summary> - public class CollectionPostScanTask : ILibraryPostScanTask + /// <param name="libraryManager">The library manager.</param> + /// <param name="collectionManager">The collection manager.</param> + /// <param name="logger">The logger.</param> + public CollectionPostScanTask( + ILibraryManager libraryManager, + ICollectionManager collectionManager, + ILogger<CollectionPostScanTask> logger) { - private readonly ILibraryManager _libraryManager; - private readonly ICollectionManager _collectionManager; - private readonly ILogger<CollectionPostScanTask> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="collectionManager">The collection manager.</param> - /// <param name="logger">The logger.</param> - public CollectionPostScanTask( - ILibraryManager libraryManager, - ICollectionManager collectionManager, - ILogger<CollectionPostScanTask> logger) - { - _libraryManager = libraryManager; - _collectionManager = collectionManager; - _logger = logger; - } + _libraryManager = libraryManager; + _collectionManager = collectionManager; + _logger = logger; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>(); + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>(); - foreach (var library in _libraryManager.RootFolder.Children) + foreach (var library in _libraryManager.RootFolder.Children) + { + if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) { - if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) - { - continue; - } + continue; + } - var startIndex = 0; - var pagesize = 1000; + var startIndex = 0; + var pagesize = 1000; - while (true) + while (true) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery { - var movies = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = new[] { MediaType.Video }, - IncludeItemTypes = new[] { BaseItemKind.Movie }, - IsVirtualItem = false, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, - Parent = library, - StartIndex = startIndex, - Limit = pagesize, - Recursive = true - }); - - foreach (var m in movies) + MediaTypes = [MediaType.Video], + IncludeItemTypes = [BaseItemKind.Movie], + IsVirtualItem = false, + OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)], + Parent = library, + StartIndex = startIndex, + Limit = pagesize, + Recursive = true + }); + + foreach (var m in movies) + { + if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) { - if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) + if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) { - if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) - { - movieList.Add(movie.Id); - } - else - { - collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id }; - } + movieList.Add(movie.Id); + } + else + { + collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id }; } } + } - if (movies.Count < pagesize) - { - break; - } - - startIndex += pagesize; + if (movies.Count < pagesize) + { + break; } + + startIndex += pagesize; } + } - var numComplete = 0; - var count = collectionNameMoviesMap.Count; + var numComplete = 0; + var count = collectionNameMoviesMap.Count; - if (count == 0) - { - progress.Report(100); - return; - } + if (count == 0) + { + progress.Report(100); + return; + } - var boxSets = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.BoxSet }, - CollapseBoxSetItems = false, - Recursive = true - }); + var boxSets = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.BoxSet], + CollapseBoxSetItems = false, + Recursive = true + }); - foreach (var (collectionName, movieIds) in collectionNameMoviesMap) + foreach (var (collectionName, movieIds) in collectionNameMoviesMap) + { + try { - try + var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; + if (boxSet is null) { - var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; - if (boxSet is null) + // won't automatically create collection if only one movie in it + if (movieIds.Count >= 2) { - // won't automatically create collection if only one movie in it - if (movieIds.Count >= 2) + boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions { - boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions - { - Name = collectionName, - IsLocked = true - }); + Name = collectionName, + }).ConfigureAwait(false); - await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); - } + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false); } - else - { - await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); } - catch (Exception ex) + else { - _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false); } - } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); + } } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs index d21d2887b..5097e0073 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class GenresPostScanTask. +/// </summary> +public class GenresPostScanTask : ILibraryPostScanTask { /// <summary> - /// Class GenresPostScanTask. + /// The _library manager. /// </summary> - public class GenresPostScanTask : ILibraryPostScanTask - { - /// <summary> - /// The _library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - private readonly ILogger<GenresValidator> _logger; - private readonly IItemRepository _itemRepo; + private readonly ILibraryManager _libraryManager; + private readonly ILogger<GenresValidator> _logger; + private readonly IItemRepository _itemRepo; - /// <summary> - /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public GenresPostScanTask( - ILibraryManager libraryManager, - ILogger<GenresValidator> logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public GenresPostScanTask( + ILibraryManager libraryManager, + ILogger<GenresValidator> logger, + IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index e59c62e23..fbfc9f7d5 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -1,81 +1,103 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class GenresValidator. +/// </summary> +public class GenresValidator { /// <summary> - /// Class GenresValidator. + /// The library manager. + /// </summary> + private readonly ILibraryManager _libraryManager; + private readonly IItemRepository _itemRepo; + + /// <summary> + /// The logger. /// </summary> - public class GenresValidator + private readonly ILogger<GenresValidator> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="GenresValidator"/> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo) { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<GenresValidator> _logger; + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetGenreNames(); - /// <summary> - /// Initializes a new instance of the <see cref="GenresValidator"/> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + var numComplete = 0; + var count = names.Count; - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + foreach (var name in names) { - var names = _itemRepo.GetGenreNames(); - - var numComplete = 0; - var count = names.Count; + try + { + var item = _libraryManager.GetGenre(name); - foreach (var name in names) + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) { - try - { - var item = _libraryManager.GetGenre(name); + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {GenreName}", name); + } - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {GenreName}", name); - } + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; + progress.Report(percent); + } - progress.Report(percent); - } + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre], + IsDeadGenre = true, + IsLocked = false + }); - progress.Report(100); + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs index be119866b..76658a81b 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class MusicGenresPostScanTask. +/// </summary> +public class MusicGenresPostScanTask : ILibraryPostScanTask { /// <summary> - /// Class MusicGenresPostScanTask. + /// The library manager. /// </summary> - public class MusicGenresPostScanTask : ILibraryPostScanTask - { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - private readonly ILogger<MusicGenresValidator> _logger; - private readonly IItemRepository _itemRepo; + private readonly ILibraryManager _libraryManager; + private readonly ILogger<MusicGenresValidator> _logger; + private readonly IItemRepository _itemRepo; - /// <summary> - /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public MusicGenresPostScanTask( - ILibraryManager libraryManager, - ILogger<MusicGenresValidator> logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public MusicGenresPostScanTask( + ILibraryManager libraryManager, + ILogger<MusicGenresValidator> logger, + IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 1ecf4c87c..6203bce2b 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class MusicGenresValidator. +/// </summary> +public class MusicGenresValidator { /// <summary> - /// Class MusicGenresValidator. + /// The library manager. /// </summary> - public class MusicGenresValidator - { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager; - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<MusicGenresValidator> _logger; - private readonly IItemRepository _itemRepo; + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<MusicGenresValidator> _logger; + private readonly IItemRepository _itemRepo; - /// <summary> - /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetMusicGenreNames(); + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetMusicGenreNames(); - var numComplete = 0; - var count = names.Count; + var numComplete = 0; + var count = names.Count; - foreach (var name in names) + foreach (var name in names) + { + try { - try - { - var item = _libraryManager.GetMusicGenre(name); + var item = _libraryManager.GetMusicGenre(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {GenreName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {GenreName}", name); } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 725b8f76c..b7fd24fa5 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class PeopleValidator. +/// </summary> +public class PeopleValidator { /// <summary> - /// Class PeopleValidator. + /// The _library manager. /// </summary> - public class PeopleValidator + private readonly ILibraryManager _libraryManager; + + /// <summary> + /// The _logger. + /// </summary> + private readonly ILogger _logger; + + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="PeopleValidator" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The file system.</param> + public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) { - /// <summary> - /// The _library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - - /// <summary> - /// The _logger. - /// </summary> - private readonly ILogger _logger; - - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="PeopleValidator" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="fileSystem">The file system.</param> - public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) - { - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - } + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + } - /// <summary> - /// Validates the people. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) - { - var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); + /// <summary> + /// Validates the people. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) + { + var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); - var numComplete = 0; + var numComplete = 0; - var numPeople = people.Count; + var numPeople = people.Count; - _logger.LogDebug("Will refresh {0} people", numPeople); + _logger.LogDebug("Will refresh {Amount} people", numPeople); - foreach (var person in people) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var person in people) + { + cancellationToken.ThrowIfCancellationRequested(); - try - { - var item = _libraryManager.GetPerson(person); - if (item is null) - { - _logger.LogWarning("Failed to get person: {Name}", person); - continue; - } - - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ImageRefreshMode = MetadataRefreshMode.ValidationOnly, - MetadataRefreshMode = MetadataRefreshMode.ValidationOnly - }; - - await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) + try + { + var item = _libraryManager.GetPerson(person); + if (item is null) { - _logger.LogError(ex, "Error validating IBN entry {Person}", person); + _logger.LogWarning("Failed to get person: {Name}", person); + continue; } - // Update progress - numComplete++; - double percent = numComplete; - percent /= numPeople; + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ImageRefreshMode = MetadataRefreshMode.ValidationOnly, + MetadataRefreshMode = MetadataRefreshMode.ValidationOnly + }; - progress.Report(100 * percent); + await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); } - - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + catch (OperationCanceledException) { - IncludeItemTypes = [BaseItemKind.Person], - IsDeadPerson = true, - IsLocked = false - }); - - foreach (var item in deadEntities) + throw; + } + catch (Exception ex) { - _logger.LogInformation( - "Deleting dead {2} {0} {1}.", - item.Id.ToString("N", CultureInfo.InvariantCulture), - item.Name, - item.GetType().Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + _logger.LogError(ex, "Error validating IBN entry {Person}", person); } - progress.Report(100); + // Update progress + numComplete++; + double percent = numComplete; + percent /= numPeople; - _logger.LogInformation("People validation complete"); + progress.Report(100 * percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Person], + IsDeadPerson = true, + 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); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); + + _logger.LogInformation("People validation complete"); } } diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs index c682b156b..67c56c104 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class MusicGenresPostScanTask. +/// </summary> +public class StudiosPostScanTask : ILibraryPostScanTask { /// <summary> - /// Class MusicGenresPostScanTask. + /// The _library manager. /// </summary> - public class StudiosPostScanTask : ILibraryPostScanTask - { - /// <summary> - /// The _library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager; - private readonly ILogger<StudiosValidator> _logger; - private readonly IItemRepository _itemRepo; + private readonly ILogger<StudiosValidator> _logger; + private readonly IItemRepository _itemRepo; - /// <summary> - /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public StudiosPostScanTask( - ILibraryManager libraryManager, - ILogger<StudiosValidator> logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public StudiosPostScanTask( + ILibraryManager libraryManager, + ILogger<StudiosValidator> logger, + IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 26bc49c1f..5b87e4d9d 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// <summary> +/// Class StudiosValidator. +/// </summary> +public class StudiosValidator { /// <summary> - /// Class StudiosValidator. + /// The library manager. /// </summary> - public class StudiosValidator - { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; + private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; + private readonly IItemRepository _itemRepo; - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<StudiosValidator> _logger; + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<StudiosValidator> _logger; - /// <summary> - /// Initializes a new instance of the <see cref="StudiosValidator" /> class. - /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } + /// <summary> + /// Initializes a new instance of the <see cref="StudiosValidator" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo) + { + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// <summary> - /// Runs the specified progress. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetStudioNames(); + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetStudioNames(); - var numComplete = 0; - var count = names.Count; + var numComplete = 0; + var count = names.Count; - foreach (var name in names) + foreach (var name in names) + { + try { - try - { - var item = _libraryManager.GetStudio(name); + var item = _libraryManager.GetStudio(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {StudioName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); } - - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + catch (OperationCanceledException) { - IncludeItemTypes = new[] { BaseItemKind.Studio }, - IsDeadStudio = true, - IsLocked = false - }); - - foreach (var item in deadEntities) + // Don't clutter the log + throw; + } + catch (Exception ex) { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + _logger.LogError(ex, "Error refreshing {StudioName}", name); } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Studio], + IsDeadStudio = true, + 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); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index e89ede10b..1dce58923 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -129,5 +129,11 @@ "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.", "TaskAudioNormalization": "Odio Normalisering", "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon", - "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie." + "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.", + "TaskDownloadMissingLyrics": "Laai tekorte lirieke af", + "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies", + "TaskExtractMediaSegments": "Media Segment Skandeer", + "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.", + "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging", + "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings." } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 2d29eb5bf..a92148caf 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -125,8 +125,8 @@ "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "External": "خارجي", "HearingImpaired": "ضعاف السمع", - "TaskRefreshTrickplayImages": "توليد صور Trickplay", - "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.", + "TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة", + "TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.", "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل", "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.", "TaskAudioNormalization": "تسوية الصوت", @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "فحص مقاطع الوسائط", "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.", "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة", - "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة." + "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.", + "CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم", + "CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل." } diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 97aa0ca58..dec491d08 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,6 +1,6 @@ { "Sync": "Сінхранізаваць", - "Playlists": "Плэйлісты", + "Playlists": "Спісы прайгравання", "Latest": "Апошні", "LabelIpAddressValue": "IP-адрас: {0}", "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", @@ -16,7 +16,7 @@ "Collections": "Калекцыі", "Default": "Па змаўчанні", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", - "Folders": "Папкі", + "Folders": "Тэчкі", "Favorites": "Абранае", "External": "Знешні", "Genres": "Жанры", @@ -135,5 +135,7 @@ "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень", "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", - "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay" + "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", + "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", + "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." } diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 72f575753..fd3666ef1 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.", "TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения", "TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.", - "TaskExtractMediaSegments": "Сканиране за сегменти" + "TaskExtractMediaSegments": "Сканиране за сегменти", + "CleanupUserDataTask": "Задача за почистване на потребителски данни", + "CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни." } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 268a141ff..fad3715f2 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -6,29 +6,29 @@ "Channels": "চ্যানেলসমূহ", "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে", "Books": "পুস্তকসমূহ", - "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল", + "AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন", "Artists": "শিল্পীগণ", "Application": "অ্যাপ্লিকেশন", "Albums": "অ্যালবামসমূহ", - "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো", + "HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো", "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ", - "Genres": "শৈলীধারাসমূহ", + "Genres": "জনরা", "Folders": "ফোল্ডারসমূহ", "Favorites": "পছন্দসমূহ", "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে", - "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}", + "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}", "VersionNumber": "সংস্করণ {0}", "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}", "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে", - "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}", - "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}", + "UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}", + "UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}", "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে", "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে", - "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন", - "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে", + "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে", + "UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে", "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না", "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে", "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে", @@ -36,8 +36,8 @@ "User": "ব্যবহারকারী", "TvShows": "টিভি শোগুলো", "System": "সিস্টেম", - "Sync": "সমলয় স্থাপন", - "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", + "Sync": "সমন্বয় করুন", + "SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", "Songs": "সঙ্গীতসমূহ", "Shows": "টিভি পর্ব", @@ -46,18 +46,18 @@ "ScheduledTaskFailedWithName": "{0} ব্যর্থ", "ProviderValue": "প্রদানকারী: {0}", "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে", - "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে", - "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে", + "PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে", + "PluginInstalledWithName": "{0} ইন্সটল হয়েছে", "Plugin": "প্লাগিন", "Playlists": "প্লে লিস্ট সমূহ", - "Photos": "চিত্রসমূহ", - "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ", - "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে", + "Photos": "ছবিসমূহ", + "NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে", + "NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে", "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না", "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ", - "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক", - "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে", - "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে", + "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে", + "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে", + "NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে", "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে", "NotificationOptionPluginError": "প্লাগিন ব্যর্থ", "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে", @@ -76,8 +76,8 @@ "Movies": "চলচ্চিত্রসমূহ", "MixedContent": "মিশ্র কন্টেন্ট", "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে", - "HeaderRecordingGroups": "রেকর্ডিং দল", - "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে", + "HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো", + "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে", "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে", "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে", "Latest": "সর্বশেষ", @@ -85,51 +85,57 @@ "LabelIpAddressValue": "আইপি এড্রেস: {0}", "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে", "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে", - "Inherit": "থেকে পাওয়া", + "Inherit": "মূল থেকে গ্রহণ করুন", "HomeVideos": "হোম ভিডিও", "HeaderNextUp": "এরপরে আসছে", "HeaderLiveTV": "লাইভ টিভি", "HeaderFavoriteSongs": "প্রিয় গানগুলো", "HeaderFavoriteShows": "প্রিয় শোগুলো", - "TasksLibraryCategory": "গ্রন্থাগার", + "TasksLibraryCategory": "লাইব্রেরি", "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ", "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি", - "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।", - "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন", - "TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।", + "TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।", + "TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন", + "TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।", "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি", "TasksChannelsCategory": "ইন্টারনেট চ্যানেল", - "TasksApplicationCategory": "আবেদন", + "TasksApplicationCategory": "অ্যাপ্লিকেশন", "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।", "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন", "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।", "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন", - "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।", + "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।", "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন", "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।", - "TaskUpdatePlugins": "প্লাগইন আপডেট করুন", - "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।", - "TaskRefreshPeople": "পিপল রিফ্রেশ করুন", - "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।", - "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন", - "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।", + "TaskUpdatePlugins": "আপডেট প্লাগইন", + "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।", + "TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ", + "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।", + "TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি", + "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।", "Undefined": "অসঙ্গায়িত", "Forced": "জোরকরে", - "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.", - "TaskCleanActivityLog": "কাজের ফাইল খালি করুন", + "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।", + "TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন", "Default": "ডিফল্ট", - "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য", + "HearingImpaired": "শ্রবণ প্রতিবন্ধী", "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।", "External": "বাহ্যিক", "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস", "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক", "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।", - "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন", + "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি", "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।", "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে", - "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন", - "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।", + "TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন", + "TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।", "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান", - "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।", - "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন" + "TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।", + "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন", + "TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।", + "TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।", + "CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।", + "TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন", + "TaskAudioNormalization": "অডিও নর্মলাইজেশন", + "CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ" } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 6cce0e019..596df6348 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -13,10 +13,10 @@ "DeviceOnlineWithName": "{0} està connectat", "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}", "Favorites": "Preferits", - "Folders": "Carpetes", + "Folders": "Directoris", "Genres": "Gèneres", "HeaderAlbumArtists": "Artistes de l'àlbum", - "HeaderContinueWatching": "Continua veient", + "HeaderContinueWatching": "Continueu mirant", "HeaderFavoriteAlbums": "Àlbums preferits", "HeaderFavoriteArtists": "Artistes preferits", "HeaderFavoriteEpisodes": "Episodis preferits", @@ -24,11 +24,11 @@ "HeaderFavoriteSongs": "Cançons preferides", "HeaderLiveTV": "TV en directe", "HeaderNextUp": "A continuació", - "HeaderRecordingGroups": "Grups Musicals", + "HeaderRecordingGroups": "Grups musicals", "HomeVideos": "Vídeos domèstics", "Inherit": "Heretat", - "ItemAddedWithName": "{0} s'ha afegit a la biblioteca", - "ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca", + "ItemAddedWithName": "{0} s'ha afegit a la mediateca", + "ItemRemovedWithName": "{0} s'ha eliminat de la mediateca", "LabelIpAddressValue": "Adreça IP: {0}", "LabelRunningTimeValue": "Temps en marxa: {0}", "Latest": "Darrers", @@ -43,7 +43,7 @@ "NameInstallFailed": "{0} instal·lació fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada desconeguda", - "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.", + "NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.", "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible", "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada", "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada", @@ -64,7 +64,7 @@ "Playlists": "Llistes de reproducció", "Plugin": "Complement", "PluginInstalledWithName": "{0} ha estat instal·lat", - "PluginUninstalledWithName": "S'ha instalat {0}", + "PluginUninstalledWithName": "S'ha instal·lat {0}", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", @@ -72,10 +72,10 @@ "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}", "Shows": "Sèries", "Songs": "Cançons", - "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.", + "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", - "Sync": "Sincronitzar", + "Sync": "Sincronitza", "System": "Sistema", "TvShows": "Sèries de TV", "User": "Usuari", @@ -89,52 +89,54 @@ "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}", "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}", - "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca", + "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versió {0}", "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.", - "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin", + "TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin", "TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.", "TaskRefreshChannels": "Actualitza els canals", - "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.", - "TaskCleanTranscode": "Neteja les transcodificacions", - "TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.", - "TaskUpdatePlugins": "Actualitza els complements", - "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.", - "TaskRefreshPeople": "Actualitza les persones", - "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.", - "TaskCleanLogs": "Neteja els registres", - "TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.", - "TaskRefreshLibrary": "Escaneja la biblioteca de mitjans", - "TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.", - "TaskRefreshChapterImages": "Extreure les imatges dels capítols", - "TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.", - "TaskCleanCache": "Elimina la memòria cau", + "TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.", + "TaskCleanTranscode": "Neteja de les transcodificacions", + "TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.", + "TaskUpdatePlugins": "Actualització dels complements", + "TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.", + "TaskRefreshPeople": "Actualització de les persones", + "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", + "TaskCleanLogs": "Neteja dels registres", + "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", + "TaskRefreshLibrary": "Escaneig de les mediateques", + "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.", + "TaskRefreshChapterImages": "Extracció de les imatges dels capítols", + "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", + "TaskCleanCache": "Eliminació de la memòria cau", "TasksChannelsCategory": "Canals per internet", "TasksApplicationCategory": "Aplicatiu", - "TasksLibraryCategory": "Biblioteca", + "TasksLibraryCategory": "Mediateca", "TasksMaintenanceCategory": "Manteniment", - "TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.", - "TaskCleanActivityLog": "Buidar el registre d'activitat", + "TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.", + "TaskCleanActivityLog": "Buidatge del registre d'activitat", "Undefined": "Indefinit", "Forced": "Forçat", "Default": "Per defecte", - "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", - "TaskOptimizeDatabase": "Optimitzar la base de dades", - "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", - "TaskKeyframeExtractor": "Extractor de fotogrames clau", + "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.", + "TaskOptimizeDatabase": "Optimització de la base de dades", + "TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.", + "TaskKeyframeExtractor": "Extracció de fotogrames clau", "External": "Extern", "HearingImpaired": "Discapacitat auditiva", - "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps", - "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.", + "TaskRefreshTrickplayImages": "Generació d'imatges de previsualització", + "TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.", "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", - "TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció", - "TaskAudioNormalization": "Estabilització d'Àudio", - "TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.", - "TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons", - "TaskDownloadMissingLyrics": "Baixar les lletres que falten", + "TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció", + "TaskAudioNormalization": "Estabilització de l'àudio", + "TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.", + "TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons", + "TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin", "TaskExtractMediaSegments": "Escaneig de segments multimèdia", "TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.", - "TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay", - "TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca." + "TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització", + "TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.", + "CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.", + "CleanupUserDataTask": "Tasca de neteja de dades d'usuari" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index ba2e2700d..e14edcffa 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skenování segmentů médií", "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.", "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay", - "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny." + "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", + "CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.", + "CleanupUserDataTask": "Pročistit uživatelská data" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index d43d4097f..bbee38ba5 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Scan for mediesegmenter", "TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder", "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.", - "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment." + "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.", + "CleanupUserDataTask": "Brugerdata oprydningsopgave", + "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage." } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index c38af5bf4..664da8249 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -43,7 +43,7 @@ "NameInstallFailed": "Installation von {0} fehlgeschlagen", "NameSeasonNumber": "Staffel {0}", "NameSeasonUnknown": "Staffel unbekannt", - "NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.", + "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.", "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar", "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert", "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet", @@ -72,12 +72,12 @@ "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden", "Shows": "Serien", "Songs": "Lieder", - "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.", + "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.", "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}", "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden", "Sync": "Synchronisation", "System": "System", - "TvShows": "TV-Sendungen", + "TvShows": "Serien", "User": "Benutzer", "UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserDeletedWithName": "Benutzer {0} wurde gelöscht", @@ -90,32 +90,32 @@ "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet", "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet", "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt", - "ValueSpecialEpisodeName": "Extra - {0}", + "ValueSpecialEpisodeName": "Extra – {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Suche im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", - "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter", - "TaskRefreshChannelsDescription": "Aktualisiere Internet-Kanal-Informationen.", - "TaskRefreshChannels": "Aktualisiere Kanäle", - "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, die älter als einen Tag sind.", - "TaskCleanTranscode": "Räume Transkodierungs-Verzeichnis auf", + "TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", + "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen", + "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", + "TaskRefreshChannels": "Kanäle aktualisieren", + "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", + "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", - "TaskUpdatePlugins": "Aktualisiere Plugins", + "TaskUpdatePlugins": "Plugins aktualisieren", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", - "TaskRefreshPeople": "Aktualisiere Personen", + "TaskRefreshPeople": "Personen aktualisieren", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", - "TaskCleanLogs": "Räumt Log-Verzeichnis auf", - "TaskRefreshLibraryDescription": "Scannt alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.", - "TaskRefreshLibrary": "Scanne Medien-Bibliothek", + "TaskCleanLogs": "Log-Verzeichnis aufräumen", + "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.", + "TaskRefreshLibrary": "Medien-Bibliothek scannen", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.", - "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder", - "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.", - "TaskCleanCache": "Leere Zwischenspeicher", + "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren", + "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.", + "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen", "TasksChannelsCategory": "Internet-Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", "TasksMaintenanceCategory": "Wartung", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", - "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen", + "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen", "Undefined": "Undefiniert", "Forced": "Erzwungen", "Default": "Standard", @@ -128,13 +128,15 @@ "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.", "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen", - "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.", + "TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.", "TaskAudioNormalization": "Audio Normalisierung", "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.", "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter", "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen", - "TaskExtractMediaSegments": "Scanne Mediensegmente", + "TaskExtractMediaSegments": "Mediensegmente scannen", "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.", "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren", - "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben." + "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.", + "CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten", + "CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind." } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 55f266032..f3195f0ea 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -11,7 +11,7 @@ "Collections": "Συλλογές", "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε", "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε", - "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}", + "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}", "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", @@ -27,8 +27,8 @@ "HeaderRecordingGroups": "Ομάδες Ηχογράφησης", "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", - "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", - "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", + "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη", + "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}", "Latest": "Πρόσφατα", @@ -40,7 +40,7 @@ "Movies": "Ταινίες", "Music": "Μουσική", "MusicVideos": "Μουσικά Βίντεο", - "NameInstallFailed": "{0} η εγκατάσταση απέτυχε", + "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε", "NameSeasonNumber": "Κύκλος {0}", "NameSeasonUnknown": "Άγνωστος Κύκλος", "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.", @@ -54,7 +54,7 @@ "NotificationOptionPluginError": "Αποτυχία του πρόσθετου", "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", - "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", + "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", @@ -63,9 +63,9 @@ "Photos": "Φωτογραφίες", "Playlists": "Λίστες αναπαραγωγής", "Plugin": "Πρόσθετο", - "PluginInstalledWithName": "{0} εγκαταστήθηκε", - "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", - "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", + "PluginInstalledWithName": "Το {0} εγκαταστάθηκε", + "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί", + "PluginUpdatedWithName": "Το {0} ενημερώθηκε", "ProviderValue": "Πάροχος: {0}", "ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskStartedWithName": "{0} ξεκίνησε", @@ -96,7 +96,7 @@ "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", - "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", + "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων", "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", @@ -125,7 +125,7 @@ "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", "External": "Εξωτερικό", "HearingImpaired": "Με προβλήματα ακοής", - "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay", + "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay", "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.", "TaskAudioNormalization": "Ομοιομορφία ήχου", "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index ca52ffb14..720f550b3 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Media Segment Scan", "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", - "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.", + "CleanupUserDataTask": "User data cleanup task", + "CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days." } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 9702ab712..c09d5af96 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegments": "Media Segment Scan", "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", - "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.", + "CleanupUserDataTask": "User data cleanup task", + "CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favorite status etc) from media that is no longer present for at least 90 days." } diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 0b595c2ca..42cce1096 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -122,5 +122,9 @@ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn", - "External": "Ekstera" + "External": "Ekstera", + "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.", + "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)", + "TaskAudioNormalization": "Normaligo Sonnivela", + "HearingImpaired": "Surda" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 661333d29..1ec5eaa2a 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.", "TaskExtractMediaSegments": "Escaneo de segmentos de medios", "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.", - "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay" + "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", + "CleanupUserDataTask": "Tarea de limpieza de datos del usuario", + "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días." } diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json index 4df4b90d3..c9a798cac 100644 --- a/Emby.Server.Implementations/Localization/Core/eu.json +++ b/Emby.Server.Implementations/Localization/Core/eu.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.", "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua", "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.", - "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu." + "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.", + "CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.", + "CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index c9f580cd5..0814e6223 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -135,5 +135,7 @@ "TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia", "TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.", "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti", - "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan." + "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.", + "CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä", + "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään." } diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index a10912f01..6d079d2f5 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.", "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes", "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay", - "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment." + "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.", + "CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.", + "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index c337d1932..8bf41c02a 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Analyse des segments de média", "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay", "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque." + "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.", + "CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.", + "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur" } diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json index b8e787c20..8c0ae8922 100644 --- a/Emby.Server.Implementations/Localization/Core/ga.json +++ b/Emby.Server.Implementations/Localization/Core/ga.json @@ -135,5 +135,7 @@ "TaskUpdatePlugins": "Nuashonraigh Breiseáin", "TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.", "TaskCleanTranscode": "Eolaire Transcode Glan", - "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh" + "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh", + "CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora", + "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad." } diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 3ba3e6679..ff6f6d232 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -123,5 +123,18 @@ "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.", "External": "Externo", "HearingImpaired": "Problemas de audición", - "TaskKeyframeExtractor": "Extractor de fragmentos" + "TaskKeyframeExtractor": "Extractor de fragmentos", + "TaskAudioNormalization": "Normalización do audio", + "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.", + "TaskDownloadMissingLyrics": "Descargar letras que faltan", + "TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións", + "TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución", + "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.", + "TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.", + "TaskExtractMediaSegments": "Escaneo de segmentos multimedia", + "TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay", + "TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.", + "TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay", + "TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.", + "CleanupUserDataTask": "Tarefa de limpeza de datos do usuario" } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 34d5cf050..90c921898 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -32,8 +32,8 @@ "LabelIpAddressValue": "Ip כתובת: {0}", "LabelRunningTimeValue": "משך צפייה: {0}", "Latest": "אחרון", - "MessageApplicationUpdated": "שרת ג'ליפין עודכן", - "MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}", + "MessageApplicationUpdated": "שרת Jellyfin עודכן", + "MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}", "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן", "MessageServerConfigurationUpdated": "תצורת השרת עודכנה", "MixedContent": "תוכן מעורב", @@ -43,7 +43,7 @@ "NameInstallFailed": "התקנת {0} נכשלה", "NameSeasonNumber": "עונה {0}", "NameSeasonUnknown": "עונה לא ידועה", - "NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.", + "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.", "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום", "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן", "NotificationOptionAudioPlayback": "ניגון שמע החל", @@ -72,7 +72,7 @@ "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש", "Shows": "סדרות", "Songs": "שירים", - "StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.", + "StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה", "Sync": "סנכרון", @@ -100,14 +100,14 @@ "TasksLibraryCategory": "ספרייה", "TasksMaintenanceCategory": "תחזוקה", "TaskUpdatePlugins": "עדכן תוספים", - "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.", + "TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.", "TaskRefreshPeople": "רענן אנשים", "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.", "TaskCleanLogs": "ניקוי תיקיית יומן", - "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", + "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", "TasksChannelsCategory": "ערוצי אינטרנט", - "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.", + "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.", "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות", "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.", "TaskRefreshChannels": "רענן ערוץ", @@ -125,16 +125,18 @@ "TaskKeyframeExtractor": "מחלץ תמונות מפתח", "External": "חיצוני", "HearingImpaired": "לקוי שמיעה", - "TaskRefreshTrickplayImages": "יצירת תמונות המחשה", - "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.", + "TaskRefreshTrickplayImages": "יצירת תמונות Trickplay", + "TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.", "TaskAudioNormalization": "נרמול שמע", "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.", "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.", "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה", "TaskDownloadMissingLyrics": "הורדת מילים חסרות", "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים", - "TaskMoveTrickplayImages": "העברת מיקום התמונות", + "TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay", "TaskExtractMediaSegments": "סריקת מדיה", "TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.", - "TaskMoveTrickplayImagesDescription": "הזזת קבצי טריקפליי קיימים בהתאם להגדרות הספרייה." + "TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.", + "CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.", + "CleanupUserDataTask": "משימת ניקוי מידע משתמש" } diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/he_IL.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index f205e8b64..81a996330 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,6 +1,6 @@ { "Albums": "Albumok", - "AppDeviceValues": "Program: {0}, eszköz: {1}", + "AppDeviceValues": "Program: {0}, Eszköz: {1}", "Application": "Alkalmazás", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", @@ -13,7 +13,7 @@ "DeviceOnlineWithName": "{0} belépett", "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}", "Favorites": "Kedvencek", - "Folders": "Könyvtárak", + "Folders": "Mappák", "Genres": "Műfajok", "HeaderAlbumArtists": "Albumelőadók", "HeaderContinueWatching": "Megtekintés folytatása", @@ -136,5 +136,7 @@ "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése", "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése", "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.", - "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből." + "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.", + "CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.", + "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat" } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index b925a482b..2a4281685 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -129,5 +129,13 @@ "TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.", "TaskAudioNormalization": "Normalisasi Audio", "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar", - "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada." + "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.", + "TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu", + "TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.", + "CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.", + "TaskExtractMediaSegments": "Scan Segmen media", + "TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay", + "TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang", + "CleanupUserDataTask": "Tugas Pembersihan Data Pengguna" } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 672c686fa..6f94df9d7 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -131,5 +131,8 @@ "TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista", "TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.", "TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög", - "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar" + "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar", + "TaskExtractMediaSegments": "Skönnun efnishluta", + "CleanupUserDataTask": "Hreinsun notendagagna", + "CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga." } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 297b3abce..421c4ee30 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -58,8 +58,8 @@ "NotificationOptionServerRestartRequired": "Riavvio del server necessario", "NotificationOptionTaskFailed": "Operazione pianificata fallita", "NotificationOptionUserLockedOut": "Utente bloccato", - "NotificationOptionVideoPlayback": "La riproduzione video è iniziata", - "NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta", + "NotificationOptionVideoPlayback": "Riproduzione video iniziata", + "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta", "Photos": "Foto", "Playlists": "Playlist", "Plugin": "Plugin", @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Sposta le immagini Trickplay", "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.", "TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.", - "TaskExtractMediaSegments": "Scansiona Segmento Media" + "TaskExtractMediaSegments": "Scansiona Segmento Media", + "CleanupUserDataTask": "Task di pulizia dei dati utente", + "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni." } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 14a576592..d564d54ce 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImages": "Trickplayの画像を移動", "TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。", "TaskDownloadMissingLyrics": "失われた歌詞をダウンロード", - "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。" + "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。", + "CleanupUserDataTask": "ユーザーデータのクリーンアップタスク", + "CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。" } diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json index 5e2b3756b..9f49be53b 100644 --- a/Emby.Server.Implementations/Localization/Core/kn.json +++ b/Emby.Server.Implementations/Localization/Core/kn.json @@ -25,7 +25,7 @@ "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ", "External": "ಹೊರಗಿನ", - "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ", + "FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}", "Favorites": "ಮೆಚ್ಚಿನವುಗಳು", "Folders": "ಫೋಲ್ಡರ್ಗಳು", "Forced": "ಬಲವಂತವಾಗಿ", @@ -123,5 +123,13 @@ "TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ", "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", "TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ", - "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ." + "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.", + "TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.", + "TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ", + "TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು", + "TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ", + "TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ", + "TaskRefreshTrickplayImages": "ಟ್ರಿಕ್ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ", + "TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ", + "TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ." } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 46fc49f5e..3918ab81c 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}", "Channels": "Kanalai", "ChapterNameValue": "Scena{0}", - "Collections": "Kolekcijos", + "Collections": "Rinkiniai", "DeviceOfflineWithName": "{0} buvo atjungtas", "DeviceOnlineWithName": "{0} prisijungęs", "FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti", @@ -17,18 +17,18 @@ "Genres": "Žanrai", "HeaderAlbumArtists": "Albumo atlikėjai", "HeaderContinueWatching": "Žiūrėti toliau", - "HeaderFavoriteAlbums": "Mėgstami Albumai", - "HeaderFavoriteArtists": "Mėgstami Atlikėjai", + "HeaderFavoriteAlbums": "Mėgstami albumai", + "HeaderFavoriteArtists": "Mėgstami atlikėjai", "HeaderFavoriteEpisodes": "Mėgstamiausios serijos", "HeaderFavoriteShows": "Mėgstamiausios TV Laidos", "HeaderFavoriteSongs": "Mėgstamos Dainos", "HeaderLiveTV": "Tiesioginė TV", - "HeaderNextUp": "Toliau eilėje", + "HeaderNextUp": "Toliau", "HeaderRecordingGroups": "Įrašų grupės", "HomeVideos": "Namų vaizdo įrašai", "Inherit": "Paveldėti", - "ItemAddedWithName": "{0} - buvo įkeltas į mediateką", - "ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos", + "ItemAddedWithName": "{0} - buvo įkeltas į biblioteką", + "ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos", "LabelIpAddressValue": "IP adresas: {0}", "LabelRunningTimeValue": "Trukmė: {0}", "Latest": "Naujausi", @@ -36,7 +36,7 @@ "MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti", "MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti", - "MixedContent": "Mixed content", + "MixedContent": "Mišrus turinys", "Movies": "Filmai", "Music": "Muzika", "MusicVideos": "Muzikiniai vaizdo įrašai", @@ -53,21 +53,21 @@ "NotificationOptionNewLibraryContent": "Naujas turinys įkeltas", "NotificationOptionPluginError": "Įskiepio klaida", "NotificationOptionPluginInstalled": "Įskiepis įdiegtas", - "NotificationOptionPluginUninstalled": "Įskiepis pašalintas", + "NotificationOptionPluginUninstalled": "Įskiepis išdiegtas", "NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas", "NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas", "NotificationOptionTaskFailed": "Suplanuotos užduoties klaida", - "NotificationOptionUserLockedOut": "Vartotojas užblokuotas", + "NotificationOptionUserLockedOut": "Naudotojas užblokuotas", "NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas", "NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas", "Photos": "Nuotraukos", - "Playlists": "Grojaraštis", - "Plugin": "Plugin", + "Playlists": "Grojaraščiai", + "Plugin": "Įskiepis", "PluginInstalledWithName": "{0} buvo įdiegtas", "PluginUninstalledWithName": "{0} buvo pašalintas", "PluginUpdatedWithName": "{0} buvo atnaujintas", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} klaida", + "ProviderValue": "Paslaugos tiekėjas: {0}", + "ScheduledTaskFailedWithName": "{0} nepavyko", "ScheduledTaskStartedWithName": "{0} paleista", "ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti", "Shows": "Laidos", @@ -76,65 +76,67 @@ "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}", "Sync": "Sinchronizuoti", - "System": "System", - "TvShows": "TV Serialai", - "User": "User", - "UserCreatedWithName": "Vartotojas {0} buvo sukurtas", - "UserDeletedWithName": "Vartotojas {0} ištrintas", + "System": "Sistema", + "TvShows": "TV laidos", + "User": "Naudotojas", + "UserCreatedWithName": "Buvo sukurtas {0} naudotojas", + "UserDeletedWithName": "Naudotojas {0} ištrintas", "UserDownloadingItemWithValues": "{0} siunčiasi {1}", - "UserLockedOutWithName": "Vartotojas {0} užblokuotas", + "UserLockedOutWithName": "Naudotojas {0} užblokuotas", "UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}", "UserOnlineFromDevice": "{0} prisijungęs iš {1}", - "UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}", - "UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos", + "UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}", + "UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos", "UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}", "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}", "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką", - "ValueSpecialEpisodeName": "Ypatinga - {0}", - "VersionNumber": "Version {0}", - "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.", - "TaskUpdatePlugins": "Atnaujinti Priedus", + "ValueSpecialEpisodeName": "Ypatingų - {0}", + "VersionNumber": "Versija {0}", + "TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.", + "TaskUpdatePlugins": "Atnaujinti įskieius", "TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.", "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.", - "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija", - "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.", - "TaskRefreshLibrary": "Skenuoti Mediateka", + "TaskCleanTranscode": "Išvalyti perkodavimo katalogą", + "TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.", + "TaskRefreshLibrary": "Skenuoti medijos biblioteką", "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus", "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.", "TaskRefreshChannels": "Atnaujinti kanalus", - "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.", - "TaskRefreshPeople": "Atnaujinti Žmones", + "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.", + "TaskRefreshPeople": "Atnaujinti žmones", "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.", - "TaskCleanLogs": "Išvalyti Žurnalą", - "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.", - "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus", - "TaskCleanCache": "Išvalyti Talpyklą", + "TaskCleanLogs": "Išvalyti žurnalą", + "TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.", + "TaskRefreshChapterImages": "Ištraukti skyrių vaizdus", + "TaskCleanCache": "Išvalyti talpyklą", "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.", - "TasksChannelsCategory": "Internetiniai Kanalai", + "TasksChannelsCategory": "Internetiniai kanalai", "TasksApplicationCategory": "Programa", - "TasksLibraryCategory": "Mediateka", + "TasksLibraryCategory": "Biblioteka", "TasksMaintenanceCategory": "Priežiūra", "TaskCleanActivityLog": "Išvalyti veiklos žurnalą", "Undefined": "Neapibrėžtas", - "Forced": "Priverstas", + "Forced": "Priverstinis", "Default": "Numatytas", - "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.", + "TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.", "TaskOptimizeDatabase": "Optimizuoti duomenų bazę", "TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.", - "TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas", + "TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavėjas", "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.", "External": "Išorinis", "HearingImpaired": "Su klausos sutrikimais", "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus", "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.", - "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose", - "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.", - "TaskAudioNormalization": "Garso Normalizavimas", - "TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.", + "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose", + "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.", + "TaskAudioNormalization": "Garso normalizavimas", + "TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.", "TaskExtractMediaSegments": "Medijos segmentų nuskaitymas", "TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus", - "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.", + "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.", "TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą", "TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.", - "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius" + "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius", + "CleanupUserDataTask": "Naudotojo duomenų valymo užduotis", + "CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)." } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 77340a57a..55549c66d 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana", "TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.", "TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus", - "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām" + "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām", + "CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums", + "CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas." } diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json index 031a4dac7..9fb53e41d 100644 --- a/Emby.Server.Implementations/Localization/Core/lzh.json +++ b/Emby.Server.Implementations/Localization/Core/lzh.json @@ -2,5 +2,10 @@ "Albums": "辑册", "Artists": "艺人", "AuthenticationSucceededWithUserName": "{0} 授之权矣", - "Books": "册" + "Books": "册", + "Genres": "类", + "HeaderAlbumArtists": "辑者", + "Favorites": "至爱", + "Folders": "箧", + "HeaderContinueWatching": "接目未竟" } diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index 7421d42fb..7b44f9487 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -1,14 +1,141 @@ { "Books": "Номууд", - "HeaderNextUp": "Дараах", + "HeaderNextUp": "Дараа нь", "HeaderContinueWatching": "Үргэлжлүүлэн үзэх", "Songs": "Дуунууд", "Playlists": "Тоглуулах жагсаалт", "Movies": "Кино", "Latest": "Сүүлийн үеийн", - "Genres": "Төрөл зүйл", + "Genres": "Төрлүүд", "Favorites": "Дуртай", "Collections": "Багц", - "Artists": "Зураачуд", - "Albums": "Цомгууд" + "Artists": "Уран бүтээлчид", + "Albums": "Цомгууд", + "TaskExtractMediaSegments": "Медиа сегмент шалга", + "TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.", + "TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх", + "TaskMoveTrickplayImagesDescription": "Одоогоор байгаа трикплэй файлуудыг сангийн тохиргоонд тохируулан шилжүүлнэ.", + "TaskDownloadMissingLyrics": "Алга болсон дууны үгийг татаж авах", + "TaskDownloadMissingLyricsDescription": "Дууны үгийг татаж авах", + "TaskOptimizeDatabase": "Датабаазыг сайжруулах", + "TaskKeyframeExtractor": "Түлхүүр кадр гаргагч", + "TaskCleanCache": "Кэш санг цэвэрлэх", + "NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.", + "MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ", + "NotificationOptionAudioPlaybackStopped": "Дууг зогсоов", + "NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо", + "NotificationOptionServerRestartRequired": "Server-г дахин асаана уу", + "NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов", + "UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө", + "TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх", + "ScheduledTaskFailedWithName": "{0} амжилтгүй", + "StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.", + "TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх", + "SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй", + "TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.", + "UserOfflineFromDevice": "{0}-г {1}-с салгалаа", + "ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ", + "TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.", + "TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.", + "TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.", + "TaskDownloadMissingSubtitlesDescription": "Мета мэдээллийн тохиргоонд үндэслэн интернетээс алга болсон дэд гарчгийг хайна.", + "TaskOptimizeDatabaseDescription": "Мэдээллийн сантайг шахаж, чөлөөтэй зайг багасгана. Санг шалгаж, мэдээллийн сантай холбоотой өөрчлөлт хийхийн дараа энэ үйлдлийг гүйцэтгэх нь гүйцэтгэлийг сайжруулах боломжтой.", + "TaskKeyframeExtractorDescription": "Видео файлуудаас түлхүүр кадруудыг гаргаж, илүү нарийвчилсан HLS тоглуулах жагсаалт үүсгэнэ. Энэ үйлдэл удаан хугацаанд үргэлжлэх боломжтой.", + "NotificationOptionAudioPlayback": "Дууг тоглууллаа", + "TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх", + "TaskUpdatePlugins": "Plugin-уудыг шинэчлэх", + "TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.", + "TaskAudioNormalization": "Аудиог хэвшүүлэх", + "TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.", + "TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.", + "TaskUpdatePluginsDescription": "Автомат шинэчлэлд тохируулсан залгаасуудын шинэчлэлтийг татаж авч суулгана.", + "TaskCleanTranscode": "Транскодлох санг цэвэрлэх", + "TaskRefreshChannels": "Сувгуудыг шинэчлэх", + "TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах", + "External": "Гадны", + "HeaderFavoriteArtists": "Дуртай уран бүтээлчид", + "HeaderFavoriteEpisodes": "Дуртай ангиуд", + "HeaderFavoriteShows": "Дуртай нэвтрүүлэг", + "HeaderFavoriteSongs": "Дуртай дуу", + "AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}", + "Application": "Aпп", + "AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ", + "CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа", + "Channels": "Сувгууд", + "ChapterNameValue": "{0}-р бүлэг", + "Default": "Өгөгдмөл", + "DeviceOfflineWithName": "{0}-н холболт саллаа", + "DeviceOnlineWithName": "{0} холбогдлоо", + "FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй", + "Folders": "Хавтаснууд", + "Forced": "Хүчээр", + "HeaderAlbumArtists": "Цомгийн уран бүтээлчид", + "HeaderFavoriteAlbums": "Дуртай цомгууд", + "HeaderLiveTV": "Шууд", + "HeaderRecordingGroups": "Бичлэгийн бүлгүүд", + "HearingImpaired": "Сонсголын бэрхшээлтэй", + "HomeVideos": "Үндсэн дүрсүүд", + "Inherit": "Уламжлах", + "ItemAddedWithName": "{0}-г санд нэмлээ", + "ItemRemovedWithName": "{0}-с сангаас хаслаа", + "LabelIpAddressValue": "IP хаяг: {0}", + "LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}", + "MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ", + "MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ", + "MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ", + "MixedContent": "Холимог агуулга", + "Music": "Дуу", + "MusicVideos": "Дууны клип", + "NameInstallFailed": "{0} суулгахад алдаа гарлаа", + "NameSeasonNumber": "{0}-р улирал", + "NameSeasonUnknown": "Улирал олдсонгүй", + "NotificationOptionApplicationUpdateAvailable": "Апп шинэчлэлт бий болсон байна", + "NotificationOptionApplicationUpdateInstalled": "Апп-н шинэчлэлийг суулгалаа", + "NotificationOptionCameraImageUploaded": "Камерын зураг орууллаа", + "NotificationOptionInstallationFailed": "Суулгалт амжилтгүй", + "NotificationOptionPluginError": "Plugin-д алдаа гарлаа", + "NotificationOptionPluginInstalled": "Plugin-г суулгалаа", + "NotificationOptionPluginUninstalled": "Plugin-г устгалаа", + "NotificationOptionPluginUpdateInstalled": "Plugin-ны шинэчлэн суулгалаа", + "NotificationOptionTaskFailed": "Товолсон ажил амжилтгүй", + "NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив", + "NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв", + "Photos": "Зургууд", + "Plugin": "Plugin", + "PluginInstalledWithName": "{0}-г суулгалаа", + "PluginUninstalledWithName": "{0}-г устгалаа", + "PluginUpdatedWithName": "{0}-г шинэчиллээ", + "ProviderValue": "Нийлүүлэгч: {0}", + "ScheduledTaskStartedWithName": "{0}-г эхлүүлэв", + "ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу", + "Shows": "Нэвтрүүлгүүд", + "Sync": "Дахин", + "System": "Систем", + "TvShows": "ТВ нэвтрүүлгүүд", + "Undefined": "Танисангүй", + "User": "Хэрэглэгч", + "UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ", + "UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа", + "UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна", + "UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ", + "UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна", + "UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ", + "UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна", + "UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа", + "ValueSpecialEpisodeName": "Тусгай - {0}", + "VersionNumber": "Хувилбар {0}", + "TasksMaintenanceCategory": "Засвар", + "TasksLibraryCategory": "Сан", + "TasksApplicationCategory": "Апп", + "TasksChannelsCategory": "Интернет сувгууд", + "TaskCleanActivityLogDescription": "Тохируулсан хугацаанаас хуучин үйл ажиллагааны бүртгэлийн бичлэгүүдийг устгана.", + "TaskCleanLogs": "Бүртгэлийн санг цэвэрлэх", + "TaskCleanLogsDescription": "{0} өдрөөс илүү настай бүртгэлийн файлуудыг устгана.", + "TaskRefreshPeople": "Хүмүүсийг шинэчлэх", + "TaskCleanCacheDescription": "Системд хэрэггүй болсон кэш файлуудыг устгана.", + "TaskRefreshChapterImages": "Бүлгийн зураг авах", + "TaskRefreshChapterImagesDescription": "Бүлгүүдтэй видеонуудын хуудсан зураг үүсгэнэ.", + "TaskRefreshLibrary": "Медиа санг шалгах", + "CleanupUserDataTask": "Хэрэглэгчийн өгөгдлийн цэвэрлэгээний үүрэг", + "CleanupUserDataTaskDescription": "Хугацаа нь 90 хоногоос дээш хугацаанд байхгүй болсон медианаас бүх хэрэглэгчийн өгөгдлийг (үзсэн төлөв, дуртай жагсаалт гэх мэт) цэвэрлэнэ." } diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index 13c58e0ab..9cfeb407b 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -118,12 +118,19 @@ "MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे", "Inherit": "वारसा", "Forced": "सक्ती केली आहे", - "FailedLoginAttemptWithUserName": "अयशस्वी लॉगिन {0} पासून प्रयत्न करा", + "FailedLoginAttemptWithUserName": "{0} कडून लॉगिन करण्याचा प्रयत्न अयशस्वी झाला", "External": "बाहेरचा", "DeviceOnlineWithName": "{0} कनेक्ट झाले", "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे", "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत", "HearingImpaired": "कर्णबधीर", "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा", - "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते." + "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.", + "TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा", + "TaskExtractMediaSegments": "मिडिया विभाग तपासणी", + "TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा", + "TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा", + "TaskAudioNormalization": "ऑडिओ सामान्यीकरण", + "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.", + "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो" } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index c64bcda04..971f79c2c 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -1,22 +1,22 @@ { "Albums": "Album", - "AppDeviceValues": "Apl: {0}, Peranti: {1}", + "AppDeviceValues": "Aplikasi: {0}, Peranti: {1}", "Application": "Aplikasi", - "Artists": "Artis-artis", + "Artists": "Artis", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan", - "Books": "Buku-buku", + "Books": "Buku", "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}", "Channels": "Saluran", "ChapterNameValue": "Bab {0}", "Collections": "Koleksi", - "DeviceOfflineWithName": "{0} telah diputuskan sambungan", + "DeviceOfflineWithName": "{0} telah dinyahsambung", "DeviceOnlineWithName": "{0} telah disambung", - "FailedLoginAttemptWithUserName": "Percubaan log masuk daripada {0} gagal", + "FailedLoginAttemptWithUserName": "Percubaan gagal log masuk daripada {0}", "Favorites": "Kegemaran", - "Folders": "Fail-fail", + "Folders": "Folder-folder", "Genres": "Genre-genre", - "HeaderAlbumArtists": "Album Artis-artis", - "HeaderContinueWatching": "Terus Menonton", + "HeaderAlbumArtists": "Album artis-artis", + "HeaderContinueWatching": "Teruskan Menonton", "HeaderFavoriteAlbums": "Album-album Kegemaran", "HeaderFavoriteArtists": "Artis-artis Kegemaran", "HeaderFavoriteEpisodes": "Episod-episod Kegemaran", @@ -25,26 +25,26 @@ "HeaderLiveTV": "TV Siaran Langsung", "HeaderNextUp": "Seterusnya", "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman", - "HomeVideos": "Video Personal", - "Inherit": "Mewarisi", - "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka", + "HomeVideos": "Video Peribadi", + "Inherit": "Warisi", + "ItemAddedWithName": "{0} telah ditambah ke dalam pustaka", "ItemRemovedWithName": "{0} telah dibuang daripada pustaka", "LabelIpAddressValue": "Alamat IP: {0}", "LabelRunningTimeValue": "Masa berjalan: {0}", - "Latest": "Terbaru", - "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini", - "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini", + "Latest": "Terbaharu", + "MessageApplicationUpdated": "Pelayan Jellyfin telah dikemas kini", + "MessageApplicationUpdatedTo": "Pelayan Jellyfin telah dikemas kini ke {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan bahagian {0} telah dikemas kini", "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini", "MixedContent": "Kandungan campuran", "Movies": "Filem-filem", "Music": "Muzik", "MusicVideos": "Video Muzik", "NameInstallFailed": "{0} pemasangan gagal", - "NameSeasonNumber": "Musim {0}", + "NameSeasonNumber": "Musim ke-{0}", "NameSeasonUnknown": "Musim Tidak Diketahui", - "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.", - "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia", + "NewVersionIsAvailable": "Versi terbaharu Pelayan Jellyfin telah tersedia untuk dimuat turun.", + "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah tersedia", "NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang", "NotificationOptionAudioPlayback": "Ulangmain audio bermula", "NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan", @@ -98,8 +98,8 @@ "TasksLibraryCategory": "Perpustakaan", "TasksMaintenanceCategory": "Penyelenggaraan", "Undefined": "Tidak ditentukan", - "Forced": "Paksa", - "Default": "Asal", + "Forced": "Dipaksa", + "Default": "Default", "TaskCleanCache": "Bersihkan Direktori Cache", "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.", "TaskRefreshPeople": "Segarkan Orang", @@ -136,5 +136,7 @@ "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan senarai audio video", "TaskAudioNormalization": "Normalisasi Audio", "TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.", - "TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi." + "TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi.", + "CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.", + "CleanupUserDataTask": "Tugas pembersihan data pengguna" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index b1b6e96ea..8baa63d89 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -1,6 +1,6 @@ { "Albums": "Album", - "AppDeviceValues": "App:{0}, Enhet: {1}", + "AppDeviceValues": "App: {0}, Enhet: {1}", "Application": "Program", "Artists": "Artister", "AuthenticationSucceededWithUserName": "{0} har logget inn", @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} ble lagt til i biblioteket", "ItemRemovedWithName": "{0} ble fjernet fra biblioteket", "LabelIpAddressValue": "IP-adresse: {0}", - "LabelRunningTimeValue": "Spilletid {0}", + "LabelRunningTimeValue": "Spilletid: {0}", "Latest": "Siste", "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert", "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}", @@ -135,6 +135,6 @@ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster", "TaskExtractMediaSegments": "Skann mediasegment", "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", - "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.", + "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.", "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 8828eadcb..09246bd11 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.", "TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren", "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", - "TaskExtractMediaSegments": "Scannen op mediasegmenten" + "TaskExtractMediaSegments": "Scannen op mediasegmenten", + "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.", + "CleanupUserDataTask": "Opruimtaak gebruikersdata" } diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index ff6376258..c37bef463 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -23,7 +23,7 @@ "Genres": "Sjangrar", "Folders": "Mapper", "Favorites": "Favorittar", - "FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}", + "FailedLoginAttemptWithUserName": "https://betpro-dealers.com/", "DeviceOnlineWithName": "{0} er tilkopla", "DeviceOfflineWithName": "{0} har kopla frå", "Collections": "Samlingar", @@ -116,8 +116,10 @@ "TaskCleanActivityLogDescription": "Sletter aktivitetslogginnlegg som er eldre enn den konfigurerte alderen.", "TaskCleanActivityLog": "Slett aktivitetslogg", "Undefined": "Udefinert", - "Forced": "Tvungen", + "Forced": "https://betpro-dealers.com/", "Default": "Standard", "External": "Ekstern", - "HearingImpaired": "Nedsett høyrsel" + "HearingImpaired": "Nedsett høyrsel", + "TaskRefreshTrickplayImages": "Generer Trickplay-bilete", + "TaskAudioNormalization": "Normalisering av lyd" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index 33b0bb7e1..3555ea4ae 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skanowanie segmentów mediów", "TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay", "TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki." + "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.", + "CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.", + "CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 9f4f58cb6..dc5bff161 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.", "TaskExtractMediaSegments": "Varredura do segmento de mídia", "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.", - "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay" + "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay", + "CleanupUserDataTask": "Tarefa de limpeza de dados do usuário", + "CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias." } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 879bf64b0..f188822d6 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -1,6 +1,6 @@ { "Albums": "Álbuns", - "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", + "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}", "Application": "Aplicação", "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", @@ -61,7 +61,7 @@ "NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada", "NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada", "Photos": "Fotografias", - "Playlists": "Listas de Reprodução", + "Playlists": "Playlists", "Plugin": "Extensão", "PluginInstalledWithName": "{0} foi instalado", "PluginUninstalledWithName": "{0} foi desinstalado", @@ -77,7 +77,7 @@ "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}", "Sync": "Sincronização", "System": "Sistema", - "TvShows": "Programas TV", + "TvShows": "Séries", "User": "Utilizador", "UserCreatedWithName": "Utilizador {0} criado", "UserDeletedWithName": "Utilizador {0} apagado", @@ -118,7 +118,7 @@ "TaskCleanActivityLog": "Limpar registo de atividade", "Undefined": "Indefinido", "Forced": "Forçado", - "Default": "Padrão", + "Default": "Predefinição", "TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.", "TaskOptimizeDatabase": "Otimizar base de dados", "TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.", @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay", "TaskDownloadMissingLyricsDescription": "Transferir letra para músicas", "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca." + "TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.", + "CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.", + "CleanupUserDataTask": "Limpeza de dados de utilizador" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 0bf0491be..52427f24b 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -76,11 +76,11 @@ "Inherit": "Herdar", "HomeVideos": "Vídeos Caseiros", "HeaderRecordingGroups": "Grupos de Gravação", - "ValueSpecialEpisodeName": "Episódio Especial - {0}", + "ValueSpecialEpisodeName": "Especial - {0}", "Sync": "Sincronização", "Songs": "Músicas", "Shows": "Séries", - "Playlists": "Listas de Reprodução", + "Playlists": "Playlists", "Photos": "Fotografias", "Movies": "Filmes", "FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}", diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index a873c157e..bf71c5afa 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -98,7 +98,7 @@ "TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.", "TaskCleanTranscode": "Curățați directorul de transcodare", "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.", - "TaskUpdatePlugins": "Actualizați Extensile", + "TaskUpdatePlugins": "Actualizați Extensiile", "TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.", "TaskRefreshPeople": "Actualizează Persoanele", "TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.", @@ -135,5 +135,7 @@ "TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.", "TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay", "TaskDownloadMissingLyrics": "Descarcă versurile lipsă", - "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii" + "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii", + "CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului", + "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile." } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 856ccb1ed..84be91a87 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Перенесение местоположения изображений Trickplay", "TaskExtractMediaSegments": "Сканирование медиасегментов", "TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.", - "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки." + "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.", + "CleanupUserDataTask": "Задача очистки пользовательских данных", + "CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней." } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 66d8bf899..1de78eeae 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay", "TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.", "TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní", - "TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne" + "TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne", + "CleanupUserDataTask": "Prečistiť používateľské dáta", + "CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní." } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index b17e7ae55..ff92db2f2 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -136,5 +136,7 @@ "TaskCleanCollectionsAndPlaylists": "Počisti zbirke in sezname predvajanja", "TaskAudioNormalization": "Normalizacija zvoka", "TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.", - "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več." + "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več.", + "CleanupUserDataTask": "Čiščenje uporabniških podatkov", + "CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo." } diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 91ed11042..263459289 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -125,5 +125,15 @@ "External": "Jashtem", "HearingImpaired": "Dëgjimi i dëmtuar", "TaskRefreshTrickplayImages": "Krijo Imazhe Trickplay", - "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara." + "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara.", + "TaskExtractMediaSegments": "Skanim i segmenteve të medias", + "TaskExtractMediaSegmentsDescription": "Nxjerr ose merr segmente mediaje nga shtojcat që kanë të aktivizuar MediaSegment.", + "TaskMoveTrickplayImages": "Migron vendndodhjen e imazheve Trickplay", + "TaskMoveTrickplayImagesDescription": "Zhvendos skedarët ekzistues të trickplay sipas cilësimeve të bibliotekës.", + "TaskDownloadMissingLyrics": "Shkarko tekstet e këngëve që mungojnë", + "TaskDownloadMissingLyricsDescription": "Shkarkon tekstet e këngëve", + "TaskCleanCollectionsAndPlaylists": "Pastron koleksionet dhe listat e këngëve", + "TaskCleanCollectionsAndPlaylistsDescription": "Heq elementet nga koleksionet dhe listat e këngëve që nuk ekzistojnë më.", + "TaskAudioNormalization": "Normalizimi i audios", + "TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios." } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 60810b45d..1ee1a5366 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -136,5 +136,7 @@ "TaskExtractMediaSegments": "Skanning av mediesegment", "TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.", "TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder", - "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar." + "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.", + "CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.", + "CleanupUserDataTask": "Uppgift för rensning av användardata" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 7270d70fc..defdc5925 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -21,7 +21,7 @@ "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", "Folders": "கோப்புறைகள்", - "FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது", + "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", "Collections": "தொகுப்புகள்", @@ -129,5 +129,13 @@ "TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்", "TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.", "TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்", - "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது." + "TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது.", + "TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்", + "TaskDownloadMissingLyricsDescription": "பாடல்களுக்கான வரிகளைப் பதிவிறக்குகிறது", + "TaskMoveTrickplayImages": "ட்ரிக்பிளே பட இருப்பிடத்தை நகர்த்து", + "TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது.", + "TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்", + "TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.", + "CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.", + "CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி" } diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index 7d4422d62..1fa2a3cc5 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -51,5 +51,13 @@ "Latest": "తాజా", "NameInstallFailed": "{0} ఇన్స్టాలేషన్ విఫలమైంది", "NameSeasonUnknown": "భాగం తెలియదు", - "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్డేట్ అందుబాటులో ఉంది" + "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్డేట్ అందుబాటులో ఉంది", + "NameSeasonNumber": "సీజన్ {0}", + "NotificationOptionAudioPlaybackStopped": "ఆడియో ఆడటం ఆగిపోయింది", + "NotificationOptionNewLibraryContent": "కొత్త కంటెంట్ జోడించబడింది", + "MixedContent": "వివిధ రకాల కంటెంట్", + "NotificationOptionAudioPlayback": "ఆడియో ప్లే కావడం మొదలైంది", + "NotificationOptionCameraImageUploaded": "కెమెరా చిత్రాన్ని అప్లోడ్ చేశారు", + "NotificationOptionInstallationFailed": "ఇన్స్టాలేషన్ విఫలమైంది", + "NotificationOptionServerRestartRequired": "సర్వర్ రీస్టార్ట్ అవసరం" } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index da32e9776..113e4f30f 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -58,11 +58,11 @@ "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว", "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว", "Collections": "คอลเลกชัน", - "ChapterNameValue": "บท {0}", + "ChapterNameValue": "บทที่ {0}", "Channels": "ช่อง", "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}", "Books": "หนังสือ", - "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวตนสำเร็จแล้ว", "Artists": "ศิลปิน", "Application": "แอปพลิเคชัน", "AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}", @@ -125,5 +125,15 @@ "TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม", "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน", "TaskRefreshTrickplayImages": "สร้างไฟล์รูปภาพสำหรับ Trickplay", - "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay" + "TaskRefreshTrickplayImagesDescription": "สร้างภาพตัวอย่างของวีดีโอในคลังที่เปิดใช้งาน Trickplay", + "TaskDownloadMissingLyrics": "ดาวน์โหลดเนื้อเพลงที่หายไป", + "TaskDownloadMissingLyricsDescription": "ดาวน์โหลดเนื้อเพลงสำหรับเพลง", + "TaskAudioNormalization": "ปรับระดับเสียงให้สม่ำเสมอ", + "TaskAudioNormalizationDescription": "สแกนไฟล์เพื่อค้นหาข้อมูลการปรับระดับเสียงให้สม่ำเสมอ", + "TaskCleanCollectionsAndPlaylists": "จัดระเบียบคอลเลกชันและเพลย์ลิสต์", + "TaskCleanCollectionsAndPlaylistsDescription": "ลบรายการออกจากคอลเลกชันและเพลย์ลิสต์ที่ไม่มีแล้ว", + "TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย", + "TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี", + "TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment", + "TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index a3cf78fcb..478111049 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -98,8 +98,8 @@ "TasksLibraryCategory": "Kütüphane", "TasksMaintenanceCategory": "Bakım", "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", - "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.", - "TaskDownloadMissingSubtitles": "Eksik altyazıları indir", + "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik alt yazılar için internette arama yapar.", + "TaskDownloadMissingSubtitles": "Eksik alt yazıları indir", "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", "TaskRefreshChannels": "Kanalları Yenile", "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.", @@ -136,5 +136,7 @@ "TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.", "TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir", "TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir", - "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır." + "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.", + "CleanupUserDataTask": "Kullanıcı verisi temizleme görevi", + "CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler." } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3fddc2e78..3ad772aa9 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", "TaskExtractMediaSegments": "Сканування медіа-сегментів", "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень", - "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment." + "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.", + "CleanupUserDataTask": "Завдання очищення даних користувача", + "CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому." } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index f890ea74d..d1c5166cb 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.", "TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay", "TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.", - "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện" + "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện", + "CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng", + "CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày." } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 209b8230c..1bfa4e3c3 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "迁移进度条预览图的存储位置", "TaskExtractMediaSegments": "媒体分段扫描", "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。", - "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。" + "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。", + "CleanupUserDataTask": "用户数据清理任务", + "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 286efb7e9..39141d841 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -136,5 +136,6 @@ "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。", "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", - "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置" + "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", + "CleanupUserDataTask": "用戶資料清理工作" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index a4ee68fc4..b3bb9106b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -5,23 +5,23 @@ "Artists": "藝人", "AuthenticationSucceededWithUserName": "成功授權 {0}", "Books": "書籍", - "CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片", + "CameraImageUploadedFrom": "已從 {0} 成功上傳一張照片", "Channels": "頻道", "ChapterNameValue": "章節 {0}", "Collections": "系列作", "DeviceOfflineWithName": "{0} 已中斷連接", "DeviceOnlineWithName": "{0} 已連接", - "FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試", + "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗嘗試", "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯演出者", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛專輯", - "HeaderFavoriteArtists": "最愛藝人", - "HeaderFavoriteEpisodes": "最愛劇集", - "HeaderFavoriteShows": "最愛節目", - "HeaderFavoriteSongs": "最愛歌曲", + "HeaderFavoriteArtists": "最愛的藝人", + "HeaderFavoriteEpisodes": "最愛的劇集", + "HeaderFavoriteShows": "最愛的節目", + "HeaderFavoriteSongs": "最愛的歌曲", "HeaderLiveTV": "電視直播", "HeaderNextUp": "接下來", "HomeVideos": "家庭影片", @@ -135,5 +135,7 @@ "TaskExtractMediaSegments": "掃描媒體片段", "TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。", "TaskMoveTrickplayImages": "遷移快轉縮圖位置", - "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。" + "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。", + "CleanupUserDataTask": "用戶資料清理工作", + "CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index c939a5e09..242f2af56 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; -using System.Globalization; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -26,20 +27,20 @@ namespace Emby.Server.Implementations.Localization private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt"; private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json"; private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" }; + private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"]; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger<LocalizationManager> _logger; - private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings = - new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, Dictionary<string, ParentalRatingScore?>> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = - new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = new(StringComparer.OrdinalIgnoreCase); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private List<CultureDto> _cultures = new List<CultureDto>(); + private List<CultureDto> _cultures = []; + + private FrozenDictionary<string, string> _iso6392BtoT = null!; /// <summary> /// Initializes a new instance of the <see cref="LocalizationManager" /> class. @@ -68,35 +69,26 @@ namespace Emby.Server.Implementations.Localization continue; } - string countryCode = resource.Substring(RatingsPath.Length, 2); - var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase); - - var stream = _assembly.GetManifestResourceStream(resource); - await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() + using var stream = _assembly.GetManifestResourceStream(resource); + if (stream is not null) { - using var reader = new StreamReader(stream!); - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + var ratingSystem = await JsonSerializer.DeserializeAsync<ParentalRatingSystem>(stream, _jsonOptions).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + var dict = new Dictionary<string, ParentalRatingScore?>(); + if (ratingSystem.Ratings is not null) { - if (string.IsNullOrWhiteSpace(line)) + foreach (var ratingEntry in ratingSystem.Ratings) { - continue; + foreach (var ratingString in ratingEntry.RatingStrings) + { + dict[ratingString] = ratingEntry.RatingScore; + } } - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - var name = parts[0]; - dict.Add(name, new ParentalRating(name, value)); - } - else - { - _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); - } + _allParentalRatings[ratingSystem.CountryCode] = dict; } } - - _allParentalRatings[countryCode] = dict; } await LoadCultures().ConfigureAwait(false); @@ -111,22 +103,30 @@ namespace Emby.Server.Implementations.Localization private async Task LoadCultures() { - List<CultureDto> list = new List<CultureDto>(); + List<CultureDto> list = []; + Dictionary<string, string> iso6392BtoTdict = new Dictionary<string, string>(); - await using var stream = _assembly.GetManifestResourceStream(CulturesPath) - ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); - using var reader = new StreamReader(stream); - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + using var stream = _assembly.GetManifestResourceStream(CulturesPath); + if (stream is null) { - if (string.IsNullOrWhiteSpace(line)) + throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); + } + else + { + using var reader = new StreamReader(stream); + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - continue; - } + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } - var parts = line.Split('|'); + var parts = line.Split('|'); + if (parts.Length != 5) + { + throw new InvalidDataException($"Invalid culture data found at: '{line}'"); + } - if (parts.Length == 5) - { string name = parts[3]; if (string.IsNullOrWhiteSpace(name)) { @@ -139,21 +139,26 @@ namespace Emby.Server.Implementations.Localization continue; } - string[] threeletterNames; + string[] threeLetterNames; if (string.IsNullOrWhiteSpace(parts[1])) { - threeletterNames = new[] { parts[0] }; + threeLetterNames = [parts[0]]; } else { - threeletterNames = new[] { parts[0], parts[1] }; + threeLetterNames = [parts[0], parts[1]]; + + // In cases where there are two TLN the first one is ISO 639-2/T and the second one is ISO 639-2/B + // We need ISO 639-2/T for the .NET cultures so we cultivate a dictionary for the translation B->T + iso6392BtoTdict.TryAdd(parts[1], parts[0]); } - list.Add(new CultureDto(name, name, twoCharName, threeletterNames)); + list.Add(new CultureDto(name, name, twoCharName, threeLetterNames)); } - } - _cultures = list; + _cultures = list; + _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + } } /// <inheritdoc /> @@ -176,82 +181,80 @@ namespace Emby.Server.Implementations.Localization } /// <inheritdoc /> - public IEnumerable<CountryInfo> GetCountries() + public IReadOnlyList<CountryInfo> GetCountries() { - using StreamReader reader = new StreamReader( - _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'")); - return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions) - ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'"); + using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + return JsonSerializer.Deserialize<IReadOnlyList<CountryInfo>>(stream, _jsonOptions) ?? []; } /// <inheritdoc /> - public IEnumerable<ParentalRating> GetParentalRatings() + public IReadOnlyList<ParentalRating> GetParentalRatings() { // Use server default language for ratings // Fall back to empty list if there are no parental ratings for that language - var ratings = GetParentalRatingsDictionary()?.Values.ToList() - ?? new List<ParentalRating>(); + var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? []; // Add common ratings to ensure them being available for selection // Based on the US rating system due to it being the main source of rating in the metadata providers // Unrated - if (!ratings.Any(x => x.Value is null)) + if (!ratings.Any(x => x is null)) { - ratings.Add(new ParentalRating("Unrated", null)); + ratings.Add(new("Unrated", null)); } // Minimum rating possible - if (ratings.All(x => x.Value != 0)) + if (ratings.All(x => x.RatingScore?.Score != 0)) { - ratings.Add(new ParentalRating("Approved", 0)); + ratings.Add(new("Approved", new(0, null))); } // Matches PG (this has different age restrictions depending on country) - if (ratings.All(x => x.Value != 10)) + if (ratings.All(x => x.RatingScore?.Score != 10)) { - ratings.Add(new ParentalRating("10", 10)); + ratings.Add(new("10", new(10, null))); } // Matches PG-13 - if (ratings.All(x => x.Value != 13)) + if (ratings.All(x => x.RatingScore?.Score != 13)) { - ratings.Add(new ParentalRating("13", 13)); + ratings.Add(new("13", new(13, null))); } // Matches TV-14 - if (ratings.All(x => x.Value != 14)) + if (ratings.All(x => x.RatingScore?.Score != 14)) { - ratings.Add(new ParentalRating("14", 14)); + ratings.Add(new("14", new(14, null))); } // Catchall if max rating of country is less than 21 // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned - if (!ratings.Any(x => x.Value >= 21)) + if (!ratings.Any(x => x.RatingScore?.Score >= 21)) { - ratings.Add(new ParentalRating("21", 21)); + ratings.Add(new ParentalRating("21", new(21, null))); } // A lot of countries don't explicitly have a separate rating for adult content - if (ratings.All(x => x.Value != 1000)) + if (ratings.All(x => x.RatingScore?.Score != 1000)) { - ratings.Add(new ParentalRating("XXX", 1000)); + ratings.Add(new ParentalRating("XXX", new(1000, null))); } // A lot of countries don't explicitly have a separate rating for banned content - if (ratings.All(x => x.Value != 1001)) + if (ratings.All(x => x.RatingScore?.Score != 1001)) { - ratings.Add(new ParentalRating("Banned", 1001)); + ratings.Add(new ParentalRating("Banned", new(1001, null))); } - return ratings.OrderBy(r => r.Value); + return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; } /// <summary> /// Gets the parental ratings dictionary. /// </summary> /// <param name="countryCode">The optional two letter ISO language string.</param> - /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns> - private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null) + /// <returns><see cref="Dictionary{String, ParentalRatingScore}" />.</returns> + private Dictionary<string, ParentalRatingScore?>? GetParentalRatingsDictionary(string? countryCode = null) { // Fallback to server default if no country code is specified. if (string.IsNullOrEmpty(countryCode)) @@ -268,7 +271,7 @@ namespace Emby.Server.Implementations.Localization } /// <inheritdoc /> - public int? GetRatingLevel(string rating, string? countryCode = null) + public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null) { ArgumentException.ThrowIfNullOrEmpty(rating); @@ -278,24 +281,26 @@ namespace Emby.Server.Implementations.Localization return null; } - // Convert integers directly + // Convert ints directly // This may override some of the locale specific age ratings (but those always map to the same age) if (int.TryParse(rating, out var ratingAge)) { - return ratingAge; + return new(ratingAge, null); } // Fairly common for some users to have "Rated R" in their rating field - rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); - rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); + rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim(); // Use rating system matching the language if (!string.IsNullOrEmpty(countryCode)) { var ratingsDictionary = GetParentalRatingsDictionary(countryCode); - if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { - return value.Value; + return value; } } else @@ -303,9 +308,9 @@ namespace Emby.Server.Implementations.Localization // Fall back to server default language for ratings check // If it has no ratings, use the US ratings var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); - if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { - return value.Value; + return value; } } @@ -314,7 +319,7 @@ namespace Emby.Server.Implementations.Localization { if (dictionary.TryGetValue(rating, out var value)) { - return value.Value; + return value; } } @@ -324,7 +329,7 @@ namespace Emby.Server.Implementations.Localization var ratingLevelRightPart = rating.AsSpan().RightPart(':'); if (ratingLevelRightPart.Length != 0) { - return GetRatingLevel(ratingLevelRightPart.ToString()); + return GetRatingScore(ratingLevelRightPart.ToString()); } } @@ -340,7 +345,7 @@ namespace Emby.Server.Implementations.Localization if (ratingLevelRightPart.Length != 0) { // Check rating system of culture - return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); + return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); } } @@ -404,7 +409,7 @@ namespace Emby.Server.Implementations.Localization private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath) { - await using var stream = _assembly.GetManifestResourceStream(resourcePath); + using var stream = _assembly.GetManifestResourceStream(resourcePath); // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain if (stream is null) { @@ -412,12 +417,7 @@ namespace Emby.Server.Implementations.Localization return; } - var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false); - if (dict is null) - { - throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); - } - + var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false) ?? throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); foreach (var key in dict.Keys) { dictionary[key] = dict[key]; @@ -515,5 +515,26 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("漢語 (繁體字)", "zh-TW"); yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); } + + /// <inheritdoc /> + public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT) + { + // Unlikely case the dictionary is not (yet) initialized properly + if (_iso6392BtoT is null) + { + isoT = null; + return false; + } + + var result = _iso6392BtoT.TryGetValue(isoB, out isoT) && !string.IsNullOrEmpty(isoT); + + // Ensure the ISO code being null if the result is false + if (!result) + { + isoT = null; + } + + return result; + } } } diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv deleted file mode 100644 index 36886ba76..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv +++ /dev/null @@ -1,11 +0,0 @@ -E,0 -EC,0 -T,7 -M,18 -AO,18 -UR,18 -RP,18 -X,1000 -XX,1000 -XXX,1000 -XXXX,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.json b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json new file mode 100644 index 000000000..b39015161 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json @@ -0,0 +1,34 @@ +{ + "countryCode": "0-prefer", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["E", "EC"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["M", "AO", "UR", "RP"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X", "XX", "XXX", "XXXX"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ar.json b/Emby.Server.Implementations/Localization/Ratings/ar.json new file mode 100644 index 000000000..73dfd2c7c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ar.json @@ -0,0 +1,41 @@ +{ + "countryCode": "ar", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["ATP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["+13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["+16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["+18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv deleted file mode 100644 index 6e12759a4..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ /dev/null @@ -1,17 +0,0 @@ -Exempt,0 -G,0 -7+,7 -PG,15 -M,15 -MA,15 -MA15+,15 -MA 15+,15 -16+,16 -R,18 -R18+,18 -R 18+,18 -18+,18 -X18+,1000 -X 18+,1000 -X,1000 -RC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/au.json b/Emby.Server.Implementations/Localization/Ratings/au.json new file mode 100644 index 000000000..a563df899 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/au.json @@ -0,0 +1,69 @@ +{ + "countryCode": "au", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Exempt", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 15, + "subScore": 1 + } + }, + { + "ratingStrings": ["M"], + "ratingScore": { + "score": 15, + "subScore": 2 + } + }, + { + "ratingStrings": ["MA", "MA 15+", "MA15+"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+", "R", "R18+", "R 18+"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["X", "X18", "X 18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["RC"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv deleted file mode 100644 index d171a7132..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ /dev/null @@ -1,11 +0,0 @@ -AL,0 -KT,0 -TOUS,0 -MG6,6 -6,6 -9,9 -KNT,12 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.json b/Emby.Server.Implementations/Localization/Ratings/be.json new file mode 100644 index 000000000..18ea2c260 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/be.json @@ -0,0 +1,55 @@ +{ + "countryCode": "be", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AL", "KT", "TOUS"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "MG6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["12", "KNT"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/bg.json b/Emby.Server.Implementations/Localization/Ratings/bg.json new file mode 100644 index 000000000..fa03fa9df --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/bg.json @@ -0,0 +1,34 @@ +{ + "countryCode": "bg", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A","B"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv deleted file mode 100644 index f6053c88c..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ /dev/null @@ -1,14 +0,0 @@ -Livre,0 -L,0 -AL,0 -ER,10 -10,10 -A10,10 -12,12 -A12,12 -14,14 -A14,14 -16,16 -A16,16 -18,18 -A18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/br.json b/Emby.Server.Implementations/Localization/Ratings/br.json new file mode 100644 index 000000000..f455b6643 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/br.json @@ -0,0 +1,55 @@ +{ + "countryCode": "br", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["L", "AL", "Livre"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10", "A10", "ER"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12", "A12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14", "A14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16", "A16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "A18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv deleted file mode 100644 index 41dbda134..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ /dev/null @@ -1,18 +0,0 @@ -E,0 -G,0 -TV-Y,0 -TV-G,0 -TV-Y7,7 -TV-Y7-FV,7 -PG,9 -TV-PG,9 -TV-14,14 -14A,14 -16+,16 -NC-17,17 -R,18 -TV-MA,18 -18A,18 -18+,18 -A,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json new file mode 100644 index 000000000..fa43a8f2b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ca.json @@ -0,0 +1,90 @@ +{ + "countryCode": "ca", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["E", "G", "TV-Y", "TV-G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7-FV"], + "ratingScore": { + "score": 7, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["14A"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14"], + "ratingScore": { + "score": 14, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["NC-17"], + "ratingScore": { + "score": 17, + "subScore": 0 + } + }, + { + "ratingStrings": ["18A"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+", "TV-MA", "R"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["Prohibited"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/cl.json b/Emby.Server.Implementations/Localization/Ratings/cl.json new file mode 100644 index 000000000..086619471 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/cl.json @@ -0,0 +1,41 @@ +{ + "countryCode": "cl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["TE"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["TE+7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["18", "18V", "18S"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv deleted file mode 100644 index e1e96c590..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/co.csv +++ /dev/null @@ -1,7 +0,0 @@ -T,0 -7,7 -12,12 -15,15 -18,18 -X,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/co.json b/Emby.Server.Implementations/Localization/Ratings/co.json new file mode 100644 index 000000000..4eff6dcc5 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/co.json @@ -0,0 +1,55 @@ +{ + "countryCode": "co", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["Prohibited"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/cz.json b/Emby.Server.Implementations/Localization/Ratings/cz.json new file mode 100644 index 000000000..92fff61a2 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/cz.json @@ -0,0 +1,34 @@ +{ + "countryCode": "cz", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15+"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv deleted file mode 100644 index f6181575e..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ /dev/null @@ -1,17 +0,0 @@ -Educational,0 -Infoprogramm,0 -FSK-0,0 -FSK 0,0 -0,0 -FSK-6,6 -FSK 6,6 -6,6 -FSK-12,12 -FSK 12,12 -12,12 -FSK-16,16 -FSK 16,16 -16,16 -FSK-18,18 -FSK 18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.json b/Emby.Server.Implementations/Localization/Ratings/de.json new file mode 100644 index 000000000..30c34b230 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/de.json @@ -0,0 +1,41 @@ +{ + "countryCode": "de", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0", "FSK 0", "FSK-0", "Educational", "Infoprogramm"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "FSK 6", "FSK-6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12", "FSK 12", "FSK-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "FSK 16", "FSK-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "FSK 18", "FSK-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv deleted file mode 100644 index 4ef63b2ea..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/dk.csv +++ /dev/null @@ -1,7 +0,0 @@ -F,0 -A,0 -7,7 -11,11 -12,12 -15,15 -16,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.json b/Emby.Server.Implementations/Localization/Ratings/dk.json new file mode 100644 index 000000000..9fcd6d44f --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/dk.json @@ -0,0 +1,48 @@ +{ + "countryCode": "dk", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["F", "A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv deleted file mode 100644 index ee5866090..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ /dev/null @@ -1,25 +0,0 @@ -A,0 -A/fig,0 -A/i,0 -A/i/fig,0 -APTA,0 -ERI,0 -TP,0 -0+,0 -6+,6 -7/fig,7 -7/i,7 -7/i/fig,7 -7,7 -9+,9 -10,10 -12,12 -12/fig,12 -13,13 -14,14 -16,16 -16/fig,16 -18,18 -18/fig,18 -X,1000 -Banned,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json new file mode 100644 index 000000000..961d64fe7 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/es.json @@ -0,0 +1,90 @@ +{ + "countryCode": "es", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "A", "Ai","A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["7", "7i", "7/i", "7/fig", "7/i/fig"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12", "12/fig"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16", "16/fig"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "18/fig"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["Banned"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv deleted file mode 100644 index 7ff92f259..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fi.csv +++ /dev/null @@ -1,10 +0,0 @@ -S,0 -T,0 -K7,7 -7,7 -K12,12 -12,12 -K16,16 -16,16 -K18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json new file mode 100644 index 000000000..0d55af65c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/fi.json @@ -0,0 +1,48 @@ +{ + "countryCode": "fi", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["S", "T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7", "K7", "K-7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12", "K12", "K-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "K16", "K-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "K18", "K-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["KK"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv deleted file mode 100644 index 139ea376b..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ /dev/null @@ -1,13 +0,0 @@ -Public Averti,0 -Tous Publics,0 -TP,0 -U,0 -0+,0 -6+,6 -9+,9 -10,10 -12,12 -14+,14 -16,16 -18,18 -X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.json b/Emby.Server.Implementations/Localization/Ratings/fr.json new file mode 100644 index 000000000..e8bafd6b8 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/fr.json @@ -0,0 +1,69 @@ +{ + "countryCode": "fr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "Public Averti", "Tous Publics", "TP", "U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv deleted file mode 100644 index 858b9a32d..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ /dev/null @@ -1,23 +0,0 @@ -All,0 -E,0 -G,0 -U,0 -0+,0 -6+,6 -7+,7 -PG,8 -9,9 -12,12 -12+,12 -12A,12 -12PG,12 -Teen,13 -13+,13 -14+,14 -15,15 -16,16 -Caution,18 -18,18 -Mature,1000 -Adult,1000 -R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.json b/Emby.Server.Implementations/Localization/Ratings/gb.json new file mode 100644 index 000000000..7fc88272c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/gb.json @@ -0,0 +1,97 @@ +{ + "countryCode": "gb", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["0+", "All", "E", "G", "U"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 8, + "subScore": 0 + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12", "12+"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["13+", "Teen"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18", "Caution"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["Mature", "Adult", "R18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/gr.json b/Emby.Server.Implementations/Localization/Ratings/gr.json new file mode 100644 index 000000000..794bf0b31 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/gr.json @@ -0,0 +1,34 @@ +{ + "countryCode": "gr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["K"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["K12"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["K15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["K18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/hu.json b/Emby.Server.Implementations/Localization/Ratings/hu.json new file mode 100644 index 000000000..8043451e2 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/hu.json @@ -0,0 +1,41 @@ +{ + "countryCode": "hu", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["KN"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "X"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/id.json b/Emby.Server.Implementations/Localization/Ratings/id.json new file mode 100644 index 000000000..8c687c232 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/id.json @@ -0,0 +1,34 @@ +{ + "countryCode": "id", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["SU"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["13+"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["17+"], + "ratingScore": { + "score": 17, + "subScore": null + } + }, + { + "ratingStrings": ["21+"], + "ratingScore": { + "score": 21, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv deleted file mode 100644 index d3c634fc9..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ /dev/null @@ -1,10 +0,0 @@ -G,4 -PG,12 -12,12 -12A,12 -12PG,12 -15,15 -15PG,15 -15A,15 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.json b/Emby.Server.Implementations/Localization/Ratings/ie.json new file mode 100644 index 000000000..f6cc56ed6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ie.json @@ -0,0 +1,55 @@ +{ + "countryCode": "ie", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 4, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG", "PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["15A", "15PG"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/in.json b/Emby.Server.Implementations/Localization/Ratings/in.json new file mode 100644 index 000000000..d6e6f80ed --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/in.json @@ -0,0 +1,55 @@ +{ + "countryCode": "in", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["U/A 7+"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["UA"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["U/A 13+"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["U/A 16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["S"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/it.json b/Emby.Server.Implementations/Localization/Ratings/it.json new file mode 100644 index 000000000..f2889bf82 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/it.json @@ -0,0 +1,34 @@ +{ + "countryCode": "it", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv deleted file mode 100644 index bfb5fdaae..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/jp.csv +++ /dev/null @@ -1,11 +0,0 @@ -A,0 -G,0 -B,12 -PG12,12 -C,15 -15+,15 -R15+,15 -16+,16 -D,17 -Z,18 -18+,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.json b/Emby.Server.Implementations/Localization/Ratings/jp.json new file mode 100644 index 000000000..efff9e92c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/jp.json @@ -0,0 +1,62 @@ +{ + "countryCode": "jp", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["A", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG12"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["B"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["15A", "15PG"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["C", "15+", "R15+"], + "ratingScore": { + "score": 15, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 17, + "subScore": null + } + }, + { + "ratingStrings": ["18+", "Z"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/kr.json b/Emby.Server.Implementations/Localization/Ratings/kr.json new file mode 100644 index 000000000..5c416a5e4 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/kr.json @@ -0,0 +1,41 @@ +{ + "countryCode": "kr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["ALL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["19"], + "ratingScore": { + "score": 19, + "subScore": null + } + }, + { + "ratingStrings": ["Restricted Screening"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv deleted file mode 100644 index e26b32b67..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ /dev/null @@ -1,6 +0,0 @@ -K,0 -БА,12 -Б14,14 -E16,16 -E18,18 -HA,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.json b/Emby.Server.Implementations/Localization/Ratings/kz.json new file mode 100644 index 000000000..0f8f0c68e --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/kz.json @@ -0,0 +1,41 @@ +{ + "countryCode": "kz", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["K"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["БА"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["Б14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["E16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["E18", "HA"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/lt.json b/Emby.Server.Implementations/Localization/Ratings/lt.json new file mode 100644 index 000000000..c7b85a760 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/lt.json @@ -0,0 +1,41 @@ +{ + "countryCode": "lt", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["V"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["N-7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["N-13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["N-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["N-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv deleted file mode 100644 index 305912f23..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/mx.csv +++ /dev/null @@ -1,6 +0,0 @@ -A,0 -AA,0 -B,12 -B-15,15 -C,18 -D,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.json b/Emby.Server.Implementations/Localization/Ratings/mx.json new file mode 100644 index 000000000..9dc3b89bd --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/mx.json @@ -0,0 +1,41 @@ +{ + "countryCode": "mx", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A", "AA"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["B"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["B-15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv deleted file mode 100644 index 44f372b2d..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nl.csv +++ /dev/null @@ -1,8 +0,0 @@ -AL,0 -MG6,6 -6,6 -9,9 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.json b/Emby.Server.Implementations/Localization/Ratings/nl.json new file mode 100644 index 000000000..2e43eb83a --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/nl.json @@ -0,0 +1,55 @@ +{ + "countryCode": "nl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "MG6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv deleted file mode 100644 index 6856a2dbb..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ /dev/null @@ -1,10 +0,0 @@ -A,0 -6,6 -7,7 -9,9 -11,11 -12,12 -15,15 -18,18 -C,18 -Not approved,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.json b/Emby.Server.Implementations/Localization/Ratings/no.json new file mode 100644 index 000000000..a5e952316 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/no.json @@ -0,0 +1,69 @@ +{ + "countryCode": "no", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["Not approved"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv deleted file mode 100644 index 633da78fe..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ /dev/null @@ -1,16 +0,0 @@ -Exempt,0 -G,0 -GY,13 -PG,13 -R13,13 -RP13,13 -R15,15 -M,16 -R16,16 -RP16,16 -GA,18 -R18,18 -RP18,18 -MA,1000 -R,1001 -Objectionable,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json new file mode 100644 index 000000000..23b23c8ca --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/nz.json @@ -0,0 +1,76 @@ +{ + "countryCode": "nz", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Exempt", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["RP13", "PG"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["GY", "R13"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["R15"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["RP16", "M"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["R16"], + "ratingScore": { + "score": 16, + "subScore": 1 + } + }, + { + "ratingStrings": ["RP18"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["R18", "GA"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["MA"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["Objectionable", "R"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ph.json b/Emby.Server.Implementations/Localization/Ratings/ph.json new file mode 100644 index 000000000..0bce9df8f --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ph.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ph", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["R-13"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["R-16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["R-18"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/pl.json b/Emby.Server.Implementations/Localization/Ratings/pl.json new file mode 100644 index 000000000..c3001ffb3 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/pl.json @@ -0,0 +1,41 @@ +{
+ "countryCode": "pl",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["b.o.", "AL"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7", "od 7 lat"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "od 12 lat"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "od 16 lat"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "od 18 lat", "R"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/pt.json b/Emby.Server.Implementations/Localization/Ratings/pt.json new file mode 100644 index 000000000..2ab796c84 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/pt.json @@ -0,0 +1,62 @@ +{ + "countryCode": "pt", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["Públicos"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["M/3"], + "ratingScore": { + "score": 3, + "subScore": null + } + }, + { + "ratingStrings": ["M/6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["M/12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["M/14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["M/16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["M/18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["P"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv deleted file mode 100644 index 44c23e248..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ro.csv +++ /dev/null @@ -1,6 +0,0 @@ -AG,0 -AP-12,12 -N-15,15 -IM-18,18 -IM-18-XXX,1000 -IC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json new file mode 100644 index 000000000..aa6f7fe55 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ro.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ro", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AG", "AP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12", "AP-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15", "N-15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18", "IM-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["18+", "IM-18-XXX"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["IC"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv deleted file mode 100644 index 8b264070b..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ru.csv +++ /dev/null @@ -1,6 +0,0 @@ -0+,0 -6+,6 -12+,12 -16+,16 -18+,18 -Refused classification,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.json b/Emby.Server.Implementations/Localization/Ratings/ru.json new file mode 100644 index 000000000..d1b8b13aa --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ru.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ru", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["Refused classification"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv deleted file mode 100644 index e129c3561..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/se.csv +++ /dev/null @@ -1,10 +0,0 @@ -Alla,0 -Barntillåten,0 -Btl,0 -0+,0 -7,7 -9+,9 -10+,10 -11,11 -14,14 -15,15 diff --git a/Emby.Server.Implementations/Localization/Ratings/se.json b/Emby.Server.Implementations/Localization/Ratings/se.json new file mode 100644 index 000000000..70084995d --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/se.json @@ -0,0 +1,55 @@ +{ + "countryCode": "se", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "Alla", "Barntillåten", "Btl"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10+"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/sg.json b/Emby.Server.Implementations/Localization/Ratings/sg.json new file mode 100644 index 000000000..47d9e2833 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/sg.json @@ -0,0 +1,48 @@ +{ + "countryCode": "sg", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["PG13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["NC16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["M18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["R21"], + "ratingScore": { + "score": 21, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv deleted file mode 100644 index dbafd8efa..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/sk.csv +++ /dev/null @@ -1,6 +0,0 @@ -NR,0
-U,0
-7,7
-12,12
-15,15
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.json b/Emby.Server.Implementations/Localization/Ratings/sk.json new file mode 100644 index 000000000..5ec6111ec --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/sk.json @@ -0,0 +1,41 @@ +{ + "countryCode": "sk", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U", "NR"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/th.json b/Emby.Server.Implementations/Localization/Ratings/th.json new file mode 100644 index 000000000..44bfab21c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/th.json @@ -0,0 +1,48 @@ +{ + "countryCode": "th", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["P", "G"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["20"], + "ratingScore": { + "score": 20, + "subScore": null + } + }, + { + "ratingStrings": ["Banned"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/tr.json b/Emby.Server.Implementations/Localization/Ratings/tr.json new file mode 100644 index 000000000..5a3868856 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/tr.json @@ -0,0 +1,69 @@ +{ + "countryCode": "tr", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Genel İzleyici Kitlesi"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6A"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 1 + } + }, + { + "ratingStrings": ["10A"], + "ratingScore": { + "score": 10, + "subScore": 0 + } + }, + { + "ratingStrings": ["10+"], + "ratingScore": { + "score": 10, + "subScore": 1 + } + }, + { + "ratingStrings": ["13A"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["13+"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/tw.json b/Emby.Server.Implementations/Localization/Ratings/tw.json new file mode 100644 index 000000000..a7869c122 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/tw.json @@ -0,0 +1,41 @@ +{ + "countryCode": "tw", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15+"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ua.json b/Emby.Server.Implementations/Localization/Ratings/ua.json new file mode 100644 index 000000000..d8fe95168 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ua.json @@ -0,0 +1,34 @@ +{ + "countryCode": "ua", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv deleted file mode 100644 index 75b1c2058..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/uk.csv +++ /dev/null @@ -1,22 +0,0 @@ -All,0 -E,0 -G,0 -U,0 -0+,0 -6+,6 -7+,7 -PG,8 -9+,9 -12,12 -12+,12 -12A,12 -Teen,13 -13+,13 -14+,14 -15,15 -16,16 -Caution,18 -18,18 -Mature,1000 -Adult,1000 -R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.json b/Emby.Server.Implementations/Localization/Ratings/uk.json new file mode 100644 index 000000000..7fc88272c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/uk.json @@ -0,0 +1,97 @@ +{ + "countryCode": "gb", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["0+", "All", "E", "G", "U"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 8, + "subScore": 0 + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12", "12+"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["13+", "Teen"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18", "Caution"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["Mature", "Adult", "R18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv deleted file mode 100644 index 9aa5c00eb..000000000 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ /dev/null @@ -1,52 +0,0 @@ -Approved,0 -G,0 -TV-G,0 -TV-Y,0 -TV-Y7,7 -TV-Y7-FV,7 -PG,10 -TV-PG,10 -TV-PG-D,10 -TV-PG-L,10 -TV-PG-S,10 -TV-PG-V,10 -TV-PG-DL,10 -TV-PG-DS,10 -TV-PG-DV,10 -TV-PG-LS,10 -TV-PG-LV,10 -TV-PG-SV,10 -TV-PG-DLS,10 -TV-PG-DLV,10 -TV-PG-DSV,10 -TV-PG-LSV,10 -TV-PG-DLSV,10 -PG-13,13 -TV-14,14 -TV-14-D,14 -TV-14-L,14 -TV-14-S,14 -TV-14-V,14 -TV-14-DL,14 -TV-14-DS,14 -TV-14-DV,14 -TV-14-LS,14 -TV-14-LV,14 -TV-14-SV,14 -TV-14-DLS,14 -TV-14-DLV,14 -TV-14-DSV,14 -TV-14-LSV,14 -TV-14-DLSV,14 -NC-17,17 -R,17 -TV-MA,17 -TV-MA-L,17 -TV-MA-S,17 -TV-MA-V,17 -TV-MA-LS,17 -TV-MA-LV,17 -TV-MA-SV,17 -TV-MA-LSV,17 -TV-X,18 -TV-AO,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.json b/Emby.Server.Implementations/Localization/Ratings/us.json new file mode 100644 index 000000000..08a637312 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/us.json @@ -0,0 +1,83 @@ +{ + "countryCode": "us", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Approved", "G", "TV-G", "TV-Y"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7-FV"], + "ratingScore": { + "score": 7, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 10, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-PG-D", "TV-PG-L", "TV-PG-S", "TV-PG-V", "TV-PG-DL", "TV-PG-DS", "TV-PG-DV", "TV-PG-LS", "TV-PG-LV", "TV-PG-SV", "TV-PG-DLS", "TV-PG-DLV", "TV-PG-DSV", "TV-PG-LSV", "TV-PG-DLSV"], + "ratingScore": { + "score": 10, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG-13"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14-D", "TV-14-L", "TV-14-S", "TV-14-V", "TV-14-DL", "TV-14-DS", "TV-14-DV", "TV-14-LS", "TV-14-LV", "TV-14-SV", "TV-14-DLS", "TV-14-DLV", "TV-14-DSV", "TV-14-LSV", "TV-14-DLSV"], + "ratingScore": { + "score": 14, + "subScore": 1 + } + }, + { + "ratingStrings": ["R"], + "ratingScore": { + "score": 17, + "subScore": 0 + } + }, + { + "ratingStrings": ["NC-17", "TV-MA", "TV-MA-L", "TV-MA-S", "TV-MA-V", "TV-MA-LS", "TV-MA-LV", "TV-MA-SV", "TV-MA-LSV"], + "ratingScore": { + "score": 17, + "subScore": 1 + } + }, + { + "ratingStrings": ["TV-X", "TV-AO"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/za.json b/Emby.Server.Implementations/Localization/Ratings/za.json new file mode 100644 index 000000000..fe13af797 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/za.json @@ -0,0 +1,55 @@ +{ + "countryCode": "za", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["PG", "7-9PG"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["10-12PG"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X18", "XX"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 0a11b3e45..d92dc880b 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -336,7 +336,7 @@ "TwoLetterISORegionName": "IE" }, { - "DisplayName": "Islamic Republic of Pakistan", + "DisplayName": "Pakistan", "Name": "PK", "ThreeLetterISORegionName": "PAK", "TwoLetterISORegionName": "PK" diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index b55c0fa33..97da67481 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -10,7 +10,6 @@ afr||af|Afrikaans|afrikaans ain|||Ainu|aïnou aka||ak|Akan|akan akk|||Akkadian|akkadien -alb|sqi|sq|Albanian|albanais ale|||Aleut|aléoute alg|||Algonquian languages|algonquines, langues alt|||Southern Altai|altai du Sud @@ -21,7 +20,6 @@ apa|||Apache languages|apaches, langues ara||ar|Arabic|arabe arc|||Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)|araméen d'empire (700-300 BCE) arg||an|Aragonese|aragonais -arm|hye|hy|Armenian|arménien arn|||Mapudungun; Mapuche|mapudungun; mapuche; mapuce arp|||Arapaho|arapaho art|||Artificial languages|artificielles, langues @@ -41,7 +39,6 @@ bak||ba|Bashkir|bachkir bal|||Baluchi|baloutchi bam||bm|Bambara|bambara ban|||Balinese|balinais -baq|eus|eu|Basque|basque bas|||Basa|basa bat|||Baltic languages|baltes, langues bej|||Beja; Bedawiyet|bedja @@ -56,6 +53,7 @@ bin|||Bini; Edo|bini; edo bis||bi|Bislama|bichlamar bla|||Siksika|blackfoot bnt|||Bantu (Other)|bantoues, autres langues +bod|tib|bo|Tibetan|tibétain bos||bs|Bosnian|bosniaque bra|||Braj|braj bre||br|Breton|breton @@ -63,7 +61,6 @@ btk|||Batak languages|batak, langues bua|||Buriat|bouriate bug|||Buginese|bugi bul||bg|Bulgarian|bulgare -bur|mya|my|Burmese|birman byn|||Blin; Bilin|blin; bilen cad|||Caddo|caddo cai|||Central American Indian languages|amérindiennes de L'Amérique centrale, langues @@ -72,14 +69,11 @@ cat||ca|Catalan; Valencian|catalan; valencien cau|||Caucasian languages|caucasiennes, langues ceb|||Cebuano|cebuano cel|||Celtic languages|celtiques, langues; celtes, langues +ces|cze|cs|Czech|tchèque cha||ch|Chamorro|chamorro chb|||Chibcha|chibcha che||ce|Chechen|tchétchène chg|||Chagatai|djaghataï -chi|zho|zh|Chinese|chinois -chi|zho|ze|Chinese; Bilingual|chinois -chi|zho|zh-tw|Chinese; Traditional|chinois -chi|zho|zh-hk|Chinese; Hong Kong|chinois chk|||Chuukese|chuuk chm|||Mari|mari chn|||Chinook jargon|chinook, jargon @@ -101,13 +95,14 @@ crh|||Crimean Tatar; Crimean Turkish|tatar de Crimé crp|||Creoles and pidgins |créoles et pidgins csb|||Kashubian|kachoube cus|||Cushitic languages|couchitiques, langues -cze|ces|cs|Czech|tchèque +cym|wel|cy|Welsh|gallois dak|||Dakota|dakota dan||da|Danish|danois dar|||Dargwa|dargwa day|||Land Dayak languages|dayak, langues del|||Delaware|delaware den|||Slave (Athapascan)|esclave (athapascan) +deu|ger|de|German|allemand dgr|||Dogrib|dogrib din|||Dinka|dinka div||dv|Divehi; Dhivehi; Maldivian|maldivien @@ -116,28 +111,30 @@ dra|||Dravidian languages|dravidiennes, langues dsb|||Lower Sorbian|bas-sorabe dua|||Duala|douala dum|||Dutch, Middle (ca.1050-1350)|néerlandais moyen (ca. 1050-1350) -dut|nld|nl|Dutch; Flemish|néerlandais; flamand dyu|||Dyula|dioula dzo||dz|Dzongkha|dzongkha efi|||Efik|efik egy|||Egyptian (Ancient)|égyptien eka|||Ekajuk|ekajuk +ell|gre|el|Greek, Modern (1453-)|grec moderne (après 1453) elx|||Elamite|élamite eng||en|English|anglais enm|||English, Middle (1100-1500)|anglais moyen (1100-1500) epo||eo|Esperanto|espéranto est||et|Estonian|estonien +eus|baq|eu|Basque|basque ewe||ee|Ewe|éwé ewo|||Ewondo|éwondo fan|||Fang|fang fao||fo|Faroese|féroïen +fas|per|fa|Persian|persan fat|||Fanti|fanti fij||fj|Fijian|fidjien fil|||Filipino; Pilipino|filipino; pilipino fin||fi|Finnish|finnois fiu|||Finno-Ugrian languages|finno-ougriennes, langues fon|||Fon|fon -fre|fra|fr|French|français +fra|fre|fr|French|français frm|||French, Middle (ca.1400-1600)|français moyen (1400-1600) fro|||French, Old (842-ca.1400)|français ancien (842-ca.1400) frc||fr-ca|French (Canada)|french @@ -150,8 +147,6 @@ gaa|||Ga|ga gay|||Gayo|gayo gba|||Gbaya|gbaya gem|||Germanic languages|germaniques, langues -geo|kat|ka|Georgian|géorgien -ger|deu|de|German|allemand gez|||Geez|guèze gil|||Gilbertese|kiribati gla||gd|Gaelic; Scottish Gaelic|gaélique; gaélique écossais @@ -165,7 +160,6 @@ gor|||Gorontalo|gorontalo got|||Gothic|gothique grb|||Grebo|grebo grc|||Greek, Ancient (to 1453)|grec ancien (jusqu'à 1453) -gre|ell|el|Greek, Modern (1453-)|grec moderne (après 1453) grn||gn|Guarani|guarani gsw|||Swiss German; Alemannic; Alsatian|suisse alémanique; alémanique; alsacien guj||gu|Gujarati|goudjrati @@ -186,9 +180,10 @@ hrv||hr|Croatian|croate hsb|||Upper Sorbian|haut-sorabe hun||hu|Hungarian|hongrois hup|||Hupa|hupa +hye|arm|hy|Armenian|arménien iba|||Iban|iban ibo||ig|Igbo|igbo -ice|isl|is|Icelandic|islandais +isl|ice|is|Icelandic|islandais ido||io|Ido|ido iii||ii|Sichuan Yi; Nuosu|yi de Sichuan ijo|||Ijo languages|ijo, langues @@ -217,6 +212,7 @@ kam|||Kamba|kamba kan||kn|Kannada|kannada kar|||Karen languages|karen, langues kas||ks|Kashmiri|kashmiri +kat|geo|ka|Georgian|géorgien kau||kr|Kanuri|kanouri kaw|||Kawi|kawi kaz||kk|Kazakh|kazakh @@ -263,7 +259,6 @@ lui|||Luiseno|luiseno lun|||Lunda|lunda luo|||Luo (Kenya and Tanzania)|luo (Kenya et Tanzanie) lus|||Lushai|lushai -mac|mkd|mk|Macedonian|macédonien mad|||Madurese|madourais mag|||Magahi|magahi mah||mh|Marshallese|marshall @@ -271,11 +266,9 @@ mai|||Maithili|maithili mak|||Makasar|makassar mal||ml|Malayalam|malayalam man|||Mandingo|mandingue -mao|mri|mi|Maori|maori map|||Austronesian languages|austronésiennes, langues mar||mr|Marathi|marathe mas|||Masai|massaï -may|msa|ms|Malay|malais mdf|||Moksha|moksa mdr|||Mandar|mandar men|||Mende|mendé @@ -283,6 +276,7 @@ mga|||Irish, Middle (900-1200)|irlandais moyen (900-1200) mic|||Mi'kmaq; Micmac|mi'kmaq; micmac min|||Minangkabau|minangkabau mis|||Uncoded languages|langues non codées +mkd|mac|mk|Macedonian|macédonien mkh|||Mon-Khmer languages|môn-khmer, langues mlg||mg|Malagasy|malgache mlt||mt|Maltese|maltais @@ -292,11 +286,14 @@ mno|||Manobo languages|manobo, langues moh|||Mohawk|mohawk mon||mn|Mongolian|mongol mos|||Mossi|moré +mri|mao|mi|Maori|maori +msa|may|ms|Malay|malais mul|||Multiple languages|multilingue mun|||Munda languages|mounda, langues mus|||Creek|muskogee mwl|||Mirandese|mirandais mwr|||Marwari|marvari +mya|bur|my|Burmese|birman myn|||Mayan languages|maya, langues myv|||Erzya|erza nah|||Nahuatl languages|nahuatl, langues @@ -313,6 +310,7 @@ new|||Nepal Bhasa; Newari|nepal bhasa; newari nia|||Nias|nias nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues niu|||Niuean|niué +nld|dut|nl|Dutch; Flemish|néerlandais; flamand nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål nog|||Nogai|nogaï; nogay @@ -343,15 +341,14 @@ pan||pa|Panjabi; Punjabi|pendjabi pap|||Papiamento|papiamento pau|||Palauan|palau peo|||Persian, Old (ca.600-400 B.C.)|perse, vieux (ca. 600-400 av. J.-C.) -per|fas|fa|Persian|persan phi|||Philippine languages|philippines, langues phn|||Phoenician|phénicien pli||pi|Pali|pali pol||pl|Polish|polonais pon|||Pohnpeian|pohnpei por||pt|Portuguese|portugais -pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt) -pob||pt-br|Portuguese (Brazil)|portugais (pt-br) +por||pt-pt|Portuguese (Portugal)|portugais (pt-pt) +por||pt-br|Portuguese (Brazil)|portugais (pt-br) pra|||Prakrit languages|prâkrit, langues pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) pus||ps|Pushto; Pashto|pachto @@ -363,7 +360,7 @@ rar|||Rarotongan; Cook Islands Maori|rarotonga; maori des îles Cook roa|||Romance languages|romanes, langues roh||rm|Romansh|romanche rom|||Romany|tsigane -rum|ron|ro|Romanian; Moldavian; Moldovan|roumain; moldave +ron|rum|ro|Romanian; Moldavian; Moldovan|roumain; moldave run||rn|Rundi|rundi rup|||Aromanian; Arumanian; Macedo-Romanian|aroumain; macédo-roumain rus||ru|Russian|russe @@ -376,6 +373,7 @@ sam|||Samaritan Aramaic|samaritain san||sa|Sanskrit|sanskrit sas|||Sasak|sasak sat|||Santali|santal +srp||sr|Serbian|serbe scn|||Sicilian|sicilien sco|||Scots|écossais sel|||Selkup|selkoupe @@ -388,7 +386,7 @@ sin||si|Sinhala; Sinhalese|singhalais sio|||Siouan languages|sioux, langues sit|||Sino-Tibetan languages|sino-tibétaines, langues sla|||Slavic languages|slaves, langues -slo|slk|sk|Slovak|slovaque +slk|slo|sk|Slovak|slovaque slv||sl|Slovenian|slovène sma|||Southern Sami|sami du Sud sme||se|Northern Sami|sami du Nord @@ -406,9 +404,9 @@ son|||Songhai languages|songhai, langues sot||st|Sotho, Southern|sotho du Sud spa||es-mx|Spanish; Latin|espagnol; Latin spa||es|Spanish; Castilian|espagnol; castillan +sqi|alb|sq|Albanian|albanais srd||sc|Sardinian|sarde srn|||Sranan Tongo|sranan tongo -srp|scc|sr|Serbian|serbe srr|||Serer|sérère ssa|||Nilo-Saharan languages|nilo-sahariennes, langues ssw||ss|Swati|swati @@ -431,7 +429,6 @@ tet|||Tetum|tetum tgk||tg|Tajik|tadjik tgl||tl|Tagalog|tagalog tha||th|Thai|thaï -tib|bod|bo|Tibetan|tibétain tig|||Tigre|tigré tir||ti|Tigrinya|tigrigna tiv|||Tiv|tiv @@ -470,7 +467,6 @@ wak|||Wakashan languages|wakashanes, langues wal|||Walamo|walamo war|||Waray|waray was|||Washo|washo -wel|cym|cy|Welsh|gallois wen|||Sorbian languages|sorabes, langues wln||wa|Walloon|wallon wol||wo|Wolof|wolof @@ -486,6 +482,10 @@ zbl|||Blissymbols; Blissymbolics; Bliss|symboles Bliss; Bliss zen|||Zenaga|zenaga zgh|||Standard Moroccan Tamazight|amazighe standard marocain zha||za|Zhuang; Chuang|zhuang; chuang +zho|chi|zh|Chinese|chinois +zho|chi|ze|Chinese; Bilingual|chinois +zho|chi|zh-tw|Chinese; Traditional|chinois +zho|chi|zh-hk|Chinese; Hong Kong|chinois znd|||Zande languages|zandé, langues zul||zu|Zulu|zoulou zun|||Zuni|zuni diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs deleted file mode 100644 index ea7896861..000000000 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ /dev/null @@ -1,272 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.MediaEncoder -{ - public class EncodingManager : IEncodingManager - { - private readonly IFileSystem _fileSystem; - private readonly ILogger<EncodingManager> _logger; - private readonly IMediaEncoder _encoder; - private readonly IChapterRepository _chapterManager; - private readonly ILibraryManager _libraryManager; - - /// <summary> - /// The first chapter ticks. - /// </summary> - private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; - - public EncodingManager( - ILogger<EncodingManager> logger, - IFileSystem fileSystem, - IMediaEncoder encoder, - IChapterRepository chapterManager, - ILibraryManager libraryManager) - { - _logger = logger; - _fileSystem = fileSystem; - _encoder = encoder; - _chapterManager = chapterManager; - _libraryManager = libraryManager; - } - - /// <summary> - /// Gets the chapter images data path. - /// </summary> - /// <value>The chapter images data path.</value> - private static string GetChapterImagesPath(BaseItem item) - { - return Path.Combine(item.GetInternalMetadataPath(), "chapters"); - } - - /// <summary> - /// Determines whether [is eligible for chapter image extraction] [the specified video]. - /// </summary> - /// <param name="video">The video.</param> - /// <param name="libraryOptions">The library options for the video.</param> - /// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns> - private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) - { - if (video.IsPlaceHolder) - { - return false; - } - - if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) - { - return false; - } - - // Can't extract images if there are no video streams - return video.DefaultVideoStreamIndex.HasValue; - } - - private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters) - { - if (chapters.Count < 2) - { - return 0; - } - - long sum = 0; - for (int i = 1; i < chapters.Count; i++) - { - sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; - } - - return sum / chapters.Count; - } - - public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) - { - if (chapters.Count == 0) - { - return true; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - - if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) - { - extractImages = false; - } - - var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); - var threshold = TimeSpan.FromSeconds(1).Ticks; - if (averageChapterDuration < threshold) - { - _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); - extractImages = false; - } - - var success = true; - var changesMade = false; - - var runtimeTicks = video.RunTimeTicks ?? 0; - - var currentImages = GetSavedChapterImages(video, directoryService); - - foreach (var chapter in chapters) - { - if (chapter.StartPositionTicks >= runtimeTicks) - { - _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name); - break; - } - - var path = GetChapterImagePath(video, chapter.StartPositionTicks); - - if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) - { - if (extractImages) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - // Add some time for the first chapter to make sure we don't end up with a black image - var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - - var inputPath = video.Path; - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - var container = video.Container; - var mediaSource = new MediaSourceInfo - { - VideoType = video.VideoType, - IsoType = video.IsoType, - Protocol = video.PathProtocol.Value, - }; - - var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); - File.Copy(tempFile, path, true); - - try - { - _fileSystem.DeleteFile(tempFile); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); - } - - chapter.ImagePath = path; - chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); - changesMade = true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)); - success = false; - break; - } - } - else if (!string.IsNullOrEmpty(chapter.ImagePath)) - { - chapter.ImagePath = null; - changesMade = true; - } - } - else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) - { - chapter.ImagePath = path; - chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); - changesMade = true; - } - else if (libraryOptions?.EnableChapterImageExtraction != true) - { - // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image - chapter.ImagePath = null; - changesMade = true; - } - } - - if (saveChapters && changesMade) - { - _chapterManager.SaveChapters(video.Id, chapters); - } - - DeleteDeadImages(currentImages, chapters); - - return success; - } - - private string GetChapterImagePath(Video video, long chapterPositionTicks) - { - var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; - - return Path.Combine(GetChapterImagesPath(video), filename); - } - - private static IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService) - { - var path = GetChapterImagesPath(video); - if (!Directory.Exists(path)) - { - return Array.Empty<string>(); - } - - try - { - return directoryService.GetFilePaths(path); - } - catch (IOException) - { - return Array.Empty<string>(); - } - } - - private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters) - { - var deadImages = images - .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) - .ToList(); - - foreach (var image in deadImages) - { - _logger.LogDebug("Deleting dead chapter image {Path}", image); - - try - { - _fileSystem.DeleteFile(image); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting {Path}.", image); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index daeb7fed8..1ce363de5 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -9,8 +9,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -134,14 +134,16 @@ namespace Emby.Server.Implementations.Playlists try { - Directory.CreateDirectory(path); + var info = Directory.CreateDirectory(path); var playlist = new Playlist { Name = name, Path = path, OwnerUserId = request.UserId, Shares = request.Users ?? [], - OpenAccess = request.Public ?? false + OpenAccess = request.Public ?? false, + DateCreated = info.CreationTimeUtc, + DateModified = info.LastWriteTimeUtc }; playlist.SetMediaType(request.MediaType); @@ -283,6 +285,16 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } + internal static int DetermineAdjustedIndex(int newPriorIndexAllChildren, int newIndex) + { + if (newIndex == 0) + { + return newPriorIndexAllChildren > 0 ? newPriorIndexAllChildren - 1 : 0; + } + + return newPriorIndexAllChildren + 1; + } + public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId) { if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) @@ -305,12 +317,12 @@ namespace Emby.Server.Implementations.Playlists var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1; var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId; var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId)); - var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1; + var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex); var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); if (item is null) { - _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId); + _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId); return; } diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index db3aeaaf3..a5be2b616 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 8eeca3667..91ccb16ef 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins Overview = packageInfo.Overview, Owner = packageInfo.Owner, TargetAbi = versionInfo.TargetAbi ?? string.Empty, - Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture), + Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal), Version = versionInfo.Version, Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state. AutoUpdate = true, diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 985f0a8f8..24f554981 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -16,663 +16,662 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks +namespace Emby.Server.Implementations.ScheduledTasks; + +/// <summary> +/// Class ScheduledTaskWorker. +/// </summary> +public class ScheduledTaskWorker : IScheduledTaskWorker { + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger _logger; + private readonly ITaskManager _taskManager; + private readonly Lock _lastExecutionResultSyncLock = new(); + private bool _readFromFile; + private TaskResult _lastExecutionResult; + private Task _currentTask; + private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers; + private string _id; + /// <summary> - /// Class ScheduledTaskWorker. + /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. /// </summary> - public class ScheduledTaskWorker : IScheduledTaskWorker + /// <param name="scheduledTask">The scheduled task.</param> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="taskManager">The task manager.</param> + /// <param name="logger">The logger.</param> + /// <exception cref="ArgumentNullException"> + /// scheduledTask + /// or + /// applicationPaths + /// or + /// taskManager + /// or + /// jsonSerializer + /// or + /// logger. + /// </exception> + public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) { - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger _logger; - private readonly ITaskManager _taskManager; - private readonly Lock _lastExecutionResultSyncLock = new(); - private bool _readFromFile; - private TaskResult _lastExecutionResult; - private Task _currentTask; - private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers; - private string _id; - - /// <summary> - /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. - /// </summary> - /// <param name="scheduledTask">The scheduled task.</param> - /// <param name="applicationPaths">The application paths.</param> - /// <param name="taskManager">The task manager.</param> - /// <param name="logger">The logger.</param> - /// <exception cref="ArgumentNullException"> - /// scheduledTask - /// or - /// applicationPaths - /// or - /// taskManager - /// or - /// jsonSerializer - /// or - /// logger. - /// </exception> - public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) - { - ArgumentNullException.ThrowIfNull(scheduledTask); - ArgumentNullException.ThrowIfNull(applicationPaths); - ArgumentNullException.ThrowIfNull(taskManager); - ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(scheduledTask); + ArgumentNullException.ThrowIfNull(applicationPaths); + ArgumentNullException.ThrowIfNull(taskManager); + ArgumentNullException.ThrowIfNull(logger); - ScheduledTask = scheduledTask; - _applicationPaths = applicationPaths; - _taskManager = taskManager; - _logger = logger; + ScheduledTask = scheduledTask; + _applicationPaths = applicationPaths; + _taskManager = taskManager; + _logger = logger; - InitTriggerEvents(); - } + InitTriggerEvents(); + } - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<double>> TaskProgress; + /// <inheritdoc /> + public event EventHandler<GenericEventArgs<double>> TaskProgress; - /// <inheritdoc /> - public IScheduledTask ScheduledTask { get; private set; } + /// <inheritdoc /> + public IScheduledTask ScheduledTask { get; private set; } - /// <inheritdoc /> - public TaskResult LastExecutionResult + /// <inheritdoc /> + public TaskResult LastExecutionResult + { + get { - get - { - var path = GetHistoryFilePath(); + var path = GetHistoryFilePath(); - lock (_lastExecutionResultSyncLock) + lock (_lastExecutionResultSyncLock) + { + if (_lastExecutionResult is null && !_readFromFile) { - if (_lastExecutionResult is null && !_readFromFile) + if (File.Exists(path)) { - if (File.Exists(path)) + var bytes = File.ReadAllBytes(path); + if (bytes.Length > 0) { - var bytes = File.ReadAllBytes(path); - if (bytes.Length > 0) + try { - try - { - _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions); - } - catch (JsonException ex) - { - _logger.LogError(ex, "Error deserializing {File}", path); - } + _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions); } - else + catch (JsonException ex) { - _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path); + _logger.LogError(ex, "Error deserializing {File}", path); } } - - _readFromFile = true; + else + { + _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path); + } } - } - return _lastExecutionResult; + _readFromFile = true; + } } - private set - { - _lastExecutionResult = value; + return _lastExecutionResult; + } - var path = GetHistoryFilePath(); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + private set + { + _lastExecutionResult = value; - lock (_lastExecutionResultSyncLock) - { - using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); - JsonSerializer.Serialize(jsonStream, value, _jsonOptions); - } + var path = GetHistoryFilePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_lastExecutionResultSyncLock) + { + using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonStream, value, _jsonOptions); } } + } - /// <inheritdoc /> - public string Name => ScheduledTask.Name; + /// <inheritdoc /> + public string Name => ScheduledTask.Name; - /// <inheritdoc /> - public string Description => ScheduledTask.Description; + /// <inheritdoc /> + public string Description => ScheduledTask.Description; - /// <inheritdoc /> - public string Category => ScheduledTask.Category; + /// <inheritdoc /> + public string Category => ScheduledTask.Category; - /// <summary> - /// Gets or sets the current cancellation token. - /// </summary> - /// <value>The current cancellation token source.</value> - private CancellationTokenSource CurrentCancellationTokenSource { get; set; } + /// <summary> + /// Gets or sets the current cancellation token. + /// </summary> + /// <value>The current cancellation token source.</value> + private CancellationTokenSource CurrentCancellationTokenSource { get; set; } - /// <summary> - /// Gets or sets the current execution start time. - /// </summary> - /// <value>The current execution start time.</value> - private DateTime CurrentExecutionStartTime { get; set; } + /// <summary> + /// Gets or sets the current execution start time. + /// </summary> + /// <value>The current execution start time.</value> + private DateTime CurrentExecutionStartTime { get; set; } - /// <inheritdoc /> - public TaskState State + /// <inheritdoc /> + public TaskState State + { + get { - get + if (CurrentCancellationTokenSource is not null) { - if (CurrentCancellationTokenSource is not null) - { - return CurrentCancellationTokenSource.IsCancellationRequested - ? TaskState.Cancelling - : TaskState.Running; - } - - return TaskState.Idle; + return CurrentCancellationTokenSource.IsCancellationRequested + ? TaskState.Cancelling + : TaskState.Running; } + + return TaskState.Idle; } + } - /// <inheritdoc /> - public double? CurrentProgress { get; private set; } + /// <inheritdoc /> + public double? CurrentProgress { get; private set; } - /// <summary> - /// Gets or sets the triggers that define when the task will run. - /// </summary> - /// <value>The triggers.</value> - private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers + /// <summary> + /// Gets or sets the triggers that define when the task will run. + /// </summary> + /// <value>The triggers.</value> + private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers + { + get => _triggers; + set { - get => _triggers; - set - { - ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(value); - // Cleanup current triggers - if (_triggers is not null) - { - DisposeTriggers(); - } + // Cleanup current triggers + if (_triggers is not null) + { + DisposeTriggers(); + } - _triggers = value.ToArray(); + _triggers = value.ToArray(); - ReloadTriggerEvents(false); - } + ReloadTriggerEvents(false); } + } - /// <inheritdoc /> - public IReadOnlyList<TaskTriggerInfo> Triggers + /// <inheritdoc /> + public IReadOnlyList<TaskTriggerInfo> Triggers + { + get { - get - { - return Array.ConvertAll(InternalTriggers, i => i.Item1); - } + return Array.ConvertAll(InternalTriggers, i => i.Item1); + } - set - { - ArgumentNullException.ThrowIfNull(value); + set + { + ArgumentNullException.ThrowIfNull(value); - // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var triggerList = value.Where(i => i is not null).ToArray(); + // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly + var triggerList = value.Where(i => i is not null).ToArray(); - SaveTriggers(triggerList); + SaveTriggers(triggerList); - InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))); - } + InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))); } + } - /// <inheritdoc /> - public string Id + /// <inheritdoc /> + public string Id + { + get { - get - { - return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } + return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); } + } - private void InitTriggerEvents() - { - _triggers = LoadTriggers(); - ReloadTriggerEvents(true); - } + private void InitTriggerEvents() + { + _triggers = LoadTriggers(); + ReloadTriggerEvents(true); + } - /// <inheritdoc /> - public void ReloadTriggerEvents() - { - ReloadTriggerEvents(false); - } + /// <inheritdoc /> + public void ReloadTriggerEvents() + { + ReloadTriggerEvents(false); + } - /// <summary> - /// Reloads the trigger events. - /// </summary> - /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> - private void ReloadTriggerEvents(bool isApplicationStartup) + /// <summary> + /// Reloads the trigger events. + /// </summary> + /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> + private void ReloadTriggerEvents(bool isApplicationStartup) + { + foreach (var triggerInfo in InternalTriggers) { - foreach (var triggerInfo in InternalTriggers) - { - var trigger = triggerInfo.Item2; + var trigger = triggerInfo.Item2; - trigger.Stop(); + trigger.Stop(); - trigger.Triggered -= OnTriggerTriggered; - trigger.Triggered += OnTriggerTriggered; - trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); - } + trigger.Triggered -= OnTriggerTriggered; + trigger.Triggered += OnTriggerTriggered; + trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); } + } - /// <summary> - /// Handles the Triggered event of the trigger control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> - private async void OnTriggerTriggered(object sender, EventArgs e) - { - var trigger = (ITaskTrigger)sender; + /// <summary> + /// Handles the Triggered event of the trigger control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> + private async void OnTriggerTriggered(object sender, EventArgs e) + { + var trigger = (ITaskTrigger)sender; - if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) - { - return; - } + if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) + { + return; + } - _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); + _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); - trigger.Stop(); + trigger.Stop(); - _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); + _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000).ConfigureAwait(false); - trigger.Start(LastExecutionResult, _logger, Name, false); - } + trigger.Start(LastExecutionResult, _logger, Name, false); + } - /// <summary> - /// Executes the task. - /// </summary> - /// <param name="options">Task options.</param> - /// <returns>Task.</returns> - /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception> - public async Task Execute(TaskOptions options) - { - var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); + /// <summary> + /// Executes the task. + /// </summary> + /// <param name="options">Task options.</param> + /// <returns>Task.</returns> + /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception> + public async Task Execute(TaskOptions options) + { + var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); - _currentTask = task; + _currentTask = task; - try - { - await task.ConfigureAwait(false); - } - finally - { - _currentTask = null; - GC.Collect(); - } + try + { + await task.ConfigureAwait(false); } - - private async Task ExecuteInternal(TaskOptions options) + finally { - // Cancel the current execution, if any - if (CurrentCancellationTokenSource is not null) - { - throw new InvalidOperationException("Cannot execute a Task that is already running"); - } - - var progress = new Progress<double>(); + _currentTask = null; + GC.Collect(); + } + } - CurrentCancellationTokenSource = new CancellationTokenSource(); + private async Task ExecuteInternal(TaskOptions options) + { + // Cancel the current execution, if any + if (CurrentCancellationTokenSource is not null) + { + throw new InvalidOperationException("Cannot execute a Task that is already running"); + } - _logger.LogDebug("Executing {0}", Name); + var progress = new Progress<double>(); - ((TaskManager)_taskManager).OnTaskExecuting(this); + CurrentCancellationTokenSource = new CancellationTokenSource(); - progress.ProgressChanged += OnProgressChanged; + _logger.LogDebug("Executing {0}", Name); - TaskCompletionStatus status; - CurrentExecutionStartTime = DateTime.UtcNow; + ((TaskManager)_taskManager).OnTaskExecuting(this); - Exception failureException = null; + progress.ProgressChanged += OnProgressChanged; - try - { - if (options is not null && options.MaxRuntimeTicks.HasValue) - { - CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); - } + TaskCompletionStatus status; + CurrentExecutionStartTime = DateTime.UtcNow; - await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); + Exception failureException = null; - status = TaskCompletionStatus.Completed; - } - catch (OperationCanceledException) + try + { + if (options is not null && options.MaxRuntimeTicks.HasValue) { - status = TaskCompletionStatus.Cancelled; + CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); } - catch (Exception ex) - { - _logger.LogError(ex, "Error executing Scheduled Task"); - failureException = ex; - - status = TaskCompletionStatus.Failed; - } + await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); - var startTime = CurrentExecutionStartTime; - var endTime = DateTime.UtcNow; + status = TaskCompletionStatus.Completed; + } + catch (OperationCanceledException) + { + status = TaskCompletionStatus.Cancelled; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing Scheduled Task"); - progress.ProgressChanged -= OnProgressChanged; - CurrentCancellationTokenSource.Dispose(); - CurrentCancellationTokenSource = null; - CurrentProgress = null; + failureException = ex; - OnTaskCompleted(startTime, endTime, status, failureException); + status = TaskCompletionStatus.Failed; } - /// <summary> - /// Progress_s the progress changed. - /// </summary> - /// <param name="sender">The sender.</param> - /// <param name="e">The e.</param> - private void OnProgressChanged(object sender, double e) - { - e = Math.Min(e, 100); + var startTime = CurrentExecutionStartTime; + var endTime = DateTime.UtcNow; - CurrentProgress = e; + progress.ProgressChanged -= OnProgressChanged; + CurrentCancellationTokenSource.Dispose(); + CurrentCancellationTokenSource = null; + CurrentProgress = null; - TaskProgress?.Invoke(this, new GenericEventArgs<double>(e)); - } + OnTaskCompleted(startTime, endTime, status, failureException); + } - /// <summary> - /// Stops the task if it is currently executing. - /// </summary> - /// <exception cref="InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception> - public void Cancel() - { - if (State != TaskState.Running) - { - throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); - } + /// <summary> + /// Progress_s the progress changed. + /// </summary> + /// <param name="sender">The sender.</param> + /// <param name="e">The e.</param> + private void OnProgressChanged(object sender, double e) + { + e = Math.Min(e, 100); - CancelIfRunning(); - } + CurrentProgress = e; - /// <summary> - /// Cancels if running. - /// </summary> - public void CancelIfRunning() - { - if (State == TaskState.Running) - { - _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); - CurrentCancellationTokenSource.Cancel(); - } - } + TaskProgress?.Invoke(this, new GenericEventArgs<double>(e)); + } - /// <summary> - /// Gets the scheduled tasks configuration directory. - /// </summary> - /// <returns>System.String.</returns> - private string GetScheduledTasksConfigurationDirectory() + /// <summary> + /// Stops the task if it is currently executing. + /// </summary> + /// <exception cref="InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception> + public void Cancel() + { + if (State != TaskState.Running) { - return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); } - /// <summary> - /// Gets the scheduled tasks data directory. - /// </summary> - /// <returns>System.String.</returns> - private string GetScheduledTasksDataDirectory() - { - return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); - } + CancelIfRunning(); + } - /// <summary> - /// Gets the history file path. - /// </summary> - /// <value>The history file path.</value> - private string GetHistoryFilePath() + /// <summary> + /// Cancels if running. + /// </summary> + public void CancelIfRunning() + { + if (State == TaskState.Running) { - return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); + _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); + CurrentCancellationTokenSource.Cancel(); } + } - /// <summary> - /// Gets the configuration file path. - /// </summary> - /// <returns>System.String.</returns> - private string GetConfigurationFilePath() - { - return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); - } + /// <summary> + /// Gets the scheduled tasks configuration directory. + /// </summary> + /// <returns>System.String.</returns> + private string GetScheduledTasksConfigurationDirectory() + { + return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + } - /// <summary> - /// Loads the triggers. - /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers() - { - // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var settings = LoadTriggerSettings().Where(i => i is not null); + /// <summary> + /// Gets the scheduled tasks data directory. + /// </summary> + /// <returns>System.String.</returns> + private string GetScheduledTasksDataDirectory() + { + return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); + } - return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray(); - } + /// <summary> + /// Gets the history file path. + /// </summary> + /// <value>The history file path.</value> + private string GetHistoryFilePath() + { + return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); + } - private TaskTriggerInfo[] LoadTriggerSettings() - { - string path = GetConfigurationFilePath(); - TaskTriggerInfo[] list = null; - if (File.Exists(path)) - { - var bytes = File.ReadAllBytes(path); - list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions); - } + /// <summary> + /// Gets the configuration file path. + /// </summary> + /// <returns>System.String.</returns> + private string GetConfigurationFilePath() + { + return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); + } - // Return defaults if file doesn't exist. - return list ?? GetDefaultTriggers(); - } + /// <summary> + /// Loads the triggers. + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers() + { + // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly + var settings = LoadTriggerSettings().Where(i => i is not null); + + return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray(); + } - private TaskTriggerInfo[] GetDefaultTriggers() + private TaskTriggerInfo[] LoadTriggerSettings() + { + string path = GetConfigurationFilePath(); + TaskTriggerInfo[] list = null; + if (File.Exists(path)) { - try - { - return ScheduledTask.GetDefaultTriggers().ToArray(); - } - catch - { - return - [ - new() - { - IntervalTicks = TimeSpan.FromDays(1).Ticks, - Type = TaskTriggerInfoType.IntervalTrigger - } - ]; - } + var bytes = File.ReadAllBytes(path); + list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions); } - /// <summary> - /// Saves the triggers. - /// </summary> - /// <param name="triggers">The triggers.</param> - private void SaveTriggers(TaskTriggerInfo[] triggers) - { - var path = GetConfigurationFilePath(); + // Return defaults if file doesn't exist. + return list ?? GetDefaultTriggers(); + } - Directory.CreateDirectory(Path.GetDirectoryName(path)); - using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); - JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); + private TaskTriggerInfo[] GetDefaultTriggers() + { + try + { + return ScheduledTask.GetDefaultTriggers().ToArray(); } - - /// <summary> - /// Called when [task completed]. - /// </summary> - /// <param name="startTime">The start time.</param> - /// <param name="endTime">The end time.</param> - /// <param name="status">The status.</param> - /// <param name="ex">The exception.</param> - private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) + catch { - var elapsedTime = endTime - startTime; + return + [ + new() + { + IntervalTicks = TimeSpan.FromDays(1).Ticks, + Type = TaskTriggerInfoType.IntervalTrigger + } + ]; + } + } - _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); + /// <summary> + /// Saves the triggers. + /// </summary> + /// <param name="triggers">The triggers.</param> + private void SaveTriggers(TaskTriggerInfo[] triggers) + { + var path = GetConfigurationFilePath(); - var result = new TaskResult - { - StartTimeUtc = startTime, - EndTimeUtc = endTime, - Status = status, - Name = Name, - Id = Id - }; + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); + } - result.Key = ScheduledTask.Key; + /// <summary> + /// Called when [task completed]. + /// </summary> + /// <param name="startTime">The start time.</param> + /// <param name="endTime">The end time.</param> + /// <param name="status">The status.</param> + /// <param name="ex">The exception.</param> + private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) + { + var elapsedTime = endTime - startTime; - if (ex is not null) - { - result.ErrorMessage = ex.Message; - result.LongErrorMessage = ex.StackTrace; - } + _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); - LastExecutionResult = result; + var result = new TaskResult + { + StartTimeUtc = startTime, + EndTimeUtc = endTime, + Status = status, + Name = Name, + Id = Id + }; - ((TaskManager)_taskManager).OnTaskCompleted(this, result); - } + result.Key = ScheduledTask.Key; - /// <inheritdoc /> - public void Dispose() + if (ex is not null) { - Dispose(true); - GC.SuppressFinalize(this); + result.ErrorMessage = ex.Message; + result.LongErrorMessage = ex.StackTrace; } - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) + LastExecutionResult = result; + + ((TaskManager)_taskManager).OnTaskCompleted(this, result); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) { - if (dispose) - { - DisposeTriggers(); + DisposeTriggers(); - var wasRunning = State == TaskState.Running; - var startTime = CurrentExecutionStartTime; + var wasRunning = State == TaskState.Running; + var startTime = CurrentExecutionStartTime; - var token = CurrentCancellationTokenSource; - if (token is not null) + var token = CurrentCancellationTokenSource; + if (token is not null) + { + try { - try - { - _logger.LogInformation("{Name}: Cancelling", Name); - token.Cancel(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); - } + _logger.LogInformation("{Name}: Cancelling", Name); + token.Cancel(); } - - var task = _currentTask; - if (task is not null) + catch (Exception ex) { - try - { - _logger.LogInformation("{Name}: Waiting on Task", Name); - var exited = task.Wait(2000); - - if (exited) - { - _logger.LogInformation("{Name}: Task exited", Name); - } - else - { - _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling Task.WaitAll();"); - } + _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); } + } - if (token is not null) + var task = _currentTask; + if (task is not null) + { + try { - try + _logger.LogInformation("{Name}: Waiting on Task", Name); + var exited = task.Wait(2000); + + if (exited) { - _logger.LogDebug("{Name}: Disposing CancellationToken", Name); - token.Dispose(); + _logger.LogInformation("{Name}: Task exited", Name); } - catch (Exception ex) + else { - _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); + _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); } } - - if (wasRunning) + catch (Exception ex) { - OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); + _logger.LogError(ex, "Error calling Task.WaitAll();"); } } - } - - /// <summary> - /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. - /// </summary> - /// <param name="info">The info.</param> - /// <returns>BaseTaskTrigger.</returns> - /// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception> - private ITaskTrigger GetTrigger(TaskTriggerInfo info) - { - var options = new TaskOptions - { - MaxRuntimeTicks = info.MaxRuntimeTicks - }; - if (info.Type == TaskTriggerInfoType.DailyTrigger) + if (token is not null) { - if (!info.TimeOfDayTicks.HasValue) + try { - throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); + _logger.LogDebug("{Name}: Disposing CancellationToken", Name); + token.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); } - - return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); } - if (info.Type == TaskTriggerInfoType.WeeklyTrigger) + if (wasRunning) { - if (!info.TimeOfDayTicks.HasValue) - { - throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); - } + OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); + } + } + } - if (!info.DayOfWeek.HasValue) - { - throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); - } + /// <summary> + /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. + /// </summary> + /// <param name="info">The info.</param> + /// <returns>BaseTaskTrigger.</returns> + /// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception> + private ITaskTrigger GetTrigger(TaskTriggerInfo info) + { + var options = new TaskOptions + { + MaxRuntimeTicks = info.MaxRuntimeTicks + }; - return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); + if (info.Type == TaskTriggerInfoType.DailyTrigger) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); } - if (info.Type == TaskTriggerInfoType.IntervalTrigger) + return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); + } + + if (info.Type == TaskTriggerInfoType.WeeklyTrigger) + { + if (!info.TimeOfDayTicks.HasValue) { - if (!info.IntervalTicks.HasValue) - { - throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); - } + throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); + } - return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); + if (!info.DayOfWeek.HasValue) + { + throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); } - if (info.Type == TaskTriggerInfoType.StartupTrigger) + return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); + } + + if (info.Type == TaskTriggerInfoType.IntervalTrigger) + { + if (!info.IntervalTicks.HasValue) { - return new StartupTrigger(options); + throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); } - throw new ArgumentException("Unrecognized trigger type: " + info.Type); + return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); } - /// <summary> - /// Disposes each trigger. - /// </summary> - private void DisposeTriggers() + if (info.Type == TaskTriggerInfoType.StartupTrigger) { - foreach (var triggerInfo in InternalTriggers) + return new StartupTrigger(options); + } + + throw new ArgumentException("Unrecognized trigger type: " + info.Type); + } + + /// <summary> + /// Disposes each trigger. + /// </summary> + private void DisposeTriggers() + { + foreach (var triggerInfo in InternalTriggers) + { + var trigger = triggerInfo.Item2; + trigger.Triggered -= OnTriggerTriggered; + trigger.Stop(); + if (trigger is IDisposable disposable) { - var trigger = triggerInfo.Item2; - trigger.Triggered -= OnTriggerTriggered; - trigger.Stop(); - if (trigger is IDisposable disposable) - { - disposable.Dispose(); - } + disposable.Dispose(); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index a5e4104ff..4ec2c9c78 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -8,255 +8,254 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks +namespace Emby.Server.Implementations.ScheduledTasks; + +/// <summary> +/// Class TaskManager. +/// </summary> +public class TaskManager : ITaskManager { /// <summary> - /// Class TaskManager. + /// The _task queue. /// </summary> - public class TaskManager : ITaskManager - { - /// <summary> - /// The _task queue. - /// </summary> - private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue = - new ConcurrentQueue<Tuple<Type, TaskOptions>>(); - - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger<TaskManager> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="TaskManager" /> class. - /// </summary> - /// <param name="applicationPaths">The application paths.</param> - /// <param name="logger">The logger.</param> - public TaskManager( - IApplicationPaths applicationPaths, - ILogger<TaskManager> logger) - { - _applicationPaths = applicationPaths; - _logger = logger; + private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue = + new ConcurrentQueue<Tuple<Type, TaskOptions>>(); - ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); - } + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger<TaskManager> _logger; - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting; + /// <summary> + /// Initializes a new instance of the <see cref="TaskManager" /> class. + /// </summary> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="logger">The logger.</param> + public TaskManager( + IApplicationPaths applicationPaths, + ILogger<TaskManager> logger) + { + _applicationPaths = applicationPaths; + _logger = logger; - /// <inheritdoc /> - public event EventHandler<TaskCompletionEventArgs>? TaskCompleted; + ScheduledTasks = []; + } - /// <inheritdoc /> - public IReadOnlyList<IScheduledTaskWorker> ScheduledTasks { get; private set; } + /// <inheritdoc /> + public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting; - /// <inheritdoc /> - public void CancelIfRunningAndQueue<T>(TaskOptions options) - where T : IScheduledTask - { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - ((ScheduledTaskWorker)task).CancelIfRunning(); + /// <inheritdoc /> + public event EventHandler<TaskCompletionEventArgs>? TaskCompleted; - QueueScheduledTask<T>(options); - } + /// <inheritdoc /> + public IReadOnlyList<IScheduledTaskWorker> ScheduledTasks { get; private set; } - /// <inheritdoc /> - public void CancelIfRunningAndQueue<T>() - where T : IScheduledTask - { - CancelIfRunningAndQueue<T>(new TaskOptions()); - } + /// <inheritdoc /> + public void CancelIfRunningAndQueue<T>(TaskOptions options) + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); - /// <inheritdoc /> - public void CancelIfRunning<T>() - where T : IScheduledTask - { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - ((ScheduledTaskWorker)task).CancelIfRunning(); - } + QueueScheduledTask<T>(options); + } - /// <inheritdoc /> - public void QueueScheduledTask<T>(TaskOptions options) + /// <inheritdoc /> + public void CancelIfRunningAndQueue<T>() where T : IScheduledTask - { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + { + CancelIfRunningAndQueue<T>(new TaskOptions()); + } - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name); - } - else - { - QueueScheduledTask(scheduledTask, options); - } - } + /// <inheritdoc /> + public void CancelIfRunning<T>() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); + } - /// <inheritdoc /> - public void QueueScheduledTask<T>() - where T : IScheduledTask - { - QueueScheduledTask<T>(new TaskOptions()); - } + /// <inheritdoc /> + public void QueueScheduledTask<T>(TaskOptions options) + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); - /// <inheritdoc /> - public void QueueIfNotRunning<T>() - where T : IScheduledTask + if (scheduledTask is null) { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - - if (task.State != TaskState.Running) - { - QueueScheduledTask<T>(new TaskOptions()); - } + _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", typeof(T).Name); } - - /// <inheritdoc /> - public void Execute<T>() - where T : IScheduledTask + else { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + QueueScheduledTask(scheduledTask, options); + } + } - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name); - } - else - { - var type = scheduledTask.ScheduledTask.GetType(); + /// <inheritdoc /> + public void QueueScheduledTask<T>() + where T : IScheduledTask + { + QueueScheduledTask<T>(new TaskOptions()); + } - _logger.LogDebug("Queuing task {0}", type.Name); + /// <inheritdoc /> + public void QueueIfNotRunning<T>() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - lock (_taskQueue) - { - if (scheduledTask.State == TaskState.Idle) - { - Execute(scheduledTask, new TaskOptions()); - } - } - } + if (task.State != TaskState.Running) + { + QueueScheduledTask<T>(new TaskOptions()); } + } - /// <inheritdoc /> - public void QueueScheduledTask(IScheduledTask task, TaskOptions options) - { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); + /// <inheritdoc /> + public void Execute<T>() + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name); - } - else - { - QueueScheduledTask(scheduledTask, options); - } + if (scheduledTask is null) + { + _logger.LogError("Unable to find scheduled task of type {Type} in Execute.", typeof(T).Name); } - - /// <summary> - /// Queues the scheduled task. - /// </summary> - /// <param name="task">The task.</param> - /// <param name="options">The task options.</param> - private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) + else { - var type = task.ScheduledTask.GetType(); + var type = scheduledTask.ScheduledTask.GetType(); - _logger.LogDebug("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {Name}", type.Name); lock (_taskQueue) { - if (task.State == TaskState.Idle) + if (scheduledTask.State == TaskState.Idle) { - Execute(task, options); - return; + Execute(scheduledTask, new TaskOptions()); } - - _taskQueue.Enqueue(new Tuple<Type, TaskOptions>(type, options)); } } + } - /// <inheritdoc /> - public void AddTasks(IEnumerable<IScheduledTask> tasks) - { - var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger)); + /// <inheritdoc /> + public void QueueScheduledTask(IScheduledTask task, TaskOptions options) + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); - ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); + if (scheduledTask is null) + { + _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", task.GetType().Name); } - - /// <inheritdoc /> - public void Dispose() + else { - Dispose(true); - GC.SuppressFinalize(this); + QueueScheduledTask(scheduledTask, options); } + } + + /// <summary> + /// Queues the scheduled task. + /// </summary> + /// <param name="task">The task.</param> + /// <param name="options">The task options.</param> + private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) + { + var type = task.ScheduledTask.GetType(); + + _logger.LogDebug("Queuing task {Name}", type.Name); - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) + lock (_taskQueue) { - foreach (var task in ScheduledTasks) + if (task.State == TaskState.Idle) { - task.Dispose(); + Execute(task, options); + return; } - } - /// <inheritdoc /> - public void Cancel(IScheduledTaskWorker task) - { - ((ScheduledTaskWorker)task).Cancel(); + _taskQueue.Enqueue(new Tuple<Type, TaskOptions>(type, options)); } + } - /// <inheritdoc /> - public Task Execute(IScheduledTaskWorker task, TaskOptions options) - { - return ((ScheduledTaskWorker)task).Execute(options); - } + /// <inheritdoc /> + public void AddTasks(IEnumerable<IScheduledTask> tasks) + { + var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger)); + + ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// <summary> - /// Called when [task executing]. - /// </summary> - /// <param name="task">The task.</param> - internal void OnTaskExecuting(IScheduledTaskWorker task) + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + foreach (var task in ScheduledTasks) { - TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task)); + task.Dispose(); } + } - /// <summary> - /// Called when [task completed]. - /// </summary> - /// <param name="task">The task.</param> - /// <param name="result">The result.</param> - internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) - { - TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); + /// <inheritdoc /> + public void Cancel(IScheduledTaskWorker task) + { + ((ScheduledTaskWorker)task).Cancel(); + } - ExecuteQueuedTasks(); - } + /// <inheritdoc /> + public Task Execute(IScheduledTaskWorker task, TaskOptions options) + { + return ((ScheduledTaskWorker)task).Execute(options); + } + + /// <summary> + /// Called when [task executing]. + /// </summary> + /// <param name="task">The task.</param> + internal void OnTaskExecuting(IScheduledTaskWorker task) + { + TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task)); + } + + /// <summary> + /// Called when [task completed]. + /// </summary> + /// <param name="task">The task.</param> + /// <param name="result">The result.</param> + internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) + { + TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); + + ExecuteQueuedTasks(); + } - /// <summary> - /// Executes the queued tasks. - /// </summary> - private void ExecuteQueuedTasks() + /// <summary> + /// Executes the queued tasks. + /// </summary> + private void ExecuteQueuedTasks() + { + lock (_taskQueue) { - lock (_taskQueue) - { - var list = new List<Tuple<Type, TaskOptions>>(); + var list = new List<Tuple<Type, TaskOptions>>(); - while (_taskQueue.TryDequeue(out var item)) + while (_taskQueue.TryDequeue(out var item)) + { + if (list.All(i => i.Item1 != item.Item1)) { - if (list.All(i => i.Item1 != item.Item1)) - { - list.Add(item); - } + list.Add(item); } + } - foreach (var enqueuedType in list) - { - var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1); + foreach (var enqueuedType in list) + { + var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1); - if (scheduledTask.State == TaskState.Idle) - { - Execute(scheduledTask, enqueuedType.Item2); - } + if (scheduledTask.State == TaskState.Idle) + { + Execute(scheduledTask, enqueuedType.Item2); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs index 031d14776..e912e9f01 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs @@ -76,93 +76,111 @@ public partial class AudioNormalizationTask : IScheduledTask /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - foreach (var library in _libraryManager.RootFolder.Children) + var numComplete = 0; + var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray(); + double percent = 0; + + foreach (var library in libraries) { - var libraryOptions = _libraryManager.GetLibraryOptions(library); - if (!libraryOptions.EnableLUFSScan) - { - continue; - } + var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true }); - // Album gain - var albums = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = [BaseItemKind.MusicAlbum], - Parent = library, - Recursive = true - }); + double nextPercent = numComplete + 1; + nextPercent /= libraries.Length; + nextPercent -= percent; + // Split the progress for this single library into two halves: album gain and track gain. + // The first half will be for album gain, the second half for track gain. + nextPercent /= 2; + var albumComplete = 0; foreach (var a in albums) { - if (a.NormalizationGain.HasValue || a.LUFS.HasValue) + if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue) { - continue; + // Album gain + var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList(); + + // Skip albums that don't have multiple tracks, album gain is useless here + if (albumTracks.Count > 1) + { + _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); + var tempDir = _applicationPaths.TempDirectory; + Directory.CreateDirectory(tempDir); + var tempFile = Path.Join(tempDir, a.Id + ".concat"); + var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); + await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); + try + { + a.LUFS = await CalculateLUFSAsync( + string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), + OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file + cancellationToken).ConfigureAwait(false); + } + finally + { + File.Delete(tempFile); + } + } } - // Skip albums that don't have multiple tracks, album gain is useless here - var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList(); - if (albumTracks.Count <= 1) - { - continue; - } + // Update sub-progress for album gain + albumComplete++; + double albumPercent = albumComplete; + albumPercent /= albums.Count; - _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id); - var tempDir = _applicationPaths.TempDirectory; - Directory.CreateDirectory(tempDir); - var tempFile = Path.Join(tempDir, a.Id + ".concat"); - var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal))); - await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false); - try - { - a.LUFS = await CalculateLUFSAsync( - string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile), - cancellationToken).ConfigureAwait(false); - } - finally - { - File.Delete(tempFile); - } + progress.Report(100 * (percent + (albumPercent * nextPercent))); } + // Update progress to start at the track gain percent calculation + percent += nextPercent; + _itemRepository.SaveItems(albums, cancellationToken); // Track gain - var tracks = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = [MediaType.Audio], - IncludeItemTypes = [BaseItemKind.Audio], - Parent = library, - Recursive = true - }); + var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true }); + var tracksComplete = 0; foreach (var t in tracks) { - if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol) + if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol) { - continue; + t.LUFS = await CalculateLUFSAsync( + string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), + false, + cancellationToken).ConfigureAwait(false); } - t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false); + // Update sub-progress for track gain + tracksComplete++; + double trackPercent = tracksComplete; + trackPercent /= tracks.Count; + + progress.Report(100 * (percent + (trackPercent * nextPercent))); } _itemRepository.SaveItems(tracks, cancellationToken); + + // Update progress + numComplete++; + percent = numComplete; + percent /= libraries.Length; + + progress.Report(100 * percent); } + + progress.Report(100.0); } /// <inheritdoc /> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return - [ - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks - } - ]; + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; } - private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken) + private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken) { var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -"; @@ -189,18 +207,28 @@ public partial class AudioNormalizationTask : IScheduledTask } using var reader = process.StandardError; - await foreach (var line in reader.ReadAllLinesAsync(cancellationToken)) + float? lufs = null; + await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false)) { Match match = LUFSRegex().Match(line); - if (match.Success) { - return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); + lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); + break; } } - _logger.LogError("Failed to find LUFS value in output"); - return null; + if (lufs is null) + { + _logger.LogError("Failed to find LUFS value in output"); + } + + if (waitForExit) + { + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + } + + return lufs; } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 563e90fbe..f81309560 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -11,171 +11,157 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Class ChapterImagesTask. +/// </summary> +public class ChapterImagesTask : IScheduledTask { + private readonly ILogger<ChapterImagesTask> _logger; + private readonly ILibraryManager _libraryManager; + private readonly IApplicationPaths _appPaths; + private readonly IChapterManager _chapterManager; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + /// <summary> - /// Class ChapterImagesTask. + /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> - public class ChapterImagesTask : IScheduledTask + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + public ChapterImagesTask( + ILogger<ChapterImagesTask> logger, + ILibraryManager libraryManager, + IApplicationPaths appPaths, + IChapterManager chapterManager, + IFileSystem fileSystem, + ILocalizationManager localization) { - private readonly ILogger<ChapterImagesTask> _logger; - private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; - private readonly IApplicationPaths _appPaths; - private readonly IEncodingManager _encodingManager; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - private readonly IChapterRepository _chapterRepository; - - /// <summary> - /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param> - /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> - /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - /// <param name="chapterRepository">Instance of the <see cref="IChapterRepository"/> interface.</param> - public ChapterImagesTask( - ILogger<ChapterImagesTask> logger, - ILibraryManager libraryManager, - IItemRepository itemRepo, - IApplicationPaths appPaths, - IEncodingManager encodingManager, - IFileSystem fileSystem, - ILocalizationManager localization, - IChapterRepository chapterRepository) - { - _logger = logger; - _libraryManager = libraryManager; - _itemRepo = itemRepo; - _appPaths = appPaths; - _encodingManager = encodingManager; - _fileSystem = fileSystem; - _localization = localization; - _chapterRepository = chapterRepository; - } + _logger = logger; + _libraryManager = libraryManager; + _appPaths = appPaths; + _chapterManager = chapterManager; + _fileSystem = fileSystem; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - /// <inheritdoc /> - public string Key => "RefreshChapterImages"; + /// <inheritdoc /> + public string Key => "RefreshChapterImages"; - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - return - [ - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.DailyTrigger, - TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, - MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks - } - ]; - } + Type = TaskTriggerInfoType.DailyTrigger, + TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, + MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks + }; + } - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var videos = _libraryManager.GetItemList(new InternalItemsQuery { - var videos = _libraryManager.GetItemList(new InternalItemsQuery + MediaTypes = [MediaType.Video], + IsFolder = false, + Recursive = true, + DtoOptions = new DtoOptions(false) { - MediaTypes = [MediaType.Video], - IsFolder = false, - Recursive = true, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - }, - SourceTypes = [SourceType.Library], - IsVirtualItem = false - }) - .OfType<Video>() - .ToList(); + EnableImages = false + }, + SourceTypes = [SourceType.Library], + IsVirtualItem = false + }) + .OfType<Video>() + .ToList(); - var numComplete = 0; + var numComplete = 0; - var failHistoryPath = Path.Combine(_appPaths.CachePath, "chapter-failures.txt"); + var failHistoryPath = Path.Combine(_appPaths.CachePath, "chapter-failures.txt"); - List<string> previouslyFailedImages; + List<string> previouslyFailedImages; - if (File.Exists(failHistoryPath)) + if (File.Exists(failHistoryPath)) + { + try { - try - { - previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false)) - .Split('|', StringSplitOptions.RemoveEmptyEntries) - .ToList(); - } - catch (IOException) - { - previouslyFailedImages = new List<string>(); - } + previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false)) + .Split('|', StringSplitOptions.RemoveEmptyEntries) + .ToList(); } - else + catch (IOException) { - previouslyFailedImages = new List<string>(); + previouslyFailedImages = []; } + } + else + { + previouslyFailedImages = []; + } - var directoryService = new DirectoryService(_fileSystem); - - foreach (var video in videos) - { - cancellationToken.ThrowIfCancellationRequested(); + var directoryService = new DirectoryService(_fileSystem); - var key = video.Path + video.DateModified.Ticks; + foreach (var video in videos) + { + cancellationToken.ThrowIfCancellationRequested(); - var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase); + var key = video.Path + video.DateModified.Ticks; - try - { - var chapters = _chapterRepository.GetChapters(video.Id); + var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase); - var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false); + try + { + var chapters = _chapterManager.GetChapters(video.Id); - if (!success) - { - previouslyFailedImages.Add(key); + var success = await _chapterManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false); - var parentPath = Path.GetDirectoryName(failHistoryPath); - if (parentPath is not null) - { - Directory.CreateDirectory(parentPath); - } + if (!success) + { + previouslyFailedImages.Add(key); - string text = string.Join('|', previouslyFailedImages); - await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false); + var parentPath = Path.GetDirectoryName(failHistoryPath); + if (parentPath is not null) + { + Directory.CreateDirectory(parentPath); } - numComplete++; - double percent = numComplete; - percent /= videos.Count; - - progress.Report(100 * percent); - } - catch (ObjectDisposedException ex) - { - // TODO Investigate and properly fix. - _logger.LogError(ex, "Object Disposed"); - break; + string text = string.Join('|', previouslyFailedImages); + await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false); } + + numComplete++; + double percent = numComplete; + percent /= videos.Count; + + progress.Report(100 * percent); + } + catch (ObjectDisposedException ex) + { + // TODO Investigate and properly fix. + _logger.LogError(ex, "Object Disposed"); + break; } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs index fe1832165..1621bbaa1 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs @@ -7,71 +7,70 @@ using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes old activity log entries. +/// </summary> +public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask { + private readonly ILocalizationManager _localization; + private readonly IActivityManager _activityManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + /// <summary> - /// Deletes old activity log entries. + /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class. /// </summary> - public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public CleanActivityLogTask( + ILocalizationManager localization, + IActivityManager activityManager, + IServerConfigurationManager serverConfigurationManager) { - private readonly ILocalizationManager _localization; - private readonly IActivityManager _activityManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - - /// <summary> - /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class. - /// </summary> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - public CleanActivityLogTask( - ILocalizationManager localization, - IActivityManager activityManager, - IServerConfigurationManager serverConfigurationManager) - { - _localization = localization; - _activityManager = activityManager; - _serverConfigurationManager = serverConfigurationManager; - } + _localization = localization; + _activityManager = activityManager; + _serverConfigurationManager = serverConfigurationManager; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanActivityLog"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanActivityLog"); - /// <inheritdoc /> - public string Key => "CleanActivityLog"; + /// <inheritdoc /> + public string Key => "CleanActivityLog"; - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public bool IsLogged => true; - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays; + if (!retentionDays.HasValue || retentionDays < 0) { - var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays; - if (!retentionDays.HasValue || retentionDays < 0) - { - throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); - } - - var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value); - return _activityManager.CleanAsync(startDate); + throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); } - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return []; - } + var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value); + return _activityManager.CleanAsync(startDate); + } + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return []; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index 316e4a8f0..7f68f7701 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -27,7 +27,6 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask private readonly IPlaylistManager _playlistManager; private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger; private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class. @@ -37,21 +36,18 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> public CleanupCollectionAndPlaylistPathsTask( ILocalizationManager localization, ICollectionManager collectionManager, IPlaylistManager playlistManager, ILogger<CleanupCollectionAndPlaylistPathsTask> logger, - IProviderManager providerManager, - IFileSystem fileSystem) + IProviderManager providerManager) { _localization = localization; _collectionManager = collectionManager; _playlistManager = playlistManager; _logger = logger; _providerManager = providerManager; - _fileSystem = fileSystem; } /// <inheritdoc /> @@ -84,7 +80,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask var collection = collections[index]; _logger.LogDebug("Checking boxset {CollectionName}", collection.Name); - CleanupLinkedChildren(collection, cancellationToken); + await CleanupLinkedChildrenAsync(collection, cancellationToken).ConfigureAwait(false); progress.Report(50D / collections.Length * (index + 1)); } } @@ -104,12 +100,12 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask var playlist = playlists[index]; _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name); - CleanupLinkedChildren(playlist, cancellationToken); + await CleanupLinkedChildrenAsync(playlist, cancellationToken).ConfigureAwait(false); progress.Report(50D / playlists.Length * (index + 1)); } } - private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken) + private async Task CleanupLinkedChildrenAsync<T>(T folder, CancellationToken cancellationToken) where T : Folder { List<LinkedChild>? itemsToRemove = null; @@ -127,14 +123,17 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask { _logger.LogDebug("Updating {FolderName}", folder.Name); folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); - _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit); - folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken); + await _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit).ConfigureAwait(false); + await folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } /// <inheritdoc /> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return [new TaskTriggerInfo() { Type = TaskTriggerInfoType.StartupTrigger }]; + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfoType.StartupTrigger, + }; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupUserDataTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupUserDataTask.cs new file mode 100644 index 000000000..4156050eb --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupUserDataTask.cs @@ -0,0 +1,77 @@ +#pragma warning disable RS0030 // Do not use banned APIs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Task to clean up any detached userdata from the database. +/// </summary> +public class CleanupUserDataTask : IScheduledTask +{ + private readonly ILocalizationManager _localization; + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; + private readonly ILogger<CleanupUserDataTask> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="CleanupUserDataTask"/> class. + /// </summary> + /// <param name="localization">The localisation Provider.</param> + /// <param name="dbProvider">The DB context factory.</param> + /// <param name="logger">A logger.</param> + public CleanupUserDataTask(ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbProvider, ILogger<CleanupUserDataTask> logger) + { + _localization = localization; + _dbProvider = dbProvider; + _logger = logger; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("CleanupUserDataTask"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("CleanupUserDataTaskDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public string Key => nameof(CleanupUserDataTask); + + /// <inheritdoc/> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + const int LimitDays = 90; + var userDataDate = DateTime.UtcNow.AddDays(LimitDays * -1); + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var detachedUserData = dbContext.UserData.Where(e => e.ItemId == BaseItemRepository.PlaceholderId); + _logger.LogInformation("There are {NoDetached} detached UserData entries.", detachedUserData.Count()); + + detachedUserData = detachedUserData.Where(e => e.RetentionDate < userDataDate); + + _logger.LogInformation("{NoDetached} are older then {Limit} days.", detachedUserData.Count(), LimitDays); + + await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } + + progress.Report(100); + } + + /// <inheritdoc/> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield break; + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index ff295d9b7..0e77f0102 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -11,134 +11,133 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes old cache files. +/// </summary> +public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask { /// <summary> - /// Deletes old cache files. + /// Gets or sets the application paths. + /// </summary> + /// <value>The application paths.</value> + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger<DeleteCacheFileTask> _logger; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + + /// <summary> + /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class. /// </summary> - public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + public DeleteCacheFileTask( + IApplicationPaths appPaths, + ILogger<DeleteCacheFileTask> logger, + IFileSystem fileSystem, + ILocalizationManager localization) { - /// <summary> - /// Gets or sets the application paths. - /// </summary> - /// <value>The application paths.</value> - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger<DeleteCacheFileTask> _logger; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - - /// <summary> - /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class. - /// </summary> - /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - public DeleteCacheFileTask( - IApplicationPaths appPaths, - ILogger<DeleteCacheFileTask> logger, - IFileSystem fileSystem, - ILocalizationManager localization) - { - _applicationPaths = appPaths; - _logger = logger; - _fileSystem = fileSystem; - _localization = localization; - } + _applicationPaths = appPaths; + _logger = logger; + _fileSystem = fileSystem; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanCache"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCache"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - /// <inheritdoc /> - public string Key => "DeleteCacheFiles"; + /// <inheritdoc /> + public string Key => "DeleteCacheFiles"; - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public bool IsLogged => true; - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - return - [ - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } - ]; - } + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; + } - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var minDateModified = DateTime.UtcNow.AddDays(-30); + + try + { + DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken); + } + catch (DirectoryNotFoundException) { - var minDateModified = DateTime.UtcNow.AddDays(-30); - - try - { - DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken); - } - catch (DirectoryNotFoundException) - { - // No biggie here. Nothing to delete - } - - progress.Report(90); - - minDateModified = DateTime.UtcNow.AddDays(-1); - - try - { - DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken); - } - catch (DirectoryNotFoundException) - { - // No biggie here. Nothing to delete - } - - return Task.CompletedTask; + // No biggie here. Nothing to delete } - /// <summary> - /// Deletes the cache files from directory with a last write time less than a given date. - /// </summary> - /// <param name="directory">The directory.</param> - /// <param name="minDateModified">The min date modified.</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The task cancellation token.</param> - private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken) + progress.Report(90); + + minDateModified = DateTime.UtcNow.AddDays(-1); + + try { - var filesToDelete = _fileSystem.GetFiles(directory, true) - .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) - .ToList(); + DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken); + } + catch (DirectoryNotFoundException) + { + // No biggie here. Nothing to delete + } - var index = 0; + return Task.CompletedTask; + } - foreach (var file in filesToDelete) - { - double percent = index; - percent /= filesToDelete.Count; + /// <summary> + /// Deletes the cache files from directory with a last write time less than a given date. + /// </summary> + /// <param name="directory">The directory.</param> + /// <param name="minDateModified">The min date modified.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The task cancellation token.</param> + private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken) + { + var filesToDelete = _fileSystem.GetFiles(directory, true) + .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) + .ToList(); - progress.Report(100 * percent); + var index = 0; - cancellationToken.ThrowIfCancellationRequested(); + foreach (var file in filesToDelete) + { + double percent = index; + percent /= filesToDelete.Count; - FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger); + progress.Report(100 * percent); - index++; - } + cancellationToken.ThrowIfCancellationRequested(); - FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger); + FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger); - progress.Report(100); + index++; } + + FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger); + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index a091c2bd9..699529527 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -9,93 +9,93 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes old log files. +/// </summary> +public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask { + private readonly IConfigurationManager _configurationManager; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + /// <summary> - /// Deletes old log files. + /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class. /// </summary> - public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization) { - private readonly IConfigurationManager _configurationManager; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - - /// <summary> - /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class. - /// </summary> - /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization) - { - _configurationManager = configurationManager; - _fileSystem = fileSystem; - _localization = localization; - } + _configurationManager = configurationManager; + _fileSystem = fileSystem; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanLogs"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanLogs"); - /// <inheritdoc /> - public string Description => string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("TaskCleanLogsDescription"), - _configurationManager.CommonConfiguration.LogFileRetentionDays); + /// <inheritdoc /> + public string Description => string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("TaskCleanLogsDescription"), + _configurationManager.CommonConfiguration.LogFileRetentionDays); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - /// <inheritdoc /> - public string Key => "CleanLogFiles"; + /// <inheritdoc /> + public string Key => "CleanLogFiles"; - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public bool IsLogged => true; - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - return - [ - new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } - ]; - } + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; + } - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - // Delete log files more than n days old - var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays); + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + // Delete log files more than n days old + var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays); - // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_' - var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true) - .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal) - && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) - .ToList(); + // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_' + var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true) + .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal) + && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) + .ToList(); - var index = 0; + var index = 0; - foreach (var file in filesToDelete) - { - double percent = index / (double)filesToDelete.Count; + foreach (var file in filesToDelete) + { + double percent = index / (double)filesToDelete.Count; - progress.Report(100 * percent); + progress.Report(100 * percent); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - _fileSystem.DeleteFile(file.FullName); + _fileSystem.DeleteFile(file.FullName); - index++; - } + index++; + } - progress.Report(100); + progress.Report(100); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index d0896cc81..9cc2cc512 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -10,118 +10,115 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes all transcoding temp files. +/// </summary> +public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask { + private readonly ILogger<DeleteTranscodeFileTask> _logger; + private readonly IConfigurationManager _configurationManager; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + /// <summary> - /// Deletes all transcoding temp files. + /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class. /// </summary> - public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask + /// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + public DeleteTranscodeFileTask( + ILogger<DeleteTranscodeFileTask> logger, + IFileSystem fileSystem, + IConfigurationManager configurationManager, + ILocalizationManager localization) { - private readonly ILogger<DeleteTranscodeFileTask> _logger; - private readonly IConfigurationManager _configurationManager; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - - /// <summary> - /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - public DeleteTranscodeFileTask( - ILogger<DeleteTranscodeFileTask> logger, - IFileSystem fileSystem, - IConfigurationManager configurationManager, - ILocalizationManager localization) - { - _logger = logger; - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _localization = localization; - } + _logger = logger; + _fileSystem = fileSystem; + _configurationManager = configurationManager; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanTranscode"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanTranscode"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - /// <inheritdoc /> - public string Key => "DeleteTranscodeFiles"; + /// <inheritdoc /> + public string Key => "DeleteTranscodeFiles"; - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public bool IsLogged => true; - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - return - [ - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.StartupTrigger - }, - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks - } - ]; - } + Type = TaskTriggerInfoType.StartupTrigger + }; - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + yield return new TaskTriggerInfo { - var minDateModified = DateTime.UtcNow.AddDays(-1); - progress.Report(50); - - DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken); + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; + } - return Task.CompletedTask; - } + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var minDateModified = DateTime.UtcNow.AddDays(-1); + progress.Report(50); - /// <summary> - /// Deletes the transcoded temp files from directory with a last write time less than a given date. - /// </summary> - /// <param name="directory">The directory.</param> - /// <param name="minDateModified">The min date modified.</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The task cancellation token.</param> - private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken) - { - var filesToDelete = _fileSystem.GetFiles(directory, true) - .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) - .ToList(); + DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken); - var index = 0; + return Task.CompletedTask; + } - foreach (var file in filesToDelete) - { - double percent = index; - percent /= filesToDelete.Count; + /// <summary> + /// Deletes the transcoded temp files from directory with a last write time less than a given date. + /// </summary> + /// <param name="directory">The directory.</param> + /// <param name="minDateModified">The min date modified.</param> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The task cancellation token.</param> + private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken) + { + var filesToDelete = _fileSystem.GetFiles(directory, true) + .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) + .ToList(); - progress.Report(100 * percent); + var index = 0; - cancellationToken.ThrowIfCancellationRequested(); + foreach (var file in filesToDelete) + { + double percent = index; + percent /= filesToDelete.Count; - FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger); + progress.Report(100 * percent); - index++; - } + cancellationToken.ThrowIfCancellationRequested(); - FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger); + FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger); - progress.Report(100); + index++; } + + FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger); + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs index de1e60d30..51920c5b1 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -4,10 +4,10 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Controller; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; @@ -62,11 +62,11 @@ public class MediaSegmentExtractionTask : IScheduledTask var query = new InternalItemsQuery { - MediaTypes = new[] { MediaType.Video, MediaType.Audio }, + MediaTypes = [MediaType.Video, MediaType.Audio], IsVirtualItem = false, IncludeItemTypes = _itemTypes, DtoOptions = new DtoOptions(true), - SourceTypes = new[] { SourceType.Library }, + SourceTypes = [SourceType.Library], Recursive = true, Limit = pagesize }; @@ -91,7 +91,8 @@ public class MediaSegmentExtractionTask : IScheduledTask // Only local files supported if (item.IsFileProtocol && File.Exists(item.Path)) { - await _mediaSegmentManager.RunSegmentPluginProviders(item, false, cancellationToken).ConfigureAwait(false); + var libraryOptions = _libraryManager.GetLibraryOptions(item); + await _mediaSegmentManager.RunSegmentPluginProviders(item, libraryOptions, false, cancellationToken).ConfigureAwait(false); } // Update progress diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 7d4e2377d..bf8ffaf47 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -2,96 +2,81 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Optimizes Jellyfin's database by issuing a VACUUM command. +/// </summary> +public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask { + private readonly ILogger<OptimizeDatabaseTask> _logger; + private readonly ILocalizationManager _localization; + private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; + /// <summary> - /// Optimizes Jellyfin's database by issuing a VACUUM command. + /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class. /// </summary> - public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="jellyfinDatabaseProvider">Instance of the JellyfinDatabaseProvider that can be used for provider specific operations.</param> + public OptimizeDatabaseTask( + ILogger<OptimizeDatabaseTask> logger, + ILocalizationManager localization, + IJellyfinDatabaseProvider jellyfinDatabaseProvider) { - private readonly ILogger<OptimizeDatabaseTask> _logger; - private readonly ILocalizationManager _localization; - private readonly IDbContextFactory<JellyfinDbContext> _provider; + _logger = logger; + _localization = localization; + _jellyfinDatabaseProvider = jellyfinDatabaseProvider; + } - /// <summary> - /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - /// <param name="provider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param> - public OptimizeDatabaseTask( - ILogger<OptimizeDatabaseTask> logger, - ILocalizationManager localization, - IDbContextFactory<JellyfinDbContext> provider) - { - _logger = logger; - _localization = localization; - _provider = provider; - } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription"); - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription"); + /// <inheritdoc /> + public string Key => "OptimizeDatabaseTask"; - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public string Key => "OptimizeDatabaseTask"; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsLogged => true; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; + } - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + try { - return - [ - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } - ]; + await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken).ConfigureAwait(false); } - - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + catch (Exception e) { - _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); - - try - { - var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - if (context.Database.IsSqlite()) - { - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); - await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } - else - { - _logger.LogInformation("This database doesn't support optimization"); - } - } - } - catch (Exception e) - { - _logger.LogError(e, "Error while optimizing jellyfin.db"); - } + _logger.LogError(e, "Error while optimizing jellyfin.db"); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 2907f18b5..18162ad2f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -6,68 +6,64 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Class PeopleValidationTask. +/// </summary> +public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask { + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + /// <summary> - /// Class PeopleValidationTask. + /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class. /// </summary> - public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask + /// <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) { - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - - /// <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) - { - _libraryManager = libraryManager; - _localization = localization; - } + _libraryManager = libraryManager; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskRefreshPeople"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskRefreshPeople"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - /// <inheritdoc /> - public string Key => "RefreshPeople"; + /// <inheritdoc /> + public string Key => "RefreshPeople"; - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public bool IsLogged => true; - /// <summary> - /// Creates the triggers that define when the task will run. - /// </summary> - /// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <summary> + /// Creates the triggers that define when the task will run. + /// </summary> + /// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - return new[] - { - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromDays(7).Ticks - } - }; - } + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromDays(7).Ticks + }; + } - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - return _libraryManager.ValidatePeopleAsync(progress, cancellationToken); - } + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + return _libraryManager.ValidatePeopleAsync(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index b74f4d1b2..31153af20 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -10,111 +10,115 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Plugin Update Task. +/// </summary> +public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask { + private readonly ILogger<PluginUpdateTask> _logger; + + private readonly IInstallationManager _installationManager; + private readonly ILocalizationManager _localization; + /// <summary> - /// Plugin Update Task. + /// Initializes a new instance of the <see cref="PluginUpdateTask" /> class. /// </summary> - public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager, ILocalizationManager localization) { - private readonly ILogger<PluginUpdateTask> _logger; - - private readonly IInstallationManager _installationManager; - private readonly ILocalizationManager _localization; - - /// <summary> - /// Initializes a new instance of the <see cref="PluginUpdateTask" /> class. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> - /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> - public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager, ILocalizationManager localization) - { - _logger = logger; - _installationManager = installationManager; - _localization = localization; - } + _logger = logger; + _installationManager = installationManager; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); - /// <inheritdoc /> - public string Key => "PluginUpdates"; + /// <inheritdoc /> + public string Key => "PluginUpdates"; - /// <inheritdoc /> - public bool IsHidden => false; + /// <inheritdoc /> + public bool IsHidden => false; - /// <inheritdoc /> - public bool IsEnabled => true; + /// <inheritdoc /> + public bool IsEnabled => true; - /// <inheritdoc /> - public bool IsLogged => true; + /// <inheritdoc /> + public bool IsLogged => true; - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - // At startup - yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.StartupTrigger }; - - // Every so often - yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }; - } + Type = TaskTriggerInfoType.StartupTrigger + }; - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + yield return new TaskTriggerInfo { - progress.Report(0); + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; + } - var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken); - var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList(); + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + progress.Report(0); - progress.Report(10); + var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken); + var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList(); - var numComplete = 0; + progress.Report(10); - foreach (var package in packagesToInstall) - { - cancellationToken.ThrowIfCancellationRequested(); + var numComplete = 0; - try - { - await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // InstallPackage has its own inner cancellation token, so only throw this if it's ours - if (cancellationToken.IsCancellationRequested) - { - throw; - } - } - catch (HttpRequestException ex) - { - _logger.LogError(ex, "Error downloading {0}", package.Name); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error updating {0}", package.Name); - } - catch (InvalidDataException ex) - { - _logger.LogError(ex, "Error updating {0}", package.Name); - } + foreach (var package in packagesToInstall) + { + cancellationToken.ThrowIfCancellationRequested(); - // Update progress - lock (progress) + try + { + await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // InstallPackage has its own inner cancellation token, so only throw this if it's ours + if (cancellationToken.IsCancellationRequested) { - progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10); + throw; } } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Error downloading {Name}", package.Name); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error updating {Name}", package.Name); + } + catch (InvalidDataException ex) + { + _logger.LogError(ex, "Error updating {Name}", package.Name); + } - progress.Report(100); + // Update progress + lock (progress) + { + progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10); + } } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index 172448dde..1865189d0 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -7,60 +7,59 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Class RefreshMediaLibraryTask. +/// </summary> +public class RefreshMediaLibraryTask : IScheduledTask { /// <summary> - /// Class RefreshMediaLibraryTask. + /// The _library manager. /// </summary> - public class RefreshMediaLibraryTask : IScheduledTask - { - /// <summary> - /// The _library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; - /// <summary> - /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> 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 RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) - { - _libraryManager = libraryManager; - _localization = localization; - } + /// <summary> + /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> 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 RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) + { + _libraryManager = libraryManager; + _localization = localization; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskRefreshLibrary"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskRefreshLibrary"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription"); - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - /// <inheritdoc /> - public string Key => "RefreshLibrary"; + /// <inheritdoc /> + public string Key => "RefreshLibrary"; - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - yield return new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(12).Ticks - }; - } + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(12).Ticks + }; + } - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); - progress.Report(0); + progress.Report(0); - return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); - } + await ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index 6d2a74da4..9abcd9c7b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -3,85 +3,84 @@ using System.Threading; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Triggers +namespace Emby.Server.Implementations.ScheduledTasks.Triggers; + +/// <summary> +/// Represents a task trigger that fires everyday. +/// </summary> +public sealed class DailyTrigger : ITaskTrigger, IDisposable { + private readonly TimeSpan _timeOfDay; + private Timer? _timer; + private bool _disposed; + /// <summary> - /// Represents a task trigger that fires everyday. + /// Initializes a new instance of the <see cref="DailyTrigger"/> class. /// </summary> - public sealed class DailyTrigger : ITaskTrigger, IDisposable + /// <param name="timeOfDay">The time of day to trigger the task to run.</param> + /// <param name="taskOptions">The options of this task.</param> + public DailyTrigger(TimeSpan timeOfDay, TaskOptions taskOptions) { - private readonly TimeSpan _timeOfDay; - private Timer? _timer; - private bool _disposed = false; - - /// <summary> - /// Initializes a new instance of the <see cref="DailyTrigger"/> class. - /// </summary> - /// <param name="timeofDay">The time of day to trigger the task to run.</param> - /// <param name="taskOptions">The options of this task.</param> - public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions) - { - _timeOfDay = timeofDay; - TaskOptions = taskOptions; - } + _timeOfDay = timeOfDay; + TaskOptions = taskOptions; + } - /// <inheritdoc /> - public event EventHandler<EventArgs>? Triggered; + /// <inheritdoc /> + public event EventHandler<EventArgs>? Triggered; - /// <inheritdoc /> - public TaskOptions TaskOptions { get; } + /// <inheritdoc /> + public TaskOptions TaskOptions { get; } - /// <inheritdoc /> - public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) - { - DisposeTimer(); + /// <inheritdoc /> + public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) + { + DisposeTimer(); - var now = DateTime.Now; + var now = DateTime.Now; - var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date; - triggerDate = triggerDate.Add(_timeOfDay); + var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date; + triggerDate = triggerDate.Add(_timeOfDay); - var dueTime = triggerDate - now; + var dueTime = triggerDate - now; - logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime); + logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime); - _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); - } + _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); + } - /// <inheritdoc /> - public void Stop() - { - DisposeTimer(); - } + /// <inheritdoc /> + public void Stop() + { + DisposeTimer(); + } - /// <summary> - /// Disposes the timer. - /// </summary> - private void DisposeTimer() - { - _timer?.Dispose(); - _timer = null; - } + /// <summary> + /// Disposes the timer. + /// </summary> + private void DisposeTimer() + { + _timer?.Dispose(); + _timer = null; + } - /// <summary> - /// Called when [triggered]. - /// </summary> - private void OnTriggered() - { - Triggered?.Invoke(this, EventArgs.Empty); - } + /// <summary> + /// Called when [triggered]. + /// </summary> + private void OnTriggered() + { + Triggered?.Invoke(this, EventArgs.Empty); + } - /// <inheritdoc /> - public void Dispose() + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) { - if (_disposed) - { - return; - } + return; + } - DisposeTimer(); + DisposeTimer(); - _disposed = true; - } + _disposed = true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 9425b47d0..d6773b65e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -4,104 +4,103 @@ using System.Threading; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Triggers +namespace Emby.Server.Implementations.ScheduledTasks.Triggers; + +/// <summary> +/// Represents a task trigger that runs repeatedly on an interval. +/// </summary> +public sealed class IntervalTrigger : ITaskTrigger, IDisposable { + private readonly TimeSpan _interval; + private DateTime _lastStartDate; + private Timer? _timer; + private bool _disposed; + /// <summary> - /// Represents a task trigger that runs repeatedly on an interval. + /// Initializes a new instance of the <see cref="IntervalTrigger"/> class. /// </summary> - public sealed class IntervalTrigger : ITaskTrigger, IDisposable + /// <param name="interval">The interval.</param> + /// <param name="taskOptions">The options of this task.</param> + public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions) { - private readonly TimeSpan _interval; - private DateTime _lastStartDate; - private Timer? _timer; - private bool _disposed = false; - - /// <summary> - /// Initializes a new instance of the <see cref="IntervalTrigger"/> class. - /// </summary> - /// <param name="interval">The interval.</param> - /// <param name="taskOptions">The options of this task.</param> - public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions) - { - _interval = interval; - TaskOptions = taskOptions; - } + _interval = interval; + TaskOptions = taskOptions; + } + + /// <inheritdoc /> + public event EventHandler<EventArgs>? Triggered; - /// <inheritdoc /> - public event EventHandler<EventArgs>? Triggered; + /// <inheritdoc /> + public TaskOptions TaskOptions { get; } + + /// <inheritdoc /> + public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) + { + DisposeTimer(); - /// <inheritdoc /> - public TaskOptions TaskOptions { get; } + DateTime now = DateTime.UtcNow; + DateTime triggerDate; - /// <inheritdoc /> - public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) + if (lastResult is null) { - DisposeTimer(); - - DateTime now = DateTime.UtcNow; - DateTime triggerDate; - - if (lastResult is null) - { - // Task has never been completed before - triggerDate = now.AddHours(1); - } - else - { - triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate, now.AddMinutes(1) }.Max().Add(_interval); - } - - var dueTime = triggerDate - now; - var maxDueTime = TimeSpan.FromDays(7); - - if (dueTime > maxDueTime) - { - dueTime = maxDueTime; - } - - _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); + // Task has never been completed before + triggerDate = now.AddHours(1); } - - /// <inheritdoc /> - public void Stop() + else { - DisposeTimer(); + triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate, now.AddMinutes(1) }.Max().Add(_interval); } - /// <summary> - /// Disposes the timer. - /// </summary> - private void DisposeTimer() + var dueTime = triggerDate - now; + var maxDueTime = TimeSpan.FromDays(7); + + if (dueTime > maxDueTime) { - _timer?.Dispose(); - _timer = null; + dueTime = maxDueTime; } - /// <summary> - /// Called when [triggered]. - /// </summary> - private void OnTriggered() - { - DisposeTimer(); + _timer = new Timer(_ => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1)); + } + + /// <inheritdoc /> + public void Stop() + { + DisposeTimer(); + } - if (Triggered is not null) - { - _lastStartDate = DateTime.UtcNow; - Triggered(this, EventArgs.Empty); - } + /// <summary> + /// Disposes the timer. + /// </summary> + private void DisposeTimer() + { + _timer?.Dispose(); + _timer = null; + } + + /// <summary> + /// Called when [triggered]. + /// </summary> + private void OnTriggered() + { + DisposeTimer(); + + if (Triggered is not null) + { + _lastStartDate = DateTime.UtcNow; + Triggered(this, EventArgs.Empty); } + } - /// <inheritdoc /> - public void Dispose() + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) { - if (_disposed) - { - return; - } + return; + } - DisposeTimer(); + DisposeTimer(); - _disposed = true; - } + _disposed = true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 535aa20f9..86ceff6ce 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -3,52 +3,51 @@ using System.Threading.Tasks; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Triggers +namespace Emby.Server.Implementations.ScheduledTasks.Triggers; + +/// <summary> +/// Class StartupTaskTrigger. +/// </summary> +public sealed class StartupTrigger : ITaskTrigger { + private const int DelayMs = 3000; + /// <summary> - /// Class StartupTaskTrigger. + /// Initializes a new instance of the <see cref="StartupTrigger"/> class. /// </summary> - public sealed class StartupTrigger : ITaskTrigger + /// <param name="taskOptions">The options of this task.</param> + public StartupTrigger(TaskOptions taskOptions) { - private const int DelayMs = 3000; - - /// <summary> - /// Initializes a new instance of the <see cref="StartupTrigger"/> class. - /// </summary> - /// <param name="taskOptions">The options of this task.</param> - public StartupTrigger(TaskOptions taskOptions) - { - TaskOptions = taskOptions; - } + TaskOptions = taskOptions; + } - /// <inheritdoc /> - public event EventHandler<EventArgs>? Triggered; + /// <inheritdoc /> + public event EventHandler<EventArgs>? Triggered; - /// <inheritdoc /> - public TaskOptions TaskOptions { get; } + /// <inheritdoc /> + public TaskOptions TaskOptions { get; } - /// <inheritdoc /> - public async void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) + /// <inheritdoc /> + public async void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) + { + if (isApplicationStartup) { - if (isApplicationStartup) - { - await Task.Delay(DelayMs).ConfigureAwait(false); + await Task.Delay(DelayMs).ConfigureAwait(false); - OnTriggered(); - } + OnTriggered(); } + } - /// <inheritdoc /> - public void Stop() - { - } + /// <inheritdoc /> + public void Stop() + { + } - /// <summary> - /// Called when [triggered]. - /// </summary> - private void OnTriggered() - { - Triggered?.Invoke(this, EventArgs.Empty); - } + /// <summary> + /// Called when [triggered]. + /// </summary> + private void OnTriggered() + { + Triggered?.Invoke(this, EventArgs.Empty); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index ad94fdda5..79568f8a1 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -3,108 +3,107 @@ using System.Threading; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Triggers +namespace Emby.Server.Implementations.ScheduledTasks.Triggers; + +/// <summary> +/// Represents a task trigger that fires on a weekly basis. +/// </summary> +public sealed class WeeklyTrigger : ITaskTrigger, IDisposable { + private readonly TimeSpan _timeOfDay; + private readonly DayOfWeek _dayOfWeek; + private Timer? _timer; + private bool _disposed; + /// <summary> - /// Represents a task trigger that fires on a weekly basis. + /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class. /// </summary> - public sealed class WeeklyTrigger : ITaskTrigger, IDisposable + /// <param name="timeOfDay">The time of day to trigger the task to run.</param> + /// <param name="dayOfWeek">The day of week.</param> + /// <param name="taskOptions">The options of this task.</param> + public WeeklyTrigger(TimeSpan timeOfDay, DayOfWeek dayOfWeek, TaskOptions taskOptions) { - private readonly TimeSpan _timeOfDay; - private readonly DayOfWeek _dayOfWeek; - private Timer? _timer; - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class. - /// </summary> - /// <param name="timeofDay">The time of day to trigger the task to run.</param> - /// <param name="dayOfWeek">The day of week.</param> - /// <param name="taskOptions">The options of this task.</param> - public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions) - { - _timeOfDay = timeofDay; - _dayOfWeek = dayOfWeek; - TaskOptions = taskOptions; - } + _timeOfDay = timeOfDay; + _dayOfWeek = dayOfWeek; + TaskOptions = taskOptions; + } - /// <inheritdoc /> - public event EventHandler<EventArgs>? Triggered; + /// <inheritdoc /> + public event EventHandler<EventArgs>? Triggered; - /// <inheritdoc /> - public TaskOptions TaskOptions { get; } + /// <inheritdoc /> + public TaskOptions TaskOptions { get; } - /// <inheritdoc /> - public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) - { - DisposeTimer(); + /// <inheritdoc /> + public void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup) + { + DisposeTimer(); - var triggerDate = GetNextTriggerDateTime(); + var triggerDate = GetNextTriggerDateTime(); - _timer = new Timer(_ => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1)); - } + _timer = new Timer(_ => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1)); + } - /// <summary> - /// Gets the next trigger date time. - /// </summary> - /// <returns>DateTime.</returns> - private DateTime GetNextTriggerDateTime() + /// <summary> + /// Gets the next trigger date time. + /// </summary> + /// <returns>DateTime.</returns> + private DateTime GetNextTriggerDateTime() + { + var now = DateTime.Now; + + // If it's on the same day + if (now.DayOfWeek == _dayOfWeek) { - var now = DateTime.Now; + // It's either later today, or a week from now + return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay); + } - // If it's on the same day - if (now.DayOfWeek == _dayOfWeek) - { - // It's either later today, or a week from now - return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay); - } + var triggerDate = now.Date; - var triggerDate = now.Date; + // Walk the date forward until we get to the trigger day + while (triggerDate.DayOfWeek != _dayOfWeek) + { + triggerDate = triggerDate.AddDays(1); + } - // Walk the date forward until we get to the trigger day - while (triggerDate.DayOfWeek != _dayOfWeek) - { - triggerDate = triggerDate.AddDays(1); - } + // Return the trigger date plus the time offset + return triggerDate.Add(_timeOfDay); + } - // Return the trigger date plus the time offset - return triggerDate.Add(_timeOfDay); - } + /// <inheritdoc /> + public void Stop() + { + DisposeTimer(); + } - /// <inheritdoc /> - public void Stop() - { - DisposeTimer(); - } + /// <summary> + /// Disposes the timer. + /// </summary> + private void DisposeTimer() + { + _timer?.Dispose(); + _timer = null; + } - /// <summary> - /// Disposes the timer. - /// </summary> - private void DisposeTimer() - { - _timer?.Dispose(); - _timer = null; - } + /// <summary> + /// Called when [triggered]. + /// </summary> + private void OnTriggered() + { + Triggered?.Invoke(this, EventArgs.Empty); + } - /// <summary> - /// Called when [triggered]. - /// </summary> - private void OnTriggered() + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) { - Triggered?.Invoke(this, EventArgs.Empty); + return; } - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - DisposeTimer(); + DisposeTimer(); - _disposed = true; - } + _disposed = true; } } diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 725df98da..f049e6647 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -96,5 +96,12 @@ namespace Emby.Server.Implementations /// <inheritdoc /> public string VirtualInternalMetadataPath => "%MetadataPath%"; + + /// <inheritdoc/> + public override void MakeSanityCheckOrThrow() + { + base.MakeSanityCheckOrThrow(); + CreateAndCheckMarker(RootFolderPath, "root"); + } } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 030da6f73..cf2ca047c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -7,11 +7,13 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Entities.Security; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; @@ -62,6 +64,9 @@ namespace Emby.Server.Implementations.Session private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions + = new(StringComparer.OrdinalIgnoreCase); + private Timer _idleTimer; private Timer _inactiveTimer; @@ -309,7 +314,7 @@ namespace Emby.Server.Implementations.Session _activeConnections.TryRemove(key, out _); if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId)) { - await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false); + await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false); } await OnSessionEnded(session).ConfigureAwait(false); @@ -317,6 +322,42 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> + public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId) + { + bool liveStreamNeedsToBeClosed = false; + + if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings)) + { + if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId)) + { + if (!string.IsNullOrEmpty(correspondingId)) + { + activeSessionMappings.TryRemove(correspondingId, out _); + } + + liveStreamNeedsToBeClosed = true; + } + + if (activeSessionMappings.IsEmpty) + { + _activeLiveStreamSessions.TryRemove(liveStreamId, out _); + } + } + + if (liveStreamNeedsToBeClosed) + { + try + { + await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error closing live stream"); + } + } + } + + /// <inheritdoc /> public async ValueTask ReportSessionEnded(string sessionId) { CheckDisposed(); @@ -343,6 +384,11 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime) { + if (session is null) + { + return; + } + if (string.IsNullOrEmpty(info.MediaSourceId)) { info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); @@ -410,7 +456,7 @@ namespace Emby.Server.Implementations.Session var nowPlayingQueue = info.NowPlayingQueue; - if (nowPlayingQueue?.Length > 0) + if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue)) { session.NowPlayingQueue = nowPlayingQueue; @@ -428,6 +474,7 @@ namespace Emby.Server.Implementations.Session private void RemoveNowPlayingItem(SessionInfo session) { session.NowPlayingItem = null; + session.FullNowPlayingItem = null; session.PlayState = new PlayerStateInfo(); if (!string.IsNullOrEmpty(session.DeviceId)) @@ -462,13 +509,11 @@ namespace Emby.Server.Implementations.Session ArgumentException.ThrowIfNullOrEmpty(deviceId); var key = GetSessionKey(appName, deviceId); - - CheckDisposed(); - - if (!_activeConnections.TryGetValue(key, out var sessionInfo)) + SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user); + SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession); + if (ReferenceEquals(newSession, sessionInfo)) { - sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user); - _activeConnections[key] = sessionInfo; + OnSessionStarted(newSession); } sessionInfo.UserId = user?.Id ?? Guid.Empty; @@ -492,7 +537,7 @@ namespace Emby.Server.Implementations.Session return sessionInfo; } - private SessionInfo CreateSession( + private SessionInfo CreateSessionInfo( string key, string appName, string appVersion, @@ -536,7 +581,6 @@ namespace Emby.Server.Implementations.Session sessionInfo.HasCustomDeviceName = true; } - OnSessionStarted(sessionInfo); return sessionInfo; } @@ -675,6 +719,11 @@ namespace Emby.Server.Implementations.Session private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { + if (session is null) + { + return null; + } + var item = session.FullNowPlayingItem; if (item is not null && item.Id.Equals(itemId)) { @@ -725,6 +774,11 @@ namespace Emby.Server.Implementations.Session } } + if (!string.IsNullOrEmpty(info.LiveStreamId)) + { + UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId); + } + var eventArgs = new PlaybackStartEventArgs { Item = libraryItem, @@ -782,6 +836,32 @@ namespace Emby.Server.Implementations.Session return OnPlaybackProgress(info, false); } + private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId) + { + var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<string, string>()); + + if (!string.IsNullOrEmpty(playSessionId)) + { + if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId != playSessionId) + { + if (!string.IsNullOrEmpty(currentPlaySessionId)) + { + activeSessionMappings.TryRemove(currentPlaySessionId, out _); + } + + activeSessionMappings[sessionId] = playSessionId; + activeSessionMappings[playSessionId] = sessionId; + } + } + else + { + if (!activeSessionMappings.TryGetValue(sessionId, out _)) + { + activeSessionMappings[sessionId] = string.Empty; + } + } + } + /// <summary> /// Used to report playback progress for an item. /// </summary> @@ -794,7 +874,11 @@ namespace Emby.Server.Implementations.Session ArgumentNullException.ThrowIfNull(info); - var session = GetSession(info.SessionId); + var session = GetSession(info.SessionId, false); + if (session is null) + { + return; + } var libraryItem = info.ItemId.IsEmpty() ? null @@ -818,6 +902,11 @@ namespace Emby.Server.Implementations.Session } } + if (!string.IsNullOrEmpty(info.LiveStreamId)) + { + UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId); + } + var eventArgs = new PlaybackProgressEventArgs { Item = libraryItem, @@ -1000,14 +1089,7 @@ namespace Emby.Server.Implementations.Session if (!string.IsNullOrEmpty(info.LiveStreamId)) { - try - { - await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing live stream"); - } + await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false); } var eventArgs = new PlaybackStopEventArgs @@ -1724,7 +1806,6 @@ namespace Emby.Server.Implementations.Session fields.Remove(ItemFields.DateLastSaved); fields.Remove(ItemFields.DisplayPreferencesId); fields.Remove(ItemFields.Etag); - fields.Remove(ItemFields.InheritedParentalRatingValue); fields.Remove(ItemFields.ItemCounts); fields.Remove(ItemFields.MediaSourceCount); fields.Remove(ItemFields.MediaStreams); @@ -2055,6 +2136,7 @@ namespace Emby.Server.Implementations.Session } _activeConnections.Clear(); + _activeLiveStreamSessions.Clear(); } } } diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index d4606abd2..6a26e92e1 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -5,6 +5,8 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Controller.Session; @@ -44,6 +46,7 @@ namespace Emby.Server.Implementations.Session private readonly Lock _webSocketsLock = new(); private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; private readonly ILogger<SessionWebSocketListener> _logger; private readonly ILoggerFactory _loggerFactory; @@ -57,14 +60,17 @@ namespace Emby.Server.Implementations.Session /// </summary> /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> + /// <param name="userManager">The user manager.</param> /// <param name="loggerFactory">The logger factory.</param> public SessionWebSocketListener( ILogger<SessionWebSocketListener> logger, ISessionManager sessionManager, + IUserManager userManager, ILoggerFactory loggerFactory) { _logger = logger; _sessionManager = sessionManager; + _userManager = userManager; _loggerFactory = loggerFactory; _keepAlive = new System.Timers.Timer(TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor)) { @@ -107,33 +113,9 @@ namespace Emby.Server.Implementations.Session /// <inheritdoc /> public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection, HttpContext httpContext) { - var session = await GetSession(httpContext, connection.RemoteEndPoint?.ToString()).ConfigureAwait(false); - if (session is not null) - { - EnsureController(session, connection); - await KeepAliveWebSocket(connection).ConfigureAwait(false); - } - else - { - _logger.LogWarning("Unable to determine session based on query string: {0}", httpContext.Request.QueryString); - } - } - - private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint) - { - if (!httpContext.User.Identity?.IsAuthenticated ?? false) - { - return null; - } - - var deviceId = httpContext.User.GetDeviceId(); - if (httpContext.Request.Query.TryGetValue("deviceId", out var queryDeviceId)) - { - deviceId = queryDeviceId; - } - - return await _sessionManager.GetSessionByAuthenticationToken(httpContext.User.GetToken(), deviceId, remoteEndpoint) - .ConfigureAwait(false); + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, httpContext).ConfigureAwait(false); + EnsureController(session, connection); + await KeepAliveWebSocket(connection).ConfigureAwait(false); } private void EnsureController(SessionInfo session, IWebSocketConnection connection) diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index e1c26d012..f10e7fcbb 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -26,10 +26,10 @@ namespace Emby.Server.Implementations.Sorting public IUserManager UserManager { get; set; } /// <summary> - /// Gets or sets the user data repository. + /// Gets or sets the user data manager. /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } + /// <value>The user data manager.</value> + public IUserDataManager UserDataManager { get; set; } /// <summary> /// Gets the name. diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index d668c17bf..2c8e2b37d 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -1,8 +1,8 @@ #nullable disable using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -28,10 +28,10 @@ namespace Emby.Server.Implementations.Sorting public IUserManager UserManager { get; set; } /// <summary> - /// Gets or sets the user data repository. + /// Gets or sets the user data manager. /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } + /// <value>The user data manager.</value> + public IUserDataManager UserDataManager { get; set; } /// <summary> /// Gets the name. @@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private DateTime GetDate(BaseItem x) { - var userdata = UserDataRepository.GetUserData(User, x); + var userdata = UserDataManager.GetUserData(User, x); if (userdata is not null && userdata.LastPlayedDate.HasValue) { diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 622a341b6..01c1e596f 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -1,8 +1,8 @@ #nullable disable #pragma warning disable CS1591 -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -25,10 +25,10 @@ namespace Emby.Server.Implementations.Sorting public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked; /// <summary> - /// Gets or sets the user data repository. + /// Gets or sets the user data manager. /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } + /// <value>The user data manager.</value> + public IUserDataManager UserDataManager { get; set; } /// <summary> /// Gets or sets the user manager. diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 2a3e456c2..6f206c877 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -26,10 +26,10 @@ namespace Emby.Server.Implementations.Sorting public ItemSortBy Type => ItemSortBy.IsUnplayed; /// <summary> - /// Gets or sets the user data repository. + /// Gets or sets the user data manager. /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } + /// <value>The user data manager.</value> + public IUserDataManager UserDataManager { get; set; } /// <summary> /// Gets or sets the user manager. diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index afd8ccf9f..fd1326327 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -26,10 +26,10 @@ namespace Emby.Server.Implementations.Sorting public ItemSortBy Type => ItemSortBy.IsUnplayed; /// <summary> - /// Gets or sets the user data repository. + /// Gets or sets the user data manager. /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } + /// <value>The user data manager.</value> + public IUserDataManager UserDataManager { get; set; } /// <summary> /// Gets or sets the user manager. diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index b4ee2c723..789af01cc 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -1,45 +1,54 @@ -#pragma warning disable CS1591 - using System; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Querying; -namespace Emby.Server.Implementations.Sorting +namespace Emby.Server.Implementations.Sorting; + +/// <summary> +/// Class providing comparison for official ratings. +/// </summary> +public class OfficialRatingComparer : IBaseItemComparer { - public class OfficialRatingComparer : IBaseItemComparer + private readonly ILocalizationManager _localizationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="OfficialRatingComparer"/> class. + /// </summary> + /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> + public OfficialRatingComparer(ILocalizationManager localizationManager) { - private readonly ILocalizationManager _localization; + _localizationManager = localizationManager; + } - public OfficialRatingComparer(ILocalizationManager localization) + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public ItemSortBy Type => ItemSortBy.OfficialRating; + + /// <summary> + /// Compares the specified x. + /// </summary> + /// <param name="x">The x.</param> + /// <param name="y">The y.</param> + /// <returns>System.Int32.</returns> + public int Compare(BaseItem? x, BaseItem? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + var zeroRating = new ParentalRatingScore(0, 0); + + var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating; + var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating; + var scoreCompare = ratingX.Score.CompareTo(ratingY.Score); + if (scoreCompare is 0) { - _localization = localization; + return (ratingX.SubScore ?? 0).CompareTo(ratingY.SubScore ?? 0); } - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public ItemSortBy Type => ItemSortBy.OfficialRating; - - /// <summary> - /// Compares the specified x. - /// </summary> - /// <param name="x">The x.</param> - /// <param name="y">The y.</param> - /// <returns>System.Int32.</returns> - public int Compare(BaseItem? x, BaseItem? y) - { - ArgumentNullException.ThrowIfNull(x); - - ArgumentNullException.ThrowIfNull(y); - - var levelX = string.IsNullOrEmpty(x.OfficialRating) ? 0 : _localization.GetRatingLevel(x.OfficialRating) ?? 0; - var levelY = string.IsNullOrEmpty(y.OfficialRating) ? 0 : _localization.GetRatingLevel(y.OfficialRating) ?? 0; - - return levelX.CompareTo(levelY); - } + return scoreCompare; } } diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index 12f88bf4d..26e28b03b 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -1,7 +1,7 @@ #nullable disable -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -27,10 +27,10 @@ namespace Emby.Server.Implementations.Sorting public ItemSortBy Type => ItemSortBy.PlayCount; /// <summary> - /// Gets or sets the user data repository. + /// Gets or sets the user data manager. /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } + /// <value>The user data manager.</value> + public IUserDataManager UserDataManager { get; set; } /// <summary> /// Gets or sets the user manager. @@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private int GetValue(BaseItem x) { - var userdata = UserDataRepository.GetUserData(User, x); + var userdata = UserDataManager.GetUserData(User, x); return userdata is null ? 0 : userdata.PlayCount; } diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index e0b438ef1..861ca2d3a 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -5,7 +5,6 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index a7821c0e0..c2e834ad5 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; @@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.SyncPlay SetState(waitingState); } - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); + var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo()); SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); _state.SessionJoined(this, _state.Type, session, cancellationToken); @@ -291,10 +291,10 @@ namespace Emby.Server.Implementations.SyncPlay { AddSession(session); - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); + var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo()); SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); + var updateOthers = new SyncPlayUserJoinedUpdate(GroupId, session.UserName); SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); _state.SessionJoined(this, _state.Type, session, cancellationToken); @@ -314,10 +314,10 @@ namespace Emby.Server.Implementations.SyncPlay RemoveSession(session); - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, GroupId.ToString()); + var updateSession = new SyncPlayGroupLeftUpdate(GroupId, GroupId.ToString()); SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); + var updateOthers = new SyncPlayUserLeftUpdate(GroupId, session.UserName); SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); _logger.LogInformation("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString()); @@ -426,12 +426,6 @@ namespace Emby.Server.Implementations.SyncPlay } /// <inheritdoc /> - public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data) - { - return new GroupUpdate<T>(GroupId, type, data); - } - - /// <inheritdoc /> public long SanitizePositionTicks(long? positionTicks) { var ticks = positionTicks ?? 0; diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index fdfff8f3b..b45d75455 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.SyncPlay } /// <inheritdoc /> - public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) + public GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) { if (session is null) { @@ -132,6 +132,7 @@ namespace Emby.Server.Implementations.SyncPlay UpdateSessionsCounter(session.UserId, 1); group.CreateGroup(session, request, cancellationToken); + return group.GetInfo(); } } @@ -159,7 +160,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId); - var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty); + var error = new SyncPlayGroupDoesNotExistUpdate(Guid.Empty, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -171,7 +172,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue.", session.Id, group.GroupId.ToString()); - var error = new GroupUpdate<string>(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.Empty); + var error = new SyncPlayLibraryAccessDeniedUpdate(group.GroupId, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -248,7 +249,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id); - var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty); + var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); } } @@ -289,6 +290,31 @@ namespace Emby.Server.Implementations.SyncPlay } /// <inheritdoc /> + public GroupInfoDto GetGroup(SessionInfo session, Guid groupId) + { + ArgumentNullException.ThrowIfNull(session); + + var user = _userManager.GetUserById(session.UserId); + + lock (_groupsLock) + { + foreach (var (_, group) in _groups) + { + // Locking required as group is not thread-safe. + lock (group) + { + if (group.GroupId.Equals(groupId) && group.HasAccessToPlayQueue(user)) + { + return group.GetInfo(); + } + } + } + } + + return null; + } + + /// <inheritdoc /> public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken) { if (session is null) @@ -327,7 +353,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id); - var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty); + var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); } } diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index c4552474c..d140426dd 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -1,9 +1,12 @@ +using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Server.Implementations.StorageHelpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -19,6 +22,7 @@ public class SystemManager : ISystemManager private readonly IServerConfigurationManager _configurationManager; private readonly IStartupOptions _startupOptions; private readonly IInstallationManager _installationManager; + private readonly ILibraryManager _libraryManager; /// <summary> /// Initializes a new instance of the <see cref="SystemManager"/> class. @@ -29,13 +33,15 @@ public class SystemManager : ISystemManager /// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param> /// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param> /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param> + /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param> public SystemManager( IHostApplicationLifetime applicationLifetime, IServerApplicationHost applicationHost, IServerApplicationPaths applicationPaths, IServerConfigurationManager configurationManager, IStartupOptions startupOptions, - IInstallationManager installationManager) + IInstallationManager installationManager, + ILibraryManager libraryManager) { _applicationLifetime = applicationLifetime; _applicationHost = applicationHost; @@ -43,6 +49,7 @@ public class SystemManager : ISystemManager _configurationManager = configurationManager; _startupOptions = startupOptions; _installationManager = installationManager; + _libraryManager = libraryManager; } /// <inheritdoc /> @@ -53,9 +60,11 @@ public class SystemManager : ISystemManager HasPendingRestart = _applicationHost.HasPendingRestart, IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested, Version = _applicationHost.ApplicationVersionString, + ProductName = _applicationHost.Name, WebSocketPortNumber = _applicationHost.HttpPort, CompletedInstallations = _installationManager.CompletedInstallations.ToArray(), Id = _applicationHost.SystemId, +#pragma warning disable CS0618 // Type or member is obsolete ProgramDataPath = _applicationPaths.ProgramDataPath, WebPath = _applicationPaths.WebPath, LogPath = _applicationPaths.LogDirectoryPath, @@ -63,14 +72,42 @@ public class SystemManager : ISystemManager InternalMetadataPath = _applicationPaths.InternalMetadataPath, CachePath = _applicationPaths.CachePath, TranscodingTempPath = _configurationManager.GetTranscodePath(), +#pragma warning restore CS0618 // Type or member is obsolete ServerName = _applicationHost.FriendlyName, LocalAddress = _applicationHost.GetSmartApiUrl(request), + StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted, SupportsLibraryMonitor = true, PackageName = _startupOptions.PackageName, CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications }; } + /// <inheritdoc/> + public SystemStorageInfo GetSystemStorageInfo() + { + var virtualFolderInfos = _libraryManager + .GetVirtualFolders() + .Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is. + .Select(e => new LibraryStorageInfo() + { + Id = Guid.Parse(e.ItemId), + Name = e.Name, + Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() + }); + + return new SystemStorageInfo() + { + ProgramDataFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.ProgramDataPath), + WebFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.WebPath), + LogFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.LogDirectoryPath), + ImageCacheFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.ImageCachePath), + InternalMetadataFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.InternalMetadataPath), + CacheFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.CachePath), + TranscodingTempFolder = StorageHelper.GetFreeSpaceOf(_configurationManager.GetTranscodePath()), + Libraries = virtualFolderInfos.ToArray() + }; + } + /// <inheritdoc /> public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) { diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f8ce473da..ee2e18f73 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Entities; +using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -91,7 +93,7 @@ namespace Emby.Server.Implementations.TV if (!string.IsNullOrEmpty(presentationUniqueKey)) { - return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request); + return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request); } if (limit.HasValue) @@ -99,25 +101,9 @@ namespace Emby.Server.Implementations.TV limit = limit.Value + 10; } - var items = _libraryManager - .GetItemList( - new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, - SeriesPresentationUniqueKey = presentationUniqueKey, - Limit = limit, - DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false }, - GroupBySeriesPresentationUniqueKey = true - }, - parentsFolders.ToList()) - .Cast<Episode>() - .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey)) - .Select(GetUniqueSeriesKey) - .ToList(); - - // Avoid implicitly captured closure - var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options); + var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff); + + var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options); return GetResult(episodes, request); } @@ -133,36 +119,11 @@ namespace Emby.Server.Implementations.TV .OrderByDescending(i => i.LastWatchedDate); } - // If viewing all next up for all series, remove first episodes - // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty(); - var anyFound = false; - return allNextUp - .Where(i => - { - if (request.DisableFirstEpisode) - { - return i.LastWatchedDate != DateTime.MinValue; - } - - if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff)) - { - anyFound = true; - return true; - } - - return !anyFound && i.LastWatchedDate == DateTime.MinValue; - }) .Select(i => i.GetEpisodeFunction()) .Where(i => i is not null)!; } - private static string GetUniqueSeriesKey(Episode episode) - { - return episode.SeriesPresentationUniqueKey; - } - private static string GetUniqueSeriesKey(Series series) { return series.GetPresentationUniqueKey(); @@ -178,13 +139,13 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { BaseItemKind.Episode }, + IncludeItemTypes = [BaseItemKind.Episode], IsPlayed = true, Limit = 1, ParentIndexNumberNotEquals = 0, DtoOptions = new DtoOptions { - Fields = new[] { ItemFields.SortName }, + Fields = [ItemFields.SortName], EnableImages = false } }; @@ -202,8 +163,8 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Episode], + OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)], Limit = 1, IsPlayed = includePlayed, IsVirtualItem = false, @@ -228,7 +189,7 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, - IncludeItemTypes = new[] { BaseItemKind.Episode }, + IncludeItemTypes = [BaseItemKind.Episode], IsPlayed = includePlayed, IsVirtualItem = false, DtoOptions = dtoOptions @@ -248,7 +209,7 @@ namespace Emby.Server.Implementations.TV consideredEpisodes.Add(nextEpisode); } - var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) }) + var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)]) .Cast<Episode>(); if (lastWatchedEpisode is not null) { |
