aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs58
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs3
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs34
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs6
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs269
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs96
-rw-r--r--Emby.Server.Implementations/Data/ItemTypeLookup.cs64
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs62
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs12
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs5971
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs369
-rw-r--r--Emby.Server.Implementations/Data/SynchronousMode.cs30
-rw-r--r--Emby.Server.Implementations/Data/TempStoreMode.cs23
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs3
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs14
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs4
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs8
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs3
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs14
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs4
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs4
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs40
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs5
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Images/MusicGenreImageProvider.cs1
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs207
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs64
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs88
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs29
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs36
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs5
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs19
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs149
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs43
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json77
-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.json20
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json70
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ht.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/lb.json139
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json22
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/mt.json22
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json17
-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/sl-SI.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json6
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.csv8
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.csv2
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv2
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.csv3
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv2
-rw-r--r--Emby.Server.Implementations/Localization/countries.json2
-rw-r--r--Emby.Server.Implementations/Localization/iso6392.txt48
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs4
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs4
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs4
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs14
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs26
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs10
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs23
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs113
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs8
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs98
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs72
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs4
103 files changed, 1359 insertions, 7381 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index dc845b2d7..f0cca9efd 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
- /// <summary>
- /// Gets the path to the program data folder.
- /// </summary>
- /// <value>The program data path.</value>
+ /// <inheritdoc/>
public string ProgramDataPath { get; }
/// <inheritdoc/>
public string WebPath { get; }
- /// <summary>
- /// Gets the path to the system folder.
- /// </summary>
- /// <value>The path to the system folder.</value>
+ /// <inheritdoc/>
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
- /// <summary>
- /// Gets the folder path to the data directory.
- /// </summary>
- /// <value>The data directory.</value>
+ /// <inheritdoc/>
public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
- /// <summary>
- /// Gets the image cache path.
- /// </summary>
- /// <value>The image cache path.</value>
+ /// <inheritdoc/>
public string ImageCachePath => Path.Combine(CachePath, "images");
- /// <summary>
- /// Gets the path to the plugin directory.
- /// </summary>
- /// <value>The plugins path.</value>
+ /// <inheritdoc/>
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
- /// <summary>
- /// Gets the path to the plugin configurations directory.
- /// </summary>
- /// <value>The plugin configurations path.</value>
+ /// <inheritdoc/>
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
- /// <summary>
- /// Gets the path to the log directory.
- /// </summary>
- /// <value>The log directory path.</value>
+ /// <inheritdoc/>
public string LogDirectoryPath { get; }
- /// <summary>
- /// Gets the path to the application configuration root directory.
- /// </summary>
- /// <value>The configuration directory path.</value>
+ /// <inheritdoc/>
public string ConfigurationDirectoryPath { get; }
- /// <summary>
- /// Gets the path to the system configuration file.
- /// </summary>
- /// <value>The system configuration file path.</value>
+ /// <inheritdoc/>
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
- /// <summary>
- /// Gets or sets the folder path to the cache directory.
- /// </summary>
- /// <value>The cache directory.</value>
+ /// <inheritdoc/>
public string CachePath { get; set; }
- /// <summary>
- /// Gets the folder path to the temp directory within the cache folder.
- /// </summary>
- /// <value>The temp directory.</value>
+ /// <inheritdoc/>
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
+
+ /// <inheritdoc />
+ public string TrickplayPath => Path.Combine(DataPath, "trickplay");
}
}
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 9e98d5ce0..9bc3a0204 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@@ -19,7 +20,7 @@ namespace Emby.Server.Implementations.AppBase
public abstract class BaseConfigurationManager : IConfigurationManager
{
private readonly ConcurrentDictionary<string, object> _configurations = new();
- private readonly object _configurationSyncLock = new();
+ private readonly Lock _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 13516896a..4d959905d 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -35,11 +35,12 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
+using Jellyfin.Database.Implementations;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
-using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@@ -56,6 +57,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
@@ -83,7 +85,6 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
@@ -268,6 +269,11 @@ namespace Emby.Server.Implementations
public string ExpandVirtualPath(string path)
{
+ if (path is null)
+ {
+ return null;
+ }
+
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@@ -492,13 +498,18 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+ serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
+ serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
+ serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
+ serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
+ serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+ serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
+ serviceCollection.AddSingleton<IPathManager, PathManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -540,8 +551,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
-
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthService, AuthService>();
@@ -565,10 +574,15 @@ namespace Emby.Server.Implementations
/// <summary>
/// Create services registered with the service container that need to be initialized at application startup.
/// </summary>
+ /// <param name="startupConfig">The configuration used to initialise the application.</param>
/// <returns>A task representing the service initialization operation.</returns>
- public async Task InitializeServices()
+ public async Task InitializeServices(IConfiguration startupConfig)
{
- var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
+ var factory = Resolve<IDbContextFactory<JellyfinDbContext>>();
+ var provider = Resolve<IJellyfinDatabaseProvider>();
+ provider.DbContextFactory = factory;
+
+ var jellyfinDb = await factory.CreateDbContextAsync().ConfigureAwait(false);
await using (jellyfinDb.ConfigureAwait(false))
{
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
@@ -579,9 +593,6 @@ namespace Emby.Server.Implementations
}
}
- ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
- ((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
-
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
@@ -635,6 +646,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
+ BaseItem.ChapterRepository = Resolve<IChapterRepository>();
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index e414792ba..60f515f24 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
@@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
{
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
- throw new ArgumentException("No collection exists with the supplied Id");
+ throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
}
List<BaseItem>? itemList = null;
@@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
if (item is null)
{
- throw new ArgumentException("No item exists with the supplied Id");
+ throw new ArgumentException("No item exists with the supplied Id " + id);
}
if (!currentLinkedChildrenIds.Contains(id))
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
deleted file mode 100644
index 8ed72c208..000000000
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ /dev/null
@@ -1,269 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Jellyfin.Extensions;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- public abstract class BaseSqliteRepository : IDisposable
- {
- private bool _disposed = false;
- private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
- private SqliteConnection _writeConnection;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
- {
- Logger = logger;
- }
-
- /// <summary>
- /// Gets or sets the path to the DB file.
- /// </summary>
- protected string DbFilePath { get; set; }
-
- /// <summary>
- /// Gets the logger.
- /// </summary>
- /// <value>The logger.</value>
- protected ILogger<BaseSqliteRepository> Logger { get; }
-
- /// <summary>
- /// Gets the cache size.
- /// </summary>
- /// <value>The cache size or null.</value>
- protected virtual int? CacheSize => null;
-
- /// <summary>
- /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
- /// </summary>
- protected virtual string LockingMode => "NORMAL";
-
- /// <summary>
- /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
- /// </summary>
- /// <value>The journal mode.</value>
- protected virtual string JournalMode => "WAL";
-
- /// <summary>
- /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
- /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
- /// </summary>
- /// <value>The journal size limit.</value>
- protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
-
- /// <summary>
- /// Gets the page size.
- /// </summary>
- /// <value>The page size or null.</value>
- protected virtual int? PageSize => null;
-
- /// <summary>
- /// Gets the temp store mode.
- /// </summary>
- /// <value>The temp store mode.</value>
- /// <see cref="TempStoreMode"/>
- protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
-
- /// <summary>
- /// Gets the synchronous mode.
- /// </summary>
- /// <value>The synchronous mode or null.</value>
- /// <see cref="SynchronousMode"/>
- protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
-
- public virtual void Initialize()
- {
- // Configuration and pragmas can affect VACUUM so it needs to be last.
- using (var connection = GetConnection())
- {
- connection.Execute("VACUUM");
- }
- }
-
- protected ManagedConnection GetConnection(bool readOnly = false)
- {
- if (!readOnly)
- {
- _writeLock.Wait();
- if (_writeConnection is not null)
- {
- return new ManagedConnection(_writeConnection, _writeLock);
- }
-
- var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
- writeConnection.Open();
-
- if (CacheSize.HasValue)
- {
- writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
- }
-
- var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
- connection.Open();
-
- if (CacheSize.HasValue)
- {
- connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- connection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- connection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- connection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- connection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return new ManagedConnection(connection, null);
- }
-
- public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
- return command;
- }
-
- protected bool TableExists(ManagedConnection connection, string name)
- {
- using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
- foreach (var row in statement.ExecuteQuery())
- {
- if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
-
- protected List<string> GetColumnNames(ManagedConnection connection, string table)
- {
- var columnNames = new List<string>();
-
- foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
- {
- if (row.TryGetString(1, out var columnName))
- {
- columnNames.Add(columnName);
- }
- }
-
- return columnNames;
- }
-
- protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
- {
- if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
- }
-
- protected void CheckDisposed()
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- _writeLock.Wait();
- try
- {
- _writeConnection.Dispose();
- }
- finally
- {
- _writeLock.Release();
- }
-
- _writeLock.Dispose();
- }
-
- _writeConnection = null;
- _writeLock = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 4516b89dc..63481b1f8 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,66 +1,84 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Data
+namespace Emby.Server.Implementations.Data;
+
+public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
- public class CleanDatabaseScheduledTask : ILibraryPostScanTask
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ public CleanDatabaseScheduledTask(
+ ILibraryManager libraryManager,
+ ILogger<CleanDatabaseScheduledTask> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _dbProvider = dbProvider;
+ }
- public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- }
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
+ }
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
- CleanDeadItems(cancellationToken, progress);
- return Task.CompletedTask;
- }
+ HasDeadParentId = true
+ });
- private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
- {
- var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
- {
- HasDeadParentId = true
- });
+ var numComplete = 0;
+ var numItems = itemIds.Count + 1;
- var numComplete = 0;
- var numItems = itemIds.Count;
+ _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems);
- _logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
+ foreach (var itemId in itemIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
- foreach (var itemId in itemIds)
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is not null)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- var item = _libraryManager.GetItemById(itemId);
+ _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
- if (item is not null)
+ _libraryManager.DeleteItem(item, new DeleteOptions
{
- _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
+ DeleteFileLocation = false
+ });
+ }
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- });
- }
+ numComplete++;
+ double percent = numComplete;
+ percent /= numItems;
+ progress.Report(percent * 100);
+ }
- numComplete++;
- double percent = numComplete;
- percent /= numItems;
- progress.Report(percent * 100);
+ var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using (transaction.ConfigureAwait(false))
+ {
+ await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
-
- progress.Report(100);
}
+
+ progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
new file mode 100644
index 000000000..82c0a8b6c
--- /dev/null
+++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
@@ -0,0 +1,64 @@
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Threading.Channels;
+using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Playlists;
+
+namespace Emby.Server.Implementations.Data;
+
+/// <inheritdoc />
+public class ItemTypeLookup : IItemTypeLookup
+{
+ /// <inheritdoc />
+ public IReadOnlyList<string> MusicGenreTypes { get; } = [
+ typeof(Audio).FullName!,
+ typeof(MusicVideo).FullName!,
+ typeof(MusicAlbum).FullName!,
+ typeof(MusicArtist).FullName!,
+ ];
+
+ /// <inheritdoc />
+ public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
+ {
+ { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
+ { BaseItemKind.Audio, typeof(Audio).FullName! },
+ { BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
+ { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
+ { BaseItemKind.Book, typeof(Book).FullName! },
+ { BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
+ { BaseItemKind.Channel, typeof(Channel).FullName! },
+ { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
+ { BaseItemKind.Episode, typeof(Episode).FullName! },
+ { BaseItemKind.Folder, typeof(Folder).FullName! },
+ { BaseItemKind.Genre, typeof(Genre).FullName! },
+ { BaseItemKind.Movie, typeof(Movie).FullName! },
+ { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
+ { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
+ { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
+ { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
+ { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
+ { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
+ { BaseItemKind.Person, typeof(Person).FullName! },
+ { BaseItemKind.Photo, typeof(Photo).FullName! },
+ { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
+ { BaseItemKind.Playlist, typeof(Playlist).FullName! },
+ { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
+ { BaseItemKind.Season, typeof(Season).FullName! },
+ { BaseItemKind.Series, typeof(Series).FullName! },
+ { BaseItemKind.Studio, typeof(Studio).FullName! },
+ { BaseItemKind.Trailer, typeof(Trailer).FullName! },
+ { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
+ { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
+ { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
+ { BaseItemKind.UserView, typeof(UserView).FullName! },
+ { BaseItemKind.Video, typeof(Video).FullName! },
+ { BaseItemKind.Year, typeof(Year).FullName! }
+ }.ToFrozenDictionary();
+}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
deleted file mode 100644
index 860950b30..000000000
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Microsoft.Data.Sqlite;
-
-namespace Emby.Server.Implementations.Data;
-
-public sealed class ManagedConnection : IDisposable
-{
- private readonly SemaphoreSlim? _writeLock;
-
- private SqliteConnection _db;
-
- private bool _disposed = false;
-
- public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
- {
- _db = db;
- _writeLock = writeLock;
- }
-
- public SqliteTransaction BeginTransaction()
- => _db.BeginTransaction();
-
- public SqliteCommand CreateCommand()
- => _db.CreateCommand();
-
- public void Execute(string commandText)
- => _db.Execute(commandText);
-
- public SqliteCommand PrepareStatement(string sql)
- => _db.PrepareStatement(sql);
-
- public IEnumerable<SqliteDataReader> Query(string commandText)
- => _db.Query(commandText);
-
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- if (_writeLock is null)
- {
- // Read connections are managed with an internal pool
- _db.Dispose();
- }
- else
- {
- // Write lock is managed by BaseSqliteRepository
- // Don't dispose here
- _writeLock.Release();
- }
-
- _db = null!;
-
- _disposed = true;
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 25ef57d27..0efef4ded 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -127,8 +127,16 @@ namespace Emby.Server.Implementations.Data
return false;
}
- result = reader.GetGuid(index);
- return true;
+ try
+ {
+ result = reader.GetGuid(index);
+ return true;
+ }
+ catch
+ {
+ result = Guid.Empty;
+ return false;
+ }
}
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
deleted file mode 100644
index 3477194cf..000000000
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ /dev/null
@@ -1,5971 +0,0 @@
-#nullable disable
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using Emby.Server.Implementations.Playlists;
-using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Querying;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- /// <summary>
- /// Class SQLiteItemRepository.
- /// </summary>
- public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
- {
- private const string FromText = " from TypedBaseItems A";
- private const string ChaptersTableName = "Chapters2";
-
- private const string SaveItemCommandText =
- @"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
-
- private readonly IServerConfigurationManager _config;
- private readonly IServerApplicationHost _appHost;
- private readonly ILocalizationManager _localization;
- // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method
- private readonly IImageProcessor _imageProcessor;
-
- private readonly TypeMapper _typeMapper;
- private readonly JsonSerializerOptions _jsonOptions;
-
- private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
-
- private static readonly string[] _retrieveItemColumns =
- {
- "type",
- "data",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "Name",
- "Path",
- "PremiereDate",
- "Overview",
- "ParentIndexNumber",
- "ProductionYear",
- "OfficialRating",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "guid",
- "Genres",
- "ParentId",
- "Audio",
- "ExternalServiceId",
- "IsInMixedFolder",
- "DateLastSaved",
- "LockedFields",
- "Studios",
- "Tags",
- "TrailerTypes",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "LUFS",
- "NormalizationGain",
- "CriticRating",
- "IsVirtualItem",
- "SeriesName",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "PresentationUniqueKey",
- "InheritedParentalRatingValue",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
-
- private static readonly string[] _mediaStreamSaveColumns =
- {
- "ItemId",
- "StreamIndex",
- "StreamType",
- "Codec",
- "Language",
- "ChannelLayout",
- "Profile",
- "AspectRatio",
- "Path",
- "IsInterlaced",
- "BitRate",
- "Channels",
- "SampleRate",
- "IsDefault",
- "IsForced",
- "IsExternal",
- "Height",
- "Width",
- "AverageFrameRate",
- "RealFrameRate",
- "Level",
- "PixelFormat",
- "BitDepth",
- "IsAnamorphic",
- "RefFrames",
- "CodecTag",
- "Comment",
- "NalLengthSize",
- "IsAvc",
- "Title",
- "TimeBase",
- "CodecTimeBase",
- "ColorPrimaries",
- "ColorSpace",
- "ColorTransfer",
- "DvVersionMajor",
- "DvVersionMinor",
- "DvProfile",
- "DvLevel",
- "RpuPresentFlag",
- "ElPresentFlag",
- "BlPresentFlag",
- "DvBlSignalCompatibilityId",
- "IsHearingImpaired",
- "Rotation"
- };
-
- private static readonly string _mediaStreamSaveColumnsInsertQuery =
- $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
-
- private static readonly string _mediaStreamSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
-
- private static readonly string[] _mediaAttachmentSaveColumns =
- {
- "ItemId",
- "AttachmentIndex",
- "Codec",
- "CodecTag",
- "Comment",
- "Filename",
- "MIMEType"
- };
-
- private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
-
- private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix();
-
- private static readonly BaseItemKind[] _programTypes = new[]
- {
- BaseItemKind.Program,
- BaseItemKind.TvChannel,
- BaseItemKind.LiveTvProgram,
- BaseItemKind.LiveTvChannel
- };
-
- private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
- {
- BaseItemKind.Series,
- BaseItemKind.Season,
- BaseItemKind.MusicAlbum,
- BaseItemKind.MusicArtist,
- BaseItemKind.PhotoAlbum
- };
-
- private static readonly BaseItemKind[] _serviceTypes = new[]
- {
- BaseItemKind.TvChannel,
- BaseItemKind.LiveTvChannel
- };
-
- private static readonly BaseItemKind[] _startDateTypes = new[]
- {
- BaseItemKind.Program,
- BaseItemKind.LiveTvProgram
- };
-
- private static readonly BaseItemKind[] _seriesTypes = new[]
- {
- BaseItemKind.Book,
- BaseItemKind.AudioBook,
- BaseItemKind.Episode,
- BaseItemKind.Season
- };
-
- private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
- {
- BaseItemKind.Series,
- BaseItemKind.Season,
- BaseItemKind.PhotoAlbum
- };
-
- private static readonly BaseItemKind[] _artistsTypes = new[]
- {
- BaseItemKind.Audio,
- BaseItemKind.MusicAlbum,
- BaseItemKind.MusicVideo,
- BaseItemKind.AudioBook
- };
-
- private static readonly Dictionary<BaseItemKind, string> _baseItemKindNames = new()
- {
- { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
- { BaseItemKind.Audio, typeof(Audio).FullName },
- { BaseItemKind.AudioBook, typeof(AudioBook).FullName },
- { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName },
- { BaseItemKind.Book, typeof(Book).FullName },
- { BaseItemKind.BoxSet, typeof(BoxSet).FullName },
- { BaseItemKind.Channel, typeof(Channel).FullName },
- { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName },
- { BaseItemKind.Episode, typeof(Episode).FullName },
- { BaseItemKind.Folder, typeof(Folder).FullName },
- { BaseItemKind.Genre, typeof(Genre).FullName },
- { BaseItemKind.Movie, typeof(Movie).FullName },
- { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName },
- { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName },
- { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName },
- { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName },
- { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName },
- { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName },
- { BaseItemKind.Person, typeof(Person).FullName },
- { BaseItemKind.Photo, typeof(Photo).FullName },
- { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName },
- { BaseItemKind.Playlist, typeof(Playlist).FullName },
- { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName },
- { BaseItemKind.Season, typeof(Season).FullName },
- { BaseItemKind.Series, typeof(Series).FullName },
- { BaseItemKind.Studio, typeof(Studio).FullName },
- { BaseItemKind.Trailer, typeof(Trailer).FullName },
- { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName },
- { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName },
- { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName },
- { BaseItemKind.UserView, typeof(UserView).FullName },
- { BaseItemKind.Video, typeof(Video).FullName },
- { BaseItemKind.Year, typeof(Year).FullName }
- };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
- /// </summary>
- /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
- /// <exception cref="ArgumentNullException">config is null.</exception>
- public SqliteItemRepository(
- IServerConfigurationManager config,
- IServerApplicationHost appHost,
- ILogger<SqliteItemRepository> logger,
- ILocalizationManager localization,
- IImageProcessor imageProcessor,
- IConfiguration configuration)
- : base(logger)
- {
- _config = config;
- _appHost = appHost;
- _localization = localization;
- _imageProcessor = imageProcessor;
-
- _typeMapper = new TypeMapper();
- _jsonOptions = JsonDefaults.Options;
-
- DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
-
- CacheSize = configuration.GetSqliteCacheSize();
- }
-
- /// <inheritdoc />
- protected override int? CacheSize { get; }
-
- /// <inheritdoc />
- protected override TempStoreMode TempStore => TempStoreMode.Memory;
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- public override void Initialize()
- {
- base.Initialize();
-
- const string CreateMediaStreamsTableCommand
- = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
-
- const string CreateMediaAttachmentsTableCommand
- = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
-
- string[] queries =
- {
- "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
-
- "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
- "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
- "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)",
-
- "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)",
-
- "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
-
- "drop index if exists idxPeopleItemId",
- "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)",
- "create index if not exists idxPeopleName on People(Name)",
-
- "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
-
- CreateMediaStreamsTableCommand,
- CreateMediaAttachmentsTableCommand,
-
- "pragma shrink_memory"
- };
-
- string[] postQueries =
- {
- "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
- "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
-
- "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)",
- "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)",
- "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)",
-
- // covering index
- "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)",
-
- // series
- "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)",
-
- // series counts
- // seriesdateplayed sort order
- "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)",
-
- // live tv programs
- "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)",
-
- // covering index for getitemvalues
- "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)",
-
- // used by movie suggestions
- "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)",
- "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)",
-
- // latest items
- "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)",
- "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)",
-
- // resume
- "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
-
- // items by name
- "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
- "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
-
- // Used to update inherited tags
- "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
-
- "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)",
- "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)"
- };
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- connection.Execute(string.Join(';', queries));
-
- var existingColumnNames = GetColumnNames(connection, "AncestorIds");
- AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "TypedBaseItems");
-
- AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "ItemValues");
- AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, ChaptersTableName);
- AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "MediaStreams");
- AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames);
-
- connection.Execute(string.Join(';', postQueries));
-
- transaction.Commit();
- }
- }
-
- /// <inheritdoc />
- public void SaveImages(BaseItem item)
- {
- ArgumentNullException.ThrowIfNull(item);
-
- CheckDisposed();
-
- var images = SerializeImages(item.ImageInfos);
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id");
- saveImagesStatement.TryBind("@Id", item.Id);
- saveImagesStatement.TryBind("@Images", images);
-
- saveImagesStatement.ExecuteNonQuery();
- transaction.Commit();
- }
-
- /// <summary>
- /// Saves the items.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">
- /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
- /// </exception>
- public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(items);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- CheckDisposed();
-
- var itemsLen = items.Count;
- var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen];
- for (int i = 0; i < itemsLen; i++)
- {
- var item = items[i];
- var ancestorIds = item.SupportsAncestors ?
- item.GetAncestorIds().Distinct().ToList() :
- null;
-
- var topParent = item.GetTopParent();
-
- var userdataKey = item.GetUserDataKeys().FirstOrDefault();
- var inheritedTags = item.GetInheritedTags();
-
- tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
- }
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- SaveItemsInTransaction(connection, tuples);
- transaction.Commit();
- }
-
- private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
- {
- using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
- using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
- {
- var requiresReset = false;
- foreach (var tuple in tuples)
- {
- if (requiresReset)
- {
- saveItemStatement.Parameters.Clear();
- deleteAncestorsStatement.Parameters.Clear();
- }
-
- var item = tuple.Item;
- var topParent = tuple.TopParent;
- var userDataKey = tuple.UserDataKey;
-
- SaveItem(item, topParent, userDataKey, saveItemStatement);
-
- var inheritedTags = tuple.InheritedTags;
-
- if (item.SupportsAncestors)
- {
- UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement);
- }
-
- UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db);
-
- requiresReset = true;
- }
- }
- }
-
- private string GetPathToSave(string path)
- {
- if (path is null)
- {
- return null;
- }
-
- return _appHost.ReverseVirtualPath(path);
- }
-
- private string RestorePath(string path)
- {
- return _appHost.ExpandVirtualPath(path);
- }
-
- private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement)
- {
- Type type = item.GetType();
-
- saveItemStatement.TryBind("@guid", item.Id);
- saveItemStatement.TryBind("@type", type.FullName);
-
- if (TypeRequiresDeserialization(type))
- {
- saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true);
- }
- else
- {
- saveItemStatement.TryBindNull("@data");
- }
-
- saveItemStatement.TryBind("@Path", GetPathToSave(item.Path));
-
- if (item is IHasStartDate hasStartDate)
- {
- saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate);
- }
- else
- {
- saveItemStatement.TryBindNull("@StartDate");
- }
-
- if (item.EndDate.HasValue)
- {
- saveItemStatement.TryBind("@EndDate", item.EndDate.Value);
- }
- else
- {
- saveItemStatement.TryBindNull("@EndDate");
- }
-
- saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
-
- if (item is IHasProgramAttributes hasProgramAttributes)
- {
- saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie);
- saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries);
- saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle);
- saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat);
- }
- else
- {
- saveItemStatement.TryBindNull("@IsMovie");
- saveItemStatement.TryBindNull("@IsSeries");
- saveItemStatement.TryBindNull("@EpisodeTitle");
- saveItemStatement.TryBindNull("@IsRepeat");
- }
-
- saveItemStatement.TryBind("@CommunityRating", item.CommunityRating);
- saveItemStatement.TryBind("@CustomRating", item.CustomRating);
- saveItemStatement.TryBind("@IndexNumber", item.IndexNumber);
- saveItemStatement.TryBind("@IsLocked", item.IsLocked);
- saveItemStatement.TryBind("@Name", item.Name);
- saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
- saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
- saveItemStatement.TryBind("@Overview", item.Overview);
- saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
- saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
- saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
-
- var parentId = item.ParentId;
- if (parentId.IsEmpty())
- {
- saveItemStatement.TryBindNull("@ParentId");
- }
- else
- {
- saveItemStatement.TryBind("@ParentId", parentId);
- }
-
- if (item.Genres.Length > 0)
- {
- saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
- }
- else
- {
- saveItemStatement.TryBindNull("@Genres");
- }
-
- saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
-
- saveItemStatement.TryBind("@SortName", item.SortName);
-
- saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName);
-
- saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks);
- saveItemStatement.TryBind("@Size", item.Size);
-
- saveItemStatement.TryBind("@DateCreated", item.DateCreated);
- saveItemStatement.TryBind("@DateModified", item.DateModified);
-
- saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage);
- saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode);
-
- if (item.Width > 0)
- {
- saveItemStatement.TryBind("@Width", item.Width);
- }
- else
- {
- saveItemStatement.TryBindNull("@Width");
- }
-
- if (item.Height > 0)
- {
- saveItemStatement.TryBind("@Height", item.Height);
- }
- else
- {
- saveItemStatement.TryBindNull("@Height");
- }
-
- if (item.DateLastRefreshed != default(DateTime))
- {
- saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastRefreshed");
- }
-
- if (item.DateLastSaved != default(DateTime))
- {
- saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastSaved");
- }
-
- saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder);
-
- if (item.LockedFields.Length > 0)
- {
- saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
- }
- else
- {
- saveItemStatement.TryBindNull("@LockedFields");
- }
-
- if (item.Studios.Length > 0)
- {
- saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
- }
- else
- {
- saveItemStatement.TryBindNull("@Studios");
- }
-
- if (item.Audio.HasValue)
- {
- saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString());
- }
- else
- {
- saveItemStatement.TryBindNull("@Audio");
- }
-
- if (item is LiveTvChannel liveTvChannel)
- {
- saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName);
- }
- else
- {
- saveItemStatement.TryBindNull("@ExternalServiceId");
- }
-
- if (item.Tags.Length > 0)
- {
- saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
- }
- else
- {
- saveItemStatement.TryBindNull("@Tags");
- }
-
- saveItemStatement.TryBind("@IsFolder", item.IsFolder);
-
- saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString());
-
- if (topParent is null)
- {
- saveItemStatement.TryBindNull("@TopParentId");
- }
- else
- {
- saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture));
- }
-
- if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
- {
- saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
- }
- else
- {
- saveItemStatement.TryBindNull("@TrailerTypes");
- }
-
- saveItemStatement.TryBind("@CriticRating", item.CriticRating);
-
- if (string.IsNullOrWhiteSpace(item.Name))
- {
- saveItemStatement.TryBindNull("@CleanName");
- }
- else
- {
- saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name));
- }
-
- saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey);
- saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle);
-
- if (item is Video video)
- {
- saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId);
- }
- else
- {
- saveItemStatement.TryBindNull("@PrimaryVersionId");
- }
-
- if (item is Folder folder && folder.DateLastMediaAdded.HasValue)
- {
- saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastMediaAdded");
- }
-
- saveItemStatement.TryBind("@Album", item.Album);
- saveItemStatement.TryBind("@LUFS", item.LUFS);
- saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
- saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
-
- if (item is IHasSeries hasSeriesName)
- {
- saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeriesName");
- }
-
- if (string.IsNullOrWhiteSpace(userDataKey))
- {
- saveItemStatement.TryBindNull("@UserDataKey");
- }
- else
- {
- saveItemStatement.TryBind("@UserDataKey", userDataKey);
- }
-
- if (item is Episode episode)
- {
- saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
-
- var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId;
-
- saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeasonName");
- saveItemStatement.TryBindNull("@SeasonId");
- }
-
- if (item is IHasSeries hasSeries)
- {
- var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId;
-
- saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
- saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeriesId");
- saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey");
- }
-
- saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
- saveItemStatement.TryBind("@Tagline", item.Tagline);
-
- saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
- saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
-
- if (item.ProductionLocations.Length > 0)
- {
- saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
- }
- else
- {
- saveItemStatement.TryBindNull("@ProductionLocations");
- }
-
- if (item.ExtraIds.Length > 0)
- {
- saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
- }
- else
- {
- saveItemStatement.TryBindNull("@ExtraIds");
- }
-
- saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate);
- if (item.ExtraType.HasValue)
- {
- saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString());
- }
- else
- {
- saveItemStatement.TryBindNull("@ExtraType");
- }
-
- string artists = null;
- if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
- {
- artists = string.Join('|', hasArtists.Artists);
- }
-
- saveItemStatement.TryBind("@Artists", artists);
-
- string albumArtists = null;
- if (item is IHasAlbumArtist hasAlbumArtists
- && hasAlbumArtists.AlbumArtists.Count > 0)
- {
- albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
- }
-
- saveItemStatement.TryBind("@AlbumArtists", albumArtists);
- saveItemStatement.TryBind("@ExternalId", item.ExternalId);
-
- if (item is LiveTvProgram program)
- {
- saveItemStatement.TryBind("@ShowId", program.ShowId);
- }
- else
- {
- saveItemStatement.TryBindNull("@ShowId");
- }
-
- Guid ownerId = item.OwnerId;
- if (ownerId.IsEmpty())
- {
- saveItemStatement.TryBindNull("@OwnerId");
- }
- else
- {
- saveItemStatement.TryBind("@OwnerId", ownerId);
- }
-
- saveItemStatement.ExecuteNonQuery();
- }
-
- internal static string SerializeProviderIds(Dictionary<string, string> providerIds)
- {
- StringBuilder str = new StringBuilder();
- foreach (var i in providerIds)
- {
- // Ideally we shouldn't need this IsNullOrWhiteSpace check,
- // but we're seeing some cases of bad data slip through
- if (string.IsNullOrWhiteSpace(i.Value))
- {
- continue;
- }
-
- str.Append(i.Key)
- .Append('=')
- .Append(i.Value)
- .Append('|');
- }
-
- if (str.Length == 0)
- {
- return null;
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal static void DeserializeProviderIds(string value, IHasProviderIds item)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return;
- }
-
- foreach (var part in value.SpanSplit('|'))
- {
- var providerDelimiterIndex = part.IndexOf('=');
- // Don't let empty values through
- if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1)
- {
- item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString());
- }
- }
- }
-
- internal string SerializeImages(ItemImageInfo[] images)
- {
- if (images.Length == 0)
- {
- return null;
- }
-
- StringBuilder str = new StringBuilder();
- foreach (var i in images)
- {
- if (string.IsNullOrWhiteSpace(i.Path))
- {
- continue;
- }
-
- AppendItemImageInfo(str, i);
- str.Append('|');
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal ItemImageInfo[] DeserializeImages(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return Array.Empty<ItemImageInfo>();
- }
-
- // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
- var valueSpan = value.AsSpan();
- var count = valueSpan.Count('|') + 1;
-
- var position = 0;
- var result = new ItemImageInfo[count];
- foreach (var part in valueSpan.Split('|'))
- {
- var image = ItemImageInfoFromValueString(part);
-
- if (image is not null)
- {
- result[position++] = image;
- }
- }
-
- if (position == count)
- {
- return result;
- }
-
- if (position == 0)
- {
- return Array.Empty<ItemImageInfo>();
- }
-
- // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
- return result[..position];
- }
-
- private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
- {
- const char Delimiter = '*';
-
- var path = image.Path ?? string.Empty;
-
- bldr.Append(GetPathToSave(path))
- .Append(Delimiter)
- .Append(image.DateModified.Ticks)
- .Append(Delimiter)
- .Append(image.Type)
- .Append(Delimiter)
- .Append(image.Width)
- .Append(Delimiter)
- .Append(image.Height);
-
- var hash = image.BlurHash;
- if (!string.IsNullOrEmpty(hash))
- {
- bldr.Append(Delimiter)
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- .Append(hash.Replace(Delimiter, '/').Replace('|', '\\'));
- }
- }
-
- internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value)
- {
- const char Delimiter = '*';
-
- var nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- return null;
- }
-
- ReadOnlySpan<char> path = value[..nextSegment];
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- return null;
- }
-
- ReadOnlySpan<char> dateModified = value[..nextSegment];
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- nextSegment = value.Length;
- }
-
- ReadOnlySpan<char> imageType = value[..nextSegment];
-
- var image = new ItemImageInfo
- {
- Path = RestorePath(path.ToString())
- };
-
- if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
- && ticks >= DateTime.MinValue.Ticks
- && ticks <= DateTime.MaxValue.Ticks)
- {
- image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
- }
- else
- {
- return null;
- }
-
- if (Enum.TryParse(imageType, true, out ImageType type))
- {
- image.Type = type;
- }
- else
- {
- return null;
- }
-
- // Optional parameters: width*height*blurhash
- if (nextSegment + 1 < value.Length - 1)
- {
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1 || nextSegment == value.Length)
- {
- return image;
- }
-
- ReadOnlySpan<char> widthSpan = value[..nextSegment];
-
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- nextSegment = value.Length;
- }
-
- ReadOnlySpan<char> heightSpan = value[..nextSegment];
-
- if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
- && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
- {
- image.Width = width;
- image.Height = height;
- }
-
- if (nextSegment < value.Length - 1)
- {
- value = value[(nextSegment + 1)..];
- var length = value.Length;
-
- Span<char> blurHashSpan = stackalloc char[length];
- for (int i = 0; i < length; i++)
- {
- var c = value[i];
- blurHashSpan[i] = c switch
- {
- '/' => Delimiter,
- '\\' => '|',
- _ => c
- };
- }
-
- image.BlurHash = new string(blurHashSpan);
- }
- }
-
- return image;
- }
-
- /// <summary>
- /// Internal retrieve from items or users table.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
- /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
- public BaseItem RetrieveItem(Guid id)
- {
- if (id.IsEmpty())
- {
- throw new ArgumentException("Guid can't be empty", nameof(id));
- }
-
- CheckDisposed();
-
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
- {
- statement.TryBind("@guid", id);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetItem(row, new InternalItemsQuery());
- }
- }
-
- return null;
- }
-
- private bool TypeRequiresDeserialization(Type type)
- {
- if (_config.Configuration.SkipDeserializationForBasicTypes)
- {
- if (type == typeof(Channel)
- || type == typeof(UserRootFolder))
- {
- return false;
- }
- }
-
- return type != typeof(Season)
- && type != typeof(MusicArtist)
- && type != typeof(Person)
- && type != typeof(MusicGenre)
- && type != typeof(Genre)
- && type != typeof(Studio)
- && type != typeof(PlaylistsFolder)
- && type != typeof(PhotoAlbum)
- && type != typeof(Year)
- && type != typeof(Book)
- && type != typeof(LiveTvProgram)
- && type != typeof(AudioBook)
- && type != typeof(MusicAlbum);
- }
-
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
- {
- return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
- }
-
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
- {
- var typeString = reader.GetString(0);
-
- var type = _typeMapper.GetType(typeString);
-
- if (type is null)
- {
- return null;
- }
-
- BaseItem item = null;
-
- if (TypeRequiresDeserialization(type) && !skipDeserialization)
- {
- try
- {
- item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem;
- }
- catch (JsonException ex)
- {
- Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1));
- }
- }
-
- if (item is null)
- {
- try
- {
- item = Activator.CreateInstance(type) as BaseItem;
- }
- catch
- {
- }
- }
-
- if (item is null)
- {
- return null;
- }
-
- var index = 2;
-
- if (queryHasStartDate)
- {
- if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate))
- {
- hasStartDate.StartDate = startDate;
- }
-
- index++;
- }
-
- if (reader.TryReadDateTime(index++, out var endDate))
- {
- item.EndDate = endDate;
- }
-
- if (reader.TryGetGuid(index, out var guid))
- {
- item.ChannelId = guid;
- }
-
- index++;
-
- if (enableProgramAttributes)
- {
- if (item is IHasProgramAttributes hasProgramAttributes)
- {
- if (reader.TryGetBoolean(index++, out var isMovie))
- {
- hasProgramAttributes.IsMovie = isMovie;
- }
-
- if (reader.TryGetBoolean(index++, out var isSeries))
- {
- hasProgramAttributes.IsSeries = isSeries;
- }
-
- if (reader.TryGetString(index++, out var episodeTitle))
- {
- hasProgramAttributes.EpisodeTitle = episodeTitle;
- }
-
- if (reader.TryGetBoolean(index++, out var isRepeat))
- {
- hasProgramAttributes.IsRepeat = isRepeat;
- }
- }
- else
- {
- index += 4;
- }
- }
-
- if (reader.TryGetSingle(index++, out var communityRating))
- {
- item.CommunityRating = communityRating;
- }
-
- if (HasField(query, ItemFields.CustomRating))
- {
- if (reader.TryGetString(index++, out var customRating))
- {
- item.CustomRating = customRating;
- }
- }
-
- if (reader.TryGetInt32(index++, out var indexNumber))
- {
- item.IndexNumber = indexNumber;
- }
-
- if (HasField(query, ItemFields.Settings))
- {
- if (reader.TryGetBoolean(index++, out var isLocked))
- {
- item.IsLocked = isLocked;
- }
-
- if (reader.TryGetString(index++, out var preferredMetadataLanguage))
- {
- item.PreferredMetadataLanguage = preferredMetadataLanguage;
- }
-
- if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
- {
- item.PreferredMetadataCountryCode = preferredMetadataCountryCode;
- }
- }
-
- if (HasField(query, ItemFields.Width))
- {
- if (reader.TryGetInt32(index++, out var width))
- {
- item.Width = width;
- }
- }
-
- if (HasField(query, ItemFields.Height))
- {
- if (reader.TryGetInt32(index++, out var height))
- {
- item.Height = height;
- }
- }
-
- if (HasField(query, ItemFields.DateLastRefreshed))
- {
- if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
- {
- item.DateLastRefreshed = dateLastRefreshed;
- }
- }
-
- if (reader.TryGetString(index++, out var name))
- {
- item.Name = name;
- }
-
- if (reader.TryGetString(index++, out var restorePath))
- {
- item.Path = RestorePath(restorePath);
- }
-
- if (reader.TryReadDateTime(index++, out var premiereDate))
- {
- item.PremiereDate = premiereDate;
- }
-
- if (HasField(query, ItemFields.Overview))
- {
- if (reader.TryGetString(index++, out var overview))
- {
- item.Overview = overview;
- }
- }
-
- if (reader.TryGetInt32(index++, out var parentIndexNumber))
- {
- item.ParentIndexNumber = parentIndexNumber;
- }
-
- if (reader.TryGetInt32(index++, out var productionYear))
- {
- item.ProductionYear = productionYear;
- }
-
- if (reader.TryGetString(index++, out var officialRating))
- {
- item.OfficialRating = officialRating;
- }
-
- if (HasField(query, ItemFields.SortName))
- {
- if (reader.TryGetString(index++, out var forcedSortName))
- {
- item.ForcedSortName = forcedSortName;
- }
- }
-
- if (reader.TryGetInt64(index++, out var runTimeTicks))
- {
- item.RunTimeTicks = runTimeTicks;
- }
-
- if (reader.TryGetInt64(index++, out var size))
- {
- item.Size = size;
- }
-
- if (HasField(query, ItemFields.DateCreated))
- {
- if (reader.TryReadDateTime(index++, out var dateCreated))
- {
- item.DateCreated = dateCreated;
- }
- }
-
- if (reader.TryReadDateTime(index++, out var dateModified))
- {
- item.DateModified = dateModified;
- }
-
- item.Id = reader.GetGuid(index++);
-
- if (HasField(query, ItemFields.Genres))
- {
- if (reader.TryGetString(index++, out var genres))
- {
- item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (reader.TryGetGuid(index++, out var parentId))
- {
- item.ParentId = parentId;
- }
-
- if (reader.TryGetString(index++, out var audioString))
- {
- if (Enum.TryParse(audioString, true, out ProgramAudio audio))
- {
- item.Audio = audio;
- }
- }
-
- // TODO: Even if not needed by apps, the server needs it internally
- // But get this excluded from contexts where it is not needed
- if (hasServiceName)
- {
- if (item is LiveTvChannel liveTvChannel)
- {
- if (reader.TryGetString(index, out var serviceName))
- {
- liveTvChannel.ServiceName = serviceName;
- }
- }
-
- index++;
- }
-
- if (reader.TryGetBoolean(index++, out var isInMixedFolder))
- {
- item.IsInMixedFolder = isInMixedFolder;
- }
-
- if (HasField(query, ItemFields.DateLastSaved))
- {
- if (reader.TryReadDateTime(index++, out var dateLastSaved))
- {
- item.DateLastSaved = dateLastSaved;
- }
- }
-
- if (HasField(query, ItemFields.Settings))
- {
- if (reader.TryGetString(index++, out var lockedFields))
- {
- List<MetadataField> fields = null;
- foreach (var i in lockedFields.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- (fields ??= new List<MetadataField>()).Add(parsedValue);
- }
- }
-
- item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
- }
- }
-
- if (HasField(query, ItemFields.Studios))
- {
- if (reader.TryGetString(index++, out var studios))
- {
- item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (HasField(query, ItemFields.Tags))
- {
- if (reader.TryGetString(index++, out var tags))
- {
- item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (hasTrailerTypes)
- {
- if (item is Trailer trailer)
- {
- if (reader.TryGetString(index, out var trailerTypes))
- {
- List<TrailerType> types = null;
- foreach (var i in trailerTypes.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- (types ??= new List<TrailerType>()).Add(parsedValue);
- }
- }
-
- trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
- }
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.OriginalTitle))
- {
- if (reader.TryGetString(index++, out var originalTitle))
- {
- item.OriginalTitle = originalTitle;
- }
- }
-
- if (item is Video video)
- {
- if (reader.TryGetString(index, out var primaryVersionId))
- {
- video.PrimaryVersionId = primaryVersionId;
- }
- }
-
- index++;
-
- if (HasField(query, ItemFields.DateLastMediaAdded))
- {
- if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded))
- {
- folder.DateLastMediaAdded = dateLastMediaAdded;
- }
-
- index++;
- }
-
- if (reader.TryGetString(index++, out var album))
- {
- item.Album = album;
- }
-
- if (reader.TryGetSingle(index++, out var lUFS))
- {
- item.LUFS = lUFS;
- }
-
- if (reader.TryGetSingle(index++, out var normalizationGain))
- {
- item.NormalizationGain = normalizationGain;
- }
-
- if (reader.TryGetSingle(index++, out var criticRating))
- {
- item.CriticRating = criticRating;
- }
-
- if (reader.TryGetBoolean(index++, out var isVirtualItem))
- {
- item.IsVirtualItem = isVirtualItem;
- }
-
- if (item is IHasSeries hasSeriesName)
- {
- if (reader.TryGetString(index, out var seriesName))
- {
- hasSeriesName.SeriesName = seriesName;
- }
- }
-
- index++;
-
- if (hasEpisodeAttributes)
- {
- if (item is Episode episode)
- {
- if (reader.TryGetString(index, out var seasonName))
- {
- episode.SeasonName = seasonName;
- }
-
- index++;
- if (reader.TryGetGuid(index, out var seasonId))
- {
- episode.SeasonId = seasonId;
- }
- }
- else
- {
- index++;
- }
-
- index++;
- }
-
- var hasSeries = item as IHasSeries;
- if (hasSeriesFields)
- {
- if (hasSeries is not null)
- {
- if (reader.TryGetGuid(index, out var seriesId))
- {
- hasSeries.SeriesId = seriesId;
- }
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.PresentationUniqueKey))
- {
- if (reader.TryGetString(index++, out var presentationUniqueKey))
- {
- item.PresentationUniqueKey = presentationUniqueKey;
- }
- }
-
- if (HasField(query, ItemFields.InheritedParentalRatingValue))
- {
- if (reader.TryGetInt32(index++, out var parentalRating))
- {
- item.InheritedParentalRatingValue = parentalRating;
- }
- }
-
- if (HasField(query, ItemFields.ExternalSeriesId))
- {
- if (reader.TryGetString(index++, out var externalSeriesId))
- {
- item.ExternalSeriesId = externalSeriesId;
- }
- }
-
- if (HasField(query, ItemFields.Taglines))
- {
- if (reader.TryGetString(index++, out var tagLine))
- {
- item.Tagline = tagLine;
- }
- }
-
- if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds))
- {
- DeserializeProviderIds(providerIds, item);
- }
-
- index++;
-
- if (query.DtoOptions.EnableImages)
- {
- if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos))
- {
- item.ImageInfos = DeserializeImages(imageInfos);
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.ProductionLocations))
- {
- if (reader.TryGetString(index++, out var productionLocations))
- {
- item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (HasField(query, ItemFields.ExtraIds))
- {
- if (reader.TryGetString(index++, out var extraIds))
- {
- item.ExtraIds = SplitToGuids(extraIds);
- }
- }
-
- if (reader.TryGetInt32(index++, out var totalBitrate))
- {
- item.TotalBitrate = totalBitrate;
- }
-
- if (reader.TryGetString(index++, out var extraTypeString))
- {
- if (Enum.TryParse(extraTypeString, true, out ExtraType extraType))
- {
- item.ExtraType = extraType;
- }
- }
-
- if (hasArtistFields)
- {
- if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists))
- {
- hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
-
- index++;
-
- if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists))
- {
- hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
-
- index++;
- }
-
- if (reader.TryGetString(index++, out var externalId))
- {
- item.ExternalId = externalId;
- }
-
- if (HasField(query, ItemFields.SeriesPresentationUniqueKey))
- {
- if (hasSeries is not null)
- {
- if (reader.TryGetString(index, out var seriesPresentationUniqueKey))
- {
- hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- }
- }
-
- index++;
- }
-
- if (enableProgramAttributes)
- {
- if (item is LiveTvProgram program && reader.TryGetString(index, out var showId))
- {
- program.ShowId = showId;
- }
-
- index++;
- }
-
- if (reader.TryGetGuid(index, out var ownerId))
- {
- item.OwnerId = ownerId;
- }
-
- return item;
- }
-
- private static Guid[] SplitToGuids(string value)
- {
- var ids = value.Split('|');
-
- var result = new Guid[ids.Length];
-
- for (var i = 0; i < result.Length; i++)
- {
- result[i] = new Guid(ids[i]);
- }
-
- return result;
- }
-
- /// <inheritdoc />
- public List<ChapterInfo> GetChapters(BaseItem item)
- {
- CheckDisposed();
-
- var chapters = new List<ChapterInfo>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
- {
- statement.TryBind("@ItemId", item.Id);
-
- foreach (var row in statement.ExecuteQuery())
- {
- chapters.Add(GetChapter(row, item));
- }
- }
-
- return chapters;
- }
-
- /// <inheritdoc />
- public ChapterInfo GetChapter(BaseItem item, int index)
- {
- CheckDisposed();
-
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
- {
- statement.TryBind("@ItemId", item.Id);
- statement.TryBind("@ChapterIndex", index);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetChapter(row, item);
- }
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the chapter.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <param name="item">The item.</param>
- /// <returns>ChapterInfo.</returns>
- private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item)
- {
- var chapter = new ChapterInfo
- {
- StartPositionTicks = reader.GetInt64(0)
- };
-
- if (reader.TryGetString(1, out var chapterName))
- {
- chapter.Name = chapterName;
- }
-
- if (reader.TryGetString(2, out var imagePath))
- {
- chapter.ImagePath = imagePath;
- chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
- }
-
- if (reader.TryReadDateTime(3, out var imageDateModified))
- {
- chapter.ImageDateModified = imageDateModified;
- }
-
- return chapter;
- }
-
- /// <summary>
- /// Saves the chapters.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <param name="chapters">The chapters.</param>
- public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
- {
- CheckDisposed();
-
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(chapters);
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // First delete chapters
- using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId");
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertChapters(id, chapters, connection);
- transaction.Commit();
- }
-
- private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
- {
- var startIndex = 0;
- var limit = 100;
- var chapterIndex = 0;
-
- const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values ";
- var insertText = new StringBuilder(StartInsertText, 256);
-
- while (startIndex < chapters.Count)
- {
- var endIndex = Math.Min(chapters.Count, startIndex + limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
- }
-
- insertText.Length -= 1; // Remove trailing comma
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", idBlob);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var chapter = chapters[i];
-
- statement.TryBind("@ChapterIndex" + index, chapterIndex);
- statement.TryBind("@StartPositionTicks" + index, chapter.StartPositionTicks);
- statement.TryBind("@Name" + index, chapter.Name);
- statement.TryBind("@ImagePath" + index, chapter.ImagePath);
- statement.TryBind("@ImageDateModified" + index, chapter.ImageDateModified);
-
- chapterIndex++;
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- private static bool EnableJoinUserData(InternalItemsQuery query)
- {
- if (query.User is null)
- {
- return false;
- }
-
- var sortingFields = new HashSet<ItemSortBy>(query.OrderBy.Select(i => i.OrderBy));
-
- return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
- || sortingFields.Contains(ItemSortBy.IsPlayed)
- || sortingFields.Contains(ItemSortBy.IsUnplayed)
- || sortingFields.Contains(ItemSortBy.PlayCount)
- || sortingFields.Contains(ItemSortBy.DatePlayed)
- || sortingFields.Contains(ItemSortBy.SeriesDatePlayed)
- || query.IsFavoriteOrLiked.HasValue
- || query.IsFavorite.HasValue
- || query.IsResumable.HasValue
- || query.IsPlayed.HasValue
- || query.IsLiked.HasValue;
- }
-
- private bool HasField(InternalItemsQuery query, ItemFields name)
- {
- switch (name)
- {
- case ItemFields.Tags:
- return query.DtoOptions.ContainsField(name) || HasProgramAttributes(query);
- case ItemFields.CustomRating:
- case ItemFields.ProductionLocations:
- case ItemFields.Settings:
- case ItemFields.OriginalTitle:
- case ItemFields.Taglines:
- case ItemFields.SortName:
- case ItemFields.Studios:
- case ItemFields.ExtraIds:
- case ItemFields.DateCreated:
- case ItemFields.Overview:
- case ItemFields.Genres:
- case ItemFields.DateLastMediaAdded:
- case ItemFields.PresentationUniqueKey:
- case ItemFields.InheritedParentalRatingValue:
- case ItemFields.ExternalSeriesId:
- case ItemFields.SeriesPresentationUniqueKey:
- case ItemFields.DateLastRefreshed:
- case ItemFields.DateLastSaved:
- return query.DtoOptions.ContainsField(name);
- case ItemFields.ServiceName:
- return HasServiceName(query);
- default:
- return true;
- }
- }
-
- private bool HasProgramAttributes(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
- }
-
- private bool HasServiceName(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
- }
-
- private bool HasStartDate(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x));
- }
-
- private bool HasEpisodeAttributes(InternalItemsQuery query)
- {
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Episode);
- }
-
- private bool HasTrailerTypes(InternalItemsQuery query)
- {
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Trailer);
- }
-
- private bool HasArtistFields(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _artistExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
- }
-
- private bool HasSeriesFields(InternalItemsQuery query)
- {
- if (query.ParentType == BaseItemKind.PhotoAlbum)
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
- }
-
- private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
- {
- foreach (var field in _allItemFields)
- {
- if (!HasField(query, field))
- {
- switch (field)
- {
- case ItemFields.Settings:
- columns.Remove("IsLocked");
- columns.Remove("PreferredMetadataCountryCode");
- columns.Remove("PreferredMetadataLanguage");
- columns.Remove("LockedFields");
- break;
- case ItemFields.ServiceName:
- columns.Remove("ExternalServiceId");
- break;
- case ItemFields.SortName:
- columns.Remove("ForcedSortName");
- break;
- case ItemFields.Taglines:
- columns.Remove("Tagline");
- break;
- case ItemFields.Tags:
- columns.Remove("Tags");
- break;
- case ItemFields.IsHD:
- // do nothing
- break;
- default:
- columns.Remove(field.ToString());
- break;
- }
- }
- }
-
- if (!HasProgramAttributes(query))
- {
- columns.Remove("IsMovie");
- columns.Remove("IsSeries");
- columns.Remove("EpisodeTitle");
- columns.Remove("IsRepeat");
- columns.Remove("ShowId");
- }
-
- if (!HasEpisodeAttributes(query))
- {
- columns.Remove("SeasonName");
- columns.Remove("SeasonId");
- }
-
- if (!HasStartDate(query))
- {
- columns.Remove("StartDate");
- }
-
- if (!HasTrailerTypes(query))
- {
- columns.Remove("TrailerTypes");
- }
-
- if (!HasArtistFields(query))
- {
- columns.Remove("AlbumArtists");
- columns.Remove("Artists");
- }
-
- if (!HasSeriesFields(query))
- {
- columns.Remove("SeriesId");
- }
-
- if (!HasEpisodeAttributes(query))
- {
- columns.Remove("SeasonName");
- columns.Remove("SeasonId");
- }
-
- if (!query.DtoOptions.EnableImages)
- {
- columns.Remove("Images");
- }
-
- if (EnableJoinUserData(query))
- {
- columns.Add("UserDatas.UserId");
- columns.Add("UserDatas.lastPlayedDate");
- columns.Add("UserDatas.playbackPositionTicks");
- columns.Add("UserDatas.playcount");
- columns.Add("UserDatas.isFavorite");
- columns.Add("UserDatas.played");
- columns.Add("UserDatas.rating");
- }
-
- if (query.SimilarTo is not null)
- {
- var item = query.SimilarTo;
-
- var builder = new StringBuilder();
- builder.Append('(');
-
- if (item.InheritedParentalRatingValue == 0)
- {
- builder.Append("((InheritedParentalRatingValue=0) * 10)");
- }
- else
- {
- builder.Append(
- @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
- THEN 0
- ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
- END)");
- }
-
- if (item.ProductionYear.HasValue)
- {
- builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 10 Else 0 End )");
- builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
- }
-
- // genres, tags, studios, person, year?
- builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
- builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))");
-
- if (item is MusicArtist)
- {
- // Match albums where the artist is AlbumArtist against other albums.
- // It is assumed that similar albums => similar artists.
- builder.Append(
- @"+ (WITH artistValues AS (
- SELECT DISTINCT albumValues.CleanValue
- FROM ItemValues albumValues
- INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
- INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
- ), similarArtist AS (
- SELECT albumValues.ItemId
- FROM ItemValues albumValues
- INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
- INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
- ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
- }
-
- builder.Append(") as SimilarityScore");
-
- columns.Add(builder.ToString());
-
- query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds];
- query.ExcludeProviderIds = item.ProviderIds;
- }
-
- if (!string.IsNullOrEmpty(query.SearchTerm))
- {
- var builder = new StringBuilder();
- builder.Append('(');
-
- builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
- builder.Append("+ ((CleanName = @SearchTermStartsWith COLLATE NOCASE or (OriginalTitle not null and OriginalTitle = @SearchTermStartsWith COLLATE NOCASE)) * 10)");
-
- if (query.SearchTerm.Length > 1)
- {
- builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
- }
-
- builder.Append(") as SearchScore");
-
- columns.Add(builder.ToString());
- }
- }
-
- private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement)
- {
- var searchTerm = query.SearchTerm;
-
- if (string.IsNullOrEmpty(searchTerm))
- {
- return;
- }
-
- searchTerm = FixUnicodeChars(searchTerm);
- searchTerm = GetCleanValue(searchTerm);
-
- var commandText = statement.CommandText;
- if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
- }
-
- if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
- }
- }
-
- private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement)
- {
- var item = query.SimilarTo;
-
- if (item is null)
- {
- return;
- }
-
- var commandText = statement.CommandText;
-
- if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@ItemOfficialRating", item.OfficialRating);
- }
-
- if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0);
- }
-
- if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SimilarItemId", item.Id);
- }
-
- if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
- }
- }
-
- private string GetJoinUserDataText(InternalItemsQuery query)
- {
- if (!EnableJoinUserData(query))
- {
- return string.Empty;
- }
-
- return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)";
- }
-
- private string GetGroupBy(InternalItemsQuery query)
- {
- var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
- if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
- {
- return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
- }
-
- if (enableGroupByPresentationUniqueKey)
- {
- return " Group by PresentationUniqueKey";
- }
-
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return " Group by SeriesPresentationUniqueKey";
- }
-
- return string.Empty;
- }
-
- /// <inheritdoc />
- public int GetCount(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = new List<string> { "count(distinct PresentationUniqueKey)" };
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 256)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- var commandText = commandTextBuilder.ToString();
-
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- return statement.SelectScalarInt();
- }
- }
-
- /// <inheritdoc />
- public List<BaseItem> GetItemList(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = _retrieveItemColumns.ToList();
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 1024)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var commandText = commandTextBuilder.ToString();
- var items = new List<BaseItem>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
- if (item is not null)
- {
- items.Add(item);
- }
- }
- }
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.EnableGroupByMetadataKey)
- {
- var limit = query.Limit ?? int.MaxValue;
- limit -= 4;
- var newList = new List<BaseItem>();
-
- foreach (var item in items)
- {
- AddItem(newList, item);
-
- if (newList.Count >= limit)
- {
- break;
- }
- }
-
- items = newList;
- }
-
- return items;
- }
-
- private string FixUnicodeChars(string buffer)
- {
- buffer = buffer.Replace('\u2013', '-'); // en dash
- buffer = buffer.Replace('\u2014', '-'); // em dash
- buffer = buffer.Replace('\u2015', '-'); // horizontal bar
- buffer = buffer.Replace('\u2017', '_'); // double low line
- buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
- buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
- buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
- buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
- buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
- buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
- buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
- buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
- buffer = buffer.Replace('\u2032', '\''); // prime
- buffer = buffer.Replace('\u2033', '\"'); // double prime
- buffer = buffer.Replace('\u0060', '\''); // grave accent
- return buffer.Replace('\u00B4', '\''); // acute accent
- }
-
- private void AddItem(List<BaseItem> items, BaseItem newItem)
- {
- for (var i = 0; i < items.Count; i++)
- {
- var item = items[i];
-
- foreach (var providerId in newItem.ProviderIds)
- {
- if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal))
- {
- continue;
- }
-
- if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal))
- {
- if (newItem.SourceType == SourceType.Library)
- {
- items[i] = newItem;
- }
-
- return;
- }
- }
- }
-
- items.Add(newItem);
- }
-
- /// <inheritdoc />
- public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0))
- {
- var returnList = GetItemList(query);
- return new QueryResult<BaseItem>(
- query.StartIndex,
- returnList.Count,
- returnList);
- }
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = _retrieveItemColumns.ToList();
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 512)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
-
- var whereText = whereClauses.Count == 0 ?
- string.Empty :
- string.Join(" AND ", whereClauses);
-
- if (!string.IsNullOrEmpty(whereText))
- {
- commandTextBuilder.Append(" where ")
- .Append(whereText);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- var itemQuery = string.Empty;
- var totalRecordCountQuery = string.Empty;
- if (!isReturningZeroItems)
- {
- itemQuery = commandTextBuilder.ToString();
- }
-
- if (query.EnableTotalRecordCount)
- {
- commandTextBuilder.Clear();
-
- commandTextBuilder.Append(" select ");
-
- List<string> columnsToSelect;
- if (EnableGroupByPresentationUniqueKey(query))
- {
- columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
- }
- else if (query.GroupBySeriesPresentationUniqueKey)
- {
- columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
- }
- else
- {
- columnsToSelect = new List<string> { "count (guid)" };
- }
-
- SetFinalColumnsToSelect(query, columnsToSelect);
-
- commandTextBuilder.AppendJoin(',', columnsToSelect)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
- if (!string.IsNullOrEmpty(whereText))
- {
- commandTextBuilder.Append(" where ")
- .Append(whereText);
- }
-
- totalRecordCountQuery = commandTextBuilder.ToString();
- }
-
- var list = new List<BaseItem>();
- var result = new QueryResult<BaseItem>();
- using var connection = GetConnection(true);
- using var transaction = connection.BeginTransaction();
- if (!isReturningZeroItems)
- {
- using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
- using (var statement = PrepareStatement(connection, itemQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
- if (item is not null)
- {
- list.Add(item);
- }
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
- using (var statement = PrepareStatement(connection, totalRecordCountQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- result.TotalRecordCount = statement.SelectScalarInt();
- }
- }
-
- transaction.Commit();
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
- return result;
- }
-
- private string GetOrderByText(InternalItemsQuery query)
- {
- var orderBy = query.OrderBy;
- bool hasSimilar = query.SimilarTo is not null;
- bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm);
-
- if (hasSimilar || hasSearch)
- {
- List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
- if (hasSearch)
- {
- prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
- prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
- }
-
- if (hasSimilar)
- {
- prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
- prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
- }
-
- orderBy = query.OrderBy = [.. prepend, .. orderBy];
- }
- else if (orderBy.Count == 0)
- {
- return string.Empty;
- }
-
- return " ORDER BY " + string.Join(',', orderBy.Select(i =>
- {
- var sortBy = MapOrderByField(i.OrderBy, query);
- var sortOrder = i.SortOrder == SortOrder.Ascending ? "ASC" : "DESC";
- return sortBy + " " + sortOrder;
- }));
- }
-
- private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
- {
- return sortBy switch
- {
- ItemSortBy.AirTime => "SortName", // TODO
- ItemSortBy.Runtime => "RuntimeTicks",
- ItemSortBy.Random => "RANDOM()",
- ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
- ItemSortBy.DatePlayed => "LastPlayedDate",
- ItemSortBy.PlayCount => "PlayCount",
- ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
- ItemSortBy.IsFolder => "IsFolder",
- ItemSortBy.IsPlayed => "played",
- ItemSortBy.IsUnplayed => "played",
- ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
- ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
- ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
- ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
- ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
- ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
- ItemSortBy.SeriesSortName => "SeriesName",
- ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
- ItemSortBy.Album => "Album",
- ItemSortBy.DateCreated => "DateCreated",
- ItemSortBy.PremiereDate => "PremiereDate",
- ItemSortBy.StartDate => "StartDate",
- ItemSortBy.Name => "Name",
- ItemSortBy.CommunityRating => "CommunityRating",
- ItemSortBy.ProductionYear => "ProductionYear",
- ItemSortBy.CriticRating => "CriticRating",
- ItemSortBy.VideoBitRate => "VideoBitRate",
- ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
- ItemSortBy.IndexNumber => "IndexNumber",
- ItemSortBy.SimilarityScore => "SimilarityScore",
- ItemSortBy.SearchScore => "SearchScore",
- _ => "SortName"
- };
- }
-
- /// <inheritdoc />
- public List<Guid> GetItemIdsList(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- var columns = new List<string> { "guid" };
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 256)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var commandText = commandTextBuilder.ToString();
- var list = new List<Guid>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row.GetGuid(0));
- }
- }
-
- return list;
- }
-
- private bool IsAlphaNumeric(string str)
- {
- if (string.IsNullOrWhiteSpace(str))
- {
- return false;
- }
-
- for (int i = 0; i < str.Length; i++)
- {
- if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
- {
- return false;
- }
- }
-
- return true;
- }
-
- private bool IsValidPersonType(string value)
- {
- return IsAlphaNumeric(value);
- }
-
-#nullable enable
- private List<string> GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement)
- {
- if (query.IsResumable ?? false)
- {
- query.IsVirtualItem = false;
- }
-
- var minWidth = query.MinWidth;
- var maxWidth = query.MaxWidth;
-
- if (query.IsHD.HasValue)
- {
- const int Threshold = 1200;
- if (query.IsHD.Value)
- {
- minWidth = Threshold;
- }
- else
- {
- maxWidth = Threshold - 1;
- }
- }
-
- if (query.Is4K.HasValue)
- {
- const int Threshold = 3800;
- if (query.Is4K.Value)
- {
- minWidth = Threshold;
- }
- else
- {
- maxWidth = Threshold - 1;
- }
- }
-
- var whereClauses = new List<string>();
-
- if (minWidth.HasValue)
- {
- whereClauses.Add("Width>=@MinWidth");
- statement?.TryBind("@MinWidth", minWidth);
- }
-
- if (query.MinHeight.HasValue)
- {
- whereClauses.Add("Height>=@MinHeight");
- statement?.TryBind("@MinHeight", query.MinHeight);
- }
-
- if (maxWidth.HasValue)
- {
- whereClauses.Add("Width<=@MaxWidth");
- statement?.TryBind("@MaxWidth", maxWidth);
- }
-
- if (query.MaxHeight.HasValue)
- {
- whereClauses.Add("Height<=@MaxHeight");
- statement?.TryBind("@MaxHeight", query.MaxHeight);
- }
-
- if (query.IsLocked.HasValue)
- {
- whereClauses.Add("IsLocked=@IsLocked");
- statement?.TryBind("@IsLocked", query.IsLocked);
- }
-
- var tags = query.Tags.ToList();
- var excludeTags = query.ExcludeTags.ToList();
-
- if (query.IsMovie == true)
- {
- if (query.IncludeItemTypes.Length == 0
- || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
- || query.IncludeItemTypes.Contains(BaseItemKind.Trailer))
- {
- whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
- }
- else
- {
- whereClauses.Add("IsMovie=@IsMovie");
- }
-
- statement?.TryBind("@IsMovie", true);
- }
- else if (query.IsMovie.HasValue)
- {
- whereClauses.Add("IsMovie=@IsMovie");
- statement?.TryBind("@IsMovie", query.IsMovie);
- }
-
- if (query.IsSeries.HasValue)
- {
- whereClauses.Add("IsSeries=@IsSeries");
- statement?.TryBind("@IsSeries", query.IsSeries);
- }
-
- if (query.IsSports.HasValue)
- {
- if (query.IsSports.Value)
- {
- tags.Add("Sports");
- }
- else
- {
- excludeTags.Add("Sports");
- }
- }
-
- if (query.IsNews.HasValue)
- {
- if (query.IsNews.Value)
- {
- tags.Add("News");
- }
- else
- {
- excludeTags.Add("News");
- }
- }
-
- if (query.IsKids.HasValue)
- {
- if (query.IsKids.Value)
- {
- tags.Add("Kids");
- }
- else
- {
- excludeTags.Add("Kids");
- }
- }
-
- if (query.SimilarTo is not null && query.MinSimilarityScore > 0)
- {
- whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture));
- }
-
- if (!string.IsNullOrEmpty(query.SearchTerm))
- {
- whereClauses.Add("SearchScore > 0");
- }
-
- if (query.IsFolder.HasValue)
- {
- whereClauses.Add("IsFolder=@IsFolder");
- statement?.TryBind("@IsFolder", query.IsFolder);
- }
-
- var includeTypes = query.IncludeItemTypes;
- // Only specify excluded types if no included types are specified
- if (query.IncludeItemTypes.Length == 0)
- {
- var excludeTypes = query.ExcludeItemTypes;
- if (excludeTypes.Length == 1)
- {
- if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
- {
- whereClauses.Add("type<>@type");
- statement?.TryBind("@type", excludeTypeName);
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]);
- }
- }
- else if (excludeTypes.Length > 1)
- {
- var whereBuilder = new StringBuilder("type not in (");
- foreach (var excludeType in excludeTypes)
- {
- if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
- {
- whereBuilder
- .Append('\'')
- .Append(baseItemKindName)
- .Append("',");
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType);
- }
- }
-
- // Remove trailing comma.
- whereBuilder.Length--;
- whereBuilder.Append(')');
- whereClauses.Add(whereBuilder.ToString());
- }
- }
- else if (includeTypes.Length == 1)
- {
- if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
- {
- whereClauses.Add("type=@type");
- statement?.TryBind("@type", includeTypeName);
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]);
- }
- }
- else if (includeTypes.Length > 1)
- {
- var whereBuilder = new StringBuilder("type in (");
- foreach (var includeType in includeTypes)
- {
- if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
- {
- whereBuilder
- .Append('\'')
- .Append(baseItemKindName)
- .Append("',");
- }
- else
- {
- Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType);
- }
- }
-
- // Remove trailing comma.
- whereBuilder.Length--;
- whereBuilder.Append(')');
- whereClauses.Add(whereBuilder.ToString());
- }
-
- if (query.ChannelIds.Count == 1)
- {
- whereClauses.Add("ChannelId=@ChannelId");
- statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- else if (query.ChannelIds.Count > 1)
- {
- var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add($"ChannelId in ({inClause})");
- }
-
- if (!query.ParentId.IsEmpty())
- {
- whereClauses.Add("ParentId=@ParentId");
- statement?.TryBind("@ParentId", query.ParentId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.Path))
- {
- whereClauses.Add("Path=@Path");
- statement?.TryBind("@Path", GetPathToSave(query.Path));
- }
-
- if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
- {
- whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey");
- statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey);
- }
-
- if (query.MinCommunityRating.HasValue)
- {
- whereClauses.Add("CommunityRating>=@MinCommunityRating");
- statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value);
- }
-
- if (query.MinIndexNumber.HasValue)
- {
- whereClauses.Add("IndexNumber>=@MinIndexNumber");
- statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
- }
-
- if (query.MinParentAndIndexNumber.HasValue)
- {
- whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
- statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
- statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
- }
-
- if (query.MinDateCreated.HasValue)
- {
- whereClauses.Add("DateCreated>=@MinDateCreated");
- statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value);
- }
-
- if (query.MinDateLastSaved.HasValue)
- {
- whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
- statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value);
- }
-
- if (query.MinDateLastSavedForUser.HasValue)
- {
- whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
- statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value);
- }
-
- if (query.IndexNumber.HasValue)
- {
- whereClauses.Add("IndexNumber=@IndexNumber");
- statement?.TryBind("@IndexNumber", query.IndexNumber.Value);
- }
-
- if (query.ParentIndexNumber.HasValue)
- {
- whereClauses.Add("ParentIndexNumber=@ParentIndexNumber");
- statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value);
- }
-
- if (query.ParentIndexNumberNotEquals.HasValue)
- {
- whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)");
- statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value);
- }
-
- var minEndDate = query.MinEndDate;
- var maxEndDate = query.MaxEndDate;
-
- if (query.HasAired.HasValue)
- {
- if (query.HasAired.Value)
- {
- maxEndDate = DateTime.UtcNow;
- }
- else
- {
- minEndDate = DateTime.UtcNow;
- }
- }
-
- if (minEndDate.HasValue)
- {
- whereClauses.Add("EndDate>=@MinEndDate");
- statement?.TryBind("@MinEndDate", minEndDate.Value);
- }
-
- if (maxEndDate.HasValue)
- {
- whereClauses.Add("EndDate<=@MaxEndDate");
- statement?.TryBind("@MaxEndDate", maxEndDate.Value);
- }
-
- if (query.MinStartDate.HasValue)
- {
- whereClauses.Add("StartDate>=@MinStartDate");
- statement?.TryBind("@MinStartDate", query.MinStartDate.Value);
- }
-
- if (query.MaxStartDate.HasValue)
- {
- whereClauses.Add("StartDate<=@MaxStartDate");
- statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value);
- }
-
- if (query.MinPremiereDate.HasValue)
- {
- whereClauses.Add("PremiereDate>=@MinPremiereDate");
- statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value);
- }
-
- if (query.MaxPremiereDate.HasValue)
- {
- whereClauses.Add("PremiereDate<=@MaxPremiereDate");
- statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
- }
-
- StringBuilder clauseBuilder = new StringBuilder();
- const string Or = " OR ";
-
- var trailerTypes = query.TrailerTypes;
- int trailerTypesLen = trailerTypes.Length;
- if (trailerTypesLen > 0)
- {
- clauseBuilder.Append('(');
-
- for (int i = 0; i < trailerTypesLen; i++)
- {
- var paramName = "@TrailerTypes" + i;
- clauseBuilder.Append("TrailerTypes like ")
- .Append(paramName)
- .Append(Or);
- statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
- }
-
- clauseBuilder.Length -= Or.Length;
- clauseBuilder.Append(')');
-
- whereClauses.Add(clauseBuilder.ToString());
-
- clauseBuilder.Length = 0;
- }
-
- if (query.IsAiring.HasValue)
- {
- if (query.IsAiring.Value)
- {
- whereClauses.Add("StartDate<=@MaxStartDate");
- statement?.TryBind("@MaxStartDate", DateTime.UtcNow);
-
- whereClauses.Add("EndDate>=@MinEndDate");
- statement?.TryBind("@MinEndDate", DateTime.UtcNow);
- }
- else
- {
- whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)");
- statement?.TryBind("@IsAiringDate", DateTime.UtcNow);
- }
- }
-
- int personIdsLen = query.PersonIds.Length;
- if (personIdsLen > 0)
- {
- // TODO: Should this query with CleanName ?
-
- clauseBuilder.Append('(');
-
- Span<byte> idBytes = stackalloc byte[16];
- for (int i = 0; i < personIdsLen; i++)
- {
- string paramName = "@PersonId" + i;
- clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
- .Append(paramName)
- .Append("))) OR ");
-
- statement?.TryBind(paramName, query.PersonIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- clauseBuilder.Append(')');
-
- whereClauses.Add(clauseBuilder.ToString());
-
- clauseBuilder.Length = 0;
- }
-
- if (!string.IsNullOrWhiteSpace(query.Person))
- {
- whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)");
- statement?.TryBind("@PersonName", query.Person);
- }
-
- if (!string.IsNullOrWhiteSpace(query.MinSortName))
- {
- whereClauses.Add("SortName>=@MinSortName");
- statement?.TryBind("@MinSortName", query.MinSortName);
- }
-
- if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId))
- {
- whereClauses.Add("ExternalSeriesId=@ExternalSeriesId");
- statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.ExternalId))
- {
- whereClauses.Add("ExternalId=@ExternalId");
- statement?.TryBind("@ExternalId", query.ExternalId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.Name))
- {
- whereClauses.Add("CleanName=@Name");
- statement?.TryBind("@Name", GetCleanValue(query.Name));
- }
-
- // These are the same, for now
- var nameContains = query.NameContains;
- if (!string.IsNullOrWhiteSpace(nameContains))
- {
- whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)");
- if (statement is not null)
- {
- nameContains = FixUnicodeChars(nameContains);
- statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
- }
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
- {
- whereClauses.Add("SortName like @NameStartsWith");
- statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%");
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
- {
- whereClauses.Add("SortName >= @NameStartsWithOrGreater");
- // lowercase this because SortName is stored as lowercase
- statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant());
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameLessThan))
- {
- whereClauses.Add("SortName < @NameLessThan");
- // lowercase this because SortName is stored as lowercase
- statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant());
- }
-
- if (query.ImageTypes.Length > 0)
- {
- foreach (var requiredImage in query.ImageTypes)
- {
- whereClauses.Add("Images like '%" + requiredImage + "%'");
- }
- }
-
- if (query.IsLiked.HasValue)
- {
- if (query.IsLiked.Value)
- {
- whereClauses.Add("rating>=@UserRating");
- statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
- }
- else
- {
- whereClauses.Add("(rating is null or rating<@UserRating)");
- statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
- }
- }
-
- if (query.IsFavoriteOrLiked.HasValue)
- {
- if (query.IsFavoriteOrLiked.Value)
- {
- whereClauses.Add("IsFavorite=@IsFavoriteOrLiked");
- }
- else
- {
- whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)");
- }
-
- statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value);
- }
-
- if (query.IsFavorite.HasValue)
- {
- if (query.IsFavorite.Value)
- {
- whereClauses.Add("IsFavorite=@IsFavorite");
- }
- else
- {
- whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)");
- }
-
- statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
- }
-
- if (EnableJoinUserData(query))
- {
- if (query.IsPlayed.HasValue)
- {
- // We should probably figure this out for all folders, but for right now, this is the only place where we need it
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series)
- {
- if (query.IsPlayed.Value)
- {
- whereClauses.Add("PresentationUniqueKey not in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)");
- }
- else
- {
- whereClauses.Add("PresentationUniqueKey in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)");
- }
- }
- else
- {
- if (query.IsPlayed.Value)
- {
- whereClauses.Add("(played=@IsPlayed)");
- }
- else
- {
- whereClauses.Add("(played is null or played=@IsPlayed)");
- }
-
- statement?.TryBind("@IsPlayed", query.IsPlayed.Value);
- }
- }
- }
-
- if (query.IsResumable.HasValue)
- {
- if (query.IsResumable.Value)
- {
- whereClauses.Add("playbackPositionTicks > 0");
- }
- else
- {
- whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)");
- }
- }
-
- if (query.ArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") and Type<=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.AlbumArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.AlbumArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") and Type=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.ContributingArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ContributingArtistIds.Length; i++)
- {
- clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.AlbumIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.AlbumIds.Length; i++)
- {
- clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds")
- .Append(i)
- .Append(") OR ");
- statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.ExcludeArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ExcludeArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId")
- .Append(i)
- .Append(") and Type<=1)) OR ");
- statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.GenreIds.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.GenreIds.Count; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId")
- .Append(i)
- .Append(") and Type=2)) OR ");
- statement?.TryBind("@GenreId" + i, query.GenreIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.Genres.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.Genres.Count; i++)
- {
- clauseBuilder.Append("@Genre")
- .Append(i)
- .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR ");
- statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (tags.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < tags.Count; i++)
- {
- clauseBuilder.Append("@Tag")
- .Append(i)
- .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
- statement?.TryBind("@Tag" + i, GetCleanValue(tags[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (excludeTags.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < excludeTags.Count; i++)
- {
- clauseBuilder.Append("@ExcludeTag")
- .Append(i)
- .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
- statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.StudioIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.StudioIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId")
- .Append(i)
- .Append(") and Type=3)) OR ");
- statement?.TryBind("@StudioId" + i, query.StudioIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.OfficialRatings.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.OfficialRatings.Length; i++)
- {
- clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or);
- statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- clauseBuilder.Append('(');
- if (query.HasParentalRating ?? false)
- {
- clauseBuilder.Append("InheritedParentalRatingValue not null");
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
- }
-
- if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
- }
- else if (query.BlockUnratedItems.Length > 0)
- {
- const string ParamName = "@UnratedType";
- clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (");
-
- for (int i = 0; i < query.BlockUnratedItems.Length; i++)
- {
- clauseBuilder.Append(ParamName).Append(i).Append(',');
- statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString());
- }
-
- // Remove trailing comma
- clauseBuilder.Length--;
- clauseBuilder.Append("))");
-
- if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" OR (");
- }
-
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
- }
-
- if (query.MaxParentalRating.HasValue)
- {
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND ");
- }
-
- clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
-
- if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(')');
- }
-
- if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
- {
- clauseBuilder.Append(" OR InheritedParentalRatingValue not null");
- }
- }
- else if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
-
- if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
-
- clauseBuilder.Append(')');
- }
- else if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
- else if (!query.HasParentalRating ?? false)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null");
- }
-
- if (clauseBuilder.Length > 1)
- {
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.HasOfficialRating.HasValue)
- {
- if (query.HasOfficialRating.Value)
- {
- whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')");
- }
- else
- {
- whereClauses.Add("(OfficialRating is null OR OfficialRating='')");
- }
- }
-
- if (query.HasOverview.HasValue)
- {
- if (query.HasOverview.Value)
- {
- whereClauses.Add("(Overview not null AND Overview<>'')");
- }
- else
- {
- whereClauses.Add("(Overview is null OR Overview='')");
- }
- }
-
- if (query.HasOwnerId.HasValue)
- {
- if (query.HasOwnerId.Value)
- {
- whereClauses.Add("OwnerId not null");
- }
- else
- {
- whereClauses.Add("OwnerId is null");
- }
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
- }
-
- if (query.HasSubtitles.HasValue)
- {
- if (query.HasSubtitles.Value)
- {
- whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)");
- }
- else
- {
- whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)");
- }
- }
-
- if (query.HasChapterImages.HasValue)
- {
- if (query.HasChapterImages.Value)
- {
- whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)");
- }
- else
- {
- whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)");
- }
- }
-
- if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value)
- {
- whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)");
- }
-
- if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value)
- {
- whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))");
- }
-
- if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value)
- {
- whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)");
- }
-
- if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value)
- {
- whereClauses.Add("Name not in (Select Name From People)");
- }
-
- if (query.Years.Length == 1)
- {
- whereClauses.Add("ProductionYear=@Years");
- statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
- }
- else if (query.Years.Length > 1)
- {
- var val = string.Join(',', query.Years);
- whereClauses.Add("ProductionYear in (" + val + ")");
- }
-
- var isVirtualItem = query.IsVirtualItem ?? query.IsMissing;
- if (isVirtualItem.HasValue)
- {
- whereClauses.Add("IsVirtualItem=@IsVirtualItem");
- statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
- }
-
- if (query.IsSpecialSeason.HasValue)
- {
- if (query.IsSpecialSeason.Value)
- {
- whereClauses.Add("IndexNumber = 0");
- }
- else
- {
- whereClauses.Add("IndexNumber <> 0");
- }
- }
-
- if (query.IsUnaired.HasValue)
- {
- if (query.IsUnaired.Value)
- {
- whereClauses.Add("PremiereDate >= DATETIME('now')");
- }
- else
- {
- whereClauses.Add("PremiereDate < DATETIME('now')");
- }
- }
-
- if (query.MediaTypes.Length == 1)
- {
- whereClauses.Add("MediaType=@MediaTypes");
- statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString());
- }
- else if (query.MediaTypes.Length > 1)
- {
- var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'"));
- whereClauses.Add("MediaType in (" + val + ")");
- }
-
- if (query.ItemIds.Length > 0)
- {
- var includeIds = new List<string>();
- var index = 0;
- foreach (var id in query.ItemIds)
- {
- includeIds.Add("Guid = @IncludeId" + index);
- statement?.TryBind("@IncludeId" + index, id);
- index++;
- }
-
- whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")");
- }
-
- if (query.ExcludeItemIds.Length > 0)
- {
- var excludeIds = new List<string>();
- var index = 0;
- foreach (var id in query.ExcludeItemIds)
- {
- excludeIds.Add("Guid <> @ExcludeId" + index);
- statement?.TryBind("@ExcludeId" + index, id);
- index++;
- }
-
- whereClauses.Add(string.Join(" AND ", excludeIds));
- }
-
- if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0)
- {
- var excludeIds = new List<string>();
-
- var index = 0;
- foreach (var pair in query.ExcludeProviderIds)
- {
- if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var paramName = "@ExcludeProviderId" + index;
- excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
- statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- index++;
-
- break;
- }
-
- if (excludeIds.Count > 0)
- {
- whereClauses.Add(string.Join(" AND ", excludeIds));
- }
- }
-
- if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0)
- {
- var hasProviderIds = new List<string>();
-
- var index = 0;
- foreach (var pair in query.HasAnyProviderId)
- {
- if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // TODO this seems to be an idea for a better schema where ProviderIds are their own table
- // but this is not implemented
- // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
-
- // TODO this is a really BAD way to do it since the pair:
- // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567
- // and maybe even NotTmdb=1234.
-
- // this is a placeholder for this specific pair to correlate it in the bigger query
- var paramName = "@HasAnyProviderId" + index;
-
- // this is a search for the placeholder
- hasProviderIds.Add("ProviderIds like " + paramName);
-
- // this replaces the placeholder with a value, here: %key=val%
- statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- index++;
-
- break;
- }
-
- if (hasProviderIds.Count > 0)
- {
- whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")");
- }
- }
-
- if (query.HasImdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb"));
- }
-
- if (query.HasTmdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb"));
- }
-
- if (query.HasTvdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
- }
-
- var queryTopParentIds = query.TopParentIds;
-
- if (queryTopParentIds.Length > 0)
- {
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
- if (queryTopParentIds.Length == 1)
- {
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
-
- statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
- }
- }
- }
-
- if (query.AncestorIds.Length == 1)
- {
- whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
- statement?.TryBind("@AncestorId", query.AncestorIds[0]);
- }
-
- if (query.AncestorIds.Length > 1)
- {
- var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
- }
-
- if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
- {
- var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
- whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
- statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
- }
-
- if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
- {
- whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
- statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
- }
-
- if (query.ExcludeInheritedTags.Length > 0)
- {
- var paramName = "@ExcludeInheritedTags";
- if (statement is null)
- {
- int index = 0;
- string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
- }
- else
- {
- for (int index = 0; index < query.ExcludeInheritedTags.Length; index++)
- {
- statement.TryBind(paramName + index, GetCleanValue(query.ExcludeInheritedTags[index]));
- }
- }
- }
-
- if (query.IncludeInheritedTags.Length > 0)
- {
- var paramName = "@IncludeInheritedTags";
- if (statement is null)
- {
- int index = 0;
- string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
- // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
- // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
- if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
- {
- whereClauses.Add($"""
- ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
- OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
- """);
- }
-
- // A playlist should be accessible to its owner regardless of allowed tags.
- else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
- {
- whereClauses.Add($"""
- ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
- OR data like @PlaylistOwnerUserId)
- """);
- }
- else
- {
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
- }
- }
- else
- {
- for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
- {
- statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
- }
-
- if (query.User is not null)
- {
- statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%""");
- }
- }
- }
-
- if (query.SeriesStatuses.Length > 0)
- {
- var statuses = new List<string>();
-
- foreach (var seriesStatus in query.SeriesStatuses)
- {
- statuses.Add("data like '%" + seriesStatus + "%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", statuses) + ")");
- }
-
- if (query.BoxSetLibraryFolders.Length > 0)
- {
- var folderIdQueries = new List<string>();
-
- foreach (var folderId in query.BoxSetLibraryFolders)
- {
- folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")");
- }
-
- if (query.VideoTypes.Length > 0)
- {
- var videoTypes = new List<string>();
-
- foreach (var videoType in query.VideoTypes)
- {
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
- }
-
- if (query.Is3D.HasValue)
- {
- if (query.Is3D.Value)
- {
- whereClauses.Add("data like '%Video3DFormat%'");
- }
- else
- {
- whereClauses.Add("data not like '%Video3DFormat%'");
- }
- }
-
- if (query.IsPlaceHolder.HasValue)
- {
- if (query.IsPlaceHolder.Value)
- {
- whereClauses.Add("data like '%\"IsPlaceHolder\":true%'");
- }
- else
- {
- whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')");
- }
- }
-
- if (query.HasSpecialFeature.HasValue)
- {
- if (query.HasSpecialFeature.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasTrailer.HasValue)
- {
- if (query.HasTrailer.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasThemeSong.HasValue)
- {
- if (query.HasThemeSong.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasThemeVideo.HasValue)
- {
- if (query.HasThemeVideo.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- return whereClauses;
- }
-
- /// <summary>
- /// Formats a where clause for the specified provider.
- /// </summary>
- /// <param name="includeResults">Whether or not to include items with this provider's ids.</param>
- /// <param name="provider">Provider name.</param>
- /// <returns>Formatted SQL clause.</returns>
- private string GetProviderIdClause(bool includeResults, string provider)
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- "ProviderIds {0} like '%{1}=%'",
- includeResults ? string.Empty : "not",
- provider);
- }
-
-#nullable disable
- private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
- {
- var list = new List<string>();
-
- if (IsTypeInQuery(BaseItemKind.Person, query))
- {
- list.Add(typeof(Person).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.Genre, query))
- {
- list.Add(typeof(Genre).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
- {
- list.Add(typeof(MusicGenre).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
- {
- list.Add(typeof(MusicArtist).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.Studio, query))
- {
- list.Add(typeof(Studio).FullName);
- }
-
- return list;
- }
-
- private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
- {
- if (query.ExcludeItemTypes.Contains(type))
- {
- return false;
- }
-
- return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
- }
-
- private string GetCleanValue(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return value;
- }
-
- return value.RemoveDiacritics().ToLowerInvariant();
- }
-
- private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
- {
- if (!query.GroupByPresentationUniqueKey)
- {
- return false;
- }
-
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return false;
- }
-
- if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
- {
- return false;
- }
-
- if (query.User is null)
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
- || query.IncludeItemTypes.Contains(BaseItemKind.Video)
- || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
- || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
- || query.IncludeItemTypes.Contains(BaseItemKind.Series)
- || query.IncludeItemTypes.Contains(BaseItemKind.Season);
- }
-
- /// <inheritdoc />
- public void UpdateInheritedValues()
- {
- const string Statements = """
-delete from ItemValues where type = 6;
-insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4;
-insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
-FROM AncestorIds
-LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
-where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4;
-""";
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- connection.Execute(Statements);
- transaction.Commit();
- }
-
- /// <inheritdoc />
- public void DeleteItem(Guid id)
- {
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- CheckDisposed();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete people
- ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id);
-
- // Delete chapters
- ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id);
-
- // Delete media streams
- ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id);
-
- // Delete ancestors
- ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id);
-
- // Delete item values
- ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id);
-
- // Delete the item
- ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id);
-
- transaction.Commit();
- }
-
- private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
- {
- using (var statement = PrepareStatement(db, query))
- {
- statement.TryBind("@Id", value);
-
- statement.ExecuteNonQuery();
- }
- }
-
- /// <inheritdoc />
- public List<string> GetPeopleNames(InternalPeopleQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- var commandText = new StringBuilder("select Distinct p.Name from People p");
-
- var whereClauses = GetPeopleWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
- }
-
- commandText.Append(" order by ListOrder");
-
- if (query.Limit > 0)
- {
- commandText.Append(" LIMIT ").Append(query.Limit);
- }
-
- var list = new List<string>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText.ToString()))
- {
- // Run this again to bind the params
- GetPeopleWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row.GetString(0));
- }
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public List<PersonInfo> GetPeople(InternalPeopleQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p");
-
- var whereClauses = GetPeopleWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
- }
-
- commandText.Append(" order by ListOrder");
-
- if (query.Limit > 0)
- {
- commandText.Append(" LIMIT ").Append(query.Limit);
- }
-
- var list = new List<PersonInfo>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText.ToString()))
- {
- // Run this again to bind the params
- GetPeopleWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetPerson(row));
- }
- }
-
- return list;
- }
-
- private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement)
- {
- var whereClauses = new List<string>();
-
- if (query.User is not null && query.IsFavorite.HasValue)
- {
- whereClauses.Add(@"p.Name IN (
-SELECT Name FROM TypedBaseItems WHERE UserDataKey IN (
-SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
-AND Type = @InternalPersonType)");
- statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
- statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
- statement?.TryBind("@UserId", query.User.InternalId);
- }
-
- if (!query.ItemId.IsEmpty())
- {
- whereClauses.Add("ItemId=@ItemId");
- statement?.TryBind("@ItemId", query.ItemId);
- }
-
- if (!query.AppearsInItemId.IsEmpty())
- {
- whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
- statement?.TryBind("@AppearsInItemId", query.AppearsInItemId);
- }
-
- var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
-
- if (queryPersonTypes.Count == 1)
- {
- whereClauses.Add("PersonType=@PersonType");
- statement?.TryBind("@PersonType", queryPersonTypes[0]);
- }
- else if (queryPersonTypes.Count > 1)
- {
- var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
-
- whereClauses.Add("PersonType in (" + val + ")");
- }
-
- var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList();
-
- if (queryExcludePersonTypes.Count == 1)
- {
- whereClauses.Add("PersonType<>@PersonType");
- statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
- }
- else if (queryExcludePersonTypes.Count > 1)
- {
- var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
-
- whereClauses.Add("PersonType not in (" + val + ")");
- }
-
- if (query.MaxListOrder.HasValue)
- {
- whereClauses.Add("ListOrder<=@MaxListOrder");
- statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameContains))
- {
- whereClauses.Add("p.Name like @NameContains");
- statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
- }
-
- return whereClauses;
- }
-
- private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- ArgumentNullException.ThrowIfNull(ancestorIds);
-
- CheckDisposed();
-
- // First delete
- deleteAncestorsStatement.TryBind("@ItemId", itemId);
- deleteAncestorsStatement.ExecuteNonQuery();
-
- if (ancestorIds.Count == 0)
- {
- return;
- }
-
- var insertText = new StringBuilder("insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values ");
-
- for (var i = 0; i < ancestorIds.Count; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
- i.ToString(CultureInfo.InvariantCulture));
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", itemId);
-
- for (var i = 0; i < ancestorIds.Count; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var ancestorId = ancestorIds[i];
-
- statement.TryBind("@AncestorId" + index, ancestorId);
- statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.ExecuteNonQuery();
- }
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
- }
-
- /// <inheritdoc />
- public List<string> GetStudioNames()
- {
- return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
- }
-
- /// <inheritdoc />
- public List<string> GetAllArtistNames()
- {
- return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
- }
-
- /// <inheritdoc />
- public List<string> GetMusicGenreNames()
- {
- return GetItemValueNames(
- new[] { 2 },
- new string[]
- {
- typeof(Audio).FullName,
- typeof(MusicVideo).FullName,
- typeof(MusicAlbum).FullName,
- typeof(MusicArtist).FullName
- },
- Array.Empty<string>());
- }
-
- /// <inheritdoc />
- public List<string> GetGenreNames()
- {
- return GetItemValueNames(
- new[] { 2 },
- Array.Empty<string>(),
- new string[]
- {
- typeof(Audio).FullName,
- typeof(MusicVideo).FullName,
- typeof(MusicAlbum).FullName,
- typeof(MusicArtist).FullName
- });
- }
-
- private List<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
- {
- CheckDisposed();
-
- var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
- if (itemValueTypes.Length == 1)
- {
- stringBuilder.Append('=')
- .Append(itemValueTypes[0]);
- }
- else
- {
- stringBuilder.Append(" in (")
- .AppendJoin(',', itemValueTypes)
- .Append(')');
- }
-
- if (withItemTypes.Count > 0)
- {
- stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
- .AppendJoinInSingleQuotes(',', withItemTypes)
- .Append("))");
- }
-
- if (excludeItemTypes.Count > 0)
- {
- stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
- .AppendJoinInSingleQuotes(',', excludeItemTypes)
- .Append("))");
- }
-
- stringBuilder.Append(" Group By CleanValue");
- var commandText = stringBuilder.ToString();
-
- var list = new List<string>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- if (row.TryGetString(0, out var result))
- {
- list.Add(result);
- }
- }
- }
-
- return list;
- }
-
- private QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- if (!query.Limit.HasValue)
- {
- query.EnableTotalRecordCount = false;
- }
-
- CheckDisposed();
-
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0]) :
- ("Type in (" + string.Join(',', itemValueTypes) + ")");
-
- InternalItemsQuery typeSubQuery = null;
-
- string itemCountColumns = null;
-
- var stringBuilder = new StringBuilder(1024);
- var typesToCount = query.IncludeItemTypes;
-
- if (typesToCount.Length > 0)
- {
- stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
-
- typeSubQuery = new InternalItemsQuery(query.User)
- {
- ExcludeItemTypes = query.ExcludeItemTypes,
- IncludeItemTypes = query.IncludeItemTypes,
- MediaTypes = query.MediaTypes,
- AncestorIds = query.AncestorIds,
- ExcludeItemIds = query.ExcludeItemIds,
- ItemIds = query.ItemIds,
- TopParentIds = query.TopParentIds,
- ParentId = query.ParentId,
- IsPlayed = query.IsPlayed
- };
- var whereClauses = GetWhereClauses(typeSubQuery, null);
-
- stringBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses)
- .Append(" AND ")
- .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
- .Append(typeClause)
- .Append(")) as itemTypes");
-
- itemCountColumns = stringBuilder.ToString();
- stringBuilder.Clear();
- }
-
- List<string> columns = _retrieveItemColumns.ToList();
- // Unfortunately we need to add it to columns to ensure the order of the columns in the select
- if (!string.IsNullOrEmpty(itemCountColumns))
- {
- columns.Add(itemCountColumns);
- }
-
- // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
- var innerQuery = new InternalItemsQuery(query.User)
- {
- ExcludeItemTypes = query.ExcludeItemTypes,
- IncludeItemTypes = query.IncludeItemTypes,
- MediaTypes = query.MediaTypes,
- AncestorIds = query.AncestorIds,
- ItemIds = query.ItemIds,
- TopParentIds = query.TopParentIds,
- ParentId = query.ParentId,
- IsAiring = query.IsAiring,
- IsMovie = query.IsMovie,
- IsSports = query.IsSports,
- IsKids = query.IsKids,
- IsNews = query.IsNews,
- IsSeries = query.IsSeries
- };
-
- SetFinalColumnsToSelect(query, columns);
-
- var innerWhereClauses = GetWhereClauses(innerQuery, null);
-
- stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
- .Append(typeClause)
- .Append(" AND ItemId in (select guid from TypedBaseItems");
- if (innerWhereClauses.Count > 0)
- {
- stringBuilder.Append(" where ")
- .AppendJoin(" AND ", innerWhereClauses);
- }
-
- stringBuilder.Append("))");
-
- var outerQuery = new InternalItemsQuery(query.User)
- {
- IsPlayed = query.IsPlayed,
- IsFavorite = query.IsFavorite,
- IsFavoriteOrLiked = query.IsFavoriteOrLiked,
- IsLiked = query.IsLiked,
- IsLocked = query.IsLocked,
- NameLessThan = query.NameLessThan,
- NameStartsWith = query.NameStartsWith,
- NameStartsWithOrGreater = query.NameStartsWithOrGreater,
- Tags = query.Tags,
- OfficialRatings = query.OfficialRatings,
- StudioIds = query.StudioIds,
- GenreIds = query.GenreIds,
- Genres = query.Genres,
- Years = query.Years,
- NameContains = query.NameContains,
- SearchTerm = query.SearchTerm,
- SimilarTo = query.SimilarTo,
- ExcludeItemIds = query.ExcludeItemIds
- };
-
- var outerWhereClauses = GetWhereClauses(outerQuery, null);
- if (outerWhereClauses.Count != 0)
- {
- stringBuilder.Append(" AND ")
- .AppendJoin(" AND ", outerWhereClauses);
- }
-
- var whereText = stringBuilder.ToString();
- stringBuilder.Clear();
-
- stringBuilder.Append("select ")
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query))
- .Append(whereText)
- .Append(" group by PresentationUniqueKey");
-
- if (query.OrderBy.Count != 0
- || query.SimilarTo is not null
- || !string.IsNullOrEmpty(query.SearchTerm))
- {
- stringBuilder.Append(GetOrderByText(query));
- }
- else
- {
- stringBuilder.Append(" order by SortName");
- }
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- stringBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- stringBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- string commandText = string.Empty;
-
- if (!isReturningZeroItems)
- {
- commandText = stringBuilder.ToString();
- }
-
- string countText = string.Empty;
- if (query.EnableTotalRecordCount)
- {
- stringBuilder.Clear();
- var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
- SetFinalColumnsToSelect(query, columnsToSelect);
- stringBuilder.Append("select ")
- .AppendJoin(',', columnsToSelect)
- .Append(FromText)
- .Append(GetJoinUserDataText(query))
- .Append(whereText);
-
- countText = stringBuilder.ToString();
- }
-
- var list = new List<(BaseItem, ItemCounts)>();
- var result = new QueryResult<(BaseItem, ItemCounts)>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var transaction = connection.BeginTransaction())
- {
- if (!isReturningZeroItems)
- {
- using (var statement = PrepareStatement(connection, commandText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
- if (item is not null)
- {
- var countStartColumn = columns.Count - 1;
-
- list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
- }
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (var statement = PrepareStatement(connection, countText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- result.TotalRecordCount = statement.SelectScalarInt();
- }
- }
-
- transaction.Commit();
- }
-
- if (result.TotalRecordCount == 0)
- {
- result.TotalRecordCount = list.Count;
- }
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
-
- return result;
- }
-
- private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount)
- {
- var counts = new ItemCounts();
-
- if (typesToCount.Length == 0)
- {
- return counts;
- }
-
- if (!reader.TryGetString(countStartColumn, out var typeString))
- {
- return counts;
- }
-
- foreach (var typeName in typeString.AsSpan().Split('|'))
- {
- if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.SeriesCount++;
- }
- else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.EpisodeCount++;
- }
- else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.MovieCount++;
- }
- else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.AlbumCount++;
- }
- else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.ArtistCount++;
- }
- else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.SongCount++;
- }
- else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.TrailerCount++;
- }
-
- counts.ItemCount++;
- }
-
- return counts;
- }
-
- private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List<string> inheritedTags)
- {
- var list = new List<(int, string)>();
-
- if (item is IHasArtist hasArtist)
- {
- list.AddRange(hasArtist.Artists.Select(i => (0, i)));
- }
-
- if (item is IHasAlbumArtist hasAlbumArtist)
- {
- list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
- }
-
- list.AddRange(item.Genres.Select(i => (2, i)));
- list.AddRange(item.Studios.Select(i => (3, i)));
- list.AddRange(item.Tags.Select(i => (4, i)));
-
- // keywords was 5
-
- list.AddRange(inheritedTags.Select(i => (6, i)));
-
- // Remove all invalid values.
- list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
-
- return list;
- }
-
- private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- ArgumentNullException.ThrowIfNull(values);
-
- CheckDisposed();
-
- // First delete
- using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id");
- command.TryBind("@Id", itemId);
- command.ExecuteNonQuery();
-
- InsertItemValues(itemId, values, db);
- }
-
- private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
- {
- const int Limit = 100;
- var startIndex = 0;
-
- const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
- var insertText = new StringBuilder(StartInsertText);
- while (startIndex < values.Count)
- {
- var endIndex = Math.Min(values.Count, startIndex + Limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),",
- i);
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var currentValueInfo = values[i];
-
- var itemValue = currentValueInfo.Value;
-
- statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
- statement.TryBind("@Value" + index, itemValue);
- statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- /// <inheritdoc />
- public void UpdatePeople(Guid itemId, List<PersonInfo> people)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- CheckDisposed();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete all existing people first
- using var command = connection.CreateCommand();
- command.CommandText = "delete from People where ItemId=@ItemId";
- command.TryBind("@ItemId", itemId);
- command.ExecuteNonQuery();
-
- if (people is not null)
- {
- InsertPeople(itemId, people, connection);
- }
-
- transaction.Commit();
- }
-
- private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
- {
- const int Limit = 100;
- var startIndex = 0;
- var listIndex = 0;
-
- const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
- var insertText = new StringBuilder(StartInsertText);
- while (startIndex < people.Count)
- {
- var endIndex = Math.Min(people.Count, startIndex + Limit);
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),",
- i.ToString(CultureInfo.InvariantCulture));
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var person = people[i];
-
- statement.TryBind("@Name" + index, person.Name);
- statement.TryBind("@Role" + index, person.Role);
- statement.TryBind("@PersonType" + index, person.Type.ToString());
- statement.TryBind("@SortOrder" + index, person.SortOrder);
- statement.TryBind("@ListOrder" + index, listIndex);
-
- listIndex++;
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- private PersonInfo GetPerson(SqliteDataReader reader)
- {
- var item = new PersonInfo
- {
- ItemId = reader.GetGuid(0),
- Name = reader.GetString(1)
- };
-
- if (reader.TryGetString(2, out var role))
- {
- item.Role = role;
- }
-
- if (reader.TryGetString(3, out var type)
- && Enum.TryParse(type, true, out PersonKind personKind))
- {
- item.Type = personKind;
- }
-
- if (reader.TryGetInt32(4, out var sortOrder))
- {
- item.SortOrder = sortOrder;
- }
-
- return item;
- }
-
- /// <inheritdoc />
- public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(query);
-
- var cmdText = _mediaStreamSaveColumnsSelectQuery;
-
- if (query.Type.HasValue)
- {
- cmdText += " AND StreamType=@StreamType";
- }
-
- if (query.Index.HasValue)
- {
- cmdText += " AND StreamIndex=@StreamIndex";
- }
-
- cmdText += " order by StreamIndex ASC";
-
- using (var connection = GetConnection(true))
- {
- var list = new List<MediaStream>();
-
- using (var statement = PrepareStatement(connection, cmdText))
- {
- statement.TryBind("@ItemId", query.ItemId);
-
- if (query.Type.HasValue)
- {
- statement.TryBind("@StreamType", query.Type.Value.ToString());
- }
-
- if (query.Index.HasValue)
- {
- statement.TryBind("@StreamIndex", query.Index.Value);
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetMediaStream(row));
- }
- }
-
- return list;
- }
- }
-
- /// <inheritdoc />
- public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(streams);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete existing mediastreams
- using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId");
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertMediaStreams(id, streams, connection);
-
- transaction.Commit();
- }
-
- private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
- {
- const int Limit = 10;
- var startIndex = 0;
-
- var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
- while (startIndex < streams.Count)
- {
- var endIndex = Math.Min(streams.Count, startIndex + Limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- if (i != startIndex)
- {
- insertText.Append(',');
- }
-
- var index = i.ToString(CultureInfo.InvariantCulture);
- insertText.Append("(@ItemId, ");
-
- foreach (var column in _mediaStreamSaveColumns.Skip(1))
- {
- insertText.Append('@').Append(column).Append(index).Append(',');
- }
-
- insertText.Length -= 1; // Remove the last comma
-
- insertText.Append(')');
- }
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var stream = streams[i];
-
- statement.TryBind("@StreamIndex" + index, stream.Index);
- statement.TryBind("@StreamType" + index, stream.Type.ToString());
- statement.TryBind("@Codec" + index, stream.Codec);
- statement.TryBind("@Language" + index, stream.Language);
- statement.TryBind("@ChannelLayout" + index, stream.ChannelLayout);
- statement.TryBind("@Profile" + index, stream.Profile);
- statement.TryBind("@AspectRatio" + index, stream.AspectRatio);
- statement.TryBind("@Path" + index, GetPathToSave(stream.Path));
-
- statement.TryBind("@IsInterlaced" + index, stream.IsInterlaced);
- statement.TryBind("@BitRate" + index, stream.BitRate);
- statement.TryBind("@Channels" + index, stream.Channels);
- statement.TryBind("@SampleRate" + index, stream.SampleRate);
-
- statement.TryBind("@IsDefault" + index, stream.IsDefault);
- statement.TryBind("@IsForced" + index, stream.IsForced);
- statement.TryBind("@IsExternal" + index, stream.IsExternal);
-
- // Yes these are backwards due to a mistake
- statement.TryBind("@Width" + index, stream.Height);
- statement.TryBind("@Height" + index, stream.Width);
-
- statement.TryBind("@AverageFrameRate" + index, stream.AverageFrameRate);
- statement.TryBind("@RealFrameRate" + index, stream.RealFrameRate);
- statement.TryBind("@Level" + index, stream.Level);
-
- statement.TryBind("@PixelFormat" + index, stream.PixelFormat);
- statement.TryBind("@BitDepth" + index, stream.BitDepth);
- statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic);
- statement.TryBind("@IsExternal" + index, stream.IsExternal);
- statement.TryBind("@RefFrames" + index, stream.RefFrames);
-
- statement.TryBind("@CodecTag" + index, stream.CodecTag);
- statement.TryBind("@Comment" + index, stream.Comment);
- statement.TryBind("@NalLengthSize" + index, stream.NalLengthSize);
- statement.TryBind("@IsAvc" + index, stream.IsAVC);
- statement.TryBind("@Title" + index, stream.Title);
-
- statement.TryBind("@TimeBase" + index, stream.TimeBase);
- statement.TryBind("@CodecTimeBase" + index, stream.CodecTimeBase);
-
- statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
- statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
- statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
-
- statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
- statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
- statement.TryBind("@DvProfile" + index, stream.DvProfile);
- statement.TryBind("@DvLevel" + index, stream.DvLevel);
- statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
- statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
- statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
- statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
-
- statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
-
- statement.TryBind("@Rotation" + index, stream.Rotation);
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
- }
- }
-
- /// <summary>
- /// Gets the media stream.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <returns>MediaStream.</returns>
- private MediaStream GetMediaStream(SqliteDataReader reader)
- {
- var item = new MediaStream
- {
- Index = reader.GetInt32(1),
- Type = Enum.Parse<MediaStreamType>(reader.GetString(2), true)
- };
-
- if (reader.TryGetString(3, out var codec))
- {
- item.Codec = codec;
- }
-
- if (reader.TryGetString(4, out var language))
- {
- item.Language = language;
- }
-
- if (reader.TryGetString(5, out var channelLayout))
- {
- item.ChannelLayout = channelLayout;
- }
-
- if (reader.TryGetString(6, out var profile))
- {
- item.Profile = profile;
- }
-
- if (reader.TryGetString(7, out var aspectRatio))
- {
- item.AspectRatio = aspectRatio;
- }
-
- if (reader.TryGetString(8, out var path))
- {
- item.Path = RestorePath(path);
- }
-
- item.IsInterlaced = reader.GetBoolean(9);
-
- if (reader.TryGetInt32(10, out var bitrate))
- {
- item.BitRate = bitrate;
- }
-
- if (reader.TryGetInt32(11, out var channels))
- {
- item.Channels = channels;
- }
-
- if (reader.TryGetInt32(12, out var sampleRate))
- {
- item.SampleRate = sampleRate;
- }
-
- item.IsDefault = reader.GetBoolean(13);
- item.IsForced = reader.GetBoolean(14);
- item.IsExternal = reader.GetBoolean(15);
-
- if (reader.TryGetInt32(16, out var width))
- {
- item.Width = width;
- }
-
- if (reader.TryGetInt32(17, out var height))
- {
- item.Height = height;
- }
-
- if (reader.TryGetSingle(18, out var averageFrameRate))
- {
- item.AverageFrameRate = averageFrameRate;
- }
-
- if (reader.TryGetSingle(19, out var realFrameRate))
- {
- item.RealFrameRate = realFrameRate;
- }
-
- if (reader.TryGetSingle(20, out var level))
- {
- item.Level = level;
- }
-
- if (reader.TryGetString(21, out var pixelFormat))
- {
- item.PixelFormat = pixelFormat;
- }
-
- if (reader.TryGetInt32(22, out var bitDepth))
- {
- item.BitDepth = bitDepth;
- }
-
- if (reader.TryGetBoolean(23, out var isAnamorphic))
- {
- item.IsAnamorphic = isAnamorphic;
- }
-
- if (reader.TryGetInt32(24, out var refFrames))
- {
- item.RefFrames = refFrames;
- }
-
- if (reader.TryGetString(25, out var codecTag))
- {
- item.CodecTag = codecTag;
- }
-
- if (reader.TryGetString(26, out var comment))
- {
- item.Comment = comment;
- }
-
- if (reader.TryGetString(27, out var nalLengthSize))
- {
- item.NalLengthSize = nalLengthSize;
- }
-
- if (reader.TryGetBoolean(28, out var isAVC))
- {
- item.IsAVC = isAVC;
- }
-
- if (reader.TryGetString(29, out var title))
- {
- item.Title = title;
- }
-
- if (reader.TryGetString(30, out var timeBase))
- {
- item.TimeBase = timeBase;
- }
-
- if (reader.TryGetString(31, out var codecTimeBase))
- {
- item.CodecTimeBase = codecTimeBase;
- }
-
- if (reader.TryGetString(32, out var colorPrimaries))
- {
- item.ColorPrimaries = colorPrimaries;
- }
-
- if (reader.TryGetString(33, out var colorSpace))
- {
- item.ColorSpace = colorSpace;
- }
-
- if (reader.TryGetString(34, out var colorTransfer))
- {
- item.ColorTransfer = colorTransfer;
- }
-
- if (reader.TryGetInt32(35, out var dvVersionMajor))
- {
- item.DvVersionMajor = dvVersionMajor;
- }
-
- if (reader.TryGetInt32(36, out var dvVersionMinor))
- {
- item.DvVersionMinor = dvVersionMinor;
- }
-
- if (reader.TryGetInt32(37, out var dvProfile))
- {
- item.DvProfile = dvProfile;
- }
-
- if (reader.TryGetInt32(38, out var dvLevel))
- {
- item.DvLevel = dvLevel;
- }
-
- if (reader.TryGetInt32(39, out var rpuPresentFlag))
- {
- item.RpuPresentFlag = rpuPresentFlag;
- }
-
- if (reader.TryGetInt32(40, out var elPresentFlag))
- {
- item.ElPresentFlag = elPresentFlag;
- }
-
- if (reader.TryGetInt32(41, out var blPresentFlag))
- {
- item.BlPresentFlag = blPresentFlag;
- }
-
- if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
- {
- item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
- }
-
- item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
-
- if (reader.TryGetInt32(44, out var rotation))
- {
- item.Rotation = rotation;
- }
-
- if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
- {
- item.LocalizedDefault = _localization.GetLocalizedString("Default");
- item.LocalizedExternal = _localization.GetLocalizedString("External");
-
- if (item.Type is MediaStreamType.Subtitle)
- {
- item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
- item.LocalizedForced = _localization.GetLocalizedString("Forced");
- item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
- }
- }
-
- return item;
- }
-
- /// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(query);
-
- var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
-
- if (query.Index.HasValue)
- {
- cmdText += " AND AttachmentIndex=@AttachmentIndex";
- }
-
- cmdText += " order by AttachmentIndex ASC";
-
- var list = new List<MediaAttachment>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, cmdText))
- {
- statement.TryBind("@ItemId", query.ItemId);
-
- if (query.Index.HasValue)
- {
- statement.TryBind("@AttachmentIndex", query.Index.Value);
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetMediaAttachment(row));
- }
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public void SaveMediaAttachments(
- Guid id,
- IReadOnlyList<MediaAttachment> attachments,
- CancellationToken cancellationToken)
- {
- CheckDisposed();
- if (id.IsEmpty())
- {
- throw new ArgumentException("Guid can't be empty.", nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(attachments);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"))
- {
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertMediaAttachments(id, attachments, connection, cancellationToken);
-
- transaction.Commit();
- }
- }
-
- private void InsertMediaAttachments(
- Guid id,
- IReadOnlyList<MediaAttachment> attachments,
- ManagedConnection db,
- CancellationToken cancellationToken)
- {
- const int InsertAtOnce = 10;
-
- var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
- for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
- {
- var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.Append("(@ItemId, ");
-
- foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
- {
- insertText.Append('@')
- .Append(column)
- .Append(i)
- .Append(',');
- }
-
- insertText.Length -= 1;
-
- insertText.Append("),");
- }
-
- insertText.Length--;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var attachment = attachments[i];
-
- statement.TryBind("@AttachmentIndex" + index, attachment.Index);
- statement.TryBind("@Codec" + index, attachment.Codec);
- statement.TryBind("@CodecTag" + index, attachment.CodecTag);
- statement.TryBind("@Comment" + index, attachment.Comment);
- statement.TryBind("@Filename" + index, attachment.FileName);
- statement.TryBind("@MIMEType" + index, attachment.MimeType);
- }
-
- statement.ExecuteNonQuery();
- }
-
- insertText.Length = _mediaAttachmentInsertPrefix.Length;
- }
- }
-
- /// <summary>
- /// Gets the attachment.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <returns>MediaAttachment.</returns>
- private MediaAttachment GetMediaAttachment(SqliteDataReader reader)
- {
- var item = new MediaAttachment
- {
- Index = reader.GetInt32(1)
- };
-
- if (reader.TryGetString(2, out var codec))
- {
- item.Codec = codec;
- }
-
- if (reader.TryGetString(3, out var codecTag))
- {
- item.CodecTag = codecTag;
- }
-
- if (reader.TryGetString(4, out var comment))
- {
- item.Comment = comment;
- }
-
- if (reader.TryGetString(5, out var fileName))
- {
- item.FileName = fileName;
- }
-
- if (reader.TryGetString(6, out var mimeType))
- {
- item.MimeType = mimeType;
- }
-
- return item;
- }
-
- private static string BuildMediaAttachmentInsertPrefix()
- {
- var queryPrefixText = new StringBuilder();
- queryPrefixText.Append("insert into mediaattachments (");
- foreach (var column in _mediaAttachmentSaveColumns)
- {
- queryPrefixText.Append(column)
- .Append(',');
- }
-
- queryPrefixText.Length -= 1;
- queryPrefixText.Append(") values ");
- return queryPrefixText.ToString();
- }
-
-#nullable enable
-
- private readonly struct QueryTimeLogger : IDisposable
- {
- private readonly ILogger _logger;
- private readonly string _commandText;
- private readonly string _methodName;
- private readonly long _startTimestamp;
-
- public QueryTimeLogger(ILogger logger, string commandText, [CallerMemberName] string methodName = "")
- {
- _logger = logger;
- _commandText = commandText;
- _methodName = methodName;
- _startTimestamp = logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : -1;
- }
-
- public void Dispose()
- {
- if (_startTimestamp == -1)
- {
- return;
- }
-
- var elapsedMs = Stopwatch.GetElapsedTime(_startTimestamp).TotalMilliseconds;
-
-#if DEBUG
- const int SlowThreshold = 100;
-#else
- const int SlowThreshold = 10;
-#endif
-
- if (elapsedMs >= SlowThreshold)
- {
- _logger.LogDebug(
- "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
- _methodName,
- elapsedMs,
- _commandText);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
deleted file mode 100644
index bfdcc08f4..000000000
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ /dev/null
@@ -1,369 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
- {
- private readonly IUserManager _userManager;
-
- public SqliteUserDataRepository(
- ILogger<SqliteUserDataRepository> logger,
- IServerConfigurationManager config,
- IUserManager userManager)
- : base(logger)
- {
- _userManager = userManager;
-
- DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
- }
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- public override void Initialize()
- {
- base.Initialize();
-
- using (var connection = GetConnection())
- {
- var userDatasTableExists = TableExists(connection, "UserDatas");
- var userDataTableExists = TableExists(connection, "userdata");
-
- var users = userDatasTableExists ? null : _userManager.Users;
- using var transaction = connection.BeginTransaction();
- connection.Execute(string.Join(
- ';',
- "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
- "drop index if exists idx_userdata",
- "drop index if exists idx_userdata1",
- "drop index if exists idx_userdata2",
- "drop index if exists userdataindex1",
- "drop index if exists userdataindex",
- "drop index if exists userdataindex3",
- "drop index if exists userdataindex4",
- "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
- "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
- "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
- "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
-
- if (!userDataTableExists)
- {
- transaction.Commit();
- return;
- }
-
- var existingColumnNames = GetColumnNames(connection, "userdata");
-
- AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
- AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
- AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
-
- if (userDatasTableExists)
- {
- return;
- }
-
- ImportUserIds(connection, users);
-
- connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
-
- transaction.Commit();
- }
- }
-
- private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
- {
- var userIdsWithUserData = GetAllUserIdsWithUserData(db);
-
- using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
- {
- foreach (var user in users)
- {
- if (!userIdsWithUserData.Contains(user.Id))
- {
- continue;
- }
-
- statement.TryBind("@UserId", user.Id);
- statement.TryBind("@InternalUserId", user.InternalId);
-
- statement.ExecuteNonQuery();
- }
- }
- }
-
- private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
- {
- var list = new List<Guid>();
-
- using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- try
- {
- list.Add(row.GetGuid(0));
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error while getting user");
- }
- }
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(userData);
-
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- ArgumentException.ThrowIfNullOrEmpty(key);
-
- PersistUserData(userId, key, userData, cancellationToken);
- }
-
- /// <inheritdoc />
- public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(userData);
-
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- PersistAllUserData(userId, userData, cancellationToken);
- }
-
- /// <summary>
- /// Persists the user data.
- /// </summary>
- /// <param name="internalUserId">The user id.</param>
- /// <param name="key">The key.</param>
- /// <param name="userData">The user data.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- SaveUserData(connection, internalUserId, key, userData);
- transaction.Commit();
- }
- }
-
- private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
- {
- using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
- {
- statement.TryBind("@userId", internalUserId);
- statement.TryBind("@key", key);
-
- if (userData.Rating.HasValue)
- {
- statement.TryBind("@rating", userData.Rating.Value);
- }
- else
- {
- statement.TryBindNull("@rating");
- }
-
- statement.TryBind("@played", userData.Played);
- statement.TryBind("@playCount", userData.PlayCount);
- statement.TryBind("@isFavorite", userData.IsFavorite);
- statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
-
- if (userData.LastPlayedDate.HasValue)
- {
- statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
- }
- else
- {
- statement.TryBindNull("@lastPlayedDate");
- }
-
- if (userData.AudioStreamIndex.HasValue)
- {
- statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
- }
- else
- {
- statement.TryBindNull("@AudioStreamIndex");
- }
-
- if (userData.SubtitleStreamIndex.HasValue)
- {
- statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
- }
- else
- {
- statement.TryBindNull("@SubtitleStreamIndex");
- }
-
- statement.ExecuteNonQuery();
- }
- }
-
- /// <summary>
- /// Persist all user data for the specified user.
- /// </summary>
- private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- foreach (var userItemData in userDataList)
- {
- SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
- }
-
- transaction.Commit();
- }
- }
-
- /// <summary>
- /// Gets the user data.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="key">The key.</param>
- /// <returns>Task{UserItemData}.</returns>
- /// <exception cref="ArgumentNullException">
- /// userId
- /// or
- /// key.
- /// </exception>
- public UserItemData GetUserData(long userId, string key)
- {
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- ArgumentException.ThrowIfNullOrEmpty(key);
-
- using (var connection = GetConnection(true))
- {
- using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
- {
- statement.TryBind("@UserId", userId);
- statement.TryBind("@Key", key);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return ReadRow(row);
- }
- }
-
- return null;
- }
- }
-
- public UserItemData GetUserData(long userId, List<string> keys)
- {
- ArgumentNullException.ThrowIfNull(keys);
-
- if (keys.Count == 0)
- {
- return null;
- }
-
- return GetUserData(userId, keys[0]);
- }
-
- /// <summary>
- /// Return all user-data associated with the given user.
- /// </summary>
- /// <param name="userId">The internal user id.</param>
- /// <returns>The list of user item data.</returns>
- public List<UserItemData> GetAllUserData(long userId)
- {
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- var list = new List<UserItemData>();
-
- using (var connection = GetConnection())
- {
- using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
- {
- statement.TryBind("@UserId", userId);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(ReadRow(row));
- }
- }
- }
-
- return list;
- }
-
- /// <summary>
- /// Read a row from the specified reader into the provided userData object.
- /// </summary>
- /// <param name="reader">The list of result set values.</param>
- /// <returns>The user item data.</returns>
- private UserItemData ReadRow(SqliteDataReader reader)
- {
- var userData = new UserItemData
- {
- Key = reader.GetString(0)
- };
-
- if (reader.TryGetDouble(2, out var rating))
- {
- userData.Rating = rating;
- }
-
- userData.Played = reader.GetBoolean(3);
- userData.PlayCount = reader.GetInt32(4);
- userData.IsFavorite = reader.GetBoolean(5);
- userData.PlaybackPositionTicks = reader.GetInt64(6);
-
- if (reader.TryReadDateTime(7, out var lastPlayedDate))
- {
- userData.LastPlayedDate = lastPlayedDate;
- }
-
- if (reader.TryGetInt32(8, out var audioStreamIndex))
- {
- userData.AudioStreamIndex = audioStreamIndex;
- }
-
- if (reader.TryGetInt32(9, out var subtitleStreamIndex))
- {
- userData.SubtitleStreamIndex = subtitleStreamIndex;
- }
-
- return userData;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SynchronousMode.cs b/Emby.Server.Implementations/Data/SynchronousMode.cs
deleted file mode 100644
index cde524e2e..000000000
--- a/Emby.Server.Implementations/Data/SynchronousMode.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// The disk synchronization mode, controls how aggressively SQLite will write data
-/// all the way out to physical storage.
-/// </summary>
-public enum SynchronousMode
-{
- /// <summary>
- /// SQLite continues without syncing as soon as it has handed data off to the operating system.
- /// </summary>
- Off = 0,
-
- /// <summary>
- /// SQLite database engine will still sync at the most critical moments.
- /// </summary>
- Normal = 1,
-
- /// <summary>
- /// SQLite database engine will use the xSync method of the VFS
- /// to ensure that all content is safely written to the disk surface prior to continuing.
- /// </summary>
- Full = 2,
-
- /// <summary>
- /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
- /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
- /// </summary>
- Extra = 3
-}
diff --git a/Emby.Server.Implementations/Data/TempStoreMode.cs b/Emby.Server.Implementations/Data/TempStoreMode.cs
deleted file mode 100644
index d2427ce47..000000000
--- a/Emby.Server.Implementations/Data/TempStoreMode.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// Storage mode used by temporary database files.
-/// </summary>
-public enum TempStoreMode
-{
- /// <summary>
- /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
- /// is used to determine where temporary tables and indices are stored.
- /// </summary>
- Default = 0,
-
- /// <summary>
- /// Temporary tables and indices are stored in a file.
- /// </summary>
- File = 1,
-
- /// <summary>
- /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
- /// </summary>
- Memory = 2
-}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index 2459178d8..0b3c3bbd4 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -4,6 +4,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
- private readonly object _syncLock = new object();
+ private readonly Lock _syncLock = new();
private string? _id;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 0c0ba7453..0ce967e6a 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -5,11 +5,12 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager;
+ private readonly IChapterRepository _chapterRepository;
public DtoService(
ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
- ITrickplayManager trickplayManager)
+ ITrickplayManager trickplayManager,
+ IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -76,6 +79,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager;
+ _chapterRepository = chapterRepository;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -165,7 +169,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
+ private static IReadOnlyList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
@@ -327,7 +331,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
+ private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
{
if (item is MusicArtist)
{
@@ -1060,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Chapters))
{
- dto.Chapters = _itemRepo.GetChapters(item);
+ dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.Trickplay))
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 70dd5eb9a..6722c20da 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -18,9 +18,11 @@
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="BitFaster.Caching" />
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 4c668379c..933cfc8cb 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -5,8 +5,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly IUserManager _userManager;
private readonly ILogger<LibraryChangedNotifier> _logger;
- private readonly object _libraryChangedSyncLock = new();
+ private readonly Lock _libraryChangedSyncLock = new();
private readonly List<Folder> _foldersAddedTo = new();
private readonly List<Folder> _foldersRemovedFrom = new();
private readonly List<BaseItem> _itemsAdded = new();
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index aef02ce6b..fc174b7c1 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
- private readonly object _syncLock = new();
+ private readonly Lock _syncLock = new();
private Timer? _updateTimer;
@@ -144,9 +144,15 @@ namespace Emby.Server.Implementations.EntryPoints
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
+ if (dto is null)
+ {
+ return null!;
+ }
+
dto.ItemId = i.Id;
return dto;
})
+ .Where(e => e is not null)
.ToArray()
};
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 1d04f3da3..8a79cdebc 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,7 +1,8 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
+using Jellyfin.Data;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index cb6f7e1d3..a720c86fb 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer
public WebSocketState State => _socket.State;
/// <inheritdoc />
- public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
+ public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
- return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
- public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
+ public async Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
- return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer
return ret;
}
- private Task SendKeepAliveResponse()
+ private async Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
- return SendAsync(
+ await SendAsync(
new OutboundKeepAliveMessage(),
- CancellationToken.None);
+ CancellationToken.None).ConfigureAwait(false);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 774d3563c..cb5b3993b 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Processes the web socket message received.
/// </summary>
/// <param name="result">The result.</param>
- private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+ private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
{
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
}
- return Task.WhenAll(tasks);
+ await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index e75cab64c..7378cf885 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
- private readonly List<string> _affectedPaths = new List<string>();
- private readonly object _timerLock = new object();
+ private readonly List<string> _affectedPaths = new();
+ private readonly Lock _timerLock = new();
private Timer? _timer;
private bool _disposed;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 4b68f21d5..ac5933a69 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
+ catch (IOException ex)
+ {
+ // IOException generally means the file is not accessible due to filesystem issues
+ // Catch this exception and mark the file as not exist to ignore it
+ _logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
+ result.Exists = false;
+ }
}
}
@@ -534,8 +541,8 @@ namespace Emby.Server.Implementations.IO
return DriveInfo.GetDrives()
.Where(
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
- && d.IsReady
- && d.TotalSize != 0)
+ && d.IsReady
+ && d.TotalSize != 0)
.Select(d => new FileSystemMetadata
{
Name = d.Name,
@@ -553,22 +560,36 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{
- return GetFiles(path, null, false, recursive);
+ return GetFiles(path, "*", recursive);
+ }
+
+ /// <inheritdoc />
+ public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false)
+ {
+ return GetFiles(path, searchPattern, null, false, recursive);
+ }
+
+ /// <inheritdoc />
+ public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
+ {
+ return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
}
/// <inheritdoc />
- public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+ public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
- // On linux and osx the search pattern is case sensitive
+ // On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
{
- return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
+ searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0];
+
+ return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
}
- var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
+ var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions);
if (extensions is not null && extensions.Count > 0)
{
@@ -590,6 +611,9 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
+ // Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
+ // But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
+ // For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -618,7 +642,7 @@ namespace Emby.Server.Implementations.IO
{
var enumerationOptions = GetEnumerationOptions(recursive);
- // On linux and osx the search pattern is case sensitive
+ // On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 0a3d740cc..8b2869149 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -116,9 +117,9 @@ namespace Emby.Server.Implementations.Images
var mimeType = MimeTypes.GetMimeType(outputPath);
- if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(mimeType, MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
- mimeType = "image/png";
+ mimeType = MediaTypeNames.Image.Png;
}
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index f9c10ba09..0d63b3af7 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 34c722e41..273d356a3 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index c9b41f819..706de60a9 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
index 31f053f06..c472623e6 100644
--- a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 28f7ed659..c8026960d 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2,7 +2,6 @@
#pragma warning disable CA5394
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -11,6 +10,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Server.Implementations.Library.Resolvers;
@@ -18,8 +18,10 @@ using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks.Tasks;
using Emby.Server.Implementations.Sorting;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -62,7 +64,6 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
- private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -76,13 +77,16 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepository;
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;
/// <summary>
/// The _root folder sync lock.
/// </summary>
- private readonly object _rootFolderSyncLock = new object();
- private readonly object _userRootFolderSyncLock = new object();
+ private readonly Lock _rootFolderSyncLock = new();
+ private readonly Lock _userRootFolderSyncLock = new();
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
@@ -112,6 +116,8 @@ namespace Emby.Server.Implementations.Library
/// <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>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -127,7 +133,9 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IPeopleRepository peopleRepository,
+ IPathManager pathManager)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -142,14 +150,17 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _cache = new ConcurrentDictionary<Guid, BaseItem>();
- _namingOptions = namingOptions;
+ _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
+
+ _namingOptions = namingOptions;
+ _peopleRepository = peopleRepository;
+ _pathManager = pathManager;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
- RecordConfigurationValues(configurationManager.Configuration);
+ RecordConfigurationValues(_configurationManager.Configuration);
}
/// <summary>
@@ -197,33 +208,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
+ private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
+ private IIntroProvider[] IntroProviders { get; set; } = [];
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
+ private IItemResolver[] EntityResolvers { get; set; } = [];
- private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
+ private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
+ private IBaseItemComparer[] Comparers { get; set; } = [];
public bool IsScanRunning { get; private set; }
@@ -297,7 +308,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _cache[item.Id] = item;
+ _cache.AddOrUpdate(item.Id, item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -356,7 +367,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
- : Array.Empty<BaseItem>();
+ : [];
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -451,24 +462,38 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
_itemRepository.DeleteItem(item.Id);
+ _cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
+ _cache.TryRemove(child.Id, out _);
}
- _cache.TryRemove(item.Id, out _);
-
ReportItemRemoved(item, parent);
}
- private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ {
+ var list = GetInternalMetadataPaths(item);
+ foreach (var child in children)
+ {
+ list.AddRange(GetInternalMetadataPaths(child));
+ }
+
+ return list;
+ }
+
+ private List<string> GetInternalMetadataPaths(BaseItem item)
{
var list = new List<string>
{
item.GetInternalMetadataPath()
};
- list.AddRange(children.Select(i => i.GetInternalMetadataPath()));
+ if (item is Video video)
+ {
+ list.Add(_pathManager.GetTrickplayDirectory(video));
+ }
return list;
}
@@ -589,7 +614,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf);
- files = Array.Empty<FileSystemMetadata>();
+ files = [];
}
else
{
@@ -751,14 +776,7 @@ namespace Emby.Server.Implementations.Library
if (folder.Id.IsEmpty())
{
- if (string.IsNullOrEmpty(folder.Path))
- {
- folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
- }
- else
- {
- folder.Id = GetNewItemId(folder.Path, folder.GetType());
- }
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
}
var dbItem = GetItemById(folder.Id) as BasePluginFolder;
@@ -1053,9 +1071,17 @@ namespace Emby.Server.Implementations.Library
cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
- foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
+ foreach (var child in GetUserRootFolder().Children.OfType<Folder>())
{
- await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ // If the user has somehow deleted the collection directory, remove the metadata from the database.
+ if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
+ {
+ _itemRepository.DeleteItem(collectionFolder.Id);
+ }
+ else
+ {
+ await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
}
}
@@ -1230,7 +1256,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_cache.TryGetValue(id, out BaseItem? item))
+ if (_cache.TryGet(id, out var item))
{
return item;
}
@@ -1247,7 +1273,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public T? GetItemById<T>(Guid id)
- where T : BaseItem
+ where T : BaseItem
{
var item = GetItemById(id);
if (item is T typedItem)
@@ -1274,7 +1300,7 @@ namespace Emby.Server.Implementations.Library
return ItemIsVisible(item, user) ? item : null;
}
- public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
@@ -1300,7 +1326,7 @@ namespace Emby.Server.Implementations.Library
return itemList;
}
- public List<BaseItem> GetItemList(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
{
return GetItemList(query, true);
}
@@ -1324,7 +1350,7 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetCount(query);
}
- public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -1339,6 +1365,36 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemList(query);
}
+ public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, CollectionType collectionType)
+ {
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return _itemRepository.GetLatestItemList(query, collectionType);
+ }
+
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff)
+ {
+ SetTopParentIdsOrAncestors(query, parents);
+
+ if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+ }
+
+ return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
+ }
+
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
{
if (query.User is not null)
@@ -1357,7 +1413,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.GetItemList(query));
}
- public List<Guid> GetItemIds(InternalItemsQuery query)
+ public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
{
if (query.User is not null)
{
@@ -1443,7 +1499,7 @@ namespace Emby.Server.Implementations.Library
// Optimize by querying against top level views
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
- query.AncestorIds = Array.Empty<Guid>();
+ query.AncestorIds = [];
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
@@ -1563,7 +1619,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
if (!view.ParentId.IsEmpty())
@@ -1574,7 +1630,7 @@ namespace Emby.Server.Implementations.Library
return GetTopParentIdsForQuery(displayParent, user);
}
- return Array.Empty<Guid>();
+ return [];
}
// Handle grouping
@@ -1589,7 +1645,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
- return Array.Empty<Guid>();
+ return [];
}
if (item is CollectionFolder collectionFolder)
@@ -1603,7 +1659,7 @@ namespace Emby.Server.Implementations.Library
return new[] { topParent.Id };
}
- return Array.Empty<Guid>();
+ return [];
}
/// <summary>
@@ -1647,7 +1703,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogError(ex, "Error getting intros");
- return Enumerable.Empty<IntroInfo>();
+ return [];
}
}
@@ -1955,13 +2011,13 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
+ _itemRepository.SaveItems(items, cancellationToken);
+
foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
- _itemRepository.SaveItems(items, cancellationToken);
-
if (ItemUpdated is not null)
{
foreach (var item in items)
@@ -2474,8 +2530,11 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public int? GetSeasonNumberFromPath(string path)
- => SeasonPathParser.Parse(path, true, true).SeasonNumber;
+ public int? GetSeasonNumberFromPath(string path, Guid? parentId)
+ {
+ var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
+ return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
+ }
/// <inheritdoc />
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
@@ -2626,15 +2685,6 @@ namespace Emby.Server.Implementations.Library
{
episode.ParentIndexNumber = season.IndexNumber;
}
- else
- {
- /*
- Anime series don't generally have a season in their file name, however,
- TVDb needs a season to correctly get the metadata.
- Hence, a null season needs to be filled with something. */
- // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
- episode.ParentIndexNumber = 1;
- }
if (episode.ParentIndexNumber.HasValue)
{
@@ -2659,7 +2709,7 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
- var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
@@ -2736,12 +2786,12 @@ namespace Emby.Server.Implementations.Library
return path;
}
- public List<PersonInfo> GetPeople(InternalPeopleQuery query)
+ public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{
- return _itemRepository.GetPeople(query);
+ return _peopleRepository.GetPeople(query);
}
- public List<PersonInfo> GetPeople(BaseItem item)
+ public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
{
if (item.SupportsPeople)
{
@@ -2756,12 +2806,12 @@ namespace Emby.Server.Implementations.Library
}
}
- return new List<PersonInfo>();
+ return [];
}
- public List<Person> GetPeopleItems(InternalPeopleQuery query)
+ public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query)
+ return _peopleRepository.GetPeopleNames(query)
.Select(i =>
{
try
@@ -2779,9 +2829,9 @@ namespace Emby.Server.Implementations.Library
.ToList()!; // null values are filtered out
}
- public List<string> GetPeopleNames(InternalPeopleQuery query)
+ public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query);
+ return _peopleRepository.GetPeopleNames(query);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@@ -2790,16 +2840,17 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
+ public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
return;
}
- _itemRepository.UpdatePeople(item.Id, people);
if (people is not null)
{
+ people = people.Where(e => e is not null).ToArray();
+ _peopleRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
}
@@ -2882,7 +2933,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values?
- await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
+ await File.WriteAllBytesAsync(path, []).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2914,14 +2965,13 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
- List<BaseItem>? personsToSave = null;
-
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
+ var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
@@ -2938,6 +2988,7 @@ namespace Emby.Server.Implementations.Library
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
+ createEntity = true;
}
foreach (var id in person.ProviderIds)
@@ -2965,15 +3016,15 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
- (personsToSave ??= new()).Add(personEntity);
+ if (createEntity)
+ {
+ CreateItems([personEntity], null, CancellationToken.None);
+ }
+
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ CreateItems([personEntity], null, CancellationToken.None);
}
}
-
- if (personsToSave is not null)
- {
- CreateItems(personsToSave, null, CancellationToken.None);
- }
}
private void StartScanInBackground()
@@ -3027,7 +3078,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
- libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
+ libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 90a01c052..afe5b14e9 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -12,8 +13,10 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
@@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Library
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char LiveStreamIdDelimeter = '_';
+ private const char LiveStreamIdDelimiter = '_';
private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo;
@@ -51,7 +54,8 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
-
+ private readonly IMediaStreamRepository _mediaStreamRepository;
+ private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -69,7 +73,9 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IMediaStreamRepository mediaStreamRepository,
+ IMediaAttachmentRepository mediaAttachmentRepository)
{
_appHost = appHost;
_itemRepo = itemRepo;
@@ -82,6 +88,8 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager;
_appPaths = applicationPaths;
_directoryService = directoryService;
+ _mediaStreamRepository = mediaStreamRepository;
+ _mediaAttachmentRepository = mediaAttachmentRepository;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -89,9 +97,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray();
}
- public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
+ public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
- var list = _itemRepo.GetMediaStreams(query);
+ var list = _mediaStreamRepository.GetMediaStreams(query);
foreach (var stream in list)
{
@@ -121,7 +129,7 @@ namespace Emby.Server.Implementations.Library
return false;
}
- public List<MediaStream> GetMediaStreams(Guid itemId)
+ public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
@@ -131,7 +139,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list);
}
- private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
+ private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
{
foreach (var stream in streams)
{
@@ -145,13 +153,13 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
- return _itemRepo.GetMediaAttachments(query);
+ return _mediaAttachmentRepository.GetMediaAttachments(query);
}
/// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(Guid itemId)
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
{
return GetMediaAttachments(new MediaAttachmentQuery
{
@@ -159,7 +167,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -212,7 +220,7 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
- return SortMediaSources(list);
+ return SortMediaSources(list).ToArray();
}
/// <inheritdoc />>
@@ -307,7 +315,7 @@ namespace Emby.Server.Implementations.Library
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
- var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
+ var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
@@ -332,7 +340,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
- public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
+ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
ArgumentNullException.ThrowIfNull(item);
@@ -453,7 +461,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+ private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -470,8 +478,7 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0;
})
- .Where(i => i.Type != MediaSourceType.Placeholder)
- .ToList();
+ .Where(i => i.Type != MediaSourceType.Placeholder);
}
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -777,9 +784,13 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(id);
- // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
- var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
- return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
+ var info = GetLiveStreamInfo(id);
+ if (info is null)
+ {
+ return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
+ }
+
+ return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
public ILiveStream GetLiveStreamInfo(string id)
@@ -806,7 +817,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
- public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
var stream = new MediaSourceInfo
{
@@ -829,10 +840,7 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
- return new List<MediaSourceInfo>
- {
- stream
- };
+ return [stream];
}
public async Task CloseLiveStream(string id)
@@ -864,11 +872,11 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(key);
- var keys = key.Split(LiveStreamIdDelimeter, 2);
+ var keys = key.Split(LiveStreamIdDelimiter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
- var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
+ var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
return (provider, keyId);
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index ea223e3ec..631179ffc 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
@@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ // Sort in the following order: Default > No tag > Forced
var sortedStreams = streams
.Where(i => i.Type == MediaStreamType.Subtitle)
.OrderByDescending(x => x.IsExternal)
- .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- .ThenByDescending(x => x.IsForced)
.ThenByDescending(x => x.IsDefault)
- .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+ .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+ .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
+ .ThenByDescending(x => x.IsForced)
.ToList();
MediaStream? stream = null;
+
if (mode == SubtitlePlaybackMode.Default)
{
- // Load subtitles according to external, forced and default flags.
- stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ // Load subtitles according to external, default and forced flags.
+ stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
- // If no subtitles of preferred language available, use default behaviour.
+ // If no subtitles of preferred language available, use none.
+ // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
- sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
}
else
{
- // Respect forced flag.
- stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+ stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
- stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
- sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
+ stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
+ BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // Only load subtitles that are flagged forced.
- stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+ // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+ stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
}
return stream?.Index;
@@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
- filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
+ // Load subtitles according to external, default, and forced flags.
+ filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
.ToList();
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Prefer smart logic over embedded metadata
+ // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
+ filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
.ToList();
}
+ else
+ {
+ filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
+ }
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // Always load the most suitable full subtitles
- filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
+ // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
+ filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
+ .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // Always load the most suitable full subtitles
- filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
+ // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+ filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
- // Load forced subs if we have found no suitable full subtitles
- var iterStreams = filteredStreams is null || filteredStreams.Count == 0
- ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
- : filteredStreams;
+ // If filteredStreams is null, initialize it as an empty list to avoid null reference errors
+ filteredStreams ??= new List<MediaStream>();
- foreach (var stream in iterStreams)
+ foreach (var stream in filteredStreams)
{
stream.Score = GetStreamScore(stream, preferredLanguages);
}
}
+ private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
+ {
+ // If preferredLanguages is empty, treat it as "any language" (wildcard)
+ return preferredLanguages.Count == 0 ||
+ preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsLanguageUndefined(string language)
+ {
+ // Check for null, empty, or known placeholders
+ return string.IsNullOrEmpty(language) ||
+ language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
+ language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
+ {
+ return sortedStreams
+ .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
+ .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
+ .ThenByDescending(s => IsLanguageUndefined(s.Language))
+ .ToList();
+ }
+
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index a69a0f33f..28cf69500 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -2,9 +2,11 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -24,30 +26,23 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
- public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
- var list = new List<BaseItem>
- {
- item
- };
-
- list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
-
- return list;
+ return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
/// <inheritdoc />
- public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
@@ -63,12 +58,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@@ -85,7 +80,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -97,7 +92,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
new file mode 100644
index 000000000..c910abadb
--- /dev/null
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -0,0 +1,36 @@
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IPathManager implementation.
+/// </summary>
+public class PathManager : IPathManager
+{
+ private readonly IServerConfigurationManager _config;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PathManager"/> class.
+ /// </summary>
+ /// <param name="config">The server configuration manager.</param>
+ public PathManager(
+ IServerConfigurationManager config)
+ {
+ _config = config;
+ }
+
+ /// <inheritdoc />
+ public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
+ {
+ var basePath = _config.ApplicationPaths.TrickplayPath;
+ var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+
+ return saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(basePath, idString);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index b4791b945..b9f9f2972 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
- public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
+ public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
{
- var extraResult = GetExtraInfo(path, _namingOptions);
+ var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
if (extraResult.ExtraType is null)
{
extraType = null;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 4debe722b..f1aeb1340 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
+ var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var result = new MultiItemResolverResult
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index abf2d0115..6cb63a28a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
- var seasonParserResult = SeasonPathParser.Parse(path, true, true);
+ var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
var season = new Season
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index fb48d7bf1..c81a0adb8 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
- if (IsSeasonFolder(child.FullName, isTvContentType))
+ if (IsSeasonFolder(child.FullName, path, isTvContentType))
{
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
+ /// <param name="parentPath">The parentpath.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
- private static bool IsSeasonFolder(string path, bool isTvContentType)
+ private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
{
- var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+ var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 7f3f8615e..9d81b835c 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -3,8 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -171,7 +172,7 @@ namespace Emby.Server.Implementations.Library
}
};
- List<BaseItem> mediaItems;
+ IReadOnlyList<BaseItem> mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 320685b1f..0c9edd839 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -43,14 +44,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
/// <inheritdoc />
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
- var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
- var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
+ var posters = GetItemsWithImageType(ImageType.Primary)
+ .Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
+ var backdrops = GetItemsWithImageType(ImageType.Thumb)
+ .Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
if (backdrops.Count == 0)
{
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
- backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
+ backdrops = GetItemsWithImageType(ImageType.Backdrop)
+ .Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
}
_imageEncoder.CreateSplashscreen(posters, backdrops);
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index ceb3d65a4..be1d96bf0 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,17 +1,20 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
+using System.Linq;
using System.Threading;
-using Jellyfin.Data.Entities;
+using BitFaster.Caching.Lru;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@@ -22,27 +25,22 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
- private readonly ConcurrentDictionary<string, UserItemData> _userData =
- new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
-
private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
- private readonly IUserDataRepository _repository;
+ private readonly IDbContextFactory<JellyfinDbContext> _repository;
+ private readonly FastConcurrentLru<string, UserItemData> _cache;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
+ /// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public UserDataManager(
IServerConfigurationManager config,
- IUserManager userManager,
- IUserDataRepository repository)
+ IDbContextFactory<JellyfinDbContext> repository)
{
_config = config;
- _userManager = userManager;
_repository = repository;
+ _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -59,15 +57,29 @@ namespace Emby.Server.Implementations.Library
var keys = item.GetUserDataKeys();
- var userId = user.InternalId;
+ using var dbContext = _repository.CreateDbContext();
+ using var transaction = dbContext.Database.BeginTransaction();
foreach (var key in keys)
{
- _repository.SaveUserData(userId, key, userData, cancellationToken);
+ userData.Key = key;
+ var userDataEntry = Map(userData, user.Id, item.Id);
+ if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
+ {
+ dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
+ }
+ else
+ {
+ dbContext.UserData.Add(userDataEntry);
+ }
}
+ dbContext.SaveChanges();
+ transaction.Commit();
+
+ var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
- _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
+ _cache.AddOrUpdate(cacheKey, userData);
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{
@@ -86,7 +98,7 @@ namespace Emby.Server.Implementations.Library
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(userDataDto);
- var userData = GetUserData(user, item);
+ var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
if (userDataDto.PlaybackPositionTicks.HasValue)
{
@@ -126,33 +138,91 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
- private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
+ private UserData Map(UserItemData dto, Guid userId, Guid itemId)
{
- var userId = user.InternalId;
-
- var cacheKey = GetCacheKey(userId, itemId);
+ return new UserData()
+ {
+ ItemId = itemId,
+ CustomDataKey = dto.Key,
+ Item = null,
+ User = null,
+ AudioStreamIndex = dto.AudioStreamIndex,
+ IsFavorite = dto.IsFavorite,
+ LastPlayedDate = dto.LastPlayedDate,
+ Likes = dto.Likes,
+ PlaybackPositionTicks = dto.PlaybackPositionTicks,
+ PlayCount = dto.PlayCount,
+ Played = dto.Played,
+ Rating = dto.Rating,
+ UserId = userId,
+ SubtitleStreamIndex = dto.SubtitleStreamIndex,
+ };
+ }
- return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
+ private UserItemData Map(UserData dto)
+ {
+ return new UserItemData()
+ {
+ Key = dto.CustomDataKey!,
+ AudioStreamIndex = dto.AudioStreamIndex,
+ IsFavorite = dto.IsFavorite,
+ LastPlayedDate = dto.LastPlayedDate,
+ Likes = dto.Likes,
+ PlaybackPositionTicks = dto.PlaybackPositionTicks,
+ PlayCount = dto.PlayCount,
+ Played = dto.Played,
+ Rating = dto.Rating,
+ SubtitleStreamIndex = dto.SubtitleStreamIndex,
+ };
}
- private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
+ private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
{
- var userData = _repository.GetUserData(internalUserId, keys);
+ var cacheKey = GetCacheKey(user.InternalId, itemId);
- if (userData is not null)
+ if (_cache.TryGet(cacheKey, out var data))
{
- return userData;
+ return data;
}
- if (keys.Count > 0)
+ data = GetUserDataInternal(user.Id, itemId, keys);
+
+ if (data is null)
{
- return new UserItemData
+ return new UserItemData()
{
- Key = keys[0]
+ Key = keys[0],
};
}
- throw new UnreachableException();
+ return _cache.GetOrAdd(cacheKey, _ => data);
+ }
+
+ private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
+ {
+ if (keys.Count == 0)
+ {
+ return null;
+ }
+
+ 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)
+ {
+ var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
+ if (directDataReference is not null)
+ {
+ return Map(directDataReference);
+ }
+
+ return Map(userData.First());
+ }
+
+ return new UserItemData
+ {
+ Key = keys.Last()!
+ };
}
/// <summary>
@@ -165,20 +235,25 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public UserItemData GetUserData(User user, BaseItem item)
+ public UserItemData? GetUserData(User user, BaseItem item)
{
return GetUserData(user, item.Id, item.GetUserDataKeys());
}
/// <inheritdoc />
- public UserItemDataDto GetUserDataDto(BaseItem item, User user)
+ public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
/// <inheritdoc />
- public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
+ public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
- var dto = GetUserItemDataDto(userData);
+ if (userData is null)
+ {
+ return null;
+ }
+
+ var dto = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto;
@@ -188,9 +263,10 @@ namespace Emby.Server.Implementations.Library
/// Converts a UserItemData to a DTOUserItemData.
/// </summary>
/// <param name="data">The data.</param>
+ /// <param name="itemId">The reference key to an Item.</param>
/// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
- private UserItemDataDto GetUserItemDataDto(UserItemData data)
+ private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
{
ArgumentNullException.ThrowIfNull(data);
@@ -203,6 +279,7 @@ namespace Emby.Server.Implementations.Library
Rating = data.Rating,
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
+ ItemId = itemId,
Key = data.Key
};
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e9cf47d46..87214c273 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -6,8 +6,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -308,39 +310,40 @@ namespace Emby.Server.Implementations.Library
}
}
- var mediaTypes = new List<MediaType>();
+ MediaType[] mediaTypes = [];
if (includeItemTypes.Length == 0)
{
+ HashSet<MediaType> tmpMediaTypes = [];
foreach (var parent in parents.OfType<ICollectionFolder>())
{
switch (parent.CollectionType)
{
case CollectionType.books:
- mediaTypes.Add(MediaType.Book);
- mediaTypes.Add(MediaType.Audio);
+ tmpMediaTypes.Add(MediaType.Book);
+ tmpMediaTypes.Add(MediaType.Audio);
break;
case CollectionType.music:
- mediaTypes.Add(MediaType.Audio);
+ tmpMediaTypes.Add(MediaType.Audio);
break;
case CollectionType.photos:
- mediaTypes.Add(MediaType.Photo);
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Photo);
+ tmpMediaTypes.Add(MediaType.Video);
break;
case CollectionType.homevideos:
- mediaTypes.Add(MediaType.Photo);
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Photo);
+ tmpMediaTypes.Add(MediaType.Video);
break;
default:
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Video);
break;
}
}
- mediaTypes = mediaTypes.Distinct().ToList();
+ mediaTypes = tmpMediaTypes.ToArray();
}
- var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
+ var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
? new[]
{
BaseItemKind.Person,
@@ -366,12 +369,22 @@ namespace Emby.Server.Implementations.Library
Limit = limit * 5,
IsPlayed = isPlayed,
DtoOptions = options,
- MediaTypes = mediaTypes.ToArray()
+ MediaTypes = mediaTypes
};
- if (parents.Count == 0)
+ if (request.GroupItems)
{
- return _libraryManager.GetItemList(query, false);
+ if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows))
+ {
+ query.Limit = limit;
+ return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
+ }
+
+ if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music))
+ {
+ query.Limit = limit;
+ return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
+ }
}
return _libraryManager.GetItemList(query, parents);
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index 89f64ee4f..337b1afdd 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index e9c095c67..2d29eb5bf 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -16,7 +16,7 @@
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
- "HeaderContinueWatching": "استئناف المشاهدة",
+ "HeaderContinueWatching": "إستئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
@@ -31,7 +31,7 @@
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
"LabelIpAddressValue": "عنوان الآي بي: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}",
- "Latest": "أحدث",
+ "Latest": "الأحدث",
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
@@ -52,7 +52,7 @@
"NotificationOptionInstallationFailed": "فشل في التثبيت",
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
"NotificationOptionPluginError": "فشل في الملحق",
- "NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية",
+ "NotificationOptionPluginInstalled": "ثُبتت الملحق",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
@@ -90,10 +90,10 @@
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
- "ValueSpecialEpisodeName": "حلقه خاصه - {0}",
+ "ValueSpecialEpisodeName": "حلقة خاصه - {0}",
"VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
- "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
+ "TaskCleanCache": "حذف الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
@@ -129,10 +129,12 @@
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
- "TaskAudioNormalization": "تطبيع الصوت",
+ "TaskAudioNormalization": "تسوية الصوت",
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
"TaskDownloadMissingLyricsDescription": "كلمات",
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
- "TaskExtractMediaSegmentsDescription": "وسائط"
+ "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
+ "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
+ "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 97aa0ca58..d5da04fb9 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -1,6 +1,6 @@
{
"Sync": "Сінхранізаваць",
- "Playlists": "Плэйлісты",
+ "Playlists": "Спісы прайгравання",
"Latest": "Апошні",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
@@ -16,7 +16,7 @@
"Collections": "Калекцыі",
"Default": "Па змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
- "Folders": "Папкі",
+ "Folders": "Тэчкі",
"Favorites": "Абранае",
"External": "Знешні",
"Genres": "Жанры",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 4724bba3b..268a141ff 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -125,5 +125,11 @@
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
- "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।"
+ "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
+ "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
+ "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
+ "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
+ "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
+ "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
+ "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 629efdd04..6cce0e019 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -16,7 +16,7 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum",
- "HeaderContinueWatching": "Continuar veient",
+ "HeaderContinueWatching": "Continua veient",
"HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits",
@@ -24,13 +24,13 @@
"HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
- "HeaderRecordingGroups": "Grups d'enregistrament",
+ "HeaderRecordingGroups": "Grups Musicals",
"HomeVideos": "Vídeos domèstics",
- "Inherit": "Hereta",
- "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
- "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
+ "Inherit": "Heretat",
+ "ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
+ "ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}",
- "LabelRunningTimeValue": "Temps en funcionament: {0}",
+ "LabelRunningTimeValue": "Temps en marxa: {0}",
"Latest": "Darrers",
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
@@ -44,8 +44,8 @@
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
- "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
- "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
+ "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
+ "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
@@ -54,8 +54,8 @@
"NotificationOptionPluginError": "Un complement ha fallat",
"NotificationOptionPluginInstalled": "Complement instal·lat",
"NotificationOptionPluginUninstalled": "Complement desinstal·lat",
- "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
- "NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
+ "NotificationOptionPluginUpdateInstalled": "Actualització del complement instal·lada",
+ "NotificationOptionServerRestartRequired": "El servidor s'ha de reiniciar",
"NotificationOptionTaskFailed": "Tasca programada fallida",
"NotificationOptionUserLockedOut": "Usuari expulsat",
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
@@ -64,15 +64,15 @@
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
- "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
- "PluginUpdatedWithName": "{0} ha estat actualitzat",
+ "PluginUninstalledWithName": "S'ha instalat {0}",
+ "PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
- "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
- "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
+ "ScheduledTaskStartedWithName": "S'ha iniciat {0}",
+ "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
"Shows": "Sèries",
"Songs": "Cançons",
- "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
+ "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitzar",
@@ -80,41 +80,41 @@
"TvShows": "Sèries de TV",
"User": "Usuari",
"UserCreatedWithName": "S'ha creat l'usuari {0}",
- "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
+ "UserDeletedWithName": "S'ha eliminat l'usuari {0}",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
- "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
+ "UserLockedOutWithName": "S'ha expulsat a l'usuari {0}",
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
- "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
+ "UserPasswordChangedWithName": "S'ha canviat la contrasenya per a l'usuari {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
- "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
- "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
- "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
+ "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
+ "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
+ "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
- "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
+ "TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
"TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions",
- "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
- "TaskUpdatePlugins": "Actualitza els connectors",
- "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
+ "TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
+ "TaskUpdatePlugins": "Actualitza els complements",
+ "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
"TaskRefreshPeople": "Actualitza les persones",
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres",
- "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
+ "TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
- "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
- "TaskCleanCache": "Elimina arxius temporals",
- "TasksChannelsCategory": "Canals d'internet",
- "TasksApplicationCategory": "Aplicació",
+ "TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
+ "TaskCleanCache": "Elimina la memòria cau",
+ "TasksChannelsCategory": "Canals per internet",
+ "TasksApplicationCategory": "Aplicatiu",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment",
- "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
+ "TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidar el registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
@@ -128,10 +128,13 @@
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
- "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
- "TaskAudioNormalization": "Normalització d'Àudio",
- "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio.",
- "TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons",
- "TaskDownloadMissingLyrics": "Baixar lletres que falten",
- "TaskExtractMediaSegments": "Escaneig de segments multimèdia"
+ "TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
+ "TaskAudioNormalization": "Estabilització d'Àudio",
+ "TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
+ "TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
+ "TaskDownloadMissingLyrics": "Baixar les lletres que falten",
+ "TaskExtractMediaSegments": "Escaneig de segments multimèdia",
+ "TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
+ "TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca."
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index c17fbc414..d43d4097f 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -1,5 +1,5 @@
{
- "Albums": "Album",
+ "Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation",
"Artists": "Kunstnere",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 51c9e87d5..c38af5bf4 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -5,7 +5,7 @@
"Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
- "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
+ "CameraImageUploadedFrom": "Ein neues Kamerabild wurde von {0} hochgeladen",
"Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
@@ -18,7 +18,7 @@
"HeaderAlbumArtists": "Album-Interpreten",
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
- "HeaderFavoriteArtists": "Lieblings-Interpreten",
+ "HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingslieder",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 55f266032..f3195f0ea 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -11,7 +11,7 @@
"Collections": "Συλλογές",
"DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
"DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
- "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
+ "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
@@ -27,8 +27,8 @@
"HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
"HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
- "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
- "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
+ "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
+ "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα",
@@ -40,7 +40,7 @@
"Movies": "Ταινίες",
"Music": "Μουσική",
"MusicVideos": "Μουσικά Βίντεο",
- "NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
+ "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε",
"NameSeasonNumber": "Κύκλος {0}",
"NameSeasonUnknown": "Άγνωστος Κύκλος",
"NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
@@ -54,7 +54,7 @@
"NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
"NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
"NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
- "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε",
+ "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε",
"NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
"NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
"NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
@@ -63,9 +63,9 @@
"Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής",
"Plugin": "Πρόσθετο",
- "PluginInstalledWithName": "{0} εγκαταστήθηκε",
- "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
- "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
+ "PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
+ "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
+ "PluginUpdatedWithName": "Το {0} ενημερώθηκε",
"ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία",
"ScheduledTaskStartedWithName": "{0} ξεκίνησε",
@@ -96,7 +96,7 @@
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
"TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
"TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
- "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
+ "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
"TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
"TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
"TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
@@ -125,7 +125,7 @@
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό",
"HearingImpaired": "Με προβλήματα ακοής",
- "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+ "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 0b595c2ca..42cce1096 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -122,5 +122,9 @@
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
- "External": "Ekstera"
+ "External": "Ekstera",
+ "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
+ "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
+ "TaskAudioNormalization": "Normaligo Sonnivela",
+ "HearingImpaired": "Surda"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index f2f657b04..cf31960f9 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -15,7 +15,7 @@
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas de álbum",
+ "HeaderAlbumArtists": "Artistas del álbum",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index 114c76c54..4df4b90d3 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -19,25 +19,25 @@
"Artists": "Artistak",
"Albums": "Albumak",
"TaskOptimizeDatabase": "Datu basea optimizatu",
- "TaskDownloadMissingSubtitlesDescription": "Metadataren konfigurazioan oinarrituta falta diren azpitituluak bilatzen ditu interneten.",
+ "TaskDownloadMissingSubtitlesDescription": "Falta diren azpitituluak bilatzen ditu interneten metadatuen konfigurazioaren arabera.",
"TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu",
"TaskRefreshChannelsDescription": "Internet kanalen informazioa eguneratu.",
"TaskRefreshChannels": "Kanalak eguneratu",
- "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transcode fitxategiak ezabatzen ditu.",
- "TaskCleanTranscode": "Transcode direktorioa garbitu",
- "TaskUpdatePluginsDescription": "Automatikoki eguneratzeko konfiguratutako pluginen eguneraketak deskargatu eta instalatzen ditu.",
+ "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transkodifikazio fitxategiak ezabatzen ditu.",
+ "TaskCleanTranscode": "Transkodifikazio direktorioa garbitu",
+ "TaskUpdatePluginsDescription": "Automatikoki deskargatu eta instalatu eguneraketak konfiguratutako pluginetarako.",
"TaskUpdatePlugins": "Pluginak eguneratu",
- "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadata eguneratzen du.",
+ "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadatuak eguneratzen ditu.",
"TaskRefreshPeople": "Jendea eguneratu",
"TaskCleanLogsDescription": "{0} egun baino zaharragoak diren log fitxategiak ezabatzen ditu.",
"TaskCleanLogs": "Log direktorioa garbitu",
- "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatak eguneratzeko.",
- "TaskRefreshLibrary": "Multimedia Liburutegia eskaneatu",
+ "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatuak eguneratzeko.",
+ "TaskRefreshLibrary": "Multimedia liburutegia eskaneatu",
"TaskRefreshChapterImagesDescription": "Kapituluak dituzten bideoen miniaturak sortzen ditu.",
"TaskRefreshChapterImages": "Kapituluen irudiak erauzi",
"TaskCleanCacheDescription": "Sistemak behar ez dituen cache fitxategiak ezabatzen ditu.",
- "TaskCleanCache": "Cache Directorioa garbitu",
- "TaskCleanActivityLogDescription": "Konfiguratuta data baino zaharragoak diren log-ak ezabatu.",
+ "TaskCleanCache": "Cache direktorioa garbitu",
+ "TaskCleanActivityLogDescription": "Konfiguratutako baino zaharragoak diren jarduera-log sarrerak ezabatzen ditu.",
"TaskCleanActivityLog": "Erabilera Log-a garbitu",
"TasksChannelsCategory": "Internet Kanalak",
"TasksApplicationCategory": "Aplikazioa",
@@ -45,22 +45,22 @@
"TasksMaintenanceCategory": "Mantenua",
"VersionNumber": "Bertsioa {0}",
"ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da",
- "UserStoppedPlayingItemWithValues": "{0}-ek {1} ikusteaz bukatu du {2}-(a)n",
- "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(a)n",
- "UserPolicyUpdatedWithName": "{0} Erabiltzailearen politikak aldatu dira",
- "UserPasswordChangedWithName": "{0} Erabiltzailearen pasahitza aldatu da",
- "UserOnlineFromDevice": "{0} online dago {1}-tik",
- "UserOfflineFromDevice": "{0} {1}-tik deskonektatu da",
- "UserLockedOutWithName": "{0} Erabiltzailea blokeatu da",
- "UserDownloadingItemWithValues": "{1} {0}-tik deskargatzen",
+ "UserStoppedPlayingItemWithValues": "{0} {1} ikusten bukatu du {2}-(e)n",
+ "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(e)n",
+ "UserPolicyUpdatedWithName": "{0} erabiltzailearen politikak aldatu dira",
+ "UserPasswordChangedWithName": "{0} erabiltzailearen pasahitza aldatu da",
+ "UserOnlineFromDevice": "{0} online dago {1}-(e)tik",
+ "UserOfflineFromDevice": "{0} {1}-(e)tik deskonektatu da",
+ "UserLockedOutWithName": "{0} erabiltzailea blokeatu da",
+ "UserDownloadingItemWithValues": "{0} {1} deskargatzen ari da",
"UserDeletedWithName": "{0} Erabiltzailea ezabatu da",
"UserCreatedWithName": "{0} Erabiltzailea sortu da",
"User": "Erabiltzailea",
"Undefined": "Ezezaguna",
- "TvShows": "TB showak",
+ "TvShows": "TB serieak",
"System": "Sistema",
- "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0} deskargatzean huts egin du",
- "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduxeago.",
+ "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0}-tik deskargatzeak huts egin du",
+ "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduago.",
"ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da",
"ScheduledTaskStartedWithName": "{0} hasi da",
"ScheduledTaskFailedWithName": "{0} huts egin du",
@@ -89,26 +89,26 @@
"NameSeasonNumber": "{0} Denboraldia",
"NameInstallFailed": "{0} instalazioak huts egin du",
"Music": "Musika",
- "MixedContent": "Denetariko edukia",
+ "MixedContent": "Eduki mistoa",
"MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da",
- "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren konfigurazio {0} atala eguneratu da",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren {0} konfigurazio atala eguneratu da",
"MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da",
"MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da",
"Latest": "Azkena",
- "LabelRunningTimeValue": "Denbora martxan: {0}",
+ "LabelRunningTimeValue": "Iraupena: {0}",
"LabelIpAddressValue": "IP helbidea: {0}",
- "ItemRemovedWithName": "{0} liburutegitik ezabatu da",
+ "ItemRemovedWithName": "{0} liburutegitik kendu da",
"ItemAddedWithName": "{0} liburutegira gehitu da",
"HomeVideos": "Etxeko bideoak",
- "HeaderNextUp": "Nobedadeak",
+ "HeaderNextUp": "Hurrengoa",
"HeaderLiveTV": "Zuzeneko TB",
"HeaderFavoriteSongs": "Gogoko abestiak",
- "HeaderFavoriteShows": "Gogoko showak",
+ "HeaderFavoriteShows": "Gogoko serieak",
"HeaderFavoriteEpisodes": "Gogoko atalak",
"HeaderFavoriteArtists": "Gogoko artistak",
"HeaderFavoriteAlbums": "Gogoko albumak",
"Forced": "Behartuta",
- "FailedLoginAttemptWithUserName": "Login egiten akatsa, saiatu hemen {0}",
+ "FailedLoginAttemptWithUserName": "{0}-tik saioa hasteak huts egin du",
"External": "Kanpokoa",
"DeviceOnlineWithName": "{0} konektatu da",
"DeviceOfflineWithName": "{0} deskonektatu da",
@@ -117,13 +117,23 @@
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
"Application": "Aplikazioa",
"AppDeviceValues": "App: {0}, Gailua: {1}",
- "HearingImpaired": "Entzunaldia aldatua",
+ "HearingImpaired": "Entzumen urritasuna",
"ProviderValue": "Hornitzailea: {0}",
"TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
"HeaderRecordingGroups": "Grabaketa taldeak",
"Inherit": "Oinordetu",
"TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
"TaskKeyframeExtractor": "Fotograma gakoen erauzgailua",
- "TaskRefreshTrickplayImages": "\"Trickplay Irudiak Sortu",
- "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan."
+ "TaskRefreshTrickplayImages": "Trickplay irudiak sortu",
+ "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan.",
+ "TaskAudioNormalization": "Audio normalizazioa",
+ "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.",
+ "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 68ab4b617..a10912f01 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -135,5 +135,6 @@
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
"TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
- "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay"
+ "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
+ "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json
new file mode 100644
index 000000000..4fcba99e9
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ht.json
@@ -0,0 +1,3 @@
+{
+ "Books": "liv"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index f205e8b64..1a9c3ee8b 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -13,7 +13,7 @@
"DeviceOnlineWithName": "{0} belépett",
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
"Favorites": "Kedvencek",
- "Folders": "Könyvtárak",
+ "Folders": "Mappák",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Albumelőadók",
"HeaderContinueWatching": "Megtekintés folytatása",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 6b0cfb359..e05afbabe 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -58,8 +58,8 @@
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
"NotificationOptionUserLockedOut": "Utente bloccato",
- "NotificationOptionVideoPlayback": "La riproduzione video è iniziata",
- "NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
+ "NotificationOptionVideoPlayback": "Riproduzione video iniziata",
+ "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto",
"Playlists": "Playlist",
"Plugin": "Plugin",
@@ -134,5 +134,7 @@
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
"TaskDownloadMissingLyrics": "Scarica testi mancanti",
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
- "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria."
+ "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
+ "TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
+ "TaskExtractMediaSegments": "Scansiona Segmento Media"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 10f4aee25..14a576592 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -134,5 +134,6 @@
"TaskExtractMediaSegments": "メディアセグメントを読み取る",
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
- "TaskDownloadMissingLyrics": "記録されていない歌詞をダウンロード"
+ "TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
+ "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lb.json b/Emby.Server.Implementations/Localization/Core/lb.json
new file mode 100644
index 000000000..176f2ba2b
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/lb.json
@@ -0,0 +1,139 @@
+{
+ "Albums": "Alben",
+ "Application": "Applikatioun",
+ "Artists": "Kënschtler",
+ "Books": "Bicher",
+ "Channels": "Kanäl",
+ "Collections": "Kollektiounen",
+ "Default": "Standard",
+ "ChapterNameValue": "Kapitel {0}",
+ "DeviceOnlineWithName": "{0} ass Online",
+ "DeviceOfflineWithName": "{0} ass Offline",
+ "External": "Extern",
+ "Favorites": "Favoritten",
+ "Folders": "Dossieren",
+ "Forced": "Forcéiert",
+ "HeaderAlbumArtists": "Album Kënschtler",
+ "HeaderFavoriteAlbums": "Léifsten Alben",
+ "HeaderFavoriteArtists": "Léifsten Kënschtler",
+ "HeaderFavoriteEpisodes": "Léifsten Episoden",
+ "HeaderFavoriteShows": "Léifsten Shows",
+ "HeaderFavoriteSongs": "Léifsten Lidder",
+ "Genres": "Generen",
+ "HeaderContinueWatching": "Weider kucken",
+ "Inherit": "Iwwerhuelen",
+ "HeaderNextUp": "Als Nächst",
+ "HeaderRecordingGroups": "Opname Gruppen",
+ "HearingImpaired": "Daaf",
+ "HomeVideos": "Amateur Videoen",
+ "ItemRemovedWithName": "Element ewech geholl: {0}",
+ "LabelIpAddressValue": "IP Adress: {0}",
+ "LabelRunningTimeValue": "Lafzäit: {0}",
+ "Latest": "Dat Aktuellst",
+ "MessageApplicationUpdatedTo": "Jellyfin Server aktualiséiert op {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Server Konfiguratiounssektioun {0} aktualiséiert",
+ "MessageServerConfigurationUpdated": "Server Konfiguratioun aktualiséiert",
+ "Movies": "Filmer",
+ "Music": "Musek",
+ "NameInstallFailed": "{0} Installatioun net gelongen",
+ "NameSeasonNumber": "Staffel {0}",
+ "NameSeasonUnknown": "Staffel Onbekannt",
+ "MusicVideos": "Museksvideoen",
+ "NotificationOptionApplicationUpdateAvailable": "Applikatiouns Update verfügbar",
+ "NotificationOptionApplicationUpdateInstalled": "Applikatiouns Update nët Installéiert",
+ "NotificationOptionAudioPlayback": "Audio ofspillen gestart",
+ "NotificationOptionAudioPlaybackStopped": "Audio ofspillen gestoppt",
+ "NotificationOptionCameraImageUploaded": "Kamera Bild eropgelueden",
+ "NotificationOptionInstallationFailed": "Installatioun net gelongen",
+ "NotificationOptionNewLibraryContent": "Neien Bibliothéik Inhalt",
+ "NotificationOptionPluginError": "Plugin Feeler",
+ "NotificationOptionPluginInstalled": "Plugin installéiert",
+ "NotificationOptionPluginUninstalled": "Plugin desinstalléiert",
+ "NotificationOptionPluginUpdateInstalled": "Plugin Update installéiert",
+ "Photos": "Fotoen",
+ "NotificationOptionTaskFailed": "Aufgab net gelongen",
+ "NotificationOptionUserLockedOut": "Benotzer Gesperrt",
+ "NotificationOptionVideoPlaybackStopped": "Video ofspillen gestoppt",
+ "NotificationOptionVideoPlayback": "Video ofspillen gestartet",
+ "Plugin": "Plugin",
+ "PluginUninstalledWithName": "{0} desinstalléiert",
+ "PluginUpdatedWithName": "{0} aktualiséiert",
+ "ProviderValue": "Provider: {0}",
+ "ScheduledTaskFailedWithName": "Aufgab: {0} net gelongen",
+ "Playlists": "Playlëschten",
+ "Shows": "Shows",
+ "Songs": "Lidder",
+ "ServerNameNeedsToBeRestarted": "{0} muss nei gestart ginn",
+ "StartupEmbyServerIsLoading": "Jellyfin Server luedt. Probéier méi spéit nach eng Kéier.",
+ "Sync": "Synchroniséieren",
+ "System": "System",
+ "User": "Benotzer",
+ "TvShows": "TV Shows",
+ "Undefined": "Net definéiert",
+ "UserCreatedWithName": "Benotzer {0} erstellt",
+ "UserDownloadingItemWithValues": "{0} luet {1} erof",
+ "UserOfflineFromDevice": "{0} Benotzer Offline um Gerät {1}",
+ "UserLockedOutWithName": "Benotzer {0} gesperrt",
+ "UserOnlineFromDevice": "{0} Benotzer Online um Gerät {1}",
+ "UserPasswordChangedWithName": "Benotzer Passwuert geännert fir {0}",
+ "UserPolicyUpdatedWithName": "Benotzer Politik aktualiséiert fir: {0}",
+ "UserStartedPlayingItemWithValues": "{0} spillt {1} op {2} oof",
+ "ValueHasBeenAddedToLibrary": "{0} der Bibliothéik bäigefüügt",
+ "VersionNumber": "Versioun {0}",
+ "TasksMaintenanceCategory": "Ënnerhalt",
+ "TasksLibraryCategory": "Bibliothéik",
+ "ValueSpecialEpisodeName": "Spezial-Episodenumm",
+ "TasksChannelsCategory": "Internet Kanäl",
+ "TaskCleanActivityLog": "Aktivitéits Log botzen",
+ "TaskCleanActivityLogDescription": "Läscht Aktivitéitslogs méi al wéi konfiguréiert.",
+ "TaskCleanCache": "Aufgab Cache Botzen",
+ "TaskRefreshChapterImages": "Kapitel Biller erstellen",
+ "TaskRefreshChapterImagesDescription": "Erstellt Miniaturbiller fir Videoen, déi Kapitelen hunn.",
+ "TaskAudioNormalization": "Audio Normaliséierung",
+ "TaskRefreshLibrary": "Bibliothéik aktualiséieren",
+ "TaskRefreshLibraryDescription": "Scannt deng Mediebibliothéik no neien Dateien a frëscht d’Metadata op.",
+ "TaskCleanLogs": "Log Dateien botzen",
+ "TaskRefreshPeople": "Persounen aktualiséieren",
+ "TaskRefreshPeopleDescription": "Aktualiséiert Metadata fir Schauspiller a Regisseuren an denger Mediebibliothéik.",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt Trickplay-Viraussiichten fir Videoen an aktivéierte Bibliothéiken.",
+ "TaskCleanTranscode": "Transkodéieren botzen",
+ "TaskCleanTranscodeDescription": "Läscht Transkodéierungsdateien, déi méi al wéi een Dag sinn.",
+ "TaskRefreshChannels": "Kanäl aktualiséieren",
+ "TaskDownloadMissingLyrics": "Fehlend Liddertexter eroflueden",
+ "TaskDownloadMissingLyricsDescription": "Lued Liddertexter fir Lidder erof",
+ "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",
+ "PluginInstalledWithName": "{0} installéiert",
+ "TaskMoveTrickplayImagesDescription": "Verschëfft existent Trickplay-Dateien no de Bibliothéik-Astellungen.",
+ "AppDeviceValues": "App: {0}, Geräter: {1}",
+ "FailedLoginAttemptWithUserName": "Net Gelongen Umeldung {0}",
+ "HeaderLiveTV": "LiveTV",
+ "ItemAddedWithName": "Element derbäi gesat: {0}",
+ "NotificationOptionServerRestartRequired": "Server Restart Erfuerderlech",
+ "ScheduledTaskStartedWithName": "Aufgab: {0} gestart",
+ "AuthenticationSucceededWithUserName": "{0} Authentifikatioun gelongen",
+ "MixedContent": "Gemëschten Inhalt",
+ "MessageApplicationUpdated": "Jellyfin Server Aktualiséiert",
+ "SubtitleDownloadFailureFromForItem": "Ënnertitel Download Feeler vun {0} fir {1}",
+ "TaskCleanLogsDescription": "Läscht Log-Dateien, déi méi al wéi {0} Deeg sinn.",
+ "TaskUpdatePlugins": "Plugins aktualiséieren",
+ "UserDeletedWithName": "Benotzer {0} geläscht",
+ "TasksApplicationCategory": "Applikatioun",
+ "TaskCleanCacheDescription": "Läscht Cache-Dateien, déi net méi vum System gebraucht ginn.",
+ "UserStoppedPlayingItemWithValues": "{0} ass mat {1} op {2} fäerdeg",
+ "TaskAudioNormalizationDescription": "Scannt Dateien no Donnéeën fir d’Audio-Normaliséierung.",
+ "TaskRefreshTrickplayImages": "Trickplay-Biller generéieren",
+ "TaskDownloadMissingSubtitlesDescription": "Sicht am Internet no fehlenden Ënnertitelen op Basis vun der Metadata-Konfiguratioun.",
+ "TaskMoveTrickplayImages": "Trickplay-Biller-Plaz migréieren",
+ "TaskUpdatePluginsDescription": "Lued Aktualiséierungen erof a installéiert se fir Plugins, déi fir automatesch Updates konfiguréiert sinn.",
+ "TaskKeyframeExtractorDescription": "Extrahéiert Schlësselbiller aus Videodateien, fir méi präzis HLS-Playlisten ze erstellen. Dës Aufgab kann eng längere Zäit daueren.",
+ "TaskRefreshChannelsDescription": "Aktualiséiert Informatiounen iwwer Internetkanäl.",
+ "TaskExtractMediaSegmentsDescription": "Extrahéiert oder kritt Mediesegmenter aus Plugins, déi MediaSegment ënnerstëtzen.",
+ "TaskOptimizeDatabaseDescription": "Kompriméiert d’Datebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann d’Performance verbesseren."
+}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 95f738bd5..46fc49f5e 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -94,14 +94,14 @@
"VersionNumber": "Version {0}",
"TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
"TaskUpdatePlugins": "Atnaujinti Priedus",
- "TaskDownloadMissingSubtitlesDescription": "Ieško internete trūkstamų subtitrų remiantis metaduomenų konfigūracija.",
+ "TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
"TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
"TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
"TaskRefreshLibrary": "Skenuoti Mediateka",
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
- "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informacija.",
- "TaskRefreshChannels": "Atnaujinti Kanalus",
+ "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
+ "TaskRefreshChannels": "Atnaujinti kanalus",
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
"TaskRefreshPeople": "Atnaujinti Žmones",
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
@@ -119,22 +119,22 @@
"Forced": "Priverstas",
"Default": "Numatytas",
"TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
- "TaskOptimizeDatabase": "Optimizuoti duomenų bazės",
+ "TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
- "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas",
- "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.",
+ "TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas",
+ "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.",
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
- "TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose",
- "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių.",
+ "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
"TaskAudioNormalization": "Garso Normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
- "TaskExtractMediaSegments": "Medijos Segmentų Nuskaitymas",
+ "TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
- "TaskMoveTrickplayImages": "Migruoti Trickplay Vaizdų Vietą",
- "TaskMoveTrickplayImagesDescription": "Perkelia egzisuojančius trickplay failus pagal bibliotekos nustatymus.",
+ "TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
+ "TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 62277fd94..77340a57a 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -123,11 +123,17 @@
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
- "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
+ "TaskKeyframeExtractorDescription": "Izvelk atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
"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"
+ "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",
+ "TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
+ "TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
+ "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index ebd3f7560..c64bcda04 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -11,7 +11,7 @@
"Collections": "Koleksi",
"DeviceOfflineWithName": "{0} telah diputuskan sambungan",
"DeviceOnlineWithName": "{0} telah disambung",
- "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
+ "FailedLoginAttemptWithUserName": "Percubaan log masuk daripada {0} gagal",
"Favorites": "Kegemaran",
"Folders": "Fail-fail",
"Genres": "Genre-genre",
@@ -126,5 +126,15 @@
"TaskKeyframeExtractor": "Ekstrak bingkai kunci",
"TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang.",
"TaskRefreshTrickplayImagesDescription": "Jana gambar prebiu Trickplay untuk video dalam perpustakaan.",
- "TaskRefreshTrickplayImages": "Jana gambar Trickplay"
+ "TaskRefreshTrickplayImages": "Jana gambar Trickplay",
+ "TaskExtractMediaSegments": "Imbasan Segmen Media",
+ "TaskExtractMediaSegmentsDescription": "Mengekstrak atau mendapatkan segmen media daripada pemalam yang didayakan MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Mengalihkan fail trickplay sedia ada mengikut tetapan pustakan digital.",
+ "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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mt.json b/Emby.Server.Implementations/Localization/Core/mt.json
index c3da37c58..f7501ab40 100644
--- a/Emby.Server.Implementations/Localization/Core/mt.json
+++ b/Emby.Server.Implementations/Localization/Core/mt.json
@@ -12,7 +12,7 @@
"DeviceOfflineWithName": "{0} tneħħa",
"DeviceOnlineWithName": "{0} tqabbad",
"External": "Estern",
- "FailedLoginAttemptWithUserName": "Attentat ta' login minn {0}",
+ "FailedLoginAttemptWithUserName": "Attentat fallut ta' login minn {0}",
"Favorites": "Favoriti",
"Forced": "Sfurzat",
"Genres": "Ġeneri",
@@ -38,10 +38,10 @@
"Inherit": "Jiret",
"ItemAddedWithName": "{0} żdied fil-librerija",
"ItemRemovedWithName": "{0} tneħħa mil-librerija",
- "LabelIpAddressValue": "Indirizz IP: {0}",
+ "LabelIpAddressValue": "Indirizz tal-IP: {0}",
"Latest": "Tal-Aħħar",
- "MessageApplicationUpdated": "Jellyfin Server ġie aġġornat",
- "MessageApplicationUpdatedTo": "JellyFin Server ġie aġġornat għal {0}",
+ "MessageApplicationUpdated": "Il-Jellyfin Server ġie aġġornat",
+ "MessageApplicationUpdatedTo": "Il-JellyFin Server ġie aġġornat għal {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Is-sezzjoni {0} tal-konfigurazzjoni tas-server ġiet aġġornata",
"MessageServerConfigurationUpdated": "Il-konfigurazzjoni tas-server ġiet aġġornata",
"MixedContent": "Kontenut imħallat",
@@ -51,26 +51,26 @@
"NameInstallFailed": "L-installazzjoni ta' {0} falliet",
"NameSeasonNumber": "Staġun {0}",
"NameSeasonUnknown": "Staġun Mhux Magħruf",
- "NewVersionIsAvailable": "Verżjoni ġdida ta' Jellyfin Server hija disponibbli biex titniżżel.",
- "NotificationOptionApplicationUpdateAvailable": "Aġġornament tal-applikazzjoni disponibbli",
- "NotificationOptionCameraImageUploaded": "Immaġini tal-kamera mtella'",
+ "NewVersionIsAvailable": "Verżjoni ġdida tal-Jellyfin Server hija disponibbli biex titniżżel.",
+ "NotificationOptionApplicationUpdateAvailable": "Hemm aġġornament tal-applikazzjoni",
+ "NotificationOptionCameraImageUploaded": "Ritratt tal-kamera mtella'",
"LabelRunningTimeValue": "Tul: {0}",
"NotificationOptionApplicationUpdateInstalled": "Aġġornament tal-applikazzjoni ġie installat",
- "NotificationOptionAudioPlayback": "Il-playback tal-awdjo beda",
+ "NotificationOptionAudioPlayback": "Beda l-playback tal-awdjo",
"NotificationOptionAudioPlaybackStopped": "Il-playback tal-awdjo twaqqaf",
- "NotificationOptionInstallationFailed": "Falliment tal-Installazzjoni",
+ "NotificationOptionInstallationFailed": "L-Installazzjoni falliet",
"NotificationOptionNewLibraryContent": "Kontenut ġdid żdied",
"NotificationOptionPluginError": "Falliment fil-plugin",
"NotificationOptionPluginInstalled": "Plugin installat",
"NotificationOptionPluginUninstalled": "Plugin tneħħa",
- "NotificationOptionServerRestartRequired": "Hemm bżonn li tagħmel restart lis-server",
+ "NotificationOptionServerRestartRequired": "Hemm bżonn li tagħmel restart tas-server",
"NotificationOptionTaskFailed": "Falliment tat-task skedat",
"NotificationOptionUserLockedOut": "Utent imsakkar",
"Photos": "Ritratti",
"Playlists": "Playlists",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} ġie installat",
- "PluginUninstalledWithName": "{0} ġie mneħħi",
+ "PluginUninstalledWithName": "{0} tneħħa",
"PluginUpdatedWithName": "{0} ġie aġġornat",
"ProviderValue": "Fornitur: {0}",
"ScheduledTaskFailedWithName": "{0} falla",
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index a25099ee0..6062d9700 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -120,5 +120,20 @@
"Albums": "ਐਲਬਮਾਂ",
"TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ",
"External": "ਬਾਹਰੀ",
- "HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ"
+ "HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ",
+ "TaskAudioNormalizationDescription": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ ਡਾਟਾ ਲਈ ਫਾਇਲਾਂ ਖੋਜੋ।",
+ "TaskRefreshTrickplayImages": "ਟ੍ਰਿਕਪਲੇ ਤਸਵੀਰਾਂ ਤਿਆਰ ਕਰੋ",
+ "TaskExtractMediaSegments": "ਮੀਡੀਆ ਸੈਗਮੈਂਟ ਸਕੈਨ",
+ "TaskMoveTrickplayImagesDescription": "ਟ੍ਰਿਕਪਲੇ ਤਸਵੀਰਾਂ ਦੀ ਜਗਾ ਨੂੰ ਲਾਇਬ੍ਰੇਰੀ ਸੈਟਿੰਗਜ਼ ਅਨੁਸਾਰ ਬਦਲੋ।",
+ "TaskOptimizeDatabaseDescription": "ਡੇਟਾਬੇਸ ਨੂੰ ਸੰਗ੍ਰਹਿਤ ਕਰਦਾ ਹੈ ਅਤੇ ਖਾਲੀ ਜਗ੍ਹਾ ਘਟਾਉਂਦਾ ਹੈ। ਲਾਇਬ੍ਰੇਰੀ ਸਕੈਨ ਕਰਨ ਜਾਂ ਡੇਟਾਬੇਸ ਵਿੱਚ ਸੋਧਾਂ ਕਰਨ ਤੋਂ ਬਾਅਦ ਇਸ ਕੰਮ ਨੂੰ ਚਲਾਉਣਾ ਪ੍ਰਦਰਸ਼ਨ ਵਿੱਚ ਸੁਧਾਰ ਕਰ ਸਕਦਾ ਹੈ।",
+ "TaskExtractMediaSegmentsDescription": "ਮੀਡੀਆ ਸੈਗਮੈਂਟ ਨੂੰ ਮੀਡੀਆਸੈਗਮੈਂਟ ਯੋਗ ਪਲੱਗਇਨਾਂ ਤੋਂ ਨਿਕਾਲਦਾ ਜਾਂ ਪ੍ਰਾਪਤ ਕਰਦਾ ਹੈ।",
+ "TaskMoveTrickplayImages": "ਟ੍ਰਿਕਪਲੇ ਤਸਵੀਰਾਂ ਦੀ ਜਗਾ ਬਦਲੋ",
+ "TaskDownloadMissingLyrics": "ਅਧੂਰੇ ਬੋਲ ਡਾਊਨਲੋਡ ਕਰੋ",
+ "TaskDownloadMissingLyricsDescription": "ਗੀਤਾਂ ਲਈ ਡਾਊਨਲੋਡ ਕਿਤੇ ਬੋਲ",
+ "TaskKeyframeExtractor": "ਕੀ-ਫ੍ਰੇਮ ਐਕਸਟ੍ਰੈਕਟਰ",
+ "TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
+ "TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
+ "TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
+ "TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
+ "TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 879bf64b0..42ea5e0a4 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -1,6 +1,6 @@
{
"Albums": "Álbuns",
- "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
+ "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"Application": "Aplicação",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 2812832ca..0bf0491be 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -13,7 +13,7 @@
"HeaderContinueWatching": "Continuar a ver",
"HeaderAlbumArtists": "Artistas do Álbum",
"Genres": "Géneros",
- "Folders": "Diretórios",
+ "Folders": "Pastas",
"Favorites": "Favoritos",
"Channels": "Canais",
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 19be1a23e..b17e7ae55 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -126,5 +126,15 @@
"TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
"HearingImpaired": "Oslabljen sluh",
"TaskRefreshTrickplayImages": "Ustvari Trickplay slike",
- "TaskRefreshTrickplayImagesDescription": "Ustvari trickplay predoglede za posnetke v omogočenih knjižnicah."
+ "TaskRefreshTrickplayImagesDescription": "Ustvari trickplay predoglede za posnetke v omogočenih knjižnicah.",
+ "TaskExtractMediaSegmentsDescription": "Ekstrahira ali pridobi medijske segmente iz vtičnikov, ki podpirajo MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Premakne obstoječe datoteke trickplay v skladu z nastavitvami knjižnice.",
+ "TaskExtractMediaSegments": "Skeniranje segmentov v medijih",
+ "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č."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 9739358df..af40b5e5a 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -78,7 +78,7 @@
"Genres": "Жанрови",
"Folders": "Фасцикле",
"Favorites": "Омиљено",
- "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
+ "FailedLoginAttemptWithUserName": "Неуспели покушај пријавe са {0}",
"DeviceOnlineWithName": "{0} је повезан",
"DeviceOfflineWithName": "{0} је прекинуо везу",
"Collections": "Колекције",
@@ -121,7 +121,10 @@
"TaskOptimizeDatabase": "Оптимизуј банку података",
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
"External": "Спољно",
- "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
+ "TaskKeyframeExtractorDescription": "Екстрактује кључне сличице из видео датотека да би креирао више прецизнију HLS плејлисту. Овај задатак може да потраје дуже време.",
"TaskKeyframeExtractor": "Екстрактор кључних сличица",
- "HearingImpaired": "ослабљен слух"
+ "HearingImpaired": "ослабљен слух",
+ "TaskAudioNormalization": "Нормализација звука",
+ "TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте",
+ "TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука."
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index bc1fd8cb2..286efb7e9 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -134,5 +134,7 @@
"TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
"TaskAudioNormalization": "音訊同等化",
"TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
- "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。"
+ "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
+ "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
+ "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 81d5b83d6..a4ee68fc4 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
- "Collections": "系列",
+ "Collections": "系列作",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
@@ -126,8 +126,8 @@
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "生成快轉縮圖",
"TaskRefreshTrickplayImagesDescription": "為啟用快轉縮圖的媒體庫生成快轉縮圖。",
- "TaskCleanCollectionsAndPlaylists": "清理系列和播放清單",
- "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。",
+ "TaskCleanCollectionsAndPlaylists": "清理系列作和播放清單",
+ "TaskCleanCollectionsAndPlaylistsDescription": "清理系列作品與播放清單中已不存在的項目。",
"TaskAudioNormalization": "音量標準化",
"TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。",
"TaskDownloadMissingLyrics": "下載缺少的歌詞",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index ac453a5b0..754a01329 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -231,13 +231,13 @@ namespace Emby.Server.Implementations.Localization
ratings.Add(new ParentalRating("21", 21));
}
- // A lot of countries don't excplicitly have a seperate rating for adult content
+ // A lot of countries don't explicitly have a separate rating for adult content
if (ratings.All(x => x.Value != 1000))
{
ratings.Add(new ParentalRating("XXX", 1000));
}
- // A lot of countries don't excplicitly have a seperate rating for banned content
+ // A lot of countries don't explicitly have a separate rating for banned content
if (ratings.All(x => x.Value != 1001))
{
ratings.Add(new ParentalRating("Banned", 1001));
@@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
}
// Fairly common for some users to have "Rated R" in their rating field
- rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
- rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
+ rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Trim();
// Use rating system matching the language
if (!string.IsNullOrEmpty(countryCode))
diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv
index 5ec1eb262..f6053c88c 100644
--- a/Emby.Server.Implementations/Localization/Ratings/br.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/br.csv
@@ -1,8 +1,14 @@
Livre,0
L,0
-ER,9
+AL,0
+ER,10
10,10
+A10,10
12,12
+A12,12
14,14
+A14,14
16,16
+A16,16
18,18
+A18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv
index 336ee2806..41dbda134 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ca.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.csv
@@ -6,8 +6,6 @@ TV-Y7,7
TV-Y7-FV,7
PG,9
TV-PG,9
-PG-13,13
-13+,13
TV-14,14
14A,14
16+,16
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
index 619e948d8..ee5866090 100644
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/es.csv
@@ -1,7 +1,7 @@
A,0
A/fig,0
A/i,0
-A/fig/i,0
+A/i/fig,0
APTA,0
ERI,0
TP,0
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv
index 75b1c2058..858b9a32d 100644
--- a/Emby.Server.Implementations/Localization/Ratings/gb.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/gb.csv
@@ -6,10 +6,11 @@ U,0
6+,6
7+,7
PG,8
-9+,9
+9,9
12,12
12+,12
12A,12
+12PG,12
Teen,13
13+,13
14+,14
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv
index 6ef2e5012..d3c634fc9 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ie.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ie.csv
@@ -4,6 +4,7 @@ PG,12
12A,12
12PG,12
15,15
+15PG,15
15A,15
16,16
18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv
index c8f8e93db..6856a2dbb 100644
--- a/Emby.Server.Implementations/Localization/Ratings/no.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/no.csv
@@ -6,4 +6,5 @@ A,0
12,12
15,15
18,18
+C,18
Not approved,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv
index f617f0c39..633da78fe 100644
--- a/Emby.Server.Implementations/Localization/Ratings/nz.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/nz.csv
@@ -10,6 +10,7 @@ R16,16
RP16,16
GA,18
R18,18
+RP18,18
MA,1000
R,1001
Objectionable,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv
index fc91edecd..9aa5c00eb 100644
--- a/Emby.Server.Implementations/Localization/Ratings/us.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/us.csv
@@ -48,3 +48,5 @@ TV-MA-LS,17
TV-MA-LV,17
TV-MA-SV,17
TV-MA-LSV,17
+TV-X,18
+TV-AO,18
diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json
index 0a11b3e45..d92dc880b 100644
--- a/Emby.Server.Implementations/Localization/countries.json
+++ b/Emby.Server.Implementations/Localization/countries.json
@@ -336,7 +336,7 @@
"TwoLetterISORegionName": "IE"
},
{
- "DisplayName": "Islamic Republic of Pakistan",
+ "DisplayName": "Pakistan",
"Name": "PK",
"ThreeLetterISORegionName": "PAK",
"TwoLetterISORegionName": "PK"
diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt
index b55c0fa33..00c2aee62 100644
--- a/Emby.Server.Implementations/Localization/iso6392.txt
+++ b/Emby.Server.Implementations/Localization/iso6392.txt
@@ -10,7 +10,6 @@ afr||af|Afrikaans|afrikaans
ain|||Ainu|aïnou
aka||ak|Akan|akan
akk|||Akkadian|akkadien
-alb|sqi|sq|Albanian|albanais
ale|||Aleut|aléoute
alg|||Algonquian languages|algonquines, langues
alt|||Southern Altai|altai du Sud
@@ -21,7 +20,6 @@ apa|||Apache languages|apaches, langues
ara||ar|Arabic|arabe
arc|||Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)|araméen d'empire (700-300 BCE)
arg||an|Aragonese|aragonais
-arm|hye|hy|Armenian|arménien
arn|||Mapudungun; Mapuche|mapudungun; mapuche; mapuce
arp|||Arapaho|arapaho
art|||Artificial languages|artificielles, langues
@@ -41,7 +39,6 @@ bak||ba|Bashkir|bachkir
bal|||Baluchi|baloutchi
bam||bm|Bambara|bambara
ban|||Balinese|balinais
-baq|eus|eu|Basque|basque
bas|||Basa|basa
bat|||Baltic languages|baltes, langues
bej|||Beja; Bedawiyet|bedja
@@ -56,6 +53,7 @@ bin|||Bini; Edo|bini; edo
bis||bi|Bislama|bichlamar
bla|||Siksika|blackfoot
bnt|||Bantu (Other)|bantoues, autres langues
+bod|tib|bo|Tibetan|tibétain
bos||bs|Bosnian|bosniaque
bra|||Braj|braj
bre||br|Breton|breton
@@ -63,7 +61,6 @@ btk|||Batak languages|batak, langues
bua|||Buriat|bouriate
bug|||Buginese|bugi
bul||bg|Bulgarian|bulgare
-bur|mya|my|Burmese|birman
byn|||Blin; Bilin|blin; bilen
cad|||Caddo|caddo
cai|||Central American Indian languages|amérindiennes de L'Amérique centrale, langues
@@ -72,14 +69,11 @@ cat||ca|Catalan; Valencian|catalan; valencien
cau|||Caucasian languages|caucasiennes, langues
ceb|||Cebuano|cebuano
cel|||Celtic languages|celtiques, langues; celtes, langues
+ces|cze|cs|Czech|tchèque
cha||ch|Chamorro|chamorro
chb|||Chibcha|chibcha
che||ce|Chechen|tchétchène
chg|||Chagatai|djaghataï
-chi|zho|zh|Chinese|chinois
-chi|zho|ze|Chinese; Bilingual|chinois
-chi|zho|zh-tw|Chinese; Traditional|chinois
-chi|zho|zh-hk|Chinese; Hong Kong|chinois
chk|||Chuukese|chuuk
chm|||Mari|mari
chn|||Chinook jargon|chinook, jargon
@@ -101,13 +95,14 @@ crh|||Crimean Tatar; Crimean Turkish|tatar de Crimé
crp|||Creoles and pidgins |créoles et pidgins
csb|||Kashubian|kachoube
cus|||Cushitic languages|couchitiques, langues
-cze|ces|cs|Czech|tchèque
+cym|wel|cy|Welsh|gallois
dak|||Dakota|dakota
dan||da|Danish|danois
dar|||Dargwa|dargwa
day|||Land Dayak languages|dayak, langues
del|||Delaware|delaware
den|||Slave (Athapascan)|esclave (athapascan)
+deu|ger|de|German|allemand
dgr|||Dogrib|dogrib
din|||Dinka|dinka
div||dv|Divehi; Dhivehi; Maldivian|maldivien
@@ -116,28 +111,30 @@ dra|||Dravidian languages|dravidiennes, langues
dsb|||Lower Sorbian|bas-sorabe
dua|||Duala|douala
dum|||Dutch, Middle (ca.1050-1350)|néerlandais moyen (ca. 1050-1350)
-dut|nld|nl|Dutch; Flemish|néerlandais; flamand
dyu|||Dyula|dioula
dzo||dz|Dzongkha|dzongkha
efi|||Efik|efik
egy|||Egyptian (Ancient)|égyptien
eka|||Ekajuk|ekajuk
+ell|gre|el|Greek, Modern (1453-)|grec moderne (après 1453)
elx|||Elamite|élamite
eng||en|English|anglais
enm|||English, Middle (1100-1500)|anglais moyen (1100-1500)
epo||eo|Esperanto|espéranto
est||et|Estonian|estonien
+eus|baq|eu|Basque|basque
ewe||ee|Ewe|éwé
ewo|||Ewondo|éwondo
fan|||Fang|fang
fao||fo|Faroese|féroïen
+fas|per|fa|Persian|persan
fat|||Fanti|fanti
fij||fj|Fijian|fidjien
fil|||Filipino; Pilipino|filipino; pilipino
fin||fi|Finnish|finnois
fiu|||Finno-Ugrian languages|finno-ougriennes, langues
fon|||Fon|fon
-fre|fra|fr|French|français
+fra|fre|fr|French|français
frm|||French, Middle (ca.1400-1600)|français moyen (1400-1600)
fro|||French, Old (842-ca.1400)|français ancien (842-ca.1400)
frc||fr-ca|French (Canada)|french
@@ -150,8 +147,6 @@ gaa|||Ga|ga
gay|||Gayo|gayo
gba|||Gbaya|gbaya
gem|||Germanic languages|germaniques, langues
-geo|kat|ka|Georgian|géorgien
-ger|deu|de|German|allemand
gez|||Geez|guèze
gil|||Gilbertese|kiribati
gla||gd|Gaelic; Scottish Gaelic|gaélique; gaélique écossais
@@ -165,7 +160,6 @@ gor|||Gorontalo|gorontalo
got|||Gothic|gothique
grb|||Grebo|grebo
grc|||Greek, Ancient (to 1453)|grec ancien (jusqu'à 1453)
-gre|ell|el|Greek, Modern (1453-)|grec moderne (après 1453)
grn||gn|Guarani|guarani
gsw|||Swiss German; Alemannic; Alsatian|suisse alémanique; alémanique; alsacien
guj||gu|Gujarati|goudjrati
@@ -186,9 +180,10 @@ hrv||hr|Croatian|croate
hsb|||Upper Sorbian|haut-sorabe
hun||hu|Hungarian|hongrois
hup|||Hupa|hupa
+hye|arm|hy|Armenian|arménien
iba|||Iban|iban
ibo||ig|Igbo|igbo
-ice|isl|is|Icelandic|islandais
+isl|ice|is|Icelandic|islandais
ido||io|Ido|ido
iii||ii|Sichuan Yi; Nuosu|yi de Sichuan
ijo|||Ijo languages|ijo, langues
@@ -217,6 +212,7 @@ kam|||Kamba|kamba
kan||kn|Kannada|kannada
kar|||Karen languages|karen, langues
kas||ks|Kashmiri|kashmiri
+kat|geo|ka|Georgian|géorgien
kau||kr|Kanuri|kanouri
kaw|||Kawi|kawi
kaz||kk|Kazakh|kazakh
@@ -263,7 +259,6 @@ lui|||Luiseno|luiseno
lun|||Lunda|lunda
luo|||Luo (Kenya and Tanzania)|luo (Kenya et Tanzanie)
lus|||Lushai|lushai
-mac|mkd|mk|Macedonian|macédonien
mad|||Madurese|madourais
mag|||Magahi|magahi
mah||mh|Marshallese|marshall
@@ -271,11 +266,9 @@ mai|||Maithili|maithili
mak|||Makasar|makassar
mal||ml|Malayalam|malayalam
man|||Mandingo|mandingue
-mao|mri|mi|Maori|maori
map|||Austronesian languages|austronésiennes, langues
mar||mr|Marathi|marathe
mas|||Masai|massaï
-may|msa|ms|Malay|malais
mdf|||Moksha|moksa
mdr|||Mandar|mandar
men|||Mende|mendé
@@ -283,6 +276,7 @@ mga|||Irish, Middle (900-1200)|irlandais moyen (900-1200)
mic|||Mi'kmaq; Micmac|mi'kmaq; micmac
min|||Minangkabau|minangkabau
mis|||Uncoded languages|langues non codées
+mkd|mac|mk|Macedonian|macédonien
mkh|||Mon-Khmer languages|môn-khmer, langues
mlg||mg|Malagasy|malgache
mlt||mt|Maltese|maltais
@@ -292,11 +286,14 @@ mno|||Manobo languages|manobo, langues
moh|||Mohawk|mohawk
mon||mn|Mongolian|mongol
mos|||Mossi|moré
+mri|mao|mi|Maori|maori
+msa|may|ms|Malay|malais
mul|||Multiple languages|multilingue
mun|||Munda languages|mounda, langues
mus|||Creek|muskogee
mwl|||Mirandese|mirandais
mwr|||Marwari|marvari
+mya|bur|my|Burmese|birman
myn|||Mayan languages|maya, langues
myv|||Erzya|erza
nah|||Nahuatl languages|nahuatl, langues
@@ -313,6 +310,7 @@ new|||Nepal Bhasa; Newari|nepal bhasa; newari
nia|||Nias|nias
nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues
niu|||Niuean|niué
+nld|dut|nl|Dutch; Flemish|néerlandais; flamand
nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien
nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål
nog|||Nogai|nogaï; nogay
@@ -343,7 +341,6 @@ pan||pa|Panjabi; Punjabi|pendjabi
pap|||Papiamento|papiamento
pau|||Palauan|palau
peo|||Persian, Old (ca.600-400 B.C.)|perse, vieux (ca. 600-400 av. J.-C.)
-per|fas|fa|Persian|persan
phi|||Philippine languages|philippines, langues
phn|||Phoenician|phénicien
pli||pi|Pali|pali
@@ -363,7 +360,7 @@ rar|||Rarotongan; Cook Islands Maori|rarotonga; maori des îles Cook
roa|||Romance languages|romanes, langues
roh||rm|Romansh|romanche
rom|||Romany|tsigane
-rum|ron|ro|Romanian; Moldavian; Moldovan|roumain; moldave
+ron|rum|ro|Romanian; Moldavian; Moldovan|roumain; moldave
run||rn|Rundi|rundi
rup|||Aromanian; Arumanian; Macedo-Romanian|aroumain; macédo-roumain
rus||ru|Russian|russe
@@ -376,6 +373,7 @@ sam|||Samaritan Aramaic|samaritain
san||sa|Sanskrit|sanskrit
sas|||Sasak|sasak
sat|||Santali|santal
+scc|srp|sr|Serbian|serbe
scn|||Sicilian|sicilien
sco|||Scots|écossais
sel|||Selkup|selkoupe
@@ -388,7 +386,7 @@ sin||si|Sinhala; Sinhalese|singhalais
sio|||Siouan languages|sioux, langues
sit|||Sino-Tibetan languages|sino-tibétaines, langues
sla|||Slavic languages|slaves, langues
-slo|slk|sk|Slovak|slovaque
+slk|slo|sk|Slovak|slovaque
slv||sl|Slovenian|slovène
sma|||Southern Sami|sami du Sud
sme||se|Northern Sami|sami du Nord
@@ -406,9 +404,9 @@ son|||Songhai languages|songhai, langues
sot||st|Sotho, Southern|sotho du Sud
spa||es-mx|Spanish; Latin|espagnol; Latin
spa||es|Spanish; Castilian|espagnol; castillan
+sqi|alb|sq|Albanian|albanais
srd||sc|Sardinian|sarde
srn|||Sranan Tongo|sranan tongo
-srp|scc|sr|Serbian|serbe
srr|||Serer|sérère
ssa|||Nilo-Saharan languages|nilo-sahariennes, langues
ssw||ss|Swati|swati
@@ -431,7 +429,6 @@ tet|||Tetum|tetum
tgk||tg|Tajik|tadjik
tgl||tl|Tagalog|tagalog
tha||th|Thai|thaï
-tib|bod|bo|Tibetan|tibétain
tig|||Tigre|tigré
tir||ti|Tigrinya|tigrigna
tiv|||Tiv|tiv
@@ -470,7 +467,6 @@ wak|||Wakashan languages|wakashanes, langues
wal|||Walamo|walamo
war|||Waray|waray
was|||Washo|washo
-wel|cym|cy|Welsh|gallois
wen|||Sorbian languages|sorabes, langues
wln||wa|Walloon|wallon
wol||wo|Wolof|wolof
@@ -486,6 +482,10 @@ zbl|||Blissymbols; Blissymbolics; Bliss|symboles Bliss; Bliss
zen|||Zenaga|zenaga
zgh|||Standard Moroccan Tamazight|amazighe standard marocain
zha||za|Zhuang; Chuang|zhuang; chuang
+zho|chi|zh|Chinese|chinois
+zho|chi|ze|Chinese; Bilingual|chinois
+zho|chi|zh-tw|Chinese; Traditional|chinois
+zho|chi|zh-hk|Chinese; Hong Kong|chinois
znd|||Zande languages|zandé, langues
zul||zu|Zulu|zoulou
zun|||Zuni|zuni
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index eb55e32c5..ea7896861 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder
private readonly IFileSystem _fileSystem;
private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
- private readonly IChapterManager _chapterManager;
+ private readonly IChapterRepository _chapterManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder
ILogger<EncodingManager> logger,
IFileSystem fileSystem,
IMediaEncoder encoder,
- IChapterManager chapterManager,
+ IChapterRepository chapterManager,
ILibraryManager libraryManager)
{
_logger = logger;
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index daeb7fed8..7b0a16441 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -9,8 +9,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null)
{
- _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
+ _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId);
return;
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index f65d609c7..a5be2b616 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -3,14 +3,16 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Common;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Playlists
{
+ [RequiresSourceSerialisation]
public class PlaylistsFolder : BasePluginFolder
{
public PlaylistsFolder()
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 4c32d5717..8eeca3667 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.Plugins
// Now load the assemblies..
foreach (var plugin in _plugins)
{
- UpdatePluginSuperceedStatus(plugin);
+ UpdatePluginSupersededStatus(plugin);
if (plugin.IsEnabledAndSupported == false)
{
@@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Plugins
continue;
}
- UpdatePluginSuperceedStatus(plugin);
+ UpdatePluginSupersededStatus(plugin);
if (!plugin.IsEnabledAndSupported)
{
continue;
@@ -624,9 +624,9 @@ namespace Emby.Server.Implementations.Plugins
}
}
- private void UpdatePluginSuperceedStatus(LocalPlugin plugin)
+ private void UpdatePluginSupersededStatus(LocalPlugin plugin)
{
- if (plugin.Manifest.Status != PluginStatus.Superceded)
+ if (plugin.Manifest.Status != PluginStatus.Superseded)
{
return;
}
@@ -876,7 +876,7 @@ namespace Emby.Server.Implementations.Plugins
}
/// <summary>
- /// Changes the status of the other versions of the plugin to "Superceded".
+ /// Changes the status of the other versions of the plugin to "Superseded".
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
private void ProcessAlternative(LocalPlugin plugin)
@@ -896,11 +896,11 @@ namespace Emby.Server.Implementations.Plugins
return;
}
- if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded))
+ if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superseded))
{
_logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
- else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active))
+ else if (plugin.Manifest.Status == PluginStatus.Superseded && !ChangePluginState(previousVersion, PluginStatus.Active))
{
_logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index fe769baf9..985f0a8f8 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger _logger;
private readonly ITaskManager _taskManager;
- private readonly object _lastExecutionResultSyncLock = new();
+ private readonly Lock _lastExecutionResultSyncLock = new();
private bool _readFromFile;
private TaskResult _lastExecutionResult;
private Task _currentTask;
@@ -543,7 +543,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
DisposeTriggers();
- var wassRunning = State == TaskState.Running;
+ var wasRunning = State == TaskState.Running;
var startTime = CurrentExecutionStartTime;
var token = CurrentCancellationTokenSource;
@@ -596,7 +596,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
- if (wassRunning)
+ if (wasRunning)
{
OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index 031d14776..8d1d509ff 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -116,6 +116,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
+ OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
cancellationToken).ConfigureAwait(false);
}
finally
@@ -142,7 +143,10 @@ public partial class AudioNormalizationTask : IScheduledTask
continue;
}
- t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false);
+ t.LUFS = await CalculateLUFSAsync(
+ string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
+ false,
+ cancellationToken).ConfigureAwait(false);
}
_itemRepository.SaveItems(tracks, cancellationToken);
@@ -162,7 +166,7 @@ public partial class AudioNormalizationTask : IScheduledTask
];
}
- private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
+ private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
{
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
@@ -189,18 +193,28 @@ public partial class AudioNormalizationTask : IScheduledTask
}
using var reader = process.StandardError;
+ float? lufs = null;
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
{
Match match = LUFSRegex().Match(line);
-
if (match.Success)
{
- return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+ lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+ break;
}
}
- _logger.LogError("Failed to find LUFS value in output");
- return null;
+ if (lufs is null)
+ {
+ _logger.LogError("Failed to find LUFS value in output");
+ }
+
+ if (waitForExit)
+ {
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ return lufs;
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 2c7d06ed4..563e90fbe 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -32,6 +33,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
private readonly IEncodingManager _encodingManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
+ private readonly IChapterRepository _chapterRepository;
/// <summary>
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
@@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="chapterRepository">Instance of the <see cref="IChapterRepository"/> interface.</param>
public ChapterImagesTask(
ILogger<ChapterImagesTask> logger,
ILibraryManager libraryManager,
@@ -50,7 +53,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
IApplicationPaths appPaths,
IEncodingManager encodingManager,
IFileSystem fileSystem,
- ILocalizationManager localization)
+ ILocalizationManager localization,
+ IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -59,6 +63,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_encodingManager = encodingManager;
_fileSystem = fileSystem;
_localization = localization;
+ _chapterRepository = chapterRepository;
}
/// <inheritdoc />
@@ -141,7 +146,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
- var chapters = _itemRepo.GetChapters(video);
+ var chapters = _chapterRepository.GetChapters(video.Id);
var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 316e4a8f0..8901390aa 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -84,7 +84,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
var collection = collections[index];
_logger.LogDebug("Checking boxset {CollectionName}", collection.Name);
- CleanupLinkedChildren(collection, cancellationToken);
+ await CleanupLinkedChildrenAsync(collection, cancellationToken).ConfigureAwait(false);
progress.Report(50D / collections.Length * (index + 1));
}
}
@@ -104,12 +104,12 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
var playlist = playlists[index];
_logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name);
- CleanupLinkedChildren(playlist, cancellationToken);
+ await CleanupLinkedChildrenAsync(playlist, cancellationToken).ConfigureAwait(false);
progress.Report(50D / playlists.Length * (index + 1));
}
}
- private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken)
+ private async Task CleanupLinkedChildrenAsync<T>(T folder, CancellationToken cancellationToken)
where T : Folder
{
List<LinkedChild>? itemsToRemove = null;
@@ -127,8 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
{
_logger.LogDebug("Updating {FolderName}", folder.Name);
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
- _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
- folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
+ await _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
+ await folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 7d4e2377d..4d3a04377 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Server.Implementations;
+using Jellyfin.Database.Implementations;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.EntityFrameworkCore;
@@ -18,6 +18,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
private readonly ILogger<OptimizeDatabaseTask> _logger;
private readonly ILocalizationManager _localization;
private readonly IDbContextFactory<JellyfinDbContext> _provider;
+ private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
/// <summary>
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
@@ -25,14 +26,17 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="provider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
+ /// <param name="jellyfinDatabaseProvider">Instance of the JellyfinDatabaseProvider that can be used for provider specific operations.</param>
public OptimizeDatabaseTask(
ILogger<OptimizeDatabaseTask> logger,
ILocalizationManager localization,
- IDbContextFactory<JellyfinDbContext> provider)
+ IDbContextFactory<JellyfinDbContext> provider,
+ IJellyfinDatabaseProvider jellyfinDatabaseProvider)
{
_logger = logger;
_localization = localization;
_provider = provider;
+ _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
}
/// <inheritdoc />
@@ -73,20 +77,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
- var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
- await using (context.ConfigureAwait(false))
- {
- if (context.Database.IsSqlite())
- {
- await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
- await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
- _logger.LogInformation("jellyfin.db optimized successfully!");
- }
- else
- {
- _logger.LogInformation("This database doesn't support optimization");
- }
- }
+ await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index c597103dd..b74f4d1b2 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
catch (OperationCanceledException)
{
- // InstallPackage has it's own inner cancellation token, so only throw this if it's ours
+ // InstallPackage has its own inner cancellation token, so only throw this if it's ours
if (cancellationToken.IsCancellationRequested)
{
throw;
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index fe2c3d24f..ac3e10594 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -7,11 +7,13 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Entities.Security;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@@ -62,6 +64,9 @@ namespace Emby.Server.Implementations.Session
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
= new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
+ = new(StringComparer.OrdinalIgnoreCase);
+
private Timer _idleTimer;
private Timer _inactiveTimer;
@@ -309,7 +314,7 @@ namespace Emby.Server.Implementations.Session
_activeConnections.TryRemove(key, out _);
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
{
- await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
+ await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false);
}
await OnSessionEnded(session).ConfigureAwait(false);
@@ -317,6 +322,42 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
+ public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId)
+ {
+ bool liveStreamNeedsToBeClosed = false;
+
+ if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings))
+ {
+ if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId))
+ {
+ if (!string.IsNullOrEmpty(correspondingId))
+ {
+ activeSessionMappings.TryRemove(correspondingId, out _);
+ }
+
+ liveStreamNeedsToBeClosed = true;
+ }
+
+ if (activeSessionMappings.IsEmpty)
+ {
+ _activeLiveStreamSessions.TryRemove(liveStreamId, out _);
+ }
+ }
+
+ if (liveStreamNeedsToBeClosed)
+ {
+ try
+ {
+ await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing live stream");
+ }
+ }
+ }
+
+ /// <inheritdoc />
public async ValueTask ReportSessionEnded(string sessionId)
{
CheckDisposed();
@@ -343,6 +384,11 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns>
private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
{
+ if (session is null)
+ {
+ return;
+ }
+
if (string.IsNullOrEmpty(info.MediaSourceId))
{
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
@@ -675,6 +721,11 @@ namespace Emby.Server.Implementations.Session
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{
+ if (session is null)
+ {
+ return null;
+ }
+
var item = session.FullNowPlayingItem;
if (item is not null && item.Id.Equals(itemId))
{
@@ -725,6 +776,11 @@ namespace Emby.Server.Implementations.Session
}
}
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
+ }
+
var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
@@ -782,6 +838,32 @@ namespace Emby.Server.Implementations.Session
return OnPlaybackProgress(info, false);
}
+ private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
+ {
+ var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<string, string>());
+
+ if (!string.IsNullOrEmpty(playSessionId))
+ {
+ if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId != playSessionId)
+ {
+ if (!string.IsNullOrEmpty(currentPlaySessionId))
+ {
+ activeSessionMappings.TryRemove(currentPlaySessionId, out _);
+ }
+
+ activeSessionMappings[sessionId] = playSessionId;
+ activeSessionMappings[playSessionId] = sessionId;
+ }
+ }
+ else
+ {
+ if (!activeSessionMappings.TryGetValue(sessionId, out _))
+ {
+ activeSessionMappings[sessionId] = string.Empty;
+ }
+ }
+ }
+
/// <summary>
/// Used to report playback progress for an item.
/// </summary>
@@ -794,7 +876,11 @@ namespace Emby.Server.Implementations.Session
ArgumentNullException.ThrowIfNull(info);
- var session = GetSession(info.SessionId);
+ var session = GetSession(info.SessionId, false);
+ if (session is null)
+ {
+ return;
+ }
var libraryItem = info.ItemId.IsEmpty()
? null
@@ -818,6 +904,11 @@ namespace Emby.Server.Implementations.Session
}
}
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
+ }
+
var eventArgs = new PlaybackProgressEventArgs
{
Item = libraryItem,
@@ -1000,14 +1091,7 @@ namespace Emby.Server.Implementations.Session
if (!string.IsNullOrEmpty(info.LiveStreamId))
{
- try
- {
- await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream");
- }
+ await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
}
var eventArgs = new PlaybackStopEventArgs
@@ -1303,7 +1387,7 @@ namespace Emby.Server.Implementations.Session
if (item is null)
{
- _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id);
+ _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
return Array.Empty<BaseItem>();
}
@@ -1356,7 +1440,7 @@ namespace Emby.Server.Implementations.Session
if (item is null)
{
- _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id);
+ _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
return new List<BaseItem>();
}
@@ -2055,6 +2139,7 @@ namespace Emby.Server.Implementations.Session
}
_activeConnections.Clear();
+ _activeLiveStreamSessions.Clear();
}
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index aba51de8f..d4606abd2 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Lock used for accessing the WebSockets watchlist.
/// </summary>
- private readonly object _webSocketsLock = new object();
+ private readonly Lock _webSocketsLock = new();
private readonly ISessionManager _sessionManager;
private readonly ILogger<SessionWebSocketListener> _logger;
@@ -276,11 +276,11 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="webSocket">The WebSocket.</param>
/// <returns>Task.</returns>
- private Task SendForceKeepAlive(IWebSocketConnection webSocket)
+ private async Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
- return webSocket.SendAsync(
+ await webSocket.SendAsync(
new ForceKeepAliveMessage(WebSocketLostTimeout),
- CancellationToken.None);
+ CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index cf8e0fb00..c45a4a60f 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
private readonly SessionInfo _session;
private readonly List<IWebSocketConnection> _sockets;
+ private readonly ReaderWriterLockSlim _socketsLock;
private bool _disposed = false;
public WebSocketController(
@@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_session = session;
_sessionManager = sessionManager;
- _sockets = new List<IWebSocketConnection>();
+ _sockets = new();
+ _socketsLock = new();
}
- private bool HasOpenSockets => GetActiveSockets().Any();
+ private bool HasOpenSockets
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ try
+ {
+ _socketsLock.EnterReadLock();
+ return _sockets.Any(i => i.State == WebSocketState.Open);
+ }
+ finally
+ {
+ _socketsLock.ExitReadLock();
+ }
+ }
+ }
/// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
@@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
- private IEnumerable<IWebSocketConnection> GetActiveSockets()
- => _sockets.Where(i => i.State == WebSocketState.Open);
-
public void AddWebSocket(IWebSocketConnection connection)
{
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
- _sockets.Add(connection);
-
- connection.Closed += OnConnectionClosed;
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ _sockets.Add(connection);
+ connection.Closed += OnConnectionClosed;
+ }
+ finally
+ {
+ _socketsLock.ExitWriteLock();
+ }
}
private async void OnConnectionClosed(object? sender, EventArgs e)
{
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
- _sockets.Remove(connection);
- connection.Closed -= OnConnectionClosed;
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ _sockets.Remove(connection);
+ connection.Closed -= OnConnectionClosed;
+ }
+ finally
+ {
+ _socketsLock.ExitWriteLock();
+ }
+
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
}
@@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
T data,
CancellationToken cancellationToken)
{
- var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ IWebSocketConnection? socket;
+ try
+ {
+ _socketsLock.EnterReadLock();
+ socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
+ }
+ finally
+ {
+ _socketsLock.ExitReadLock();
+ }
if (socket is null)
{
@@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
return;
}
- foreach (var socket in _sockets)
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ socket.Dispose();
+ }
+
+ _sockets.Clear();
+ }
+ finally
{
- socket.Closed -= OnConnectionClosed;
- socket.Dispose();
+ _socketsLock.ExitWriteLock();
}
+ _socketsLock.Dispose();
_disposed = true;
}
@@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
return;
}
- foreach (var socket in _sockets)
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ await socket.DisposeAsync().ConfigureAwait(false);
+ }
+
+ _sockets.Clear();
+ }
+ finally
{
- socket.Closed -= OnConnectionClosed;
- await socket.DisposeAsync().ConfigureAwait(false);
+ _socketsLock.ExitWriteLock();
}
+ _socketsLock.Dispose();
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index e1c26d012..9afc51108 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -2,8 +2,8 @@
#pragma warning disable CS1591
using System;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index d668c17bf..4c013a8bd 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -1,8 +1,8 @@
#nullable disable
using System;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 622a341b6..cf7786167 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -1,8 +1,8 @@
#nullable disable
#pragma warning disable CS1591
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index 2a3e456c2..e42c8a33a 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -2,8 +2,8 @@
#pragma warning disable CS1591
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index afd8ccf9f..f54188030 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -2,8 +2,8 @@
#pragma warning disable CS1591
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 12f88bf4d..dd2149b57 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,7 +1,7 @@
#nullable disable
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index a7821c0e0..d47e47793 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 00c655634..fdfff8f3b 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <remarks>
/// This lock has priority on locks made on <see cref="Group"/>.
/// </remarks>
- private readonly object _groupsLock = new object();
+ private readonly Lock _groupsLock = new();
private bool _disposed = false;
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index d11b03a2e..ee2e18f73 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -91,7 +93,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request);
+ return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request);
}
if (limit.HasValue)
@@ -99,25 +101,9 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager
- .GetItemList(
- new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
- GroupBySeriesPresentationUniqueKey = true
- },
- parentsFolders.ToList())
- .Cast<Episode>()
- .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
- .Select(GetUniqueSeriesKey)
- .ToList();
-
- // Avoid implicitly captured closure
- var episodes = GetNextUpEpisodes(request, user, items, options);
+ var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff);
+
+ var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options);
return GetResult(episodes, request);
}
@@ -133,36 +119,11 @@ namespace Emby.Server.Implementations.TV
.OrderByDescending(i => i.LastWatchedDate);
}
- // If viewing all next up for all series, remove first episodes
- // But if that returns empty, keep those first episodes (avoid completely empty view)
- var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
- var anyFound = false;
-
return allNextUp
- .Where(i =>
- {
- if (request.DisableFirstEpisode)
- {
- return i.LastWatchedDate != DateTime.MinValue;
- }
-
- if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
- {
- anyFound = true;
- return true;
- }
-
- return !anyFound && i.LastWatchedDate == DateTime.MinValue;
- })
.Select(i => i.GetEpisodeFunction())
.Where(i => i is not null)!;
}
- private static string GetUniqueSeriesKey(Episode episode)
- {
- return episode.SeriesPresentationUniqueKey;
- }
-
private static string GetUniqueSeriesKey(Series series)
{
return series.GetPresentationUniqueKey();
@@ -178,13 +139,13 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
DtoOptions = new DtoOptions
{
- Fields = new[] { ItemFields.SortName },
+ Fields = [ItemFields.SortName],
EnableImages = false
}
};
@@ -202,8 +163,8 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Episode],
+ OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)],
Limit = 1,
IsPlayed = includePlayed,
IsVirtualItem = false,
@@ -228,7 +189,7 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
- IncludeItemTypes = new[] { BaseItemKind.Episode },
+ IncludeItemTypes = [BaseItemKind.Episode],
IsPlayed = includePlayed,
IsVirtualItem = false,
DtoOptions = dtoOptions
@@ -248,7 +209,7 @@ namespace Emby.Server.Implementations.TV
consideredEpisodes.Add(nextEpisode);
}
- var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
+ var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)])
.Cast<Episode>();
if (lastWatchedEpisode is not null)
{
@@ -262,7 +223,7 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
- if (userData.PlaybackPositionTicks > 0)
+ if (userData?.PlaybackPositionTicks > 0)
{
return null;
}
@@ -275,6 +236,11 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
+ if (userData is null)
+ {
+ return (DateTime.MinValue, GetEpisode);
+ }
+
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
return (lastWatchedDate, GetEpisode);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index ce3d6cab8..678475b31 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
- private readonly object _currentInstallationsLock = new object();
+ private readonly Lock _currentInstallationsLock = new();
/// <summary>
/// The current installations.
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.Updates
await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
- // Remove versions with a target ABI greater then the current application version.
+ // Remove versions with a target ABI greater than the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);