aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorBond-009 <bond.009@outlook.com>2026-05-06 20:49:19 +0200
committerGitHub <noreply@github.com>2026-05-06 20:49:19 +0200
commit33ed52b8ee25e1fae4763a26337b838dc9782b26 (patch)
treeee68da202f604eef267254ea8c689965098b1c3e /Emby.Server.Implementations
parentaa96ff42e616ecf5638a8f1e2e8459b94513c528 (diff)
parentd1ab428476f961426841a0561036c59c3b93878e (diff)
Merge branch 'master' into feature/season-provider-id-from-path
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs21
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs1
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs10
-rw-r--r--Emby.Server.Implementations/Chapters/ChapterManager.cs23
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs4
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs36
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs243
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs9
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs6
-rw-r--r--Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs273
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs14
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs519
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs9
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs87
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs52
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs46
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs2
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs40
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs17
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs2
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs40
-rw-r--r--Emby.Server.Implementations/Localization/Core/ab.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json192
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar_SA.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bs.json139
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_DO.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ga.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json26
-rw-r--r--Emby.Server.Implementations/Localization/Core/ht.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ka.json86
-rw-r--r--Emby.Server.Implementations/Localization/Core/km.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kn.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/lb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mn.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mt.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/my.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json106
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs79
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.json15
-rw-r--r--Emby.Server.Implementations/Localization/countries.json6
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs5
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs16
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs139
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs12
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs28
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs208
108 files changed, 1803 insertions, 910 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index de722332a4..56d977bbcb 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -90,6 +90,7 @@ namespace Emby.Server.Implementations.AppBase
CreateAndCheckMarker(ProgramDataPath, "data");
CreateAndCheckMarker(CachePath, "cache");
CreateAndCheckMarker(DataPath, "data");
+ CreateCacheDirTag(CachePath);
}
/// <inheritdoc />
@@ -100,6 +101,26 @@ namespace Emby.Server.Implementations.AppBase
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
}
+ /// <summary>
+ /// Creates a CACHEDIR.TAG file in the specified directory per the Cache Directory Tagging specification.
+ /// This signals to backup tools (e.g. Restic, Borg) that the directory contains cached data
+ /// and can be excluded from backups.
+ /// </summary>
+ /// <param name="path">The cache directory path.</param>
+ internal static void CreateCacheDirTag(string path)
+ {
+ var tagPath = Path.Combine(path, "CACHEDIR.TAG");
+ if (!File.Exists(tagPath))
+ {
+ File.WriteAllText(
+ tagPath,
+ "Signature: 8a477f597d28d172789f06886806bc55\n"
+ + "# This file is a cache directory tag created by Jellyfin.\n"
+ + "# For information about cache directory tags, see:\n"
+ + "#\thttps://bford.info/cachedir/\n");
+ }
+ }
+
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
{
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 81ef0e5f9a..ef5fa8bef9 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -228,6 +228,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Setting cache path: {Path}", cachePath);
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
+ BaseApplicationPaths.CreateCacheDirTag(cachePath);
}
/// <summary>
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index cbb0f6c565..b7aa2f3d06 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -507,7 +507,13 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
+ serviceCollection.AddSingleton<BaseItemRepository>();
+ serviceCollection.AddSingleton<IItemRepository>(sp => sp.GetRequiredService<BaseItemRepository>());
+ serviceCollection.AddSingleton<IItemQueryHelpers>(sp => sp.GetRequiredService<BaseItemRepository>());
+ serviceCollection.AddSingleton<IItemPersistenceService, ItemPersistenceService>();
+ serviceCollection.AddSingleton<INextUpService, NextUpService>();
+ serviceCollection.AddSingleton<IItemCountService, ItemCountService>();
+ serviceCollection.AddSingleton<ILinkedChildrenService, LinkedChildrenService>();
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
@@ -530,6 +536,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+ serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
@@ -641,6 +648,7 @@ namespace Emby.Server.Implementations
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
+ BaseItem.ItemCountService = Resolve<IItemCountService>();
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs
index d09ed30ae3..8a4721ce62 100644
--- a/Emby.Server.Implementations/Chapters/ChapterManager.cs
+++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -128,7 +129,7 @@ public class ChapterManager : IChapterManager
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
var threshold = TimeSpan.FromSeconds(1).Ticks;
- if (averageChapterDuration < threshold)
+ if (chapters.Count >= 2 && 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;
@@ -232,12 +233,22 @@ public class ChapterManager : IChapterManager
}
/// <inheritdoc />
- public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
+ public bool Supports(BaseItem item)
+ => item is Video or Audio;
+
+ /// <inheritdoc />
+ public void SaveChapters(BaseItem item, IReadOnlyList<ChapterInfo> chapters)
{
- // Remove any chapters that are outside of the runtime of the video
- var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
- _chapterRepository.SaveChapters(video.Id, validChapters);
- }
+ if (!Supports(item))
+ {
+ _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id);
+ return;
+ }
+
+ // Remove any chapters that are outside of the runtime of the item
+ var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList();
+ _chapterRepository.SaveChapters(item.Id, validChapters);
+}
/// <inheritdoc />
public ChapterInfo? GetChapter(Guid baseItemId, int index)
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index a320a774c6..0ede5665f9 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -272,7 +272,7 @@ namespace Emby.Server.Implementations.Collections
{
var childItem = _libraryManager.GetItemById(guidId);
- var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem is not null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
+ var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value.Equals(guidId));
if (child is null)
{
@@ -342,7 +342,7 @@ namespace Emby.Server.Implementations.Collections
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
- foreach (var childId in video.GetLocalAlternateVersionIds())
+ foreach (var childId in _libraryManager.GetLocalAlternateVersionIds(video))
{
if (!results.ContainsKey(childId))
{
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 676bb7f816..17355960c3 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -5,10 +5,12 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -35,7 +37,11 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
- await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
+ var deadItemsProgress = new Progress<double>(val => progress.Report(val * 0.8));
+ await CleanDeadItems(cancellationToken, deadItemsProgress).ConfigureAwait(false);
+
+ var playlistProgress = new Progress<double>(val => progress.Report(80 + (val * 0.2)));
+ await CleanOrphanedFilePlaylistsAsync(cancellationToken, playlistProgress).ConfigureAwait(false);
}
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
@@ -116,4 +122,32 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
progress.Report(100);
}
+
+ private async Task CleanOrphanedFilePlaylistsAsync(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var playlists = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Playlist],
+ Recursive = true
+ }).OfType<Playlist>().ToList();
+
+ var numComplete = 0;
+ var numItems = Math.Max(playlists.Count, 1);
+
+ foreach (var playlist in playlists)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (playlist.IsFile && !File.Exists(playlist.Path))
+ {
+ _logger.LogInformation("Removing file-based playlist {Name} because source file {Path} no longer exists", playlist.Name, playlist.Path);
+ _libraryManager.DeleteItem(playlist, new DeleteOptions { DeleteFileLocation = false });
+ }
+
+ numComplete++;
+ progress.Report((double)numComplete / numItems * 100);
+ }
+
+ progress.Report(100);
+ }
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index b392340f71..94e2468719 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -153,17 +153,102 @@ namespace Emby.Server.Implementations.Dto
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc />
- public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
+ public IReadOnlyList<BaseItemDto> GetBaseItemDtos(
+ IReadOnlyList<BaseItem> items,
+ DtoOptions options,
+ User? user = null,
+ BaseItem? owner = null,
+ bool skipVisibilityCheck = false)
{
- var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
+ var accessibleItems = skipVisibilityCheck || user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var returnItems = new BaseItemDto[accessibleItems.Count];
List<(BaseItem, BaseItemDto)>? programTuples = null;
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
+ // Batch-fetch user data for all items
+ Dictionary<Guid, UserItemData>? userDataBatch = null;
+ if (user is not null && options.EnableUserData)
+ {
+ userDataBatch = _userDataRepository.GetUserDataBatch(accessibleItems, user);
+ }
+
+ // Pre-compute collection folders once to avoid N+1 queries in CanDelete
+ List<Folder>? allCollectionFolders = null;
+ if (user is not null && options.ContainsField(ItemFields.CanDelete))
+ {
+ allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
+ }
+
+ // Batch-fetch child counts for all folders to avoid N+1 queries
+ Dictionary<Guid, int>? childCountBatch = null;
+ if (options.ContainsField(ItemFields.ChildCount))
+ {
+ var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
+ if (folderIds.Count > 0)
+ {
+ childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
+ }
+ }
+
+ // Batch-fetch played/total counts for all folders to avoid N+1 queries
+ Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null;
+ if (user is not null && options.EnableUserData)
+ {
+ var folderIds = accessibleItems.OfType<Folder>()
+ .Where(f => f.SupportsUserDataFromChildren && (f.SupportsPlayedStatus || options.ContainsField(ItemFields.RecursiveItemCount)))
+ .Select(f => f.Id).ToList();
+ if (folderIds.Count > 0)
+ {
+ playedCountBatch = _libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
+ }
+ }
+
+ // Batch-fetch MusicArtist lookups across all items to avoid N+1 queries.
+ IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null;
+ var artistNames = new HashSet<string>(StringComparer.Ordinal);
+ foreach (var item in accessibleItems)
+ {
+ if (item is IHasArtist hasArtist)
+ {
+ foreach (var name in hasArtist.Artists)
+ {
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ artistNames.Add(name);
+ }
+ }
+ }
+
+ if (item is IHasAlbumArtist hasAlbumArtist)
+ {
+ foreach (var name in hasAlbumArtist.AlbumArtists)
+ {
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ artistNames.Add(name);
+ }
+ }
+ }
+ }
+
+ if (artistNames.Count > 0)
+ {
+ artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
+ }
+
for (int index = 0; index < accessibleItems.Count; index++)
{
var item = accessibleItems[index];
- var dto = GetBaseItemDtoInternal(item, options, user, owner);
+ var dto = GetBaseItemDtoInternal(
+ item,
+ options,
+ user,
+ owner,
+ userDataBatch?.GetValueOrDefault(item.Id),
+ allCollectionFolders,
+ childCountBatch,
+ playedCountBatch,
+ artistsBatch);
if (item is LiveTvChannel tvChannel)
{
@@ -197,7 +282,7 @@ namespace Emby.Server.Implementations.Dto
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{
- var dto = GetBaseItemDtoInternal(item, options, user, owner);
+ var dto = GetBaseItemDtoInternal(item, options, user, owner, null);
if (item is LiveTvChannel tvChannel)
{
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
@@ -215,7 +300,16 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
+ private BaseItemDto GetBaseItemDtoInternal(
+ BaseItem item,
+ DtoOptions options,
+ User? user = null,
+ BaseItem? owner = null,
+ UserItemData? userData = null,
+ List<Folder>? allCollectionFolders = null,
+ Dictionary<Guid, int>? childCountBatch = null,
+ Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
+ IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
{
var dto = new BaseItemDto
{
@@ -252,7 +346,14 @@ namespace Emby.Server.Implementations.Dto
if (user is not null)
{
- AttachUserSpecificInfo(dto, item, user, options);
+ AttachUserSpecificInfo(
+ dto,
+ item,
+ user,
+ options,
+ userData,
+ childCountBatch,
+ playedCountBatch);
}
if (item is IHasMediaSources
@@ -268,13 +369,15 @@ namespace Emby.Server.Implementations.Dto
AttachStudios(dto, item);
}
- AttachBasicFields(dto, item, owner, options);
+ AttachBasicFields(dto, item, owner, options, artistsBatch);
if (options.ContainsField(ItemFields.CanDelete))
{
dto.CanDelete = user is null
? item.CanDelete()
- : item.CanDelete(user);
+ : allCollectionFolders is not null
+ ? item.CanDelete(user, allCollectionFolders)
+ : item.CanDelete(user);
}
if (options.ContainsField(ItemFields.CanDownload))
@@ -378,37 +481,7 @@ namespace Emby.Server.Implementations.Dto
return;
}
- var query = new InternalItemsQuery(user)
- {
- Recursive = true,
- DtoOptions = new DtoOptions(false) { EnableImages = false },
- IncludeItemTypes = relatedItemKinds
- };
-
- switch (dto.Type)
- {
- case BaseItemKind.Genre:
- case BaseItemKind.MusicGenre:
- query.GenreIds = [dto.Id];
- break;
- case BaseItemKind.MusicArtist:
- query.ArtistIds = [dto.Id];
- break;
- case BaseItemKind.Person:
- query.PersonIds = [dto.Id];
- break;
- case BaseItemKind.Studio:
- query.StudioIds = [dto.Id];
- break;
- case BaseItemKind.Year
- when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
- query.Years = [year];
- break;
- default:
- return;
- }
-
- var counts = _libraryManager.GetItemCounts(query);
+ var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
@@ -458,7 +531,14 @@ namespace Emby.Server.Implementations.Dto
/// <summary>
/// Attaches the user specific info.
/// </summary>
- private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, DtoOptions options)
+ private void AttachUserSpecificInfo(
+ BaseItemDto dto,
+ BaseItem item,
+ User user,
+ DtoOptions options,
+ UserItemData? userData = null,
+ Dictionary<Guid, int>? childCountBatch = null,
+ Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null)
{
if (item.IsFolder)
{
@@ -466,7 +546,19 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableUserData)
{
- dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
+ if (userData is not null)
+ {
+ // Use pre-fetched user data
+ dto.UserData = GetUserItemDataDto(userData, item.Id);
+ (int Played, int Total)? precomputed = playedCountBatch is not null
+ && playedCountBatch.TryGetValue(item.Id, out var counts) ? counts : null;
+ item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options, precomputed);
+ }
+ else
+ {
+ // Fall back to individual fetch
+ dto.UserData = _userDataRepository.GetUserDataDto(item, dto, user, options);
+ }
}
if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library)
@@ -485,7 +577,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ChildCount))
{
- dto.ChildCount ??= GetChildCount(folder, user);
+ dto.ChildCount ??= GetChildCount(folder, user, childCountBatch);
}
}
@@ -503,7 +595,17 @@ namespace Emby.Server.Implementations.Dto
{
if (options.EnableUserData)
{
- dto.UserData = _userDataRepository.GetUserDataDto(item, user);
+ if (userData is not null)
+ {
+ // Use pre-fetched user data
+ dto.UserData = GetUserItemDataDto(userData, item.Id);
+ item.FillUserDataDtoValues(dto.UserData, userData, dto, user, options);
+ }
+ else
+ {
+ // Fall back to individual fetch
+ dto.UserData = _userDataRepository.GetUserDataDto(item, user);
+ }
}
}
@@ -513,7 +615,25 @@ namespace Emby.Server.Implementations.Dto
}
}
- private static int GetChildCount(Folder folder, User user)
+ private static UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
+ {
+ ArgumentNullException.ThrowIfNull(data);
+
+ return new UserItemDataDto
+ {
+ IsFavorite = data.IsFavorite,
+ Likes = data.Likes,
+ PlaybackPositionTicks = data.PlaybackPositionTicks,
+ PlayCount = data.PlayCount,
+ Rating = data.Rating,
+ Played = data.Played,
+ LastPlayedDate = data.LastPlayedDate,
+ ItemId = itemId,
+ Key = data.Key
+ };
+ }
+
+ private static int GetChildCount(Folder folder, User user, Dictionary<Guid, int>? childCountBatch)
{
// Right now this is too slow to calculate for top level folders on a per-user basis
// Just return something so that apps that are expecting a value won't think the folders are empty
@@ -522,6 +642,13 @@ namespace Emby.Server.Implementations.Dto
return Random.Shared.Next(1, 10);
}
+ // Use pre-fetched batch data if available
+ if (childCountBatch is not null && childCountBatch.TryGetValue(folder.Id, out var count))
+ {
+ return count;
+ }
+
+ // Fall back to individual query for special cases (Series, Season, etc.)
return folder.GetChildCount(user);
}
@@ -815,7 +942,8 @@ namespace Emby.Server.Implementations.Dto
/// <param name="item">The item.</param>
/// <param name="owner">The owner.</param>
/// <param name="options">The options.</param>
- private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
+ /// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
+ private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -1019,6 +1147,15 @@ namespace Emby.Server.Implementations.Dto
{
dto.AlbumId = albumParent.Id;
dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
+ if (albumParent.LUFS.HasValue)
+ {
+ // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
+ dto.AlbumNormalizationGain = -18f - albumParent.LUFS;
+ }
+ else if (albumParent.NormalizationGain.HasValue)
+ {
+ dto.AlbumNormalizationGain = albumParent.NormalizationGain;
+ }
}
// if (options.ContainsField(ItemFields.MediaSourceCount))
@@ -1051,7 +1188,8 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
- var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+ var artistsLookup = artistsBatch
+ ?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.ArtistItems = hasArtist.Artists
.Where(name => !string.IsNullOrWhiteSpace(name))
@@ -1085,7 +1223,8 @@ namespace Emby.Server.Implementations.Dto
// })
// .ToList();
- var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+ var albumArtistsLookup = artistsBatch
+ ?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
.Where(name => !string.IsNullOrWhiteSpace(name))
@@ -1123,11 +1262,6 @@ namespace Emby.Server.Implementations.Dto
}
}
- if (options.ContainsField(ItemFields.Chapters))
- {
- dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
- }
-
if (options.ContainsField(ItemFields.Trickplay))
{
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
@@ -1141,6 +1275,11 @@ namespace Emby.Server.Implementations.Dto
dto.ExtraType = video.ExtraType;
}
+ if (options.ContainsField(ItemFields.Chapters))
+ {
+ dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
+ }
+
if (options.ContainsField(ItemFields.MediaStreams))
{
// Add VideoInfo
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 7cff2a25b6..1bf0f8c76c 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
+ private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary>
/// The file system watchers.
@@ -47,19 +48,23 @@ namespace Emby.Server.Implementations.IO
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
+ /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
- IHostApplicationLifetime appLifetime)
+ IHostApplicationLifetime appLifetime,
+ DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
+ _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
appLifetime.ApplicationStarted.Register(Start);
+ appLifetime.ApplicationStopping.Register(Stop);
}
/// <inheritdoc />
@@ -353,7 +358,7 @@ namespace Emby.Server.Implementations.IO
}
var fileInfo = _fileSystem.GetFileSystemInfo(path);
- if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
+ if (_dotIgnoreIgnoreRule.ShouldIgnore(fileInfo, null))
{
return;
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 4d68cb4444..199407044b 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -586,6 +586,12 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
+ if (!Directory.Exists(path))
+ {
+ _logger.LogWarning("Directory does not exist: {Path}", path);
+ return [];
+ }
+
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive
diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
index ef5d24c70f..023c1e8915 100644
--- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
+using BitFaster.Caching.Lru;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers;
@@ -15,22 +17,36 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{
private static readonly bool IsWindows = OperatingSystem.IsWindows();
- private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
- {
- for (var current = directory; current is not null; current = current.Parent)
- {
- var ignorePath = Path.Join(current.FullName, ".ignore");
- if (File.Exists(ignorePath))
- {
- return new FileInfo(ignorePath);
- }
- }
+ private readonly FastConcurrentLru<string, IgnoreFileCacheEntry> _directoryCache;
+ private readonly FastConcurrentLru<string, ParsedIgnoreCacheEntry> _rulesCache;
- return null;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DotIgnoreIgnoreRule"/> class.
+ /// </summary>
+ public DotIgnoreIgnoreRule()
+ {
+ var cacheSize = Math.Max(100, Environment.ProcessorCount * 100);
+ _directoryCache = new FastConcurrentLru<string, IgnoreFileCacheEntry>(
+ Environment.ProcessorCount,
+ cacheSize,
+ StringComparer.Ordinal);
+ _rulesCache = new FastConcurrentLru<string, ParsedIgnoreCacheEntry>(
+ Environment.ProcessorCount,
+ Math.Max(32, cacheSize / 4),
+ StringComparer.Ordinal);
}
/// <inheritdoc />
- public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
+ public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnoredInternal(fileInfo, parent);
+
+ /// <summary>
+ /// Clears the directory lookup cache. The parsed rules cache is not cleared
+ /// as it validates file modification time on each access.
+ /// </summary>
+ public void ClearDirectoryCache()
+ {
+ _directoryCache.Clear();
+ }
/// <summary>
/// Checks whether or not the file is ignored.
@@ -38,40 +54,38 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
/// <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)
+ public bool IsIgnoredInternal(FileSystemMetadata fileInfo, BaseItem? parent)
{
var searchDirectory = fileInfo.IsDirectory
- ? new DirectoryInfo(fileInfo.FullName)
- : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
+ ? fileInfo.FullName
+ : Path.GetDirectoryName(fileInfo.FullName);
- if (string.IsNullOrEmpty(searchDirectory.FullName))
+ if (string.IsNullOrEmpty(searchDirectory))
{
return false;
}
- var ignoreFile = FindIgnoreFile(searchDirectory);
+ var ignoreFile = FindIgnoreFileCached(searchDirectory);
if (ignoreFile is null)
{
return false;
}
- // Fast path in case the ignore files isn't a symlink and is empty
- if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
+ var parsedEntry = GetParsedRules(ignoreFile);
+ if (parsedEntry is null)
{
- // Ignore directory if we just have the file
- return true;
+ // File was deleted after we cached the path - clear the directory cache entry and return false
+ _directoryCache.TryRemove(searchDirectory, out _);
+ return false;
}
- var content = GetFileContent(ignoreFile);
- return string.IsNullOrWhiteSpace(content)
- || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
- }
+ // Empty file means ignore everything
+ if (parsedEntry.IsEmpty)
+ {
+ return true;
+ }
- private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
- {
- // If file has content, base ignoring off the content .gitignore-style rules
- var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- return CheckIgnoreRules(path, rules, isDirectory);
+ return parsedEntry.Rules.IsIgnored(GetPathToCheck(fileInfo.FullName, fileInfo.IsDirectory));
}
/// <summary>
@@ -117,8 +131,8 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return true;
}
- // Mitigate the problem of the Ignore library not handling Windows paths correctly.
- // See https://github.com/jellyfin/jellyfin/issues/15484
+ // Mitigate the problem of the Ignore library not handling Windows paths correctly.
+ // See https://github.com/jellyfin/jellyfin/issues/15484
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
@@ -130,11 +144,196 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule
return ignore.IsIgnored(pathToCheck);
}
- private static string GetFileContent(FileInfo ignoreFile)
+ private FileInfo? FindIgnoreFileCached(string directory)
+ {
+ // Check if we have a cached result for this directory
+ if (_directoryCache.TryGet(directory, out var cached))
+ {
+ return cached.IgnoreFileDirectory is null
+ ? null
+ : new FileInfo(Path.Join(cached.IgnoreFileDirectory, ".ignore"));
+ }
+
+ DirectoryInfo startDir;
+ try
+ {
+ startDir = new DirectoryInfo(directory);
+ }
+ catch (ArgumentException)
+ {
+ return null;
+ }
+
+ // Walk up the directory tree to find .ignore file using DirectoryInfo.Parent
+ var checkedDirs = new List<string> { directory };
+
+ for (var current = startDir; current is not null; current = current.Parent)
+ {
+ var currentPath = current.FullName;
+
+ // Check if this intermediate directory is cached
+ if (current != startDir && _directoryCache.TryGet(currentPath, out var parentCached))
+ {
+ // Cache the result for all directories we checked
+ var entry = new IgnoreFileCacheEntry(parentCached.IgnoreFileDirectory);
+ foreach (var dir in checkedDirs)
+ {
+ _directoryCache.AddOrUpdate(dir, entry);
+ }
+
+ return parentCached.IgnoreFileDirectory is null
+ ? null
+ : new FileInfo(Path.Join(parentCached.IgnoreFileDirectory, ".ignore"));
+ }
+
+ var ignoreFile = new FileInfo(Path.Join(currentPath, ".ignore"));
+ if (ignoreFile.Exists)
+ {
+ // Cache for all directories we checked
+ var entry = new IgnoreFileCacheEntry(currentPath);
+ foreach (var dir in checkedDirs)
+ {
+ _directoryCache.AddOrUpdate(dir, entry);
+ }
+
+ return ignoreFile;
+ }
+
+ if (current != startDir)
+ {
+ checkedDirs.Add(currentPath);
+ }
+ }
+
+ // No .ignore file found - cache null result for all directories
+ var nullEntry = new IgnoreFileCacheEntry((string?)null);
+ foreach (var dir in checkedDirs)
+ {
+ _directoryCache.AddOrUpdate(dir, nullEntry);
+ }
+
+ return null;
+ }
+
+ private ParsedIgnoreCacheEntry? GetParsedRules(FileInfo ignoreFile)
+ {
+ if (!ignoreFile.Exists)
+ {
+ _rulesCache.TryRemove(ignoreFile.FullName, out _);
+ return null;
+ }
+
+ var lastModified = ignoreFile.LastWriteTimeUtc;
+ var fileLength = ignoreFile.Length;
+ var key = ignoreFile.FullName;
+
+ // Check cache
+ if (_rulesCache.TryGet(key, out var cached))
+ {
+ if (cached.FileLastModified == lastModified && cached.FileLength == fileLength)
+ {
+ return cached;
+ }
+
+ // Stale - need to reparse
+ _rulesCache.TryRemove(key, out _);
+ }
+
+ // Parse the file
+ var parsedEntry = ParseIgnoreFile(ignoreFile, lastModified, fileLength);
+ _rulesCache.AddOrUpdate(key, parsedEntry);
+ return parsedEntry;
+ }
+
+ private static ParsedIgnoreCacheEntry ParseIgnoreFile(FileInfo ignoreFile, DateTime lastModified, long fileLength)
{
- ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
- return ignoreFile.Exists
- ? File.ReadAllText(ignoreFile.FullName)
- : string.Empty;
+ if (ignoreFile.LinkTarget is null && fileLength == 0)
+ {
+ return new ParsedIgnoreCacheEntry
+ {
+ Rules = new Ignore.Ignore(),
+ FileLastModified = lastModified,
+ FileLength = fileLength,
+ IsEmpty = true
+ };
+ }
+
+ // Resolve symlinks
+ var resolvedFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
+ if (!resolvedFile.Exists)
+ {
+ return new ParsedIgnoreCacheEntry
+ {
+ Rules = new Ignore.Ignore(),
+ FileLastModified = lastModified,
+ FileLength = fileLength,
+ IsEmpty = true
+ };
+ }
+
+ var content = File.ReadAllText(resolvedFile.FullName);
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ return new ParsedIgnoreCacheEntry
+ {
+ Rules = new Ignore.Ignore(),
+ FileLastModified = lastModified,
+ FileLength = fileLength,
+ IsEmpty = true
+ };
+ }
+
+ var rules = content.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ var ignore = new Ignore.Ignore();
+ var validRulesAdded = 0;
+
+ foreach (var rule in rules)
+ {
+ try
+ {
+ ignore.Add(rule);
+ validRulesAdded++;
+ }
+ catch (RegexParseException)
+ {
+ // Ignore invalid patterns
+ }
+ }
+
+ // No valid rules means treat as empty (ignore all)
+ return new ParsedIgnoreCacheEntry
+ {
+ Rules = ignore,
+ FileLastModified = lastModified,
+ FileLength = fileLength,
+ IsEmpty = validRulesAdded == 0
+ };
+ }
+
+ private static string GetPathToCheck(string path, bool isDirectory)
+ {
+ // Normalize Windows paths
+ var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
+
+ // Add trailing slash for directories to match "folder/"
+ if (isDirectory)
+ {
+ pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
+ }
+
+ return pathToCheck;
+ }
+
+ private readonly record struct IgnoreFileCacheEntry(string? IgnoreFileDirectory);
+
+ private sealed class ParsedIgnoreCacheEntry
+ {
+ public required Ignore.Ignore Rules { get; init; }
+
+ public required DateTime FileLastModified { get; init; }
+
+ public required long FileLength { get; init; }
+
+ public required bool IsEmpty { get; init; }
}
}
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 59ccb9e2c7..197ec42c50 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -31,6 +31,20 @@ namespace Emby.Server.Implementations.Library
"**/*.sample.?????",
"**/sample/*",
+ // Avoid adding Hungarian sample files
+ // https://github.com/jellyfin/jellyfin/issues/16237
+ "**/minta.?",
+ "**/minta.??",
+ "**/minta.???", // Matches minta.mkv
+ "**/minta.????", // Matches minta.webm
+ "**/minta.?????",
+ "**/*.minta.?",
+ "**/*.minta.??",
+ "**/*.minta.???",
+ "**/*.minta.????",
+ "**/*.minta.?????",
+ "**/minta/*",
+
// Directories
"**/metadata/**",
"**/metadata",
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index eee87c4d8b..11f1496086 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -30,18 +30,17 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
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.Playlists;
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;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -77,12 +76,17 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
+ private readonly IItemPersistenceService _persistenceService;
+ private readonly INextUpService _nextUpService;
+ private readonly IItemCountService _countService;
+ private readonly ILinkedChildrenService _linkedChildrenService;
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
+ private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
/// <summary>
/// The _root folder sync lock.
@@ -115,11 +119,16 @@ namespace Emby.Server.Implementations.Library
/// <param name="userViewManagerFactory">The user view manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
+ /// <param name="persistenceService">The item persistence service.</param>
+ /// <param name="nextUpService">The next up service.</param>
+ /// <param name="countService">The item count service.</param>
+ /// <param name="linkedChildrenService">The linked children service.</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="pathManager">The path manager.</param>
+ /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -133,11 +142,16 @@ namespace Emby.Server.Implementations.Library
Lazy<IUserViewManager> userViewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
+ IItemPersistenceService persistenceService,
+ INextUpService nextUpService,
+ IItemCountService countService,
+ ILinkedChildrenService linkedChildrenService,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
IDirectoryService directoryService,
IPeopleRepository peopleRepository,
- IPathManager pathManager)
+ IPathManager pathManager,
+ DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -151,6 +165,10 @@ namespace Emby.Server.Implementations.Library
_userViewManagerFactory = userViewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
+ _persistenceService = persistenceService;
+ _nextUpService = nextUpService;
+ _countService = countService;
+ _linkedChildrenService = linkedChildrenService;
_imageProcessor = imageProcessor;
_cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
@@ -158,6 +176,7 @@ namespace Emby.Server.Implementations.Library
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
_pathManager = pathManager;
+ _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -327,9 +346,17 @@ namespace Emby.Server.Implementations.Library
DeleteItem(item, options, parent, notifyParentItem);
}
- public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
+ public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
{
- var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray();
+ if (items.Count == 0)
+ {
+ return;
+ }
+
+ var pathMaps = items.Select(e =>
+ (Item: e,
+ InternalPath: GetInternalMetadataPaths(e),
+ DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
{
@@ -363,7 +390,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
+ _persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
}
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
@@ -406,6 +433,99 @@ namespace Emby.Server.Implementations.Library
item.Id);
}
+ // If deleting a primary version video, clear PrimaryVersionId from alternate versions
+ // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
+ if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
+ {
+ var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
+ var allAlternateVersions = localAlternateIds
+ .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
+ .Distinct()
+ .Select(id => GetItemById(id))
+ .OfType<Video>()
+ .ToList();
+
+ // Partition alternates by whether their files still exist on disk
+ var alternateVersions = new List<Video>();
+ var missingAlternates = new List<Video>();
+ foreach (var alt in allAlternateVersions)
+ {
+ if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
+ {
+ missingAlternates.Add(alt);
+ }
+ else
+ {
+ alternateVersions.Add(alt);
+ }
+ }
+
+ // Delete alternates whose files no longer exist to avoid ghost items.
+ // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
+ foreach (var missing in missingAlternates)
+ {
+ _logger.LogInformation(
+ "Deleting missing alternate version {Name} ({Path})",
+ missing.Name ?? "Unknown name",
+ missing.Path ?? string.Empty);
+ missing.SetPrimaryVersionId(null);
+ missing.OwnerId = Guid.Empty;
+ missing.LocalAlternateVersions = [];
+ missing.LinkedAlternateVersions = [];
+ DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
+ }
+
+ if (alternateVersions.Count > 0)
+ {
+ _logger.LogInformation(
+ "Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
+ alternateVersions.Count,
+ item.Name ?? "Unknown name");
+
+ // Promote the first alternate version to be the new primary
+ var newPrimary = alternateVersions[0];
+ newPrimary.SetPrimaryVersionId(null);
+ newPrimary.OwnerId = Guid.Empty;
+
+ // Transfer alternate version arrays from old primary to new primary
+ // so UpdateToRepositoryAsync creates correct LinkedChildren entries
+ newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
+ .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
+ .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
+ .ToArray();
+
+ newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+
+ // Re-route playlist/collection references from deleted primary to new primary
+ RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
+
+ // Update remaining alternates to point to new primary
+ foreach (var alternate in alternateVersions.Skip(1))
+ {
+ alternate.SetPrimaryVersionId(newPrimary.Id);
+ // Only set OwnerId for local alternates; linked alternates are independent items
+ alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
+ alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+ }
+ }
+ }
+ else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
+ {
+ // If deleting an alternate version, re-route references to its primary
+ RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter().GetResult();
+
+ // Remove deleted alternate from primary's LinkedAlternateVersions
+ if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
+ {
+ primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
+ .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
+ .ToArray();
+ primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+ }
+ }
+
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
: [];
@@ -450,7 +570,7 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
- _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
+ _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
_cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
@@ -576,6 +696,9 @@ namespace Emby.Server.Implementations.Library
// Trickplay
list.Add(_pathManager.GetTrickplayDirectory(video));
+ // Chapter Images
+ list.Add(_pathManager.GetChapterImageFolderPath(video));
+
// Subtitles and attachments
foreach (var mediaSource in item.GetMediaSources(false))
{
@@ -657,8 +780,63 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5();
}
- public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
- => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
+ public BaseItem? ResolvePath(
+ FileSystemMetadata fileInfo,
+ Folder? parent = null,
+ IDirectoryService? directoryService = null,
+ CollectionType? collectionType = null)
+ => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
+
+ /// <inheritdoc />
+ public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType)
+ {
+ // Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
+ // This happens when items were previously resolved without proper type context
+ // in mixed-content libraries where collectionType is null.
+ var expectedId = GetNewItemId(path, expectedVideoType);
+ if (expectedVideoType != typeof(Video))
+ {
+ var wrongTypeId = GetNewItemId(path, typeof(Video));
+ if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
+ {
+ _logger.LogInformation(
+ "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
+ wrongTypeItem.GetType().Name,
+ expectedVideoType.Name,
+ path);
+ DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
+ }
+ }
+
+ var resolved = ResolvePath(
+ _fileSystem.GetFileSystemInfo(path),
+ parent,
+ collectionType: collectionType) as Video;
+
+ if (resolved is null)
+ {
+ return null;
+ }
+
+ // Ensure the alternate version has the same concrete type as the primary video.
+ // ResolvePath may return a generic Video for files in mixed-content libraries
+ // where collectionType is null, even though the primary is a Movie/Episode/etc.
+ if (resolved.GetType() != expectedVideoType)
+ {
+ if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
+ {
+ correctVideo.Path = resolved.Path;
+ correctVideo.Name = resolved.Name;
+ correctVideo.VideoType = resolved.VideoType;
+ correctVideo.ProductionYear = resolved.ProductionYear;
+ correctVideo.ExtraType = resolved.ExtraType;
+ resolved = correctVideo;
+ }
+ }
+
+ resolved.Id = expectedId;
+ return resolved;
+ }
private BaseItem? ResolvePath(
FileSystemMetadata fileInfo,
@@ -1041,7 +1219,7 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
{
- return _itemRepository.FindArtists(names);
+ return _linkedChildrenService.FindArtists(names);
}
public MusicArtist GetArtist(string name, DtoOptions options)
@@ -1131,6 +1309,7 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
{
IsScanRunning = true;
+ ClearIgnoreRuleCache();
LibraryMonitor.Stop();
try
@@ -1139,6 +1318,7 @@ namespace Emby.Server.Implementations.Library
}
finally
{
+ ClearIgnoreRuleCache();
LibraryMonitor.Start();
IsScanRunning = false;
}
@@ -1146,6 +1326,7 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
+ ClearIgnoreRuleCache();
RootFolder.Children = null;
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -1186,8 +1367,16 @@ namespace Emby.Server.Implementations.Library
if (toDelete.Count > 0)
{
- _itemRepository.DeleteItem(toDelete.ToArray());
+ _persistenceService.DeleteItem(toDelete.ToArray());
}
+
+ ClearIgnoreRuleCache();
+ }
+
+ /// <inheritdoc />
+ public void ClearIgnoreRuleCache()
+ {
+ _dotIgnoreIgnoreRule.ClearDirectoryCache();
}
private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
@@ -1262,7 +1451,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
- _itemRepository.UpdateInheritedValues();
+ _persistenceService.UpdateInheritedValues();
progress.Report(100);
}
@@ -1421,14 +1610,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent);
}
- var itemList = _itemRepository.GetItemList(query);
- var user = query.User;
- if (user is not null)
- {
- return itemList.Where(i => i.IsVisible(user)).ToList();
- }
-
- return itemList;
+ return _itemRepository.GetItemList(query);
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1452,7 +1634,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User);
}
- return _itemRepository.GetCount(query);
+ return _countService.GetCount(query);
}
public ItemCounts GetItemCounts(InternalItemsQuery query)
@@ -1471,7 +1653,30 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User);
}
- return _itemRepository.GetItemCounts(query);
+ return _countService.GetItemCounts(query);
+ }
+
+ /// <inheritdoc/>
+ public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user)
+ {
+ var query = new InternalItemsQuery(user);
+ if (user is not null)
+ {
+ AddUserToQuery(query, user);
+ }
+
+ return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
+ }
+
+ public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
+ {
+ return _countService.GetChildCountBatch(parentIds, userId);
+ }
+
+ /// <inheritdoc/>
+ public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user)
+ {
+ return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
@@ -1516,7 +1721,17 @@ namespace Emby.Server.Implementations.Library
}
}
- return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
+ return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
+ InternalItemsQuery query,
+ IReadOnlyList<string> seriesKeys,
+ bool includeSpecials,
+ bool includeWatchedForRewatching)
+ {
+ return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching);
}
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
@@ -1683,6 +1898,25 @@ namespace Emby.Server.Implementations.Library
query.TopParentIds = [Guid.NewGuid()];
}
}
+ else if (parents.Count == 1 && parents.First() is Folder folder
+ && (folder is Playlist || folder is BoxSet)
+ && folder.LinkedChildren.Length > 0)
+ {
+ // Playlists and BoxSets store their contents in LinkedChildren and never
+ // populate AncestorIds for those items, so a recursive AncestorIds query
+ // would return zero rows. Resolve to the linked child IDs up front and
+ // route through the existing indexed ItemIds filter.
+ query.ItemIds = folder.LinkedChildren
+ .Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
+ .Select(lc => lc.ItemId!.Value)
+ .ToArray();
+
+ // Empty linked-children should still return empty rather than scanning everything.
+ if (query.ItemIds.Length == 0)
+ {
+ query.ItemIds = [Guid.NewGuid()];
+ }
+ }
else
{
// We need to be able to query from any arbitrary ancestor up the tree
@@ -1700,6 +1934,11 @@ namespace Emby.Server.Implementations.Library
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
+ if (query.User is null)
+ {
+ query.SetUser(user);
+ }
+
if (query.AncestorIds.Length == 0 &&
query.ParentId.IsEmpty() &&
query.ChannelIds.Count == 0 &&
@@ -1725,6 +1964,15 @@ namespace Emby.Server.Implementations.Library
}
}
+ /// <inheritdoc/>
+ public void ConfigureUserAccess(InternalItemsQuery query, User user)
+ {
+ ArgumentNullException.ThrowIfNull(query);
+ ArgumentNullException.ThrowIfNull(user);
+
+ AddUserToQuery(query, user);
+ }
+
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
{
if (item is UserView view)
@@ -1890,6 +2138,44 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
+ public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
+ {
+ ArgumentNullException.ThrowIfNull(video);
+
+ var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion);
+ if (linkedIds.Count > 0)
+ {
+ return linkedIds;
+ }
+
+ return [];
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
+ {
+ ArgumentNullException.ThrowIfNull(video);
+
+ var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion);
+ if (linkedIds.Count > 0)
+ {
+ return linkedIds
+ .Select(id => GetItemById(id))
+ .Where(i => i is not null)
+ .OfType<Video>()
+ .OrderBy(i => i.SortName);
+ }
+
+ return [];
+ }
+
+ /// <inheritdoc />
+ public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
+ {
+ _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
+ }
+
+ /// <inheritdoc />
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
IOrderedEnumerable<BaseItem>? orderedItems = null;
@@ -1993,10 +2279,45 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{
- _itemRepository.SaveItems(items, cancellationToken);
-
+ // Resolve and add any local alternate version items that don't exist yet
+ // This ensures they exist in the database when LinkedChildren are processed
+ var allItems = new List<BaseItem>(items);
+ var parentFolder = parent as Folder;
+ var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
foreach (var item in items)
{
+ if (item is Video video && video.LocalAlternateVersions.Length > 0)
+ {
+ var videoType = video.GetType();
+ foreach (var path in video.LocalAlternateVersions)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ continue;
+ }
+
+ // Use the primary video's type for ID calculation to ensure consistency
+ var altId = GetNewItemId(path, videoType);
+ if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
+ {
+ // Alternate version doesn't exist, resolve and create it
+ // ensuring it has the same type as the primary video
+ var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
+ if (altVideo is not null)
+ {
+ altVideo.OwnerId = video.Id;
+ altVideo.SetPrimaryVersionId(video.Id);
+ allItems.Add(altVideo);
+ }
+ }
+ }
+ }
+ }
+
+ _persistenceService.SaveItems(allItems, cancellationToken);
+
+ foreach (var item in allItems)
+ {
RegisterItem(item);
}
@@ -2144,7 +2465,7 @@ namespace Emby.Server.Implementations.Library
item.ValidateImages();
- await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false);
+ await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
RegisterItem(item);
}
@@ -2161,7 +2482,50 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow;
}
- _itemRepository.SaveItems(items, cancellationToken);
+ // Resolve and add any local alternate version items that don't exist yet
+ // This ensures they exist in the database when LinkedChildren are processed
+ var allItems = new List<BaseItem>(items);
+ var parentFolder = parent as Folder;
+ var parentCollectionType = GetTopFolderContentType(parent);
+ foreach (var item in items)
+ {
+ if (item is Video video && video.LocalAlternateVersions.Length > 0)
+ {
+ var videoType = video.GetType();
+ foreach (var path in video.LocalAlternateVersions)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ continue;
+ }
+
+ // Use the primary video's type for ID calculation to ensure consistency
+ var altId = GetNewItemId(path, videoType);
+ if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
+ {
+ // Alternate version doesn't exist, resolve and create it
+ // ensuring it has the same type as the primary video
+ var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
+ if (altVideo is not null)
+ {
+ altVideo.OwnerId = video.Id;
+ altVideo.SetPrimaryVersionId(video.Id);
+ allItems.Add(altVideo);
+ }
+ }
+ }
+ }
+ }
+
+ _persistenceService.SaveItems(allItems, cancellationToken);
+
+ foreach (var item in allItems)
+ {
+ if (!items.Contains(item))
+ {
+ RegisterItem(item);
+ }
+ }
if (parent is Folder folder)
{
@@ -2205,7 +2569,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
- await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
+ await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
}
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
@@ -2833,8 +3197,9 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
// 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);
+ var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
+ var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd));
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
@@ -2896,10 +3261,16 @@ namespace Emby.Server.Implementations.Library
extra.ExtraType = extraType;
}
- extra.ParentId = Guid.Empty;
- extra.OwnerId = owner.Id;
- extra.IsInMixedFolder = isInMixedFolder;
- return extra;
+ // Only return items that are actual extras (have ExtraType set)
+ // Note: OwnerId and ParentId are set by RefreshExtras, not here,
+ // so that RefreshExtras can detect when they need updating and set ForceSave.
+ if (extra.ExtraType is not null)
+ {
+ extra.IsInMixedFolder = isInMixedFolder;
+ return extra;
+ }
+
+ return null;
}
}
@@ -2918,7 +3289,7 @@ namespace Emby.Server.Implementations.Library
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{
- return _peopleRepository.GetPeople(query);
+ return _peopleRepository.GetPeople(query).Items;
}
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
@@ -2939,24 +3310,33 @@ namespace Emby.Server.Implementations.Library
return [];
}
- public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
+ public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
{
- return _peopleRepository.GetPeopleNames(query)
- .Select(i =>
- {
- try
+ var queryResult = _peopleRepository.GetPeople(query);
+ var baseItems = queryResult.Items.Select(i =>
{
- return GetPerson(i);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting person");
- return null;
- }
- })
- .Where(i => i is not null)
- .Where(i => query.User is null || i!.IsVisible(query.User))
- .ToList()!; // null values are filtered out
+ try
+ {
+ return GetPerson(i.Name);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
+ return null;
+ }
+ })
+ .Where(i => i is not null)
+ .Where(i => query.User is null || i!.IsVisible(query.User))
+ .OfType<BaseItem>()
+ .ToList()
+ .AsReadOnly();
+
+ return new QueryResult<BaseItem>
+ {
+ StartIndex = queryResult.StartIndex,
+ TotalRecordCount = queryResult.TotalRecordCount,
+ Items = baseItems,
+ };
}
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
@@ -3385,5 +3765,40 @@ namespace Emby.Server.Implementations.Library
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
}
+
+ /// <inheritdoc />
+ public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
+ {
+ var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
+
+ // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
+ foreach (var parentId in affectedParentIds)
+ {
+ if (GetItemById(parentId) is Folder parent)
+ {
+ foreach (var lc in parent.LinkedChildren)
+ {
+ if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
+ {
+ lc.ItemId = toChildId;
+ }
+ }
+
+ await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return _itemRepository.GetQueryFiltersLegacy(query);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 3ee1c757f2..1e885aad6e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : ItemResolver<Book>
{
- private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" };
+ private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index ad041086e5..6e9a38fd34 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -3,6 +3,7 @@
using System;
using System.Globalization;
using System.IO;
+using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Server.Implementations.Library;
@@ -81,6 +82,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
+
+ var hasAnyVideo = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
+ .Any(file => _namingOptions.VideoFileExtensions.Contains(Path.GetExtension(file)));
+
+ if (!hasAnyVideo)
+ {
+ return null;
+ }
}
if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 72c8d7a9d2..1281f1587f 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -177,53 +177,74 @@ namespace Emby.Server.Implementations.Library
};
}
- private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
+ /// <inheritdoc />
+ public Dictionary<Guid, UserItemData> GetUserDataBatch(IReadOnlyList<BaseItem> items, User user)
{
- var cacheKey = GetCacheKey(user.InternalId, itemId);
+ var result = new Dictionary<Guid, UserItemData>(items.Count);
+ var itemsNeedingQuery = new List<(BaseItem Item, List<string> Keys)>();
- if (_cache.TryGet(cacheKey, out var data))
+ foreach (var item in items)
{
- return data;
- }
-
- data = GetUserDataInternal(user.Id, itemId, keys);
-
- if (data is null)
- {
- return new UserItemData()
+ var cacheKey = GetCacheKey(user.InternalId, item.Id);
+ if (_cache.TryGet(cacheKey, out var cachedData))
{
- Key = keys[0],
- };
+ result[item.Id] = cachedData;
+ }
+ else
+ {
+ var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
+ if (userData is not null)
+ {
+ result[item.Id] = userData;
+ _cache.AddOrUpdate(cacheKey, userData);
+ }
+ else
+ {
+ var keys = item.GetUserDataKeys();
+ itemsNeedingQuery.Add((item, keys));
+ }
+ }
}
- return _cache.GetOrAdd(cacheKey, _ => data);
- }
-
- private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
- {
- if (keys.Count == 0)
+ if (itemsNeedingQuery.Count == 0)
{
- return null;
+ return result;
}
- using var context = _repository.CreateDbContext();
- var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
-
- if (userData.Length > 0)
+ // Build a single query for all missing items
+ var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
+ var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
+ if (allKeys.Count > 0)
{
- var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
- if (directDataReference is not null)
+ using var context = _repository.CreateDbContext();
+ var userDataArray = context.UserData
+ .AsNoTracking()
+ .Where(e => e.UserId.Equals(user.Id))
+ .WhereOneOrMany(allItemIds, e => e.ItemId)
+ .WhereOneOrMany(allKeys, e => e.CustomDataKey)
+ .ToArray();
+
+ var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
+ foreach (var (item, keys) in itemsNeedingQuery)
{
- return Map(directDataReference);
- }
+ UserItemData userData;
+ if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
+ {
+ var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N"));
+ userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First());
+ }
+ else
+ {
+ userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
+ }
- return Map(userData.First());
+ result[item.Id] = userData;
+ var cacheKey = GetCacheKey(user.InternalId, item.Id);
+ _cache.AddOrUpdate(cacheKey, userData);
+ }
}
- return new UserItemData
- {
- Key = keys.Last()!
- };
+ return result;
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 6fb53ff15d..9512b0ffd7 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -59,8 +59,8 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType;
- // Playlist library requires special handling because the folder only references user playlists
- if (folderViewType == CollectionType.playlists)
+ // Playlist and BoxSet libraries require special handling because the folder only references linked items
+ if (folderViewType == CollectionType.playlists || folderViewType == CollectionType.boxsets)
{
var items = folder.GetItemList(new InternalItemsQuery(user)
{
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Library
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
}
- var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ var sorted = _libraryManager.Sort(list, user, [ItemSortBy.SortName], SortOrder.Ascending).ToList();
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list
@@ -205,7 +205,7 @@ namespace Emby.Server.Implementations.Library
var libraryItems = GetItemsForLatestItems(request.User, request, options);
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
-
+ var containerIndexMap = new Dictionary<Guid, int>();
foreach (var item in libraryItems)
{
// Only grab the index container for media
@@ -213,20 +213,16 @@ namespace Emby.Server.Implementations.Library
if (container is null)
{
- list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item }));
+ list.Add(new Tuple<BaseItem, List<BaseItem>>(null!, new List<BaseItem> { item }));
+ }
+ else if (containerIndexMap.TryGetValue(container.Id, out var existingIndex))
+ {
+ list[existingIndex].Item2.Add(item);
}
else
{
- var current = list.FirstOrDefault(i => i.Item1 is not null && i.Item1.Id.Equals(container.Id));
-
- if (current is not null)
- {
- current.Item2.Add(item);
- }
- else
- {
- list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
- }
+ containerIndexMap[container.Id] = list.Count;
+ list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item }));
}
if (list.Count >= request.Limit)
@@ -255,7 +251,7 @@ namespace Emby.Server.Implementations.Library
return _channelManager.GetLatestChannelItemsInternal(
new InternalItemsQuery(user)
{
- ChannelIds = new[] { parentId },
+ ChannelIds = [parentId],
IsPlayed = request.IsPlayed,
StartIndex = request.StartIndex,
Limit = request.Limit,
@@ -301,11 +297,11 @@ namespace Emby.Server.Implementations.Library
{
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{
- includeItemTypes = new[] { BaseItemKind.Movie };
+ includeItemTypes = [BaseItemKind.Movie];
}
else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows))
{
- includeItemTypes = new[] { BaseItemKind.Episode };
+ includeItemTypes = [BaseItemKind.Episode];
}
}
}
@@ -344,29 +340,29 @@ namespace Emby.Server.Implementations.Library
}
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
- ? new[]
- {
+ ?
+ [
BaseItemKind.Person,
BaseItemKind.Studio,
BaseItemKind.Year,
BaseItemKind.MusicGenre,
BaseItemKind.Genre
- }
+ ]
: Array.Empty<BaseItemKind>();
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
- OrderBy = new[]
- {
+ OrderBy =
+ [
(ItemSortBy.DateCreated, SortOrder.Descending),
(ItemSortBy.SortName, SortOrder.Descending),
(ItemSortBy.ProductionYear, SortOrder.Descending)
- },
+ ],
IsFolder = includeItemTypes.Length == 0 ? false : null,
ExcludeItemTypes = excludeItemTypes,
IsVirtualItem = false,
- Limit = limit * 5,
+ Limit = limit * 2,
IsPlayed = isPlayed,
DtoOptions = options,
MediaTypes = mediaTypes
@@ -394,6 +390,12 @@ namespace Emby.Server.Implementations.Library
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
}
+
+ if (collectionType == CollectionType.movies)
+ {
+ query.Limit = limit;
+ return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
+ }
}
return _libraryManager.GetItemList(query, parents);
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index 7cc851b73b..fa7112eb90 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -50,21 +50,40 @@ public class ArtistsValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetAllArtistNames();
+ var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.MusicArtist]
+ }).ToHashSet();
+
+ var existingArtists = _libraryManager.GetArtists(names);
var numComplete = 0;
var count = names.Count;
+ var refreshed = 0;
foreach (var name in names)
{
try
{
- var item = _libraryManager.GetArtist(name);
+ MusicArtist? item = null;
+ if (existingArtists.TryGetValue(name, out var artists) && artists.Length > 0)
+ {
+ item = artists.OrderBy(i => i.IsAccessedByName ? 1 : 0).First();
+ }
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ // Fall back to GetArtist if not found (creates new item if needed)
+ item ??= _libraryManager.GetArtist(name);
+ var isNew = !existingArtistIds.Contains(item.Id);
+ var neverRefreshed = item.DateLastRefreshed == default;
+
+ if (isNew || neverRefreshed)
+ {
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ refreshed++;
+ }
}
catch (OperationCanceledException)
{
- // Don't clutter the log
throw;
}
catch (Exception ex)
@@ -80,31 +99,24 @@ public class ArtistsValidator
progress.Report(percent);
}
+ _logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
+
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
IsDeadArtist = true,
IsLocked = false
- }).Cast<MusicArtist>().ToList();
+ }).Cast<MusicArtist>()
+ .Where(item => item.IsAccessedByName)
+ .ToList();
foreach (var item in deadEntities)
{
- if (!item.IsAccessedByName)
- {
- continue;
- }
-
_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);
}
+ _libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index e62c638ed6..e3ef75b9ee 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -74,7 +74,7 @@ public class CollectionPostScanTask : ILibraryPostScanTask
foreach (var m in movies)
{
- if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
+ if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName) && !movie.PrimaryVersionId.HasValue)
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
index fbfc9f7d54..fc5a2fa0c5 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -48,17 +49,40 @@ public class GenresValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetGenreNames();
+ var existingGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Genre]
+ }).ToHashSet();
+
+ var existingGenres = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Genre]
+ }).Cast<Genre>()
+ .GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var numComplete = 0;
var count = names.Count;
+ var refreshed = 0;
foreach (var name in names)
{
try
{
- var item = _libraryManager.GetGenre(name);
+ Genre? item = null;
+ if (existingGenres.TryGetValue(name, out var existingGenre))
+ {
+ item = existingGenre;
+ }
+
+ // Fall back to GetGenre if not found (creates new item if needed)
+ item ??= _libraryManager.GetGenre(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ if (!existingGenreIds.Contains(item.Id))
+ {
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ refreshed++;
+ }
}
catch (OperationCanceledException)
{
@@ -78,6 +102,8 @@ public class GenresValidator
progress.Report(percent);
}
+ _logger.LogInformation("Refreshed metadata for {RefreshedCount} new genres out of {TotalCount} total", refreshed, count);
+
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
@@ -88,16 +114,10 @@ public class GenresValidator
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);
}
+ _libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
index 6203bce2bc..4365707529 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
@@ -1,6 +1,9 @@
using System;
+using System.Linq;
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;
@@ -45,17 +48,25 @@ public class MusicGenresValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetMusicGenreNames();
+ var existingMusicGenreIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.MusicGenre]
+ }).ToHashSet();
var numComplete = 0;
var count = names.Count;
+ var refreshed = 0;
foreach (var name in names)
{
try
{
var item = _libraryManager.GetMusicGenre(name);
-
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ if (!existingMusicGenreIds.Contains(item.Id))
+ {
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ refreshed++;
+ }
}
catch (OperationCanceledException)
{
@@ -75,6 +86,8 @@ public class MusicGenresValidator
progress.Report(percent);
}
+ _logger.LogInformation("Refreshed metadata for {RefreshedCount} new music genres out of {TotalCount} total", refreshed, count);
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index f9a6f0d19e..dacef102dd 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -109,7 +109,7 @@ public class PeopleValidator
var i = 0;
foreach (var item in deadEntities.Chunk(500))
{
- _libraryManager.DeleteItemsUnsafeFast(item);
+ _libraryManager.DeleteItemsUnsafeFast(item, true);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
}
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index 5b87e4d9d0..88f86ae6ca 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -49,17 +50,40 @@ public class StudiosValidator
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetStudioNames();
+ var existingStudioIds = _libraryManager.GetItemIds(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Studio]
+ }).ToHashSet();
+
+ var existingStudios = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Studio]
+ }).Cast<Studio>()
+ .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var numComplete = 0;
var count = names.Count;
+ var refreshed = 0;
foreach (var name in names)
{
try
{
- var item = _libraryManager.GetStudio(name);
+ Studio? item = null;
+ if (existingStudios.TryGetValue(name, out var existingStudio))
+ {
+ item = existingStudio;
+ }
+
+ // Fall back to GetStudio if not found (creates new item if needed)
+ item ??= _libraryManager.GetStudio(name);
- await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ if (!existingStudioIds.Contains(item.Id))
+ {
+ await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ refreshed++;
+ }
}
catch (OperationCanceledException)
{
@@ -79,6 +103,8 @@ public class StudiosValidator
progress.Report(percent);
}
+ _logger.LogInformation("Refreshed metadata for {RefreshedCount} new studios out of {TotalCount} total", refreshed, count);
+
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Studio],
@@ -89,16 +115,10 @@ public class StudiosValidator
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);
}
+ _libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json
index bc6062f429..d6d257c5ba 100644
--- a/Emby.Server.Implementations/Localization/Core/ab.json
+++ b/Emby.Server.Implementations/Localization/Core/ab.json
@@ -1,3 +1,5 @@
{
- "Albums": "аальбомқәа"
+ "Albums": "аальбомқәа",
+ "AppDeviceValues": "Апп: {0}, Априбор: {1}",
+ "Application": "Апрограмма"
}
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 59fb33941b..80c1bd0940 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
"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.",
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
"TaskExtractMediaSegments": "Media Segment Skandeer",
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 7ce8baef59..b80737d3b9 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,141 +1,139 @@
{
- "Albums": "ألبومات",
- "AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
- "Application": "تطبيق",
- "Artists": "فنانون",
- "AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}",
+ "Albums": "الألبومات",
+ "AppDeviceValues": "التطبيق: {0}، الجهاز: {1}",
+ "Application": "التطبيق",
+ "Artists": "الفنانون",
+ "AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح",
"Books": "الكتب",
- "CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}",
+ "CameraImageUploadedFrom": "تم رفع صورة كاميرا جديدة من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
- "Collections": "مجموعات",
- "DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
+ "Collections": "المجموعات",
+ "DeviceOfflineWithName": "انقطع اتصال {0}",
"DeviceOnlineWithName": "{0} متصل",
- "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}",
+ "FailedLoginAttemptWithUserName": "محاولة تسجيل دخول فاشلة من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
- "Genres": "التصنيفات",
- "HeaderAlbumArtists": "فناني الألبوم",
+ "Genres": "الأنواع",
+ "HeaderAlbumArtists": "فنانو الألبوم",
"HeaderContinueWatching": "متابعة المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
"HeaderFavoriteShows": "المسلسلات المفضلة",
"HeaderFavoriteSongs": "الأغاني المفضلة",
- "HeaderLiveTV": "التلفاز المباشر",
+ "HeaderLiveTV": "البث التلفزيوني المباشر",
"HeaderNextUp": "التالي",
"HeaderRecordingGroups": "مجموعات التسجيل",
- "HomeVideos": "الفيديوهات الشخصية",
- "Inherit": "توريث",
- "ItemAddedWithName": "أُضيف {0} للمكتبة",
- "ItemRemovedWithName": "أُزيل {0} من المكتبة",
- "LabelIpAddressValue": "عنوان الآي بي: {0}",
+ "HomeVideos": "فيديوهات منزلية",
+ "Inherit": "وراثة",
+ "ItemAddedWithName": "تمت إضافة {0} إلى المكتبة",
+ "ItemRemovedWithName": "تمت إزالة {0} من المكتبة",
+ "LabelIpAddressValue": "عنوان IP: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}",
"Latest": "الأحدث",
- "MessageApplicationUpdated": "حُدث خادم Jellyfin",
- "MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
- "MessageServerConfigurationUpdated": "حُدثت إعدادات الخادم",
+ "MessageApplicationUpdated": "تم تحديث خادم Jellyfin",
+ "MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin إلى {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث قسم إعدادات الخادم {0}",
+ "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
"MixedContent": "محتوى مختلط",
"Movies": "الأفلام",
"Music": "الموسيقى",
"MusicVideos": "الفيديوهات الموسيقية",
"NameInstallFailed": "فشل تثبيت {0}",
"NameSeasonNumber": "الموسم {0}",
- "NameSeasonUnknown": "الموسم غير معروف",
- "NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.",
- "NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
- "NotificationOptionApplicationUpdateInstalled": "نُصب تحديث التطبيق",
- "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
- "NotificationOptionAudioPlaybackStopped": "أُوقف تشغيل المقطع الصوتي",
- "NotificationOptionCameraImageUploaded": "رُفعت صورة الكاميرا",
- "NotificationOptionInstallationFailed": "فشل في التثبيت",
- "NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
- "NotificationOptionPluginError": "فشل في الملحق",
- "NotificationOptionPluginInstalled": "ثُبتت الملحق",
+ "NameSeasonUnknown": "موسم غير معروف",
+ "NewVersionIsAvailable": "يتوفر إصدار جديد من خادم Jellyfin للتنزيل.",
+ "NotificationOptionApplicationUpdateAvailable": "تحديث التطبيق متاح",
+ "NotificationOptionApplicationUpdateInstalled": "تم تثبيت تحديث التطبيق",
+ "NotificationOptionAudioPlayback": "بدأ تشغيل الصوت",
+ "NotificationOptionAudioPlaybackStopped": "توقف تشغيل الصوت",
+ "NotificationOptionCameraImageUploaded": "تم رفع صورة كاميرا",
+ "NotificationOptionInstallationFailed": "فشل التثبيت",
+ "NotificationOptionNewLibraryContent": "تمت إضافة محتوى جديد",
+ "NotificationOptionPluginError": "خطأ في الملحق",
+ "NotificationOptionPluginInstalled": "تم تثبيت الملحق",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
- "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
- "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
- "NotificationOptionTaskFailed": "فشل في المهمة المجدولة",
- "NotificationOptionUserLockedOut": "تم إقفال حساب المستخدم",
+ "NotificationOptionPluginUpdateInstalled": "تم تحديث الملحق",
+ "NotificationOptionServerRestartRequired": "مطلوب إعادة تشغيل الخادم",
+ "NotificationOptionTaskFailed": "فشل المهمة المجدولة",
+ "NotificationOptionUserLockedOut": "تم قفل حساب المستخدم",
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
- "NotificationOptionVideoPlaybackStopped": "تم إيقاف تشغيل الفيديو",
+ "NotificationOptionVideoPlaybackStopped": "توقف تشغيل الفيديو",
"Photos": "الصور",
"Playlists": "قوائم التشغيل",
"Plugin": "الملحق",
"PluginInstalledWithName": "تم تثبيت {0}",
"PluginUninstalledWithName": "تمت إزالة {0}",
"PluginUpdatedWithName": "تم تحديث {0}",
- "ProviderValue": "المزود: {0}",
- "ScheduledTaskFailedWithName": "فشلت العملية {0}",
- "ScheduledTaskStartedWithName": "تم بدء العملية {0}",
- "ServerNameNeedsToBeRestarted": "يحتاج {0} لإعادة التشغيل",
- "Shows": "العروض",
+ "ProviderValue": "المزوّد: {0}",
+ "ScheduledTaskFailedWithName": "فشلت {0}",
+ "ScheduledTaskStartedWithName": "بدأت {0}",
+ "ServerNameNeedsToBeRestarted": "يحتاج {0} إلى إعادة التشغيل",
+ "Shows": "المسلسلات",
"Songs": "الأغاني",
- "StartupEmbyServerIsLoading": "يتم تحميل خادم Jellyfin . الرجاء المحاولة بعد قليل.",
- "SubtitleDownloadFailureFromForItem": "فشل تحميل الترجمات من {0} ل {1}",
+ "StartupEmbyServerIsLoading": "يتم الآن تحميل خادم Jellyfin. يرجى المحاولة مرة أخرى بعد قليل.",
+ "SubtitleDownloadFailureFromForItem": "فشل تنزيل الترجمات من {0} لـ {1}",
"Sync": "مزامنة",
"System": "النظام",
"TvShows": "البرامج التلفزيونية",
"User": "المستخدم",
"UserCreatedWithName": "تم إنشاء المستخدم {0}",
"UserDeletedWithName": "تم حذف المستخدم {0}",
- "UserDownloadingItemWithValues": "يقوم {0} بتنزيل {1}",
- "UserLockedOutWithName": "تم منع المستخدم {0} من الدخول",
- "UserOfflineFromDevice": "تم قطع اتصال {0} من {1}",
- "UserOnlineFromDevice": "{0} متصل عبر {1}",
- "UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}",
- "UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}",
- "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
- "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
- "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
- "ValueSpecialEpisodeName": "حلقة خاصه - {0}",
+ "UserDownloadingItemWithValues": "{0} يقوم بتنزيل {1}",
+ "UserLockedOutWithName": "تم قفل حساب المستخدم {0}",
+ "UserOfflineFromDevice": "انقطع اتصال {0} من {1}",
+ "UserOnlineFromDevice": "{0} متصل من {1}",
+ "UserPasswordChangedWithName": "تم تغيير كلمة المرور للمستخدم {0}",
+ "UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم لـ {0}",
+ "UserStartedPlayingItemWithValues": "{0} يقوم بتشغيل {1} على {2}",
+ "UserStoppedPlayingItemWithValues": "أنهى {0} تشغيل {1} على {2}",
+ "ValueHasBeenAddedToLibrary": "تمت إضافة {0} إلى مكتبة المحتوى الخاصة بك",
+ "ValueSpecialEpisodeName": "خاص - {0}",
"VersionNumber": "الإصدار {0}",
- "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
- "TaskCleanCache": "حذف الملفات المؤقتة",
+ "TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
+ "TaskCleanCache": "تنظيف مجلد ذاكرة التخزين المؤقت",
"TasksChannelsCategory": "قنوات الإنترنت",
- "TasksLibraryCategory": "مكتبة",
- "TasksMaintenanceCategory": "صيانة",
- "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يُحدث البيانات الوصفية.",
- "TaskRefreshLibrary": "افحص مكتبة الوسائط",
- "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
- "TaskRefreshChapterImages": "استخراج صور الفصل",
- "TasksApplicationCategory": "تطبيق",
- "TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
- "TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة",
- "TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.",
- "TaskRefreshChannels": "إعادة تحديث القنوات",
- "TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.",
- "TaskCleanTranscode": "حذف ما بمجلد الترميز",
- "TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
- "TaskUpdatePlugins": "تحديث الإضافات",
- "TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
- "TaskRefreshPeople": "إعادة تحميل الأشخاص",
- "TaskCleanLogsDescription": "يحذف السجلات الأقدم من {0} يوم.",
- "TaskCleanLogs": "حذف مسار السجل",
- "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
- "TaskCleanActivityLog": "حذف سجل الأنشطة",
- "Default": "افتراضي",
- "Undefined": "غير معرف",
- "Forced": "ملحقة",
- "TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
+ "TasksLibraryCategory": "المكتبة",
+ "TasksMaintenanceCategory": "الصيانة",
+ "TaskRefreshLibraryDescription": "يفحص مكتبة المحتوى الخاصة بك بحثاً عن ملفات جديدة ويحدّث البيانات الوصفية.",
+ "TaskRefreshLibrary": "فحص مكتبة المحتوى",
+ "TaskRefreshChapterImagesDescription": "ينشئ صوراً مصغرة للفيديوهات التي تحتوي على فصول.",
+ "TaskRefreshChapterImages": "استخراج صور الفصول",
+ "TasksApplicationCategory": "التطبيق",
+ "TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت عن الترجمات المفقودة بناءً على إعدادات البيانات الوصفية.",
+ "TaskDownloadMissingSubtitles": "تنزيل الترجمات المفقودة",
+ "TaskRefreshChannelsDescription": "يحدّث معلومات قنوات الإنترنت.",
+ "TaskRefreshChannels": "تحديث القنوات",
+ "TaskCleanTranscodeDescription": "يحذف ملفات تحويل الترميز التي مر عليها أكثر من يوم واحد.",
+ "TaskCleanTranscode": "تنظيف مجلد تحويل الترميز",
+ "TaskUpdatePluginsDescription": "ينزّل ويثبّت التحديثات للملحقات المهيأة للتحديث التلقائي.",
+ "TaskUpdatePlugins": "تحديث الملحقات",
+ "TaskRefreshPeopleDescription": "يحدّث البيانات الوصفية للممثلين والمخرجين في مكتبة المحتوى الخاصة بك.",
+ "TaskRefreshPeople": "تحديث الأشخاص",
+ "TaskCleanLogsDescription": "يحذف ملفات السجل التي يزيد عمرها عن {0} أيام.",
+ "TaskCleanLogs": "تنظيف مجلد السجلات",
+ "TaskCleanActivityLogDescription": "يحذف إدخالات سجل النشاط الأقدم من العمر المحدد.",
+ "TaskCleanActivityLog": "تنظيف سجل النشاط",
+ "Default": "الافتراضي",
+ "Undefined": "غير محدد",
+ "Forced": "إجباري",
+ "TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقلل المساحة الحرة. قد يؤدي تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات على قاعدة البيانات إلى تحسين الأداء.",
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
- "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
- "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
+ "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لإنشاء قوائم تشغيل HLS أكثر دقة. قد يستغرق تشغيل هذه المهمة وقتاً طويلاً.",
+ "TaskKeyframeExtractor": "مستخرج الإطارات الرئيسية",
"External": "خارجي",
- "HearingImpaired": "ضعاف السمع",
- "TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
- "TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
- "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
- "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
- "TaskAudioNormalization": "تسوية الصوت",
- "TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
- "TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
- "TaskDownloadMissingLyricsDescription": "كلمات",
- "TaskExtractMediaSegments": "فحص مقاطع الوسائط",
- "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
- "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
- "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
+ "HearingImpaired": "لضعاف السمع",
+ "TaskRefreshTrickplayImages": "إنشاء صور معاينات التنقل (Trickplay)",
+ "TaskRefreshTrickplayImagesDescription": "ينشئ صور معاينات التنقل السريع للفيديوهات في المكتبات المفعّلة.",
+ "TaskAudioNormalization": "تطبيع الصوت",
+ "TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
+ "TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
+ "TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
+ "TaskExtractMediaSegments": "فحص مقاطع المحتوى",
+ "TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
+ "TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
+ "TaskMoveTrickplayImagesDescription": "ينقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.",
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
- "CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
+ "CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar_SA.json b/Emby.Server.Implementations/Localization/Core/ar_SA.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ar_SA.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 8ef3d9afdc..543d227e73 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -126,8 +126,6 @@
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.",
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
- "TaskCleanCollectionsAndPlaylists": "Ачысціць калекцыі і плэй-лісты",
- "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
"TaskAudioNormalization": "Нармалізацыя гуку",
"TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 054c7357e1..7340180241 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
"TaskDownloadMissingLyrics": "Свали липсващи текстове",
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
- "TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите",
- "TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
"TaskAudioNormalization": "Нормализиране на звука",
"TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
"TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 876773778b..c6cfbe3c67 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -127,8 +127,6 @@
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
"TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
"TaskDownloadMissingLyricsDescription": "গানের জন্য লিরিকস ডাউনলোড করুন",
- "TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
- "TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
"TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
"TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্ট বের করে বা অর্জন করে।",
"TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json
new file mode 100644
index 0000000000..3bf5ebd35a
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/bs.json
@@ -0,0 +1,139 @@
+{
+ "Albums": "Albumi",
+ "Artists": "Umjetnici",
+ "Books": "Knjige",
+ "Channels": "Kanalima",
+ "Collections": "Zbirke",
+ "Default": "Zadano",
+ "Favorites": "Omiljeni",
+ "Folders": "Mape",
+ "Genres": "Žanrovi",
+ "HeaderAlbumArtists": "Umjetnici albuma",
+ "HeaderContinueWatching": "Nastavi gledati",
+ "Movies": "Filmovi",
+ "MusicVideos": "Muzički spotovi",
+ "Photos": "Slike",
+ "Playlists": "Plejliste",
+ "Shows": "Pokazuje",
+ "Songs": "Pjesme",
+ "ValueSpecialEpisodeName": "Posebno - {0}",
+ "AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}",
+ "Application": "Prijava",
+ "AuthenticationSucceededWithUserName": "{0} uspješno autentificirano",
+ "CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}",
+ "ChapterNameValue": "Poglavlje {0}",
+ "DeviceOfflineWithName": "{0} se odspojio",
+ "DeviceOnlineWithName": "{0} je povezan",
+ "External": "Vanjsko",
+ "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}",
+ "Forced": "Prisilno",
+ "HeaderFavoriteAlbums": "Omiljeni albumi",
+ "HeaderFavoriteArtists": "Omiljeni umjetnici",
+ "HeaderFavoriteEpisodes": "Omiljene epizode",
+ "HeaderFavoriteShows": "Omiljene emisije",
+ "HeaderFavoriteSongs": "Omiljene pjesme",
+ "HeaderLiveTV": "TV uživo",
+ "HeaderNextUp": "Slijedi",
+ "HeaderRecordingGroups": "Grupe za snimanje",
+ "HearingImpaired": "Oštećen sluh",
+ "HomeVideos": "Kućni videozapisi",
+ "Inherit": "Nasljedi",
+ "ItemAddedWithName": "{0} je dodan u biblioteku",
+ "ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
+ "LabelIpAddressValue": "IP adresa: {0}",
+ "LabelRunningTimeValue": "Trajanje: {0}",
+ "Latest": "Posljednje dodano",
+ "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
+ "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana",
+ "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
+ "MixedContent": "Miješani sadržaj",
+ "Music": "Muzika",
+ "NameInstallFailed": "{0} instalacija je propala",
+ "NameSeasonNumber": "Sezona {0}",
+ "NameSeasonUnknown": "Sezona nepoznata",
+ "NewVersionIsAvailable": "Dostupna je nova verzija Jellyfin Servera za preuzimanje.",
+ "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
+ "NotificationOptionApplicationUpdateInstalled": "Ažuriranje aplikacije instalirano",
+ "NotificationOptionAudioPlayback": "Pokrenuto je reproduciranje zvuka",
+ "NotificationOptionAudioPlaybackStopped": "Zaustavljeno je reproduciranje zvuka",
+ "NotificationOptionCameraImageUploaded": "Učitana slika s kamere",
+ "NotificationOptionInstallationFailed": "Neuspjeh instalacije",
+ "NotificationOptionNewLibraryContent": "Dodan novi sadržaj",
+ "NotificationOptionPluginError": "Neuspjeh dodatka",
+ "NotificationOptionPluginInstalled": "Dodatak je instaliran",
+ "NotificationOptionPluginUninstalled": "Dodatak je deinstaliran",
+ "NotificationOptionPluginUpdateInstalled": "Ažuriranje dodatka je instalirano",
+ "NotificationOptionServerRestartRequired": "Potreban je ponovni pokret servera",
+ "NotificationOptionTaskFailed": "Neuspjeh zakazane zadatke",
+ "NotificationOptionUserLockedOut": "Korisnik je zaključan",
+ "NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa",
+ "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena",
+ "Plugin": "Plugin",
+ "PluginInstalledWithName": "{0} je instaliran",
+ "PluginUninstalledWithName": "{0} je deinstaliran",
+ "PluginUpdatedWithName": "{0} je ažurirano",
+ "ProviderValue": "Pružatelj: {0}",
+ "ScheduledTaskFailedWithName": "{0} nije uspjelo",
+ "ScheduledTaskStartedWithName": "{0} počelo",
+ "ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti",
+ "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.",
+ "SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}",
+ "Sync": "Sinkronizacija",
+ "System": "Sistem",
+ "TvShows": "TV serije",
+ "Undefined": "Nedefinirano",
+ "User": "Korisnik",
+ "UserCreatedWithName": "Korisnik {0} je kreiran",
+ "UserDeletedWithName": "Korisnik {0} je izbrisan",
+ "UserDownloadingItemWithValues": "{0} preuzima {1}",
+ "UserLockedOutWithName": "Korisnik {0} je zaključan",
+ "UserOfflineFromDevice": "{0} se odspojio od {1}",
+ "UserOnlineFromDevice": "{0} je online od {1}",
+ "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
+ "UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}",
+ "UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}",
+ "UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}",
+ "ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku",
+ "VersionNumber": "Verzija {0}",
+ "TasksMaintenanceCategory": "Održavanje",
+ "TasksLibraryCategory": "Biblioteka",
+ "TasksApplicationCategory": "Prijava",
+ "TasksChannelsCategory": "Internetski kanali",
+ "TaskCleanActivityLog": "Očisti dnevnik aktivnosti",
+ "TaskCleanActivityLogDescription": "Brisanje unosa u dnevnik aktivnosti starijih od konfigurisane starosti.",
+ "TaskCleanCache": "Očistite direktorij keša",
+ "TaskCleanCacheDescription": "Brisanje keš datoteka koje sistemu više nisu potrebne.",
+ "TaskRefreshChapterImages": "Izvadi slike iz poglavlja",
+ "TaskRefreshChapterImagesDescription": "Stvara minijature za videozapise koji imaju poglavlja.",
+ "TaskAudioNormalization": "Normalizacija zvuka",
+ "TaskAudioNormalizationDescription": "Skeneriše datoteke radi podataka za normalizaciju zvuka.",
+ "TaskRefreshLibrary": "Skenerisati medijsku biblioteku",
+ "TaskRefreshLibraryDescription": "Skenerira vašu medijsku biblioteku na nove datoteke i osvježava metapodatke.",
+ "TaskCleanLogs": "Očisti direktorij dnevnika",
+ "TaskCleanLogsDescription": "Brisanje dnevničkih datoteka starijih od {0} dana.",
+ "TaskRefreshPeople": "Osvježite ljude",
+ "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i režisere u vašoj medijskoj biblioteci.",
+ "TaskRefreshTrickplayImages": "Generirajte Trickplay slike",
+ "TaskRefreshTrickplayImagesDescription": "Stvara pregled trik-igara za videozapise u omogućenim bibliotekama.",
+ "TaskUpdatePlugins": "Ažuriraj dodatke",
+ "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja dodataka koji su konfigurisani da se automatski ažuriraju.",
+ "TaskCleanTranscode": "Očisti Transcode direktorij",
+ "TaskCleanTranscodeDescription": "Brisanje transkodiranih datoteka starijih od jednog dana.",
+ "TaskRefreshChannels": "Osvježi kanale",
+ "TaskRefreshChannelsDescription": "Osvježava informacije o internetskom kanalu.",
+ "TaskDownloadMissingLyrics": "Preuzmi nedostajuće tekstove",
+ "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
+ "TaskDownloadMissingSubtitles": "Preuzmite nedostajuće titlove",
+ "TaskDownloadMissingSubtitlesDescription": "Pretražuje internet u potrazi za nedostajućim titlovima na osnovu konfiguracije metapodataka.",
+ "TaskOptimizeDatabase": "Optimizirajte bazu podataka",
+ "TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.",
+ "TaskKeyframeExtractor": "Izvađač ključnih sličica",
+ "TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.",
+ "TaskExtractMediaSegments": "Analiza medijskog segmenta",
+ "TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.",
+ "TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
+ "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
+ "CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
+}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 1e7279be83..f9543e6f4c 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -63,8 +63,8 @@
"Photos": "Fotos",
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
- "PluginInstalledWithName": "{0} ha estat instal·lat",
- "PluginUninstalledWithName": "S'ha instal·lat {0}",
+ "PluginInstalledWithName": "S'ha instal·lat {0}",
+ "PluginUninstalledWithName": "S'ha desinstal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
@@ -126,8 +126,6 @@
"HearingImpaired": "Discapacitat auditiva",
"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 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",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 4d2477044f..8d43839110 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
- "TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
- "TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
"TaskAudioNormalization": "Normalizace zvuku",
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.",
"TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni",
diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json
index d9ebd13f07..af0e89bf80 100644
--- a/Emby.Server.Implementations/Localization/Core/cy.json
+++ b/Emby.Server.Implementations/Localization/Core/cy.json
@@ -130,7 +130,5 @@
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
- "TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae",
- "TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.",
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 8b0d8745dc..7d905f3300 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Hørehæmmet",
"TaskRefreshTrickplayImages": "Generer trickplay-billeder",
"TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
- "TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
"TaskAudioNormalization": "Lydnormalisering",
"TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index e9a1630d9d..ab1a7d2cbd 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -19,7 +19,7 @@
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblingsinterpreten",
- "HeaderFavoriteEpisodes": "Lieblingsepisoden",
+ "HeaderFavoriteEpisodes": "Lieblingsfolgen",
"HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingssongs",
"HeaderLiveTV": "Live TV",
@@ -126,8 +126,6 @@
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
"TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
- "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
- "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",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 87362ff8e0..0443207ea6 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
- "TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
- "TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον.",
"TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
"TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
"TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index bd5be0b1fc..b0094e33c3 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Hearing Impaired",
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
- "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
"TaskAudioNormalization": "Audio Normalisation",
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data.",
"TaskDownloadMissingLyrics": "Download missing lyrics",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index c09d5af96c..45b1cbb6a0 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -130,8 +130,6 @@
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
"TaskKeyframeExtractor": "Keyframe Extractor",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
- "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
"TaskExtractMediaSegments": "Media Segment Scan",
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 2bbf0d5140..7fda507797 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
- "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
- "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskDownloadMissingLyrics": "Descargar letra faltante",
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
"TaskExtractMediaSegments": "Escanear Segmentos de Media",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 6748fff4cc..d03d3ed2ff 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
- "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
- "TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskDownloadMissingLyrics": "descargar letras que faltan",
"TaskDownloadMissingLyricsDescription": "Descargar letras de canciones",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index b9c57afe6d..cf118077c6 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Discapacidad Auditiva",
"TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
"TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas.",
- "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
- "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
"TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index 34c68c33bb..dec82b73e3 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -127,9 +127,7 @@
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
"TaskAudioNormalization": "Normalización de audio",
- "TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
- "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
"TaskDownloadMissingLyrics": "Descargar letra faltante",
"TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index f98a5e5b2c..8d991fa74a 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -41,8 +41,6 @@
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
- "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
- "TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
"TvShows": "Series de TV",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"TaskRefreshChannels": "Actualizar canales",
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index 91a0aa6639..d751e35af2 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -128,13 +128,11 @@
"TaskRefreshTrickplayImagesDescription": "Loob trickplay eelvaated videotele lubatud meediakogudes.",
"TaskAudioNormalization": "Normaliseeri helitugevus",
"TaskAudioNormalizationDescription": "Otsib failidest helitugevuse normaliseerimise teavet.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest üksused, mida enam ei eksisteeri.",
- "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid",
"TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad",
"TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine",
"TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.",
- "TaskExtractMediaSegments": "Skaneeri meediasegmente",
- "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.",
+ "TaskExtractMediaSegments": "Skaneeri meedialõike",
+ "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed",
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index c9a798cacd..9e1390484f 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -130,8 +130,6 @@
"TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
"TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
"TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
- "TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.",
- "TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak",
"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.",
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 90cd3a58e9..435485db7c 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -126,8 +126,6 @@
"HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه.",
- "TaskCleanCollectionsAndPlaylists": "پاکسازی مجموعه ها و لیست پخش",
- "TaskCleanCollectionsAndPlaylistsDescription": "موارد را از مجموعه ها و لیست پخش هایی که دیگر وجود ندارند حذف میکند.",
"TaskAudioNormalizationDescription": "بررسی فایل برای داده‌های نرمال کردن صدا.",
"TaskDownloadMissingLyrics": "دانلود متن‌های ناموجود",
"TaskDownloadMissingLyricsDescription": "دانلود متن شعر‌ها",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 15a04d22cd..d3237db8b0 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -39,8 +39,8 @@
"Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
"Books": "Kirjat",
- "AuthenticationSucceededWithUserName": "{0} on todennettu",
- "Artists": "Esittäjät",
+ "AuthenticationSucceededWithUserName": "{0} todennus onnistunut",
+ "Artists": "Artistit",
"Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
"Albums": "Albumit",
@@ -126,8 +126,6 @@
"HearingImpaired": "Kuulorajoitteinen",
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
- "TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
"TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
index 40aa5f71a4..044abc7fa3 100644
--- a/Emby.Server.Implementations/Localization/Core/fo.json
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -14,5 +14,9 @@
"DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
- "Forced": "Kravt"
+ "Forced": "Kravt",
+ "FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
+ "HeaderFavoriteEpisodes": "Yndispartar",
+ "HeaderFavoriteSongs": "Yndissangir",
+ "LabelIpAddressValue": "IP atsetur: {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index a8964e8b62..b05e0d10b4 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
- "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
- "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskExtractMediaSegments": "Analyse des segments de média",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index b2a2e502ab..8937b1d0c9 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
- "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
- "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.",
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json
index 8c0ae8922a..5742e6224d 100644
--- a/Emby.Server.Implementations/Localization/Core/ga.json
+++ b/Emby.Server.Implementations/Localization/Core/ga.json
@@ -29,12 +29,10 @@
"TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
"TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú",
"TaskKeyframeExtractorDescription": "Baintear eochairfhrámaí as comhaid físe chun seinmliostaí HLS níos cruinne a chruthú. Féadfaidh an tasc seo a bheith ar siúl ar feadh i bhfad.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Baintear míreanna as bailiúcháin agus seinmliostaí nach ann dóibh a thuilleadh.",
"TaskDownloadMissingLyricsDescription": "Íosluchtaigh liricí do na hamhráin",
"TaskUpdatePluginsDescription": "Íoslódálann agus suiteálann nuashonruithe do bhreiseáin atá cumraithe le nuashonrú go huathoibríoch.",
"TaskDownloadMissingSubtitlesDescription": "Déanann sé cuardach ar an idirlíon le haghaidh fotheidil atá ar iarraidh bunaithe ar chumraíocht meiteashonraí.",
"TaskExtractMediaSegmentsDescription": "Sliocht nó faigheann codanna meán ó bhreiseáin chumasaithe MediaSegment.",
- "TaskCleanCollectionsAndPlaylists": "Glan suas bailiúcháin agus seinmliostaí",
"TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
"TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
"AppDeviceValues": "Aip: {0}, Gléas: {1}",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index b3f137febd..fc5c3fd53d 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Crea miniaturas de previsualización para os vídeos nas bibliotecas habilitadas.",
"TaskDownloadMissingLyrics": "Descargar letras que faltan",
"TaskDownloadMissingLyricsDescription": "Descarga as letras das cancións",
- "TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
- "TaskCleanCollectionsAndPlaylistsDescription": "Quita ítems que xa non existen das coleccións e listas de reprodución.",
"TaskExtractMediaSegmentsDescription": "Procura segmentos de medios cos plugins habilitados.",
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
"TaskMoveTrickplayImages": "Migrar as miniaturas de previsualización a outra ubicación",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index ef95a639f6..606f464503 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -127,9 +127,7 @@
"TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
"TaskAudioNormalization": "נרמול שמע",
- "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
- "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 80db975ccb..9968c56b21 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -127,14 +127,12 @@
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
"TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
- "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
+ "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें।",
"TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
"TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है",
"TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन",
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
- "TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।",
- "TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें",
- "CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।"
+ "CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 94db435715..e3bea78a3f 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -23,7 +23,7 @@
"HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
- "HeaderNextUp": "Slijedi",
+ "HeaderNextUp": "Sljedeće na redu",
"HeaderRecordingGroups": "Grupa snimka",
"HomeVideos": "Kućni video",
"Inherit": "Naslijedi",
@@ -73,10 +73,10 @@
"Shows": "Emisije",
"Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
- "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
+ "SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}",
"Sync": "Sinkronizacija",
"System": "Sustav",
- "TvShows": "Serije",
+ "TvShows": "TV emisije",
"User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan",
@@ -88,26 +88,26 @@
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
- "ValueSpecialEpisodeName": "Posebno - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija",
+ "ValueSpecialEpisodeName": "Posebno – {0}",
"VersionNumber": "Verzija {0}",
- "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
- "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
+ "TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.",
+ "TaskRefreshLibrary": "Skeniraj biblioteku medija",
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
"TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje",
- "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
- "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
+ "TaskDownloadMissingSubtitlesDescription": "Pretraži internet za nedsotajućim titlovima ne osnovi konfiguracije metapodataka.",
+ "TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove",
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale",
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
"TaskCleanTranscode": "Očisti mapu transkodiranja",
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke",
- "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
+ "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u biblioteci medija.",
"TaskRefreshPeople": "Osvježi osobe",
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
@@ -119,7 +119,7 @@
"Forced": "Forsirani",
"Default": "Zadano",
"TaskOptimizeDatabase": "Optimiziraj bazu podataka",
- "External": "Vanjski",
+ "External": "Eksterni",
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
@@ -128,14 +128,12 @@
"TaskRefreshTrickplayImagesDescription": "Stvara preglede brzog pregledavanja za videa u aktiviranim bibliotekama.",
"TaskAudioNormalization": "Normalizacija zvuka",
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
- "TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju",
"TaskExtractMediaSegments": "Skeniranje dijelova medija",
"TaskDownloadMissingLyrics": "Preuzmi tekstove koji nedostaju",
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
- "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.",
+ "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json
index f927d3173a..183c422a85 100644
--- a/Emby.Server.Implementations/Localization/Core/ht.json
+++ b/Emby.Server.Implementations/Localization/Core/ht.json
@@ -58,5 +58,8 @@
"ValueSpecialEpisodeName": "Spesyal - {0}",
"VersionNumber": "Vesyon {0}",
"TasksApplicationCategory": "Aplikasyon",
- "TasksMaintenanceCategory": "Antretyen"
+ "TasksMaintenanceCategory": "Antretyen",
+ "AppDeviceValues": "Aplikasyon: {0}, Aparèy: {1}",
+ "AuthenticationSucceededWithUserName": "{0} otantifye avèk siksè",
+ "CameraImageUploadedFrom": "Une nouvelle image de la caméra a été téléchargée depuis {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 7d72c1f304..8d9e5b08ba 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -127,9 +127,7 @@
"TaskRefreshTrickplayImages": "Trickplay képek előállítása",
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
"TaskAudioNormalization": "Hangerő-normalizálás",
- "TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
"TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.",
- "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása",
"TaskExtractMediaSegments": "Médiaszegmens felismerése",
"TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése",
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 2a42816852..fb228baf40 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
"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.",
"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.",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 6f94df9d79..900502ccdd 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
"TaskAudioNormalization": "Hljóðstöðlun",
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
- "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",
"TaskExtractMediaSegments": "Skönnun efnishluta",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index f0c4b50270..782f5ce53d 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Non udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
- "TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette",
- "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index bdca8ae1cd..7b0bdb296f 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -126,10 +126,8 @@
"HearingImpaired": "聴覚障害の方",
"TaskRefreshTrickplayImages": "トリックプレー画像を生成",
"TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。",
- "TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ",
"TaskAudioNormalization": "音声の正規化",
"TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。",
- "TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。",
"TaskDownloadMissingLyricsDescription": "歌詞をダウンロード",
"TaskExtractMediaSegments": "メディアセグメントを読み取る",
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json
index 2d02522fea..4f291e466b 100644
--- a/Emby.Server.Implementations/Localization/Core/ka.json
+++ b/Emby.Server.Implementations/Localization/Core/ka.json
@@ -9,46 +9,46 @@
"Artists": "არტისტი",
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
"Books": "წიგნები",
- "Forced": "ძალით",
+ "Forced": "იძულებითი",
"Inherit": "მემკვიდრეობით",
"Latest": "უახლესი",
"Movies": "ფილმები",
"Music": "მუსიკა",
"Photos": "ფოტოები",
"Playlists": "დასაკრავი სიები",
- "Plugin": "დამატება",
+ "Plugin": "მოდული",
"Shows": "სერიალები",
"Songs": "სიმღერები",
"Sync": "სინქრონიზაცია",
"System": "სისტემა",
- "Undefined": "აღუწერელი",
+ "Undefined": "განუსაზღვრელი",
"User": "მომხმარებელი",
"TasksMaintenanceCategory": "რემონტი",
"TasksLibraryCategory": "ბიბლიოთეკა",
"ChapterNameValue": "თავი {0}",
"HeaderContinueWatching": "ყურების გაგრძელება",
"HeaderFavoriteArtists": "რჩეული შემსრულებლები",
- "DeviceOfflineWithName": "{0} გაითიშა",
+ "DeviceOfflineWithName": "{0} გამოეთიშა",
"External": "გარე",
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
"HeaderFavoriteSongs": "რჩეული სიმღერები",
"HeaderRecordingGroups": "ჩამწერი ჯგუფები",
"HearingImpaired": "სმენადაქვეითებული",
- "LabelRunningTimeValue": "გაშვებულობის დრო: {0}",
+ "LabelRunningTimeValue": "ხანგრძლივობა: {0}",
"MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
"MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
"MixedContent": "შერეული შემცველობა",
- "MusicVideos": "მუსიკის ვიდეოები",
+ "MusicVideos": "მუსიკალური ვიდეოები",
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
"PluginUninstalledWithName": "{0} წაიშალა",
- "ScheduledTaskStartedWithName": "{0} გაეშვა",
+ "ScheduledTaskStartedWithName": "{0} დაიწყო",
"VersionNumber": "ვერსია {0}",
"TasksChannelsCategory": "ინტერნეტ-არხები",
- "ValueSpecialEpisodeName": "სპეციალური - {0}",
+ "ValueSpecialEpisodeName": "დამატებითი - {0}",
"TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
"Channels": "არხები",
"Collections": "კოლექციები",
@@ -56,31 +56,31 @@
"Favorites": "რჩეულები",
"Folders": "საქაღალდეები",
"HeaderFavoriteShows": "რჩეული სერიალები",
- "HeaderLiveTV": "ცოცხალი TV",
- "HeaderNextUp": "შემდეგი ზემოთ",
+ "HeaderLiveTV": "ლაივ ტელევიზია",
+ "HeaderNextUp": "შემდეგი",
"HomeVideos": "სახლის ვიდეოები",
"NameSeasonNumber": "სეზონი {0}",
"NameSeasonUnknown": "სეზონი უცნობია",
- "NotificationOptionPluginError": "დამატების შეცდომა",
- "NotificationOptionPluginInstalled": "დამატება დაყენებულია",
- "NotificationOptionPluginUninstalled": "დამატება წაიშალა",
+ "NotificationOptionPluginError": "მოდულის შეცდომა",
+ "NotificationOptionPluginInstalled": "მოდული დაყენებულია",
+ "NotificationOptionPluginUninstalled": "მოდული წაიშალა",
"ProviderValue": "მომწოდებელი: {0}",
- "ScheduledTaskFailedWithName": "{0} ავარიულია",
- "TvShows": "TV სერიალები",
+ "ScheduledTaskFailedWithName": "{0} ვერ შესრულდა",
+ "TvShows": "სატელევიზიო სერიალები",
"TaskRefreshPeople": "ხალხის განახლება",
- "TaskUpdatePlugins": "დამატებების განახლება",
+ "TaskUpdatePlugins": "მოდულების განახლება",
"TaskRefreshChannels": "არხების განახლება",
- "TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია",
+ "TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია",
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
- "DeviceOnlineWithName": "{0} შეერთებულია",
+ "DeviceOnlineWithName": "{0} დაკავშირდა",
"LabelIpAddressValue": "IP მისამართი: {0}",
"NameInstallFailed": "{0}-ის დაყენების შეცდომა",
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
- "NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია",
- "NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია",
- "NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა",
+ "NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია",
+ "NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა",
+ "NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა",
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
"PluginInstalledWithName": "{0} დაყენებულია",
@@ -91,39 +91,49 @@
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
- "TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა",
- "UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს",
- "FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა",
+ "TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა",
+ "UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს",
+ "FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან",
"MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
"MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
"ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
"UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
- "UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან",
- "UserOfflineFromDevice": "{0}-ი {1}-დან გაითიშა",
+ "UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან",
+ "UserOfflineFromDevice": "{0}-ი {1}-დან გაეთიშა",
"ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
"ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
- "UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე",
- "UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია",
+ "UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე",
+ "UserPasswordChangedWithName": "მომხმარებელი {0}-სთვის პაროლი შეიცვალა",
"UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
- "UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე",
+ "UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე",
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
"CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
- "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა",
+ "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა",
"ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
- "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.",
- "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.",
- "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.",
+ "TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებს.",
+ "TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებს.",
+ "TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენს მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებს.",
"TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.",
"TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.",
- "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
+ "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული მოდულების განახლებების გადმოწერა და დაყენება.",
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
- "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
- "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
- "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
- "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
+ "TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.",
+ "TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
+ "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.",
+ "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება",
+ "TaskAudioNormalization": "აუდიოს ნორმალიზება",
+ "TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
+ "TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
+ "TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
+ "TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
+ "TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
+ "TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
+ "TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.",
+ "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება",
+ "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ."
}
diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json
index 5d10975f32..c40b96cf24 100644
--- a/Emby.Server.Implementations/Localization/Core/km.json
+++ b/Emby.Server.Implementations/Localization/Core/km.json
@@ -127,7 +127,5 @@
"TaskUpdatePluginsDescription": "ទាញយក និងដំឡើងបច្ចុប្បន្នភាពសម្រាប់Plugins ដែលត្រូវបាន Config ដើម្បីធ្វើបច្ចុប្បន្នភាពដោយស្វ័យប្រវត្តិ.",
"TaskCleanTranscodeDescription": "លុបឯកសារ Transcode ដែលលើសពីមួយថ្ងៃ.",
"TaskDownloadMissingSubtitlesDescription": "ស្វែងរកតាមអ៊ីនធឺណិត សម្រាប់សាប់ថាយថល ដែលបាត់ដោយផ្អែកលើ metadata.",
- "TaskOptimizeDatabase": "ធ្វើឱ្យ Database ប្រសើរឡើង",
- "TaskCleanCollectionsAndPlaylistsDescription": "លុបរបស់របរចេញពីបណ្តុំ និងបញ្ជីចាក់ដែលលែងមាន.",
- "TaskCleanCollectionsAndPlaylists": "សម្អាតបណ្តុំ និងបញ្ជីចាក់"
+ "TaskOptimizeDatabase": "ធ្វើឱ្យ Database ប្រសើរឡើង"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 9f49be53b4..0850600588 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -129,7 +129,5 @@
"TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
"TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
"TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
- "TaskRefreshTrickplayImages": "ಟ್ರಿಕ್‌ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
- "TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
- "TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
+ "TaskRefreshTrickplayImages": "ಟ್ರಿಕ್‌ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 2b24ea2c8b..0451dcc9f0 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -124,12 +124,10 @@
"TaskKeyframeExtractor": "키프레임 추출",
"External": "외부",
"HearingImpaired": "청각 장애",
- "TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리",
"TaskAudioNormalization": "오디오의 볼륨 수준을 일정하게 조정",
"TaskAudioNormalizationDescription": "오디오의 볼륨 수준을 일정하게 조정하기 위해 파일을 스캔합니다.",
"TaskRefreshTrickplayImages": "비디오 탐색용 미리보기 썸네일 생성",
"TaskRefreshTrickplayImagesDescription": "활성화된 라이브러리에서 비디오의 트릭플레이 미리보기를 생성합니다.",
- "TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다.",
"TaskExtractMediaSegments": "미디어 세그먼트 스캔",
"TaskExtractMediaSegmentsDescription": "MediaSegment를 지원하는 플러그인에서 미디어 세그먼트를 추출하거나 가져옵니다.",
"TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
index 336d286fc4..613d531103 100644
--- a/Emby.Server.Implementations/Localization/Core/kw.json
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -128,9 +128,7 @@
"TaskOptimizeDatabaseDescription": "Y hwra kesstrotha ha berrhe efander rydh. Martesen y hwra gwellhe gwryth mar kwre'ta an oberen ma wosa ty dhe arhwilas an lyverva, po neb chanj aral neb a brof chanjyansow selvanylyon.",
"TaskAudioNormalizationDescription": "Y hwra arhwilas restrennow rag manylyon normalheans klewans.",
"TaskRefreshLibraryDescription": "Y hwra arhwilas dha lyverva media rag restrennow nowydh ha disegha metamanylyon.",
- "TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari",
"TaskKeyframeExtractor": "Estennell Framalhwedh",
- "TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.",
"TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir.",
"TaskExtractMediaSegments": "Arhwilas Rann Media",
"TaskExtractMediaSegmentsDescription": "Kavos rannow media a-dhyworth ystynansow gallosegys MediaSegment.",
diff --git a/Emby.Server.Implementations/Localization/Core/lb.json b/Emby.Server.Implementations/Localization/Core/lb.json
index 176f2ba2b7..2afec05dbd 100644
--- a/Emby.Server.Implementations/Localization/Core/lb.json
+++ b/Emby.Server.Implementations/Localization/Core/lb.json
@@ -104,8 +104,6 @@
"TaskDownloadMissingSubtitles": "Fehlend Ënnertitelen eroflueden",
"TaskOptimizeDatabase": "Datebank optiméieren",
"TaskKeyframeExtractor": "Schlësselbild Extrakter",
- "TaskCleanCollectionsAndPlaylists": "Sammlungen a Playlisten botzen",
- "TaskCleanCollectionsAndPlaylistsDescription": "Ewechhuele vun Elementer aus Sammlungen a Playlisten, déi net méi existéieren.",
"TaskExtractMediaSegments": "Mediesegment-Scan",
"NewVersionIsAvailable": "Nei Versioun fir Jellyfin Server ass verfügbar.",
"CameraImageUploadedFrom": "En neit Kamera Bild gouf vu {0} eropgelueden",
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index bdf63b4ca0..daff719ea7 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
- "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",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 55549c66d0..1083e3c299 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -127,9 +127,7 @@
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
"TaskAudioNormalization": "Audio normalizācija",
- "TaskCleanCollectionsAndPlaylistsDescription": "Noņem vairs neeksistējošus vienumus no kolekcijām un atskaņošanas sarakstiem.",
"TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
- "TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus",
"TaskExtractMediaSegments": "Multivides segmenta skenēšana",
"TaskExtractMediaSegmentsDescription": "Izvelk vai iegūst multivides segmentus no MediaSegment iespējotiem spraudņiem.",
"TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index 6da31227d7..efef194e14 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -124,13 +124,15 @@
"TaskCleanActivityLog": "Избриши Лог на Активности",
"External": "Надворешен",
"HearingImpaired": "Оштетен слух",
- "TaskCleanCollectionsAndPlaylists": "Исчисти ги колекциите и плејлистите",
"TaskAudioNormalizationDescription": "Скенирање датотеки за податоци за нормализација на звукот.",
"TaskDownloadMissingLyrics": "Преземи стихови кои недостасуваат",
"TaskDownloadMissingLyricsDescription": "Преземи стихови/текстови за песни",
"TaskRefreshTrickplayImages": "Генерирај слики за прегледување (Trickplay)",
"TaskAudioNormalization": "Нормализација на звукот",
"TaskRefreshTrickplayImagesDescription": "Креира трикплеј прегледи за видеа во овозможените библиотеки.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат.",
- "TaskExtractMediaSegments": "Скенирање на сегменти на содржина"
+ "TaskExtractMediaSegments": "Скенирање на сегменти на содржина",
+ "TaskMoveTrickplayImages": "Мигрирај ја локацијата на сликата од Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Ги преместува постоечките датотеки за трикплеј според поставките на библиотеката.",
+ "CleanupUserDataTask": "Задача за чистење на кориснички податоци",
+ "CleanupUserDataTaskDescription": "Ги чисти сите кориснички податоци (состојба на гледање, статус на омилени итн.) од медиуми што повеќе не се присутни најмалку 90 дена."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 8c20ded3ac..5f098bccac 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -124,8 +124,6 @@
"External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ",
- "TaskCleanCollectionsAndPlaylistsDescription": "നിലവിലില്ലാത്ത ശേഖരങ്ങളിൽ നിന്നും പ്ലേലിസ്റ്റുകളിൽ നിന്നും ഇനങ്ങൾ നീക്കംചെയ്യുന്നു.",
- "TaskCleanCollectionsAndPlaylists": "ശേഖരങ്ങളും പ്ലേലിസ്റ്റുകളും വൃത്തിയാക്കുക",
"TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക",
"TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.",
"TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക",
diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json
index a684ff2041..63f4d0cef3 100644
--- a/Emby.Server.Implementations/Localization/Core/mn.json
+++ b/Emby.Server.Implementations/Localization/Core/mn.json
@@ -27,7 +27,6 @@
"NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
"NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов",
"UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө",
- "TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх",
"ScheduledTaskFailedWithName": "{0} амжилтгүй",
"StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.",
"TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх",
@@ -44,7 +43,6 @@
"NotificationOptionAudioPlayback": "Дууг тоглууллаа",
"TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх",
"TaskUpdatePlugins": "Plugin-уудыг шинэчлэх",
- "TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.",
"TaskAudioNormalization": "Аудиог хэвшүүлэх",
"TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.",
"TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.",
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 727bbee168..267222ecbe 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -126,7 +126,6 @@
"HearingImpaired": "कर्णबधीर",
"TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
"TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.",
- "TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा",
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
@@ -135,7 +134,6 @@
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो",
"TaskExtractMediaSegmentsDescription": "सक्रिय असलेल्या प्लगिनमधून मीडिया विभाग प्राप्त करते.",
"TaskMoveTrickplayImagesDescription": "लायब्ररीच्या सेटिंग्जप्रमाणे आधीपासून अस्तित्वात असलेल्या ट्रिकप्ले फाइल्सचे स्थान बदलते.",
- "TaskCleanCollectionsAndPlaylistsDescription": "जे संग्रह आणि प्लेलिस्ट आता अस्तित्वात नाहीत, त्यांमधील घटक हटवते.",
"CleanupUserDataTask": "वापरकर्ता डेटाची स्वच्छता प्रक्रिया",
"CleanupUserDataTaskDescription": "९० दिवसांहून अधिक काळ अनुपस्थित असलेल्या माध्यमांवरील सर्व वापरकर्ता माहिती (जसे पाहण्याची स्थिती, आवडी इ.) हटवते."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 2be04be80a..743c14ac10 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -132,10 +132,8 @@
"TaskDownloadMissingLyrics": "Muat turun lirik yang hilang",
"TaskDownloadMissingLyricsDescription": "Memuat turun lirik-lirik untuk lagu-lagu",
"TaskMoveTrickplayImages": "Alih Lokasi Imej Trickplay",
- "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.",
"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/mt.json b/Emby.Server.Implementations/Localization/Core/mt.json
index f7501ab404..aa3029a262 100644
--- a/Emby.Server.Implementations/Localization/Core/mt.json
+++ b/Emby.Server.Implementations/Localization/Core/mt.json
@@ -128,8 +128,6 @@
"TaskOptimizeDatabase": "Ottimiżża d-database",
"TaskKeyframeExtractor": "Estrattur ta' Keyframes",
"TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-videos biex jagħmel playlists HLS aktar preċiżi. Dan it-task jista' jdum żmien twil biex ilesti.",
- "TaskCleanCollectionsAndPlaylists": "Naddaf il-kollezzjonijiet u l-playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu.",
"TaskDownloadMissingLyrics": "Niżżel il-lirika nieqsa",
"TaskDownloadMissingLyricsDescription": "Iniżżel il-lirika għal-kanzunetti",
"TaskExtractMediaSegments": "Scan tas-Sezzjoni tal-Midja",
diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json
index 097d0d2fba..f2cd501076 100644
--- a/Emby.Server.Implementations/Localization/Core/my.json
+++ b/Emby.Server.Implementations/Localization/Core/my.json
@@ -122,10 +122,8 @@
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
"External": "ပြင်ပ",
"TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
- "TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။",
"TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
"TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
- "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
"HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ",
"TaskDownloadMissingLyrics": "ကျန်နေသောသီချင်းစာသားများအား ဒေါင်းလုတ်ဆွဲပါ",
"TaskDownloadMissingLyricsDescription": "သီချင်းများအတွက် သီချင်းစာသား ဒေါင်းလုတ်ဆွဲပါ"
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index cd03157200..351b238f9d 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -126,10 +126,8 @@
"HearingImpaired": "Hørselshemmet",
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
"TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.",
- "TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
"TaskAudioNormalization": "Lydnormalisering",
"TaskAudioNormalizationDescription": "Skan filer for lydnormaliserende data.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes.",
"TaskDownloadMissingLyrics": "Last ned manglende tekster",
"TaskDownloadMissingLyricsDescription": "Last ned sangtekster",
"TaskExtractMediaSegments": "Skann mediasegment",
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 7c6b08fb36..0e52e32c1b 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -45,7 +45,7 @@
"Genres": "विधाहरू",
"Folders": "फोल्डरहरू",
"Favorites": "मनपर्ने",
- "FailedLoginAttemptWithUserName": "{0}को लग इन प्रयास असफल",
+ "FailedLoginAttemptWithUserName": "असफल लग इन प्रयास {0} देखि",
"DeviceOnlineWithName": "{0}को साथ जडित",
"DeviceOfflineWithName": "{0}बाट विच्छेदन भयो",
"Collections": "संग्रह",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index dbbe2cbd08..bf1cbdacd1 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,5 +1,4 @@
{
- "Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Applicatie",
"Artists": "Artiesten",
@@ -14,9 +13,8 @@
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"Folders": "Mappen",
- "Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
- "HeaderContinueWatching": "Verder kijken",
+ "HeaderContinueWatching": "Verderkijken",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
@@ -26,7 +24,7 @@
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
"HomeVideos": "Homevideo's",
- "Inherit": "Erven",
+ "Inherit": "Overnemen",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
"LabelIpAddressValue": "IP-adres: {0}",
@@ -116,7 +114,7 @@
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
- "Forced": "Gedwongen",
+ "Forced": "Geforceerd",
"Default": "Standaard",
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
"TaskOptimizeDatabase": "Database optimaliseren",
@@ -126,8 +124,6 @@
"HearingImpaired": "Slechthorenden",
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
- "TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
- "TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
"TaskAudioNormalization": "Geluidsnormalisatie",
"TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.",
"TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden",
@@ -137,5 +133,7 @@
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
"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"
+ "CleanupUserDataTask": "Opruimtaak gebruikersdata",
+ "Albums": "Albums",
+ "Genres": "Genres"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index ced9204b46..b00291ccb4 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -131,8 +131,6 @@
"TaskDownloadMissingLyrics": "ਅਧੂਰੇ ਬੋਲ ਡਾਊਨਲੋਡ ਕਰੋ",
"TaskDownloadMissingLyricsDescription": "ਗੀਤਾਂ ਲਈ ਡਾਊਨਲੋਡ ਕਿਤੇ ਬੋਲ",
"TaskKeyframeExtractor": "ਕੀ-ਫ੍ਰੇਮ ਐਕਸਟ੍ਰੈਕਟਰ",
- "TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
- "TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
"TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
"TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।",
"TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index f1c19ac1d9..a741fc14c0 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Niedosłyszący",
"TaskRefreshTrickplayImages": "Generuj obrazy Trickplay",
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy Trickplay dla filmów we włączonych bibliotekach.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
- "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
"TaskAudioNormalization": "Normalizacja dźwięku",
"TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.",
"TaskDownloadMissingLyrics": "Pobierz brakujące słowa",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 8e76c6c63c..99f76c953f 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Deficiência Auditiva",
"TaskRefreshTrickplayImages": "Gerar imagens Trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado.",
- "TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio.",
"TaskDownloadMissingLyricsDescription": "Baixar letras para músicas",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index c2ce2ba40f..1d31efcdc9 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
- "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 9ae346e253..82da1f0aff 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -126,8 +126,6 @@
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
- "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
"TaskAudioNormalization": "Normalização de áudio",
"TaskDownloadMissingLyrics": "Transferir letra em falta",
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index bf71c5afa1..30214218f8 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -128,8 +128,6 @@
"TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate.",
"TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.",
"TaskAudioNormalization": "Normalizare sunet",
- "TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare",
- "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare.",
"TaskExtractMediaSegments": "Scanează segmentele media",
"TaskMoveTrickplayImagesDescription": "Mută fișierele trickplay existente conform setărilor librăriei.",
"TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 03bce0ebdf..38920b6ede 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Для слабослышащих",
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
- "TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
- "TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
"TaskAudioNormalization": "Нормализация звука",
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука.",
"TaskDownloadMissingLyrics": "Загрузить недостающий текст",
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 7c8d860476..184e9b0a5c 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Sluchovo postihnutí",
"TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
"TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.",
- "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
- "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.",
"TaskAudioNormalization": "Normalizácia zvuku",
"TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku.",
"TaskExtractMediaSegments": "Skenovanie segmentov médií",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 7c7c88e28a..35c5b4a914 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -132,10 +132,8 @@
"TaskMoveTrickplayImages": "Preseli lokacijo Trickplay slik",
"TaskDownloadMissingLyrics": "Prenesi manjkajoča besedila pesmi",
"TaskDownloadMissingLyricsDescription": "Prenesi besedila za pesmi",
- "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č.",
"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 8f0079c94f..5a284e20b9 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -132,8 +132,6 @@
"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.",
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 76a136cf56..52f4124657 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -125,12 +125,10 @@
"TaskKeyframeExtractor": "Екстрактор кључних сличица",
"HearingImpaired": "ослабљен слух",
"TaskAudioNormalization": "Нормализација звука",
- "TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте",
"TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука.",
"TaskRefreshTrickplayImages": "Направи сличице за визуелно премотавање",
"TaskRefreshTrickplayImagesDescription": "Прављење сличица које помажу код визуелног премотавања видео-снимака.",
"TaskDownloadMissingLyrics": "Преузми стихове који недостају",
- "TaskCleanCollectionsAndPlaylistsDescription": "Уклања ставке које више не постоје из колекција и плејлиста.",
"TaskExtractMediaSegments": "Скенирај сегменте медија",
"TaskExtractMediaSegmentsDescription": "Извлачи или добавља сегменте медија у додацима који раде са MediaSegment-ом.",
"TaskMoveTrickplayImagesDescription": "Премешта постојеће сличице за визуелно премотавање сходно подешавањима библиотеке.",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 2393e21b10..a47ed248e9 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -126,9 +126,7 @@
"HearingImpaired": "Hörselskadad",
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
- "TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
"TaskAudioNormalization": "Ljudnormalisering",
- "TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
"TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata.",
"TaskDownloadMissingLyrics": "Ladda ner saknad låttext",
"TaskDownloadMissingLyricsDescription": "Laddar ner låttexter",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index defdc5925a..b68af92033 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -126,8 +126,6 @@
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
- "TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
- "TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
"TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது.",
"TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 65ddb55e94..f0a62646f7 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -130,8 +130,6 @@
"TaskDownloadMissingLyricsDescription": "ดาวน์โหลดเนื้อเพลงสำหรับเพลง",
"TaskAudioNormalization": "ปรับระดับเสียงให้สม่ำเสมอ",
"TaskAudioNormalizationDescription": "สแกนไฟล์เพื่อค้นหาข้อมูลการปรับระดับเสียงให้สม่ำเสมอ",
- "TaskCleanCollectionsAndPlaylists": "จัดระเบียบคอลเลกชันและเพลย์ลิสต์",
- "TaskCleanCollectionsAndPlaylistsDescription": "ลบรายการออกจากคอลเลกชันและเพลย์ลิสต์ที่ไม่มีแล้ว",
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย",
"TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี",
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index d13f662e4c..3789466868 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Duyma Engelli",
"TaskRefreshTrickplayImages": "Hızlı Önizleme Görsellerini Oluştur",
"TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için hızlı önizleme görselleri oluşturur.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.",
- "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin",
"TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.",
"TaskAudioNormalization": "Ses Normalleştirme",
"TaskExtractMediaSegments": "Medya Segmenti Tarama",
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 3ad772aa9c..9246d9de20 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -124,17 +124,15 @@
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
"External": "Зовнішній",
"HearingImpaired": "З порушеннями слуху",
- "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
- "TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
- "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
- "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
+ "TaskRefreshTrickplayImagesDescription": "Створює прев'ю-зображення для відео у ввімкнених медіатеках.",
+ "TaskRefreshTrickplayImages": "Створити Прев'ю-зображення",
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
"TaskAudioNormalization": "Нормалізація аудіо",
"TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень",
"TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень",
- "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
+ "TaskMoveTrickplayImagesDescription": "Переміщує наявні прев'ю-зображення відповідно до налаштувань медіатеки.",
"TaskExtractMediaSegments": "Сканування медіа-сегментів",
- "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
+ "TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень",
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
"CleanupUserDataTask": "Завдання очищення даних користувача",
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 3f4bf1f7ff..947a2c80de 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -126,8 +126,6 @@
"HearingImpaired": "Khiếm Thính",
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
- "TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
- "TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.",
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.",
"TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 0a0795d41b..6a7b7fb4e6 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -126,8 +126,6 @@
"HearingImpaired": "听力障碍",
"TaskRefreshTrickplayImages": "生成进度条预览图",
"TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成进度条预览图。",
- "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
- "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
"TaskAudioNormalization": "音频标准化",
"TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。",
"TaskDownloadMissingLyrics": "下载缺失的歌词",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index a42a33b1da..f3ad8be2a8 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -5,36 +5,36 @@
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 成功通過驗證",
"Books": "書籍",
- "CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相",
+ "CameraImageUploadedFrom": "{0} 已經成功上載咗一張新相",
"Channels": "頻道",
"ChapterNameValue": "第 {0} 章",
"Collections": "系列",
- "DeviceOfflineWithName": "{0} 斷開咗連接",
- "DeviceOnlineWithName": "{0} 連接咗",
+ "DeviceOfflineWithName": "{0} 斷開咗連線",
+ "DeviceOnlineWithName": "{0} 連線咗",
"FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗",
- "Favorites": "我的最愛",
+ "Favorites": "心水",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯歌手",
- "HeaderContinueWatching": "繼續觀看",
- "HeaderFavoriteAlbums": "最愛的專輯",
- "HeaderFavoriteArtists": "最愛的藝人",
- "HeaderFavoriteEpisodes": "最愛的劇集",
- "HeaderFavoriteShows": "最愛的節目",
- "HeaderFavoriteSongs": "最愛的歌曲",
+ "HeaderContinueWatching": "繼續睇返",
+ "HeaderFavoriteAlbums": "心水嘅專輯",
+ "HeaderFavoriteArtists": "心水嘅藝人",
+ "HeaderFavoriteEpisodes": "心水嘅劇集",
+ "HeaderFavoriteShows": "心水嘅節目",
+ "HeaderFavoriteSongs": "心水嘅歌曲",
"HeaderLiveTV": "電視直播",
- "HeaderNextUp": "繼續觀看",
+ "HeaderNextUp": "跟住落嚟",
"HeaderRecordingGroups": "錄製組",
"HomeVideos": "家庭影片",
"Inherit": "繼承",
- "ItemAddedWithName": "{0} 經已加咗入媒體庫",
- "ItemRemovedWithName": "{0} 經已由媒體庫移除咗",
- "LabelIpAddressValue": "IP 地址:{0}",
+ "ItemAddedWithName": "{0} 經已加咗入媒體櫃",
+ "ItemRemovedWithName": "{0} 經已由媒體櫃移除咗",
+ "LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 經已更新咗",
"MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本",
- "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」已經更新咗",
+ "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經已更新咗",
"MessageServerConfigurationUpdated": "伺服器設定經已更新咗",
"MixedContent": "混合內容",
"Movies": "電影",
@@ -42,27 +42,27 @@
"MusicVideos": "MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
- "NameSeasonUnknown": "未知的季度",
+ "NameSeasonUnknown": "未知嘅季度",
"NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。",
"NotificationOptionApplicationUpdateAvailable": "有得更新應用程式",
"NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗",
"NotificationOptionAudioPlayback": "開始播放音訊",
"NotificationOptionAudioPlaybackStopped": "停咗播放音訊",
- "NotificationOptionCameraImageUploaded": "相機相片上傳咗",
+ "NotificationOptionCameraImageUploaded": "相機相片上載咗",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "加咗新內容",
- "NotificationOptionPluginError": "插件錯誤",
- "NotificationOptionPluginInstalled": "安裝插件",
- "NotificationOptionPluginUninstalled": "解除安裝插件",
- "NotificationOptionPluginUpdateInstalled": "插件更新好咗",
+ "NotificationOptionPluginError": "外掛程式錯誤",
+ "NotificationOptionPluginInstalled": "安裝外掛程式",
+ "NotificationOptionPluginUninstalled": "解除安裝外掛程式",
+ "NotificationOptionPluginUpdateInstalled": "外掛程式更新好咗",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程工作失敗",
- "NotificationOptionUserLockedOut": "用戶被鎖定咗",
+ "NotificationOptionUserLockedOut": "用家被鎖定咗",
"NotificationOptionVideoPlayback": "開始播放影片",
"NotificationOptionVideoPlaybackStopped": "停咗播放影片",
"Photos": "相片",
"Playlists": "播放清單",
- "Plugin": "插件",
+ "Plugin": "外掛程式",
"PluginInstalledWithName": "裝好咗 {0}",
"PluginUninstalledWithName": "剷走咗 {0}",
"PluginUpdatedWithName": "更新好咗 {0}",
@@ -72,70 +72,68 @@
"ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
- "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。",
+ "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,唔該稍後再試。",
"SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
- "User": "用戶",
- "UserCreatedWithName": "經已建立咗新用戶 {0}",
- "UserDeletedWithName": "用戶 {0} 已經被刪除",
+ "User": "使用者",
+ "UserCreatedWithName": "經已建立咗新使用者 {0}",
+ "UserDeletedWithName": "使用者 {0} 經已被刪走",
"UserDownloadingItemWithValues": "{0} 下載緊 {1}",
- "UserLockedOutWithName": "用戶 {0} 經已被鎖定",
+ "UserLockedOutWithName": "使用者 {0} 經已被鎖定",
"UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線",
"UserOnlineFromDevice": "{0} 正喺 {1} 連線",
- "UserPasswordChangedWithName": "用戶 {0} 嘅密碼已經更改咗",
- "UserPolicyUpdatedWithName": "用戶 {0} 嘅權限已經更新咗",
+ "UserPasswordChangedWithName": "使用者 {0} 嘅密碼經已更改咗",
+ "UserPolicyUpdatedWithName": "使用者 {0} 嘅權限經已更新咗",
"UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}",
- "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體庫",
- "ValueSpecialEpisodeName": "特別篇 - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃",
+ "ValueSpecialEpisodeName": "特輯 - {0}",
"VersionNumber": "版本 {0}",
"TaskDownloadMissingSubtitles": "下載漏咗嘅字幕",
- "TaskUpdatePlugins": "更新插件",
+ "TaskUpdatePlugins": "更新外掛程式",
"TasksApplicationCategory": "應用程式",
- "TaskRefreshLibraryDescription": "掃描媒體庫嚟搵新檔案,同時重新載入元數據。",
+ "TaskRefreshLibraryDescription": "掃描媒體櫃嚟搵新檔案,同時重新載入媒體詳細資料。",
"TasksMaintenanceCategory": "維護",
- "TaskDownloadMissingSubtitlesDescription": "根據元數據設定,喺網上幫你搵返啲欠缺嘅字幕。",
+ "TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。",
"TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。",
"TaskRefreshChannels": "重新載入頻道",
- "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。",
+ "TaskCleanTranscodeDescription": "自動刪走超過一日嘅轉碼檔案。",
"TaskCleanTranscode": "清理轉碼資料夾",
- "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅插件進行下載同安裝。",
- "TaskRefreshPeopleDescription": "更新媒體庫入面演員同導演嘅元數據。",
- "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。",
+ "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。",
+ "TaskRefreshPeopleDescription": "更新媒體櫃入面演員同導演嘅媒體詳細資料。",
+ "TaskCleanLogsDescription": "自動刪走超過 {0} 日嘅紀錄檔。",
"TaskCleanLogs": "清理日誌資料夾",
- "TaskRefreshLibrary": "掃描媒體庫",
+ "TaskRefreshLibrary": "掃描媒體櫃",
"TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。",
"TaskRefreshChapterImages": "擷取章節圖片",
- "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅快取檔案。",
+ "TaskCleanCacheDescription": "刪走系統已經唔再需要嘅快取檔案。",
"TaskCleanCache": "清理快取(Cache)資料夾",
- "TasksChannelsCategory": "網絡頻道",
- "TasksLibraryCategory": "媒體庫",
+ "TasksChannelsCategory": "網路頻道",
+ "TasksLibraryCategory": "媒體櫃",
"TaskRefreshPeople": "重新載入人物",
"TaskCleanActivityLog": "清理活動紀錄",
"Undefined": "未定義",
"Forced": "強制",
- "Default": "預設",
- "TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體庫或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。",
- "TaskOptimizeDatabase": "最佳化數據庫",
- "TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。",
+ "Default": "初始",
+ "TaskOptimizeDatabaseDescription": "壓縮數據櫃並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據櫃嘅操作之後行呢個任務,或者可以提升效能。",
+ "TaskOptimizeDatabase": "最佳化數據櫃",
+ "TaskCleanActivityLogDescription": "刪走超過設定日期嘅活動記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。",
"TaskKeyframeExtractor": "關鍵影格提取器",
"External": "外部",
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "產生搜畫預覽圖",
- "TaskRefreshTrickplayImagesDescription": "爲已啟用功能嘅媒體庫影片製作快轉預覽圖。",
+ "TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體櫃影片製作快轉預覽圖。",
"TaskExtractMediaSegments": "掃描媒體分段資訊",
- "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。",
+ "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅外掛程式入面提取媒體片段。",
"TaskDownloadMissingLyrics": "下載缺失嘅歌詞",
"TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞",
- "TaskCleanCollectionsAndPlaylists": "清理媒體系列(Collections)同埋播放清單",
"TaskAudioNormalization": "音訊同等化",
"TaskAudioNormalizationDescription": "掃描檔案入面嘅音訊標准化(Audio Normalization)數據。",
- "TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。",
- "TaskMoveTrickplayImagesDescription": "根據媒體庫設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。",
+ "TaskMoveTrickplayImagesDescription": "根據媒體櫃設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。",
"TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置",
- "CleanupUserDataTask": "清理用戶資料嘅任務",
- "CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。"
+ "CleanupUserDataTask": "清理使用者資料嘅任務",
+ "CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體用家數據(包括觀看狀態、心水狀態等)。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index b3bb9106b8..1caf887094 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -126,8 +126,6 @@
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "生成快轉縮圖",
"TaskRefreshTrickplayImagesDescription": "為啟用快轉縮圖的媒體庫生成快轉縮圖。",
- "TaskCleanCollectionsAndPlaylists": "清理系列作和播放清單",
- "TaskCleanCollectionsAndPlaylistsDescription": "清理系列作品與播放清單中已不存在的項目。",
"TaskAudioNormalization": "音量標準化",
"TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。",
"TaskDownloadMissingLyrics": "下載缺少的歌詞",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index bc80c2b405..d8797e612b 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization
string twoCharName = parts[2];
if (string.IsNullOrWhiteSpace(twoCharName))
{
- continue;
+ twoCharName = string.Empty;
}
else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase))
{
@@ -320,6 +320,14 @@ namespace Emby.Server.Implementations.Localization
{
return value;
}
+
+ if (ratingsDictionary is not null && rating.Length > countryCode.Length
+ && rating.StartsWith(countryCode, StringComparison.OrdinalIgnoreCase)
+ && (rating[countryCode.Length] == '-' || rating[countryCode.Length] == ':')
+ && ratingsDictionary.TryGetValue(rating[(countryCode.Length + 1)..].Trim(), out var normalizedValue))
+ {
+ return normalizedValue;
+ }
}
else
{
@@ -345,33 +353,68 @@ namespace Emby.Server.Implementations.Localization
}
}
- // Try splitting by : to handle "Germany: FSK-18"
- if (rating.Contains(':', StringComparison.OrdinalIgnoreCase))
+ // Try splitting by country prefix separator to handle "US:PG-13", "Germany: FSK-18", "DE-FSK-18"
+ if (TryGetRatingScoreBySeparator(rating, ':', out var result)
+ || TryGetRatingScoreBySeparator(rating, '-', out result))
{
- var ratingLevelRightPart = rating.AsSpan().RightPart(':');
- if (ratingLevelRightPart.Length != 0)
- {
- return GetRatingScore(ratingLevelRightPart.ToString());
- }
+ return result;
+ }
+
+ return null;
+ }
+
+ private bool TryGetRatingScoreBySeparator(string rating, char separator, out ParentalRatingScore? result)
+ {
+ result = null;
+
+ if (rating.IndexOf(separator, StringComparison.Ordinal) < 0)
+ {
+ return false;
}
- // Handle prefix country code to handle "DE-18"
- if (rating.Contains('-', StringComparison.OrdinalIgnoreCase))
+ var ratingSpan = rating.AsSpan();
+ var countryPart = ratingSpan.LeftPart(separator).Trim().ToString();
+ var ratingPart = ratingSpan.RightPart(separator).Trim().ToString();
+ if (ratingPart.Length == 0)
{
- var ratingSpan = rating.AsSpan();
+ return false;
+ }
- // Extract culture from country prefix
- var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString());
+ string? resolvedCountryCode = null;
- var ratingLevelRightPart = ratingSpan.RightPart('-');
- if (ratingLevelRightPart.Length != 0)
+ if (_allParentalRatings.ContainsKey(countryPart))
+ {
+ resolvedCountryCode = countryPart;
+ }
+ else
+ {
+ var culture = FindLanguageInfo(countryPart);
+ if (culture is not null)
{
- // Check rating system of culture
- return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
+ resolvedCountryCode = culture.TwoLetterISOLanguageName;
}
}
- return null;
+ if (resolvedCountryCode is not null
+ && _allParentalRatings.TryGetValue(resolvedCountryCode, out var countryRatings))
+ {
+ if (countryRatings.TryGetValue(ratingPart, out result))
+ {
+ return true;
+ }
+
+ _logger.LogWarning(
+ "Rating '{Rating}' not found in the '{CountryCode}' rating system, treating as unrated",
+ rating,
+ resolvedCountryCode);
+
+ return true;
+ }
+
+ // Country not identified or no rating data available, try recursive lookup
+ result = GetRatingScore(ratingPart, resolvedCountryCode);
+
+ return true;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json
index fa43a8f2b7..76550b64c3 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ca.json
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.json
@@ -3,7 +3,7 @@
"supportsSubScores": true,
"ratings": [
{
- "ratingStrings": ["E", "G", "TV-Y", "TV-G"],
+ "ratingStrings": ["C", "E", "G", "TV-Y", "TV-G"],
"ratingScore": {
"score": 0,
"subScore": 0
@@ -24,13 +24,20 @@
}
},
{
- "ratingStrings": ["PG", "TV-PG"],
+ "ratingStrings": ["C8"],
"ratingScore": {
- "score": 9,
+ "score": 8,
"subScore": 0
}
},
{
+ "ratingStrings": ["PG", "TV-PG"],
+ "ratingScore": {
+ "score": 8,
+ "subScore": 1
+ }
+ },
+ {
"ratingStrings": ["14A"],
"ratingScore": {
"score": 14,
@@ -38,7 +45,7 @@
}
},
{
- "ratingStrings": ["TV-14"],
+ "ratingStrings": ["14+", "TV-14"],
"ratingScore": {
"score": 14,
"subScore": 1
diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json
index d92dc880b1..811a7d4094 100644
--- a/Emby.Server.Implementations/Localization/countries.json
+++ b/Emby.Server.Implementations/Localization/countries.json
@@ -750,6 +750,12 @@
"TwoLetterISORegionName": "TJ"
},
{
+ "DisplayName": "Tanzania",
+ "Name": "TZ",
+ "ThreeLetterISORegionName": "TZA",
+ "TwoLetterISORegionName": "TZ"
+ },
+ {
"DisplayName": "Thailand",
"Name": "TH",
"ThreeLetterISORegionName": "THA",
diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index a5be2b616e..3bbbdd43a0 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Playlists
}
query.Recursive = true;
- query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
- return QueryWithPostFiltering2(query);
+ query.IncludeItemTypes = [BaseItemKind.Playlist];
+
+ return QueryWithPostFiltering(query);
}
public override string GetClientTypeName()
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index 36708e2582..b2dc89be28 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
/// </summary>
public partial class AudioNormalizationTask : IScheduledTask
{
- private readonly IItemRepository _itemRepository;
+ private readonly IItemPersistenceService _persistenceService;
private readonly ILibraryManager _libraryManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IApplicationPaths _applicationPaths;
@@ -38,21 +38,21 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <summary>
/// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
/// </summary>
- /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
+ /// <param name="persistenceService">Instance of the <see cref="IItemPersistenceService"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
public AudioNormalizationTask(
- IItemRepository itemRepository,
+ IItemPersistenceService persistenceService,
ILibraryManager libraryManager,
IMediaEncoder mediaEncoder,
IApplicationPaths applicationPaths,
ILocalizationManager localizationManager,
ILogger<AudioNormalizationTask> logger)
{
- _itemRepository = itemRepository;
+ _persistenceService = persistenceService;
_libraryManager = libraryManager;
_mediaEncoder = mediaEncoder;
_applicationPaths = applicationPaths;
@@ -138,7 +138,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
if (toSaveDbItems.Count > 1)
{
- _itemRepository.SaveItems(toSaveDbItems, cancellationToken);
+ _persistenceService.SaveItems(toSaveDbItems, cancellationToken);
toSaveDbItems.Clear();
}
@@ -158,7 +158,7 @@ public partial class AudioNormalizationTask : IScheduledTask
if (toSaveDbItems.Count > 1)
{
- _itemRepository.SaveItems(toSaveDbItems, cancellationToken);
+ _persistenceService.SaveItems(toSaveDbItems, cancellationToken);
toSaveDbItems.Clear();
}
@@ -183,7 +183,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
if (toSaveDbItems.Count > 1)
{
- _itemRepository.SaveItems(toSaveDbItems, cancellationToken);
+ _persistenceService.SaveItems(toSaveDbItems, cancellationToken);
toSaveDbItems.Clear();
}
@@ -200,7 +200,7 @@ public partial class AudioNormalizationTask : IScheduledTask
if (toSaveDbItems.Count > 1)
{
- _itemRepository.SaveItems(toSaveDbItems, cancellationToken);
+ _persistenceService.SaveItems(toSaveDbItems, cancellationToken);
}
// Update progress
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
deleted file mode 100644
index 7f68f7701e..0000000000
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Playlists;
-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;
-
-/// <summary>
-/// Deletes path references from collections and playlists that no longer exists.
-/// </summary>
-public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
-{
- private readonly ILocalizationManager _localization;
- private readonly ICollectionManager _collectionManager;
- private readonly IPlaylistManager _playlistManager;
- private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger;
- private readonly IProviderManager _providerManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class.
- /// </summary>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
- /// <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>
- public CleanupCollectionAndPlaylistPathsTask(
- ILocalizationManager localization,
- ICollectionManager collectionManager,
- IPlaylistManager playlistManager,
- ILogger<CleanupCollectionAndPlaylistPathsTask> logger,
- IProviderManager providerManager)
- {
- _localization = localization;
- _collectionManager = collectionManager;
- _playlistManager = playlistManager;
- _logger = logger;
- _providerManager = providerManager;
- }
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists");
-
- /// <inheritdoc />
- public string Key => "CleanCollectionsAndPlaylists";
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
- /// <inheritdoc />
- public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
- if (collectionsFolder is null)
- {
- _logger.LogDebug("There is no collections folder to be found");
- }
- else
- {
- var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
- _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length);
-
- for (var index = 0; index < collections.Length; index++)
- {
- var collection = collections[index];
- _logger.LogDebug("Checking boxset {CollectionName}", collection.Name);
-
- await CleanupLinkedChildrenAsync(collection, cancellationToken).ConfigureAwait(false);
- progress.Report(50D / collections.Length * (index + 1));
- }
- }
-
- var playlistsFolder = _playlistManager.GetPlaylistsFolder();
- if (playlistsFolder is null)
- {
- _logger.LogDebug("There is no playlists folder to be found");
- return;
- }
-
- var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray();
- _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length);
-
- for (var index = 0; index < playlists.Length; index++)
- {
- var playlist = playlists[index];
- _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name);
-
- await CleanupLinkedChildrenAsync(playlist, cancellationToken).ConfigureAwait(false);
- progress.Report(50D / playlists.Length * (index + 1));
- }
- }
-
- private async Task CleanupLinkedChildrenAsync<T>(T folder, CancellationToken cancellationToken)
- where T : Folder
- {
- List<LinkedChild>? itemsToRemove = null;
- foreach (var linkedChild in folder.LinkedChildren)
- {
- var path = linkedChild.Path;
- if (!File.Exists(path) && !Directory.Exists(path))
- {
- _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
- (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
- }
- }
-
- if (itemsToRemove is not null)
- {
- _logger.LogDebug("Updating {FolderName}", folder.Name);
- folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
- await _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
- await folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- }
- }
-
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- yield return new TaskTriggerInfo
- {
- Type = TaskTriggerInfoType.StartupTrigger,
- };
- }
-}
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index aa5fbbdf73..5c9a94cd36 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -85,9 +85,17 @@ namespace Emby.Server.Implementations.Serialization
/// <returns>System.Object.</returns>
public object? DeserializeFromFile(Type type, string file)
{
- using (var stream = File.OpenRead(file))
+ try
{
- return DeserializeFromStream(type, stream);
+ using (var stream = File.OpenRead(file))
+ {
+ return DeserializeFromStream(type, stream);
+ }
+ }
+ catch (Exception ex)
+ {
+ ex.Data.Add("Filename", file);
+ throw;
}
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 8e14f5bdf4..1782b53e10 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -832,10 +832,6 @@ namespace Emby.Server.Implementations.Session
{
data.Played = true;
}
- else
- {
- data.Played = false;
- }
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
}
@@ -960,7 +956,7 @@ namespace Emby.Server.Implementations.Session
}
var tracksChanged = UpdatePlaybackSettings(user, info, data);
- if (!tracksChanged)
+ if (tracksChanged)
{
changed = true;
}
@@ -977,7 +973,7 @@ namespace Emby.Server.Implementations.Session
if (user.RememberAudioSelections)
{
- if (data.AudioStreamIndex != info.AudioStreamIndex)
+ if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex)
{
data.AudioStreamIndex = info.AudioStreamIndex;
changed = true;
@@ -994,7 +990,7 @@ namespace Emby.Server.Implementations.Session
if (user.RememberSubtitleSelections)
{
- if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
+ if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex)
{
data.SubtitleStreamIndex = info.SubtitleStreamIndex;
changed = true;
@@ -1025,15 +1021,22 @@ namespace Emby.Server.Implementations.Session
ArgumentNullException.ThrowIfNull(info);
+ var session = GetSession(info.SessionId);
+
+ session.StopAutomaticProgress();
+
if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
{
+ // Ensure live stream is cleaned up before throwing, to prevent tuner
+ // resource leaks when stalled clients report a negative PositionTicks.
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
+ }
+
throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative.");
}
- var session = GetSession(info.SessionId);
-
- session.StopAutomaticProgress();
-
var libraryItem = info.ItemId.IsEmpty()
? null
: GetNowPlayingItem(session, info.ItemId);
@@ -1832,7 +1835,6 @@ namespace Emby.Server.Implementations.Session
fields.Remove(ItemFields.Settings);
fields.Remove(ItemFields.SortName);
fields.Remove(ItemFields.Tags);
- fields.Remove(ItemFields.ExtraIds);
dtoOptions.Fields = fields.ToArray();
@@ -2054,7 +2056,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- var adminUserIds = _userManager.Users
+ var adminUserIds = _userManager.GetUsers()
.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
.Select(i => i.Id)
.ToList();
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index cd98dbe86e..535dc01a31 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(query, user, new[] { presentationUniqueKey }, options), query);
+ return GetNextUpBatched(query, user, [presentationUniqueKey], options);
}
BaseItem[] parents;
@@ -58,11 +58,11 @@ namespace Emby.Server.Implementations.TV
if (parent is not null)
{
- parents = new[] { parent };
+ parents = [parent];
}
else
{
- parents = Array.Empty<BaseItem>();
+ parents = [];
}
}
else
@@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
+ return GetNextUpBatched(request, user, [presentationUniqueKey], options);
}
if (limit.HasValue)
@@ -103,151 +103,143 @@ namespace Emby.Server.Implementations.TV
var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
- var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
-
- return GetResult(episodes, request);
+ return GetNextUpBatched(request, user, nextUpSeriesKeys, options);
}
- private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
+ private QueryResult<BaseItem> GetNextUpBatched(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
{
- var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false));
-
- if (request.EnableRewatching)
+ if (seriesKeys.Count == 0)
{
- allNextUp = allNextUp
- .Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true)))
- .OrderByDescending(i => i.LastWatchedDate);
+ return new QueryResult<BaseItem>();
}
- return allNextUp
- .Select(i => i.GetEpisodeFunction())
- .Where(i => i is not null)!;
- }
-
- private static string GetUniqueSeriesKey(Series series)
- {
- return series.GetPresentationUniqueKey();
- }
+ var includeSpecials = _configurationManager.Configuration.DisplaySpecialsWithinSeasons;
+ var includeRewatching = request.EnableRewatching;
- /// <summary>
- /// Gets the next up.
- /// </summary>
- /// <returns>Task{Episode}.</returns>
- private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed)
- {
- var lastQuery = new InternalItemsQuery(user)
+ var query = new InternalItemsQuery(user)
{
- AncestorWithPresentationUniqueKey = null,
- SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = [BaseItemKind.Episode],
- IsPlayed = true,
- Limit = 1,
- ParentIndexNumberNotEquals = 0,
- DtoOptions = new DtoOptions
- {
- Fields = [ItemFields.SortName],
- EnableImages = false
- }
+ DtoOptions = dtoOptions
};
- // If including played results, sort first by date played and then by season and episode numbers
- lastQuery.OrderBy = includePlayed
- ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
- : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
+ var batchResult = _libraryManager.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeRewatching);
- var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
+ var nextUpList = new List<(DateTime LastWatchedDate, Episode Episode)>();
- Episode? GetEpisode()
+ foreach (var seriesKey in seriesKeys)
{
- var nextQuery = new InternalItemsQuery(user)
+ if (!batchResult.TryGetValue(seriesKey, out var result))
{
- AncestorWithPresentationUniqueKey = null,
- SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = [BaseItemKind.Episode],
- OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
- Limit = 1,
- IsPlayed = includePlayed,
- IsVirtualItem = false,
- ParentIndexNumberNotEquals = 0,
- DtoOptions = dtoOptions
- };
-
- // Locate the next up episode based on the last watched episode's season and episode number
- var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
- var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
- if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
- {
- nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
+ continue;
}
- var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+ var nextEpisode = DetermineNextEpisode(result, user, includeSpecials, request.EnableResumable, false);
+
+ if (nextEpisode is not null)
+ {
+ DateTime lastWatchedDate = DateTime.MinValue;
+ if (result.LastWatched is not null)
+ {
+ var userData = _userDataManager.GetUserData(user, result.LastWatched);
+ lastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
+ }
+
+ nextUpList.Add((lastWatchedDate, nextEpisode));
+ }
- if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
+ if (includeRewatching)
{
- var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var nextPlayedEpisode = DetermineNextEpisodeForRewatching(result, user, includeSpecials);
+
+ if (nextPlayedEpisode is not null)
{
- AncestorWithPresentationUniqueKey = null,
- SeriesPresentationUniqueKey = seriesKey,
- ParentIndexNumber = 0,
- IncludeItemTypes = [BaseItemKind.Episode],
- IsPlayed = includePlayed,
- IsVirtualItem = false,
- DtoOptions = dtoOptions
- })
+ DateTime rewatchLastWatchedDate = DateTime.MinValue;
+ if (result.LastWatchedForRewatching is not null)
+ {
+ var userData = _userDataManager.GetUserData(user, result.LastWatchedForRewatching);
+ rewatchLastWatchedDate = userData?.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
+ }
+
+ nextUpList.Add((rewatchLastWatchedDate, nextPlayedEpisode));
+ }
+ }
+ }
+
+ var sortedEpisodes = nextUpList
+ .OrderByDescending(x => x.LastWatchedDate)
+ .Select(x => (BaseItem)x.Episode);
+
+ return GetResult(sortedEpisodes, request);
+ }
+
+ private Episode? DetermineNextEpisode(
+ MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
+ User user,
+ bool includeSpecials,
+ bool includeResumable,
+ bool includePlayed)
+ {
+ var nextEpisode = (includePlayed ? result.NextPlayedForRewatching : result.NextUp) as Episode;
+ var lastWatchedEpisode = (includePlayed ? result.LastWatchedForRewatching : result.LastWatched) as Episode;
+
+ if (includeSpecials && result.Specials?.Count > 0)
+ {
+ var consideredEpisodes = result.Specials
.Cast<Episode>()
.Where(episode => episode.AirsBeforeSeasonNumber is not null || episode.AirsAfterSeasonNumber is not null)
.ToList();
- if (lastWatchedEpisode is not null)
- {
- // Last watched episode is added, because there could be specials that aired before the last watched episode
- consideredEpisodes.Add(lastWatchedEpisode);
- }
+ if (lastWatchedEpisode is not null)
+ {
+ consideredEpisodes.Add(lastWatchedEpisode);
+ }
- if (nextEpisode is not null)
- {
- consideredEpisodes.Add(nextEpisode);
- }
+ if (nextEpisode is not null)
+ {
+ consideredEpisodes.Add(nextEpisode);
+ }
- var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
+ if (consideredEpisodes.Count > 0)
+ {
+ var sortedEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
.Cast<Episode>();
+
if (lastWatchedEpisode is not null)
{
- sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1);
+ sortedEpisodes = sortedEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1);
}
- nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
- }
-
- if (nextEpisode is not null && !includeResumable)
- {
- var userData = _userDataManager.GetUserData(user, nextEpisode);
-
- if (userData?.PlaybackPositionTicks > 0)
+ if (!includePlayed)
{
- return null;
+ sortedEpisodes = sortedEpisodes.Where(episode => _userDataManager.GetUserData(user, episode) is not { Played: true });
}
- }
- return nextEpisode;
+ nextEpisode = sortedEpisodes.FirstOrDefault();
+ }
}
- if (lastWatchedEpisode is not null)
+ if (nextEpisode is not null && !includeResumable)
{
- var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
-
- if (userData is null)
+ var userData = _userDataManager.GetUserData(user, nextEpisode);
+ if (userData?.PlaybackPositionTicks > 0)
{
- return (DateTime.MinValue, GetEpisode);
+ return null;
}
+ }
- var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
+ return nextEpisode;
+ }
- return (lastWatchedDate, GetEpisode);
- }
+ private Episode? DetermineNextEpisodeForRewatching(
+ MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult result,
+ User user,
+ bool includeSpecials)
+ {
+ return DetermineNextEpisode(result, user, includeSpecials, includeResumable: false, includePlayed: true);
+ }
- // Return the first episode
- return (DateTime.MinValue, GetEpisode);
+ private static string GetUniqueSeriesKey(Series series)
+ {
+ return series.GetPresentationUniqueKey();
}
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)