diff options
Diffstat (limited to 'Emby.Server.Implementations')
24 files changed, 993 insertions, 1087 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index 91371b833..541b23afd 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -155,94 +155,100 @@ namespace Emby.Server.Implementations.Activity public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) { - using (var connection = GetConnection(true)) - { - var commandText = BaseActivitySelectText; - var whereClauses = new List<string>(); + var commandText = BaseActivitySelectText; + var whereClauses = new List<string>(); - if (minDate.HasValue) + if (minDate.HasValue) + { + whereClauses.Add("DateCreated>=@DateCreated"); + } + if (hasUserId.HasValue) + { + if (hasUserId.Value) { - whereClauses.Add("DateCreated>=@DateCreated"); + whereClauses.Add("UserId not null"); } - if (hasUserId.HasValue) + else { - if (hasUserId.Value) - { - whereClauses.Add("UserId not null"); - } - else - { - whereClauses.Add("UserId is null"); - } + whereClauses.Add("UserId is null"); } + } - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value.ToString(_usCulture))); - } + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - var whereText = whereClauses.Count == 0 ? + if (startIndex.HasValue && startIndex.Value > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); - commandText += whereText; + whereClauses.Add( + string.Format( + CultureInfo.InvariantCulture, + "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", + pagingWhereText, + startIndex.Value)); + } - commandText += " ORDER BY DateCreated DESC"; + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - if (limit.HasValue) - { - commandText += " LIMIT " + limit.Value.ToString(_usCulture); - } + commandText += whereText; - var statementTexts = new List<string>(); - statementTexts.Add(commandText); - statementTexts.Add("select count (Id) from ActivityLog" + whereTextWithoutPaging); + commandText += " ORDER BY DateCreated DESC"; - return connection.RunInTransaction(db => - { - var list = new List<ActivityLogEntry>(); - var result = new QueryResult<ActivityLogEntry>(); + if (limit.HasValue) + { + commandText += " LIMIT " + limit.Value.ToString(_usCulture); + } + + var statementTexts = new[] + { + commandText, + "select count (Id) from ActivityLog" + whereTextWithoutPaging + }; - var statements = PrepareAll(db, statementTexts).ToList(); + var list = new List<ActivityLogEntry>(); + var result = new QueryResult<ActivityLogEntry>(); - using (var statement = statements[0]) + using (var connection = GetConnection(true)) + { + connection.RunInTransaction( + db => { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } + var statements = PrepareAll(db, statementTexts).ToList(); - foreach (var row in statement.ExecuteQuery()) + using (var statement = statements[0]) { - list.Add(GetEntry(row)); + if (minDate.HasValue) + { + statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); + } + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetEntry(row)); + } } - } - using (var statement = statements[1]) - { - if (minDate.HasValue) + using (var statement = statements[1]) { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } + if (minDate.HasValue) + { + statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); + } - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - - result.Items = list.ToArray(); - return result; - - }, ReadTransactionMode); + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } + }, + ReadTransactionMode); } + + result.Items = list.ToArray(); + return result; } private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e5e095ca1..6ab3d1bb1 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -315,8 +315,6 @@ namespace Emby.Server.Implementations private IMediaSourceManager MediaSourceManager { get; set; } - private IPlaylistManager PlaylistManager { get; set; } - private readonly IConfiguration _configuration; /// <summary> @@ -325,14 +323,6 @@ namespace Emby.Server.Implementations /// <value>The installation manager.</value> protected IInstallationManager InstallationManager { get; private set; } - /// <summary> - /// Gets or sets the zip client. - /// </summary> - /// <value>The zip client.</value> - protected IZipClient ZipClient { get; private set; } - - protected IHttpResultFactory HttpResultFactory { get; private set; } - protected IAuthService AuthService { get; private set; } public IStartupOptions StartupOptions { get; } @@ -512,13 +502,8 @@ namespace Emby.Server.Implementations return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); } - /// <summary> - /// Gets the exports. - /// </summary> - /// <typeparam name="T">The type</typeparam> - /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param> - /// <returns>IEnumerable{``0}.</returns> - public IEnumerable<T> GetExports<T>(bool manageLifetime = true) + /// <inheritdoc /> + public IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true) { var parts = GetExportTypes<T>() .Select(CreateInstanceSafe) @@ -540,6 +525,7 @@ namespace Emby.Server.Implementations /// <summary> /// Runs the startup tasks. /// </summary> + /// <returns><see cref="Task" />.</returns> public async Task RunStartupTasksAsync() { Logger.LogInformation("Running startup tasks"); @@ -552,7 +538,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("ServerId: {0}", SystemId); - var entryPoints = GetExports<IServerEntryPoint>().ToList(); + var entryPoints = GetExports<IServerEntryPoint>(); var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -684,8 +670,6 @@ namespace Emby.Server.Implementations await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); } - public static IStreamHelper StreamHelper { get; set; } - /// <summary> /// Registers resources that classes will depend on /// </summary> @@ -729,8 +713,7 @@ namespace Emby.Server.Implementations ProcessFactory = new ProcessFactory(); serviceCollection.AddSingleton(ProcessFactory); - ApplicationHost.StreamHelper = new StreamHelper(); - serviceCollection.AddSingleton(StreamHelper); + serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper)); serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider)); @@ -739,18 +722,16 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager)); - ZipClient = new ZipClient(); - serviceCollection.AddSingleton(ZipClient); + serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient)); - HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper); - serviceCollection.AddSingleton(HttpResultFactory); + serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory)); serviceCollection.AddSingleton<IServerApplicationHost>(this); serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton(ServerConfigurationManager); - LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory); + LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory.CreateLogger<LocalizationManager>()); await LocalizationManager.LoadAll().ConfigureAwait(false); serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager); @@ -774,7 +755,8 @@ namespace Emby.Server.Implementations _userRepository = GetUserRepository(); - UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + UserManager = new UserManager(LoggerFactory.CreateLogger<UserManager>(), _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + serviceCollection.AddSingleton(UserManager); LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); @@ -807,7 +789,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(HttpServer); - ImageProcessor = GetImageProcessor(); + ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); serviceCollection.AddSingleton(ImageProcessor); TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); @@ -840,8 +822,7 @@ namespace Emby.Server.Implementations CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager); serviceCollection.AddSingleton(CollectionManager); - PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LoggerFactory, UserManager, ProviderManager); - serviceCollection.AddSingleton(PlaylistManager); + serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager)); LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager); serviceCollection.AddSingleton(LiveTvManager); @@ -959,11 +940,6 @@ namespace Emby.Server.Implementations } } - private IImageProcessor GetImageProcessor() - { - return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); - } - /// <summary> /// Gets the user repository. /// </summary> @@ -1096,7 +1072,7 @@ namespace Emby.Server.Implementations GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - ImageProcessor.AddParts(GetExports<IImageEnhancer>()); + ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>(); LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 77f5d9479..1a67ab912 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -62,14 +62,14 @@ namespace Emby.Server.Implementations.Data /// <returns>Task.</returns> private void InitializeInternal() { - using (var connection = GetConnection()) + string[] queries = { - string[] queries = { - - "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" - }; + "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + using (var connection = GetConnection()) + { connection.RunQueries(queries); } } @@ -81,7 +81,6 @@ namespace Emby.Server.Implementations.Data /// <param name="userId">The user id.</param> /// <param name="client">The client.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">item</exception> public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) { @@ -99,10 +98,9 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => - { - SaveDisplayPreferences(displayPreferences, userId, client, db); - }, TransactionMode); + connection.RunInTransaction( + db => SaveDisplayPreferences(displayPreferences, userId, client, db), + TransactionMode); } } @@ -127,7 +125,6 @@ namespace Emby.Server.Implementations.Data /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">item</exception> public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken) { @@ -140,13 +137,15 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => - { - foreach (var displayPreference in displayPreferences) + connection.RunInTransaction( + db => { - SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); - } - }, TransactionMode); + foreach (var displayPreference in displayPreferences) + { + SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); + } + }, + TransactionMode); } } @@ -180,12 +179,12 @@ namespace Emby.Server.Implementations.Data return Get(row); } } - - return new DisplayPreferences - { - Id = guidId.ToString("N", CultureInfo.InvariantCulture) - }; } + + return new DisplayPreferences + { + Id = guidId.ToString("N", CultureInfo.InvariantCulture) + }; } /// <summary> @@ -215,22 +214,12 @@ namespace Emby.Server.Implementations.Data } private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row) - { - using (var stream = new MemoryStream(row[0].ToBlob())) - { - stream.Position = 0; - return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); - } - } + => _jsonSerializer.DeserializeFromString<DisplayPreferences>(row.GetString(0)); public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) - { - SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); - } + => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) - { - return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); - } + => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); } } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index c0f27b25a..0fb2c10fd 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -18,10 +18,6 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction(conn => { - //foreach (var query in queries) - //{ - // conn.Execute(query); - //} conn.ExecuteAll(string.Join(";", queries)); }); } @@ -38,7 +34,8 @@ namespace Emby.Server.Implementations.Data public static Guid ReadGuidFromBlob(this IResultSetValue result) { - return new Guid(result.ToBlob()); + // TODO: Remove ToArray when upgrading to netstandard2.1 + return new Guid(result.ToBlob().ToArray()); } public static string ToDateTimeParamValue(this DateTime dateValue) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index bb4c34f02..9d983307f 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -99,35 +99,114 @@ namespace Emby.Server.Implementations.Data /// </summary> public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) { - using (var connection = GetConnection()) - { - const string createMediaStreamsTableCommand + 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, PRIMARY KEY (ItemId, StreamIndex))"; - string[] queries = { - "PRAGMA locking_mode=EXCLUSIVE", + string[] queries = + { + "PRAGMA locking_mode=EXCLUSIVE", - "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 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 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 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)", + "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)", + "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))", + "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, + CreateMediaStreamsTableCommand, - "pragma shrink_memory" - }; + "pragma shrink_memory" + }; + + string[] postQueries = + { + // obsolete + "drop index if exists idx_TypedBaseItems", + "drop index if exists idx_mediastreams", + "drop index if exists idx_mediastreams1", + "drop index if exists idx_"+ChaptersTableName, + "drop index if exists idx_UserDataKeys1", + "drop index if exists idx_UserDataKeys2", + "drop index if exists idx_TypeTopParentId3", + "drop index if exists idx_TypeTopParentId2", + "drop index if exists idx_TypeTopParentId4", + "drop index if exists idx_Type", + "drop index if exists idx_TypeTopParentId", + "drop index if exists idx_GuidType", + "drop index if exists idx_TopParentId", + "drop index if exists idx_TypeTopParentId6", + "drop index if exists idx_ItemValues2", + "drop index if exists Idx_ProviderIds", + "drop index if exists idx_ItemValues3", + "drop index if exists idx_ItemValues4", + "drop index if exists idx_ItemValues5", + "drop index if exists idx_UserDataKeys3", + "drop table if exists UserDataKeys", + "drop table if exists ProviderIds", + "drop index if exists Idx_ProviderIds1", + "drop table if exists Images", + "drop index if exists idx_Images", + "drop index if exists idx_TypeSeriesPresentationUniqueKey", + "drop index if exists idx_SeriesPresentationUniqueKey", + "drop index if exists idx_TypeSeriesPresentationUniqueKey2", + "drop index if exists idx_AncestorIds3", + "drop index if exists idx_AncestorIds4", + "drop index if exists idx_AncestorIds2", + + "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)", + }; + + using (var connection = GetConnection()) + { connection.RunQueries(queries); connection.RunInTransaction(db => @@ -235,83 +314,6 @@ namespace Emby.Server.Implementations.Data }, TransactionMode); - string[] postQueries = - { - // obsolete - "drop index if exists idx_TypedBaseItems", - "drop index if exists idx_mediastreams", - "drop index if exists idx_mediastreams1", - "drop index if exists idx_"+ChaptersTableName, - "drop index if exists idx_UserDataKeys1", - "drop index if exists idx_UserDataKeys2", - "drop index if exists idx_TypeTopParentId3", - "drop index if exists idx_TypeTopParentId2", - "drop index if exists idx_TypeTopParentId4", - "drop index if exists idx_Type", - "drop index if exists idx_TypeTopParentId", - "drop index if exists idx_GuidType", - "drop index if exists idx_TopParentId", - "drop index if exists idx_TypeTopParentId6", - "drop index if exists idx_ItemValues2", - "drop index if exists Idx_ProviderIds", - "drop index if exists idx_ItemValues3", - "drop index if exists idx_ItemValues4", - "drop index if exists idx_ItemValues5", - "drop index if exists idx_UserDataKeys3", - "drop table if exists UserDataKeys", - "drop table if exists ProviderIds", - "drop index if exists Idx_ProviderIds1", - "drop table if exists Images", - "drop index if exists idx_Images", - "drop index if exists idx_TypeSeriesPresentationUniqueKey", - "drop index if exists idx_SeriesPresentationUniqueKey", - "drop index if exists idx_TypeSeriesPresentationUniqueKey2", - "drop index if exists idx_AncestorIds3", - "drop index if exists idx_AncestorIds4", - "drop index if exists idx_AncestorIds2", - - "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)", - }; - connection.RunQueries(postQueries); } @@ -1298,18 +1300,13 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(type)) { - using (var stream = new MemoryStream(reader[1].ToBlob())) + try { - stream.Position = 0; - - try - { - item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; - } - catch (SerializationException ex) - { - Logger.LogError(ex, "Error deserializing item"); - } + item = _jsonSerializer.DeserializeFromString(reader.GetString(1), type) as BaseItem; + } + catch (SerializationException ex) + { + Logger.LogError(ex, "Error deserializing item"); } } @@ -1999,14 +1996,14 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(chapters)); } + var idBlob = id.ToGuidBlob(); + using (var connection = GetConnection()) { connection.RunInTransaction(db => { - var idBlob = id.ToGuidBlob(); - - // First delete chapters - db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); + // First delete chapters + db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); InsertChapters(idBlob, chapters, db); @@ -2535,6 +2532,7 @@ namespace Emby.Server.Implementations.Data commandText += " where " + string.Join(" AND ", whereClauses); } + int count; using (var connection = GetConnection(true)) { using (var statement = PrepareStatement(connection, commandText)) @@ -2550,11 +2548,12 @@ namespace Emby.Server.Implementations.Data // Running this again will bind the params GetWhereClauses(query, statement); - var count = statement.ExecuteQuery().SelectScalarInt().First(); - LogQueryTime("GetCount", commandText, now); - return count; + count = statement.ExecuteQuery().SelectScalarInt().First(); } } + + LogQueryTime("GetCount", commandText, now); + return count; } public List<BaseItem> GetItemList(InternalItemsQuery query) @@ -2604,10 +2603,9 @@ namespace Emby.Server.Implementations.Data } } + var items = new List<BaseItem>(); using (var connection = GetConnection(true)) { - var items = new List<BaseItem>(); - using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -2658,11 +2656,11 @@ namespace Emby.Server.Implementations.Data items = newList; } + } - LogQueryTime("GetItemList", commandText, now); + LogQueryTime("GetItemList", commandText, now); - return items; - } + return items; } private string FixUnicodeChars(string buffer) @@ -2755,8 +2753,6 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var list = new List<BaseItem>(); - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2822,11 +2818,13 @@ namespace Emby.Server.Implementations.Data statementTexts.Add(commandText); } + var list = new List<BaseItem>(); + var result = new QueryResult<BaseItem>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => + connection.RunInTransaction(db => { - var result = new QueryResult<BaseItem>(); + var statements = PrepareAll(db, statementTexts).ToList(); if (!isReturningZeroItems) @@ -2881,14 +2879,12 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } } - - LogQueryTime("GetItems", commandText, now); - - result.Items = list.ToArray(); - return result; - }, ReadTransactionMode); } + + LogQueryTime("GetItems", commandText, now); + result.Items = list.ToArray(); + return result; } private string GetOrderByText(InternalItemsQuery query) @@ -3054,10 +3050,9 @@ namespace Emby.Server.Implementations.Data } } + var list = new List<Guid>(); using (var connection = GetConnection(true)) { - var list = new List<Guid>(); - using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -3076,11 +3071,10 @@ namespace Emby.Server.Implementations.Data list.Add(row[0].ReadGuidFromBlob()); } } - - LogQueryTime("GetItemList", commandText, now); - - return list; } + + LogQueryTime("GetItemList", commandText, now); + return list; } public List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query) @@ -3142,6 +3136,7 @@ namespace Emby.Server.Implementations.Data { path = row.GetString(1); } + list.Add(new Tuple<Guid, string>(id, path)); } } @@ -3203,7 +3198,7 @@ namespace Emby.Server.Implementations.Data } } - var list = new List<Guid>(); + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; var statementTexts = new List<string>(); @@ -3233,12 +3228,12 @@ namespace Emby.Server.Implementations.Data statementTexts.Add(commandText); } + var list = new List<Guid>(); + var result = new QueryResult<Guid>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => + connection.RunInTransaction(db => { - var result = new QueryResult<Guid>(); - var statements = PrepareAll(db, statementTexts).ToList(); if (!isReturningZeroItems) @@ -3281,14 +3276,13 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } } - - LogQueryTime("GetItemIds", commandText, now); - - result.Items = list.ToArray(); - return result; - }, ReadTransactionMode); } + + LogQueryTime("GetItemIds", commandText, now); + + result.Items = list.ToArray(); + return result; } private bool IsAlphaNumeric(string str) @@ -4864,22 +4858,25 @@ namespace Emby.Server.Implementations.Data private void UpdateInheritedTags(CancellationToken cancellationToken) { - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => + string sql = string.Join( + ";", + new string[] { - connection.ExecuteAll(string.Join(";", new string[] - { - "delete from itemvalues where type = 6", + "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 ItemId, 6, Value, CleanValue from ItemValues where Type=4", - @"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue + @"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()) + { + connection.RunInTransaction(db => + { + connection.ExecuteAll(sql); }, TransactionMode); } @@ -4933,23 +4930,23 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var idBlob = id.ToGuidBlob(); - // Delete people - ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); + // Delete people + ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); - // Delete chapters - ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); + // Delete chapters + ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); - // Delete media streams - ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); + // Delete media streams + ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); - // Delete ancestors - ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); + // Delete ancestors + ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); - // Delete item values - ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); + // Delete item values + ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); - // Delete the item - ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); + // Delete the item + ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); }, TransactionMode); } } @@ -4997,6 +4994,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type list.Add(row.GetString(0)); } } + return list; } } @@ -5247,10 +5245,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += " Group By CleanValue"; + var list = new List<string>(); using (var connection = GetConnection(true)) { - var list = new List<string>(); - using (var statement = PrepareStatement(connection, commandText)) { foreach (var row in statement.ExecuteQuery()) @@ -5262,10 +5259,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - LogQueryTime("GetItemValueNames", commandText, now); - - return list; } + + LogQueryTime("GetItemValueNames", commandText, now); + return list; } private QueryResult<(BaseItem, ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) @@ -5422,6 +5419,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { statementTexts.Add(commandText); } + if (query.EnableTotalRecordCount) { var countText = "select " @@ -5433,98 +5431,98 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statementTexts.Add(countText); } + var list = new List<(BaseItem, ItemCounts)>(); + var result = new QueryResult<(BaseItem, ItemCounts)>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => - { - var list = new List<(BaseItem, ItemCounts)>(); - var result = new QueryResult<(BaseItem, ItemCounts)>(); - - var statements = PrepareAll(db, statementTexts).ToList(); - - if (!isReturningZeroItems) + connection.RunInTransaction( + db => { - using (var statement = statements[0]) + var statements = PrepareAll(db, statementTexts).ToList(); + + if (!isReturningZeroItems) { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) + using (var statement = statements[0]) { - statement.TryBind("@UserId", query.User.InternalId); - } + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } - if (typeSubQuery != null) - { - GetWhereClauses(typeSubQuery, null); - } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null); + } - 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); + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item != null) + 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 countStartColumn = columns.Count - 1; + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item != null) + { + var countStartColumn = columns.Count - 1; - list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); + list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); + } } } - - LogQueryTime("GetItemValues", commandText, now); } - } - - if (query.EnableTotalRecordCount) - { - commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query) - + whereText; - using (var statement = statements[statements.Count - 1]) + if (query.EnableTotalRecordCount) { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + commandText = "select " + + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + + GetFromText() + + GetJoinUserDataText(query) + + whereText; - if (typeSubQuery != null) + using (var statement = statements[statements.Count - 1]) { - GetWhereClauses(typeSubQuery, null); - } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null); + } + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); - LogQueryTime("GetItemValues", commandText, now); + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } } - } - - if (result.TotalRecordCount == 0) - { - result.TotalRecordCount = list.Count; - } - result.Items = list.ToArray(); + }, + ReadTransactionMode); + } - return result; + LogQueryTime("GetItemValues", commandText, now); - }, ReadTransactionMode); + if (result.TotalRecordCount == 0) + { + result.TotalRecordCount = list.Count; } + + result.Items = list.ToArray(); + + return result; } private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount) @@ -5707,8 +5705,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var itemIdBlob = itemId.ToGuidBlob(); - // First delete chapters - db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); + // First delete chapters + db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); InsertPeople(itemIdBlob, people, db); @@ -5868,8 +5866,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var itemIdBlob = id.ToGuidBlob(); - // First delete chapters - db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); + // First delete chapters + db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); InsertMediaStreams(itemIdBlob, streams, db); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4035bb99d..9d4855bcf 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data var userDatasTableExists = TableExists(connection, "UserDatas"); var userDataTableExists = TableExists(connection, "userdata"); - var users = userDatasTableExists ? null : userManager.Users.ToArray(); + var users = userDatasTableExists ? null : userManager.Users; connection.RunInTransaction(db => { @@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Data } } - private void ImportUserIds(IDatabaseConnection db, User[] users) + private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users) { var userIdsWithUserData = GetAllUserIdsWithUserData(db); diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index d6d250fb3..11629b389 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -35,9 +35,8 @@ namespace Emby.Server.Implementations.Data public string Name => "SQLite"; /// <summary> - /// Opens the connection to the database + /// Opens the connection to the database. /// </summary> - /// <returns>Task.</returns> public void Initialize() { using (var connection = GetConnection()) @@ -180,14 +179,10 @@ namespace Emby.Server.Implementations.Data var id = row[0].ToInt64(); var guid = row[1].ReadGuidFromBlob(); - using (var stream = new MemoryStream(row[2].ToBlob())) - { - stream.Position = 0; - var user = _jsonSerializer.DeserializeFromStream<User>(stream); - user.InternalId = id; - user.Id = guid; - return user; - } + var user = _jsonSerializer.DeserializeFromString<User>(row.GetString(2)); + user.InternalId = id; + user.Id = guid; + return user; } /// <summary> diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 6e7aa1313..1a7f10634 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1364,7 +1364,7 @@ namespace Emby.Server.Implementations.Dto return null; } - var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary); + var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray(); ImageDimensions size; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c78d96d4a..b48193c58 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -34,7 +34,7 @@ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" /> <PackageReference Include="sharpcompress" Version="0.23.0" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" /> </ItemGroup> <ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index b7565adec..b2328121e 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -50,9 +50,7 @@ namespace Emby.Server.Implementations.EntryPoints public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { - var users = _userManager.Users.ToList(); - - foreach (var user in users) + foreach (var user in _userManager.Users) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 4c233456c..bdcf5d0b7 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; using MediaBrowser.Common.Extensions; @@ -470,64 +471,10 @@ namespace Emby.Server.Implementations.HttpServer urlToLog = GetUrlToLog(urlString); - if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || - string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(localPath, "/" + _config.Configuration.BaseUrl + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/" + _config.Configuration.BaseUrl, StringComparison.OrdinalIgnoreCase)) { - httpRes.Redirect(_defaultRedirectPath); - return; - } - - if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || - string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) - { - httpRes.Redirect("emby/" + _defaultRedirectPath); - return; - } - - if (localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1) - { - httpRes.StatusCode = 200; - httpRes.ContentType = "text/html"; - var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) - .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); - - if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) - { - await httpRes.WriteAsync( - "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + - newUrl + "\">" + newUrl + "</a></body></html>", - cancellationToken).ConfigureAwait(false); - return; - } - } - - if (localPath.IndexOf("dashboard/", StringComparison.OrdinalIgnoreCase) != -1 && - localPath.IndexOf("web/dashboard", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 200; - httpRes.ContentType = "text/html"; - var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) - .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); - - if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) - { - await httpRes.WriteAsync( - "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + - newUrl + "\">" + newUrl + "</a></body></html>", - cancellationToken).ConfigureAwait(false); - return; - } - } - - if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase)) - { - httpRes.Redirect(_defaultRedirectPath); - return; - } - - if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) - { - httpRes.Redirect("../" + _defaultRedirectPath); + httpRes.Redirect("/" + _config.Configuration.BaseUrl + "/" + _defaultRedirectPath); return; } @@ -543,19 +490,6 @@ namespace Emby.Server.Implementations.HttpServer return; } - if (!string.Equals(httpReq.QueryString["r"], "0", StringComparison.OrdinalIgnoreCase)) - { - if (localPath.EndsWith("web/dashboard.html", StringComparison.OrdinalIgnoreCase)) - { - httpRes.Redirect("index.html#!/dashboard.html"); - } - - if (localPath.EndsWith("web/home.html", StringComparison.OrdinalIgnoreCase)) - { - httpRes.Redirect("index.html"); - } - } - if (!string.IsNullOrEmpty(GlobalResponse)) { // We don't want the address pings in ApplicationHost to fail @@ -569,7 +503,6 @@ namespace Emby.Server.Implementations.HttpServer } var handler = GetServiceHandler(httpReq); - if (handler != null) { await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false); @@ -663,22 +596,14 @@ namespace Emby.Server.Implementations.HttpServer foreach (var route in clone) { - routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) + routes.Add(new RouteAttribute(NormalizeCustomRoutePath(_config.Configuration.BaseUrl, route.Path), route.Verbs) { Notes = route.Notes, Priority = route.Priority, Summary = route.Summary }); - routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - // needed because apps add /emby, and some users also add /emby, thereby double prefixing - routes.Add(new RouteAttribute(DoubleNormalizeEmbyRoutePath(route.Path), route.Verbs) + routes.Add(new RouteAttribute(NormalizeOldRoutePath(route.Path), route.Verbs) { Notes = route.Notes, Priority = route.Priority, @@ -719,8 +644,8 @@ namespace Emby.Server.Implementations.HttpServer return _socketListener.ProcessWebSocketRequest(context); } - //TODO Add Jellyfin Route Path Normalizer - private static string NormalizeEmbyRoutePath(string path) + // this method was left for compatibility with third party clients + private static string NormalizeOldRoutePath(string path) { if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { @@ -730,24 +655,14 @@ namespace Emby.Server.Implementations.HttpServer return "emby/" + path; } - private static string NormalizeMediaBrowserRoutePath(string path) - { - if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) - { - return "/mediabrowser" + path; - } - - return "mediabrowser/" + path; - } - - private static string DoubleNormalizeEmbyRoutePath(string path) + private static string NormalizeCustomRoutePath(string baseUrl, string path) { if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { - return "/emby/emby" + path; + return "/" + baseUrl + path; } - return "emby/emby/" + path; + return baseUrl + "/" + path; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/IO/IsoManager.cs b/Emby.Server.Implementations/IO/IsoManager.cs index f0a15097c..94e92c2a6 100644 --- a/Emby.Server.Implementations/IO/IsoManager.cs +++ b/Emby.Server.Implementations/IO/IsoManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -8,12 +9,12 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.IO { /// <summary> - /// Class IsoManager + /// Class IsoManager. /// </summary> public class IsoManager : IIsoManager { /// <summary> - /// The _mounters + /// The _mounters. /// </summary> private readonly List<IIsoMounter> _mounters = new List<IIsoMounter>(); @@ -22,9 +23,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="isoPath">The iso path.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>IsoMount.</returns> - /// <exception cref="ArgumentNullException">isoPath</exception> - /// <exception cref="ArgumentException"></exception> + /// <returns><see creaf="IsoMount" />.</returns> public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(isoPath)) @@ -36,7 +35,11 @@ namespace Emby.Server.Implementations.IO if (mounter == null) { - throw new ArgumentException(string.Format("No mounters are able to mount {0}", isoPath)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "No mounters are able to mount {0}", + isoPath)); } return mounter.Mount(isoPath, cancellationToken); @@ -60,16 +63,5 @@ namespace Emby.Server.Implementations.IO { _mounters.AddRange(mounters); } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - foreach (var mounter in _mounters) - { - mounter.Dispose(); - } - } } } diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index c7044820c..fa6bbcf91 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; -using System.Text; +using System.Security.Cryptography; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; @@ -17,32 +14,37 @@ namespace Emby.Server.Implementations.Library { public class DefaultPasswordResetProvider : IPasswordResetProvider { - public string Name => "Default Password Reset Provider"; + private const string BaseResetFileName = "passwordreset"; - public bool IsEnabled => true; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; private readonly string _passwordResetFileBase; private readonly string _passwordResetFileBaseDir; - private readonly string _passwordResetFileBaseName = "passwordreset"; - private readonly IJsonSerializer _jsonSerializer; - private readonly IUserManager _userManager; - private readonly ICryptoProvider _crypto; - - public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider) + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) { _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; - _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName); + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); _jsonSerializer = jsonSerializer; _userManager = userManager; - _crypto = cryptoProvider; } + /// <inheritdoc /> + public string Name => "Default Password Reset Provider"; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) { SerializablePasswordReset spr; - HashSet<string> usersreset = new HashSet<string>(); - foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*")) + List<string> usersreset = new List<string>(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) { using (var str = File.OpenRead(resetfile)) { @@ -53,12 +55,15 @@ namespace Emby.Server.Implementations.Library { File.Delete(resetfile); } - else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase)) + else if (string.Equals( + spr.Pin.Replace("-", string.Empty), + pin.Replace("-", string.Empty), + StringComparison.InvariantCultureIgnoreCase)) { var resetUser = _userManager.GetUserByName(spr.UserName); if (resetUser == null) { - throw new Exception($"User with a username of {spr.UserName} not found"); + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); } await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); @@ -81,10 +86,11 @@ namespace Emby.Server.Implementations.Library } } + /// <inheritdoc /> public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) { string pin = string.Empty; - using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create()) + using (var cryptoRandom = RandomNumberGenerator.Create()) { byte[] bytes = new byte[4]; cryptoRandom.GetBytes(bytes); diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index c8c8a108d..a7ea13ca6 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,13 +12,11 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; @@ -41,34 +40,19 @@ namespace Emby.Server.Implementations.Library public class UserManager : IUserManager { /// <summary> - /// Gets the users. - /// </summary> - /// <value>The users.</value> - public IEnumerable<User> Users => _users; - - private User[] _users; - - /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly object _policySyncLock = new object(); /// <summary> /// Gets the active user repository /// </summary> /// <value>The user repository.</value> - private IUserRepository UserRepository { get; set; } - public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; - + private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; - private readonly INetworkManager _networkManager; private readonly Func<IImageProcessor> _imageProcessorFactory; @@ -76,6 +60,8 @@ namespace Emby.Server.Implementations.Library private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; + private ConcurrentDictionary<Guid, User> _users; + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; @@ -85,8 +71,7 @@ namespace Emby.Server.Implementations.Library private DefaultPasswordResetProvider _defaultPasswordResetProvider; public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, + ILogger<UserManager> logger, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, @@ -96,8 +81,8 @@ namespace Emby.Server.Implementations.Library IJsonSerializer jsonSerializer, IFileSystem fileSystem) { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; + _logger = logger; + _userRepository = userRepository; _xmlSerializer = xmlSerializer; _networkManager = networkManager; _imageProcessorFactory = imageProcessorFactory; @@ -105,8 +90,51 @@ namespace Emby.Server.Implementations.Library _appHost = appHost; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - _users = Array.Empty<User>(); + _users = null; + } + + public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; + + /// <summary> + /// Occurs when [user updated]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserUpdated; + + public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; + + public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; + + public event EventHandler<GenericEventArgs<User>> UserLockedOut; + + public event EventHandler<GenericEventArgs<User>> UserCreated; + + /// <summary> + /// Occurs when [user deleted]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserDeleted; + + /// <inheritdoc /> + public IEnumerable<User> Users => _users.Values; + + /// <inheritdoc /> + public IEnumerable<Guid> UsersIds => _users.Keys; + + /// <summary> + /// Called when [user updated]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + } + + /// <summary> + /// Called when [user deleted]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); } public NameIdPair[] GetAuthenticationProviders() @@ -137,7 +165,7 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders) + public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders) { _authenticationProviders = authenticationProviders.ToArray(); @@ -150,54 +178,21 @@ namespace Emby.Server.Implementations.Library _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); } - #region UserUpdated Event - /// <summary> - /// Occurs when [user updated]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserUpdated; - public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; - public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; - public event EventHandler<GenericEventArgs<User>> UserLockedOut; - - /// <summary> - /// Called when [user updated]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// <summary> - /// Occurs when [user deleted]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserDeleted; - /// <summary> - /// Called when [user deleted]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - #endregion - /// <summary> - /// Gets a User by Id + /// Gets a User by Id. /// </summary> /// <param name="id">The id.</param> /// <returns>User.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentException"></exception> public User GetUserById(Guid id) { if (id == Guid.Empty) { - throw new ArgumentException(nameof(id), "Guid can't be empty"); + throw new ArgumentException("Guid can't be empty", nameof(id)); } - return Users.FirstOrDefault(u => u.Id == id); + _users.TryGetValue(id, out User user); + return user; } /// <summary> @@ -206,15 +201,13 @@ namespace Emby.Server.Implementations.Library /// <param name="id">The identifier.</param> /// <returns>User.</returns> public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } + => GetUserById(new Guid(id)); public User GetUserByName(string name) { if (string.IsNullOrWhiteSpace(name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentException("Invalid username", nameof(name)); } return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); @@ -222,8 +215,9 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - var users = LoadUsers(); - _users = users.ToArray(); + LoadUsers(); + + var users = Users; // If there are no local users with admin rights, make them all admins if (!users.Any(i => i.Policy.IsAdministrator)) @@ -240,14 +234,12 @@ namespace Emby.Server.Implementations.Library { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) - { - return IsValidUsername(i.ToString()); - } + => IsValidUsername(i.ToString(CultureInfo.InvariantCulture)); public string MakeValidUsername(string username) { @@ -277,8 +269,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(username)); } - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; string updatedUsername = null; @@ -299,13 +290,12 @@ namespace Emby.Server.Implementations.Library updatedUsername = authResult.username; success = authResult.success; - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) { // We should trust the user that the authprovider says, not what was typed - if (updatedUsername != username) - { - username = updatedUsername; - } + username = updatedUsername; // Search the database for the user again; the authprovider might have created it user = Users @@ -337,10 +327,11 @@ namespace Emby.Server.Implementations.Library if (user.Policy.IsDisabled) { - throw new AuthenticationException(string.Format( - CultureInfo.InvariantCulture, - "The {0} account is currently disabled. Please consult with your administrator.", - user.Name)); + throw new AuthenticationException( + string.Format( + CultureInfo.InvariantCulture, + "The {0} account is currently disabled. Please consult with your administrator.", + user.Name)); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) @@ -386,7 +377,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider GetAuthenticationProvider(User user) { - return GetAuthenticationProviders(user).First(); + return GetAuthenticationProviders(user)[0]; } private IPasswordResetProvider GetPasswordResetProvider(User user) @@ -396,7 +387,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider[] GetAuthenticationProviders(User user) { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + var authenticationProviderId = user?.Policy.AuthenticationProviderId; var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); @@ -438,16 +429,10 @@ namespace Emby.Server.Implementations.Library { try { - var requiresResolvedUser = provider as IRequiresResolvedUser; - ProviderAuthenticationResult authenticationResult = null; - if (requiresResolvedUser != null) - { - authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); - } + + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); if (authenticationResult.Username != username) { @@ -467,7 +452,6 @@ namespace Emby.Server.Implementations.Library private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) { - string updatedUsername = null; bool success = false; IAuthenticationProvider authenticationProvider = null; @@ -487,7 +471,7 @@ namespace Emby.Server.Implementations.Library foreach (var provider in GetAuthenticationProviders(user)) { var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - updatedUsername = providerAuthResult.username; + var updatedUsername = providerAuthResult.username; success = providerAuthResult.success; if (success) @@ -499,25 +483,32 @@ namespace Emby.Server.Implementations.Library } } - if (user != null) + if (user != null + && !success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user.Configuration.EnableLocalPassword) { - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) + if (password == null) { - if (password == null) - { - // legacy - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } + // legacy + success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); + } + else + { + success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); } } return (authenticationProvider, username, success); } + private string GetLocalPasswordHash(User user) + { + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : PasswordHash.ConvertToByteString(new PasswordHash(user.EasyPassword).Hash); + } + private void UpdateInvalidLoginAttemptCount(User user, int newValue) { if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) @@ -556,17 +547,18 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Loads the users from the repository + /// Loads the users from the repository. /// </summary> - /// <returns>IEnumerable{User}.</returns> - private List<User> LoadUsers() + private void LoadUsers() { - var users = UserRepository.RetrieveAllUsers(); + var users = _userRepository.RetrieveAllUsers(); // There always has to be at least one user. if (users.Count != 0) { - return users; + _users = new ConcurrentDictionary<Guid, User>( + users.Select(x => new KeyValuePair<Guid, User>(x.Id, x))); + return; } var defaultName = Environment.UserName; @@ -581,14 +573,15 @@ namespace Emby.Server.Implementations.Library user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); user.Policy.IsAdministrator = true; user.Policy.EnableContentDeletion = true; user.Policy.EnableRemoteControlOfOtherUsers = true; UpdateUserPolicy(user, user.Policy, false); - return new List<User> { user }; + _users = new ConcurrentDictionary<Guid, User>(); + _users[user.Id] = user; } public UserDto GetUserDto(User user, string remoteEndPoint = null) @@ -619,7 +612,7 @@ namespace Emby.Server.Implementations.Library Policy = user.Policy }; - if (!hasPassword && Users.Count() == 1) + if (!hasPassword && _users.Count == 1) { dto.EnableAutoLogin = true; } @@ -694,22 +687,26 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (string.IsNullOrEmpty(newName)) + if (string.IsNullOrWhiteSpace(newName)) { - throw new ArgumentNullException(nameof(newName)); + throw new ArgumentException("Invalid username", nameof(newName)); } - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + throw new ArgumentException("The new and old names must be different."); } - if (user.Name.Equals(newName, StringComparison.Ordinal)) + if (Users.Any( + u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException("The new and old names must be different."); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); } - await user.Rename(newName); + await user.Rename(newName).ConfigureAwait(false); OnUserUpdated(user); } @@ -727,23 +724,30 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + if (user.Id == Guid.Empty) + { + throw new ArgumentException("Id can't be empty.", nameof(user)); + } + + if (!_users.ContainsKey(user.Id)) { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "A user '{0}' with Id {1} does not exist.", + user.Name, + user.Id), + nameof(user)); } user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; - UserRepository.UpdateUser(user); + _userRepository.UpdateUser(user); OnUserUpdated(user); } - public event EventHandler<GenericEventArgs<User>> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - /// <summary> /// Creates the user. /// </summary> @@ -751,7 +755,7 @@ namespace Emby.Server.Implementations.Library /// <returns>User.</returns> /// <exception cref="ArgumentNullException">name</exception> /// <exception cref="ArgumentException"></exception> - public async Task<User> CreateUser(string name) + public User CreateUser(string name) { if (string.IsNullOrWhiteSpace(name)) { @@ -768,28 +772,17 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var user = InstantiateNewUser(name); + var user = InstantiateNewUser(name); - var list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); + _users[user.Id] = user; - user.DateLastSaved = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); - return user; - } - finally - { - _userListLock.Release(); - } + return user; } /// <summary> @@ -799,57 +792,59 @@ namespace Emby.Server.Implementations.Library /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">user</exception> /// <exception cref="ArgumentException"></exception> - public async Task DeleteUser(User user) + public void DeleteUser(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + if (!_users.ContainsKey(user.Id)) { - throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Name, + user.Id)); } - if (allUsers.Count == 1) + if (_users.Count == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Name)); } - if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + if (user.Policy.IsAdministrator + && Users.Count(i => i.Policy.IsAdministrator) == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Name), + nameof(user)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + var configPath = GetConfigurationFilePath(user); + + _userRepository.DeleteUser(user); try { - var configPath = GetConfigurationFilePath(user); - - UserRepository.DeleteUser(user); - - try - { - _fileSystem.DeleteFile(configPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {path}", configPath); - } - - DeleteUserPolicy(user); - - _users = allUsers.Where(i => i.Id != user.Id).ToArray(); - - OnUserDeleted(user); + _fileSystem.DeleteFile(configPath); } - finally + catch (IOException ex) { - _userListLock.Release(); + _logger.LogError(ex, "Error deleting file {path}", configPath); } + + DeleteUserPolicy(user); + + _users.TryRemove(user.Id, out _); + + OnUserDeleted(user); } /// <summary> @@ -906,8 +901,7 @@ namespace Emby.Server.Implementations.Library Name = name, Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true + DateModified = DateTime.UtcNow }; } @@ -989,7 +983,6 @@ namespace Emby.Server.Implementations.Library }; } - private readonly object _policySyncLock = new object(); public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) { var user = GetUserById(userId); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index ed254accb..85754ca8b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -31,6 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; + private readonly IStreamHelper _streamHelper; public HdHomerunHost( IServerConfigurationManager config, @@ -40,29 +42,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHttpClient httpClient, IServerApplicationHost appHost, ISocketFactory socketFactory, - INetworkManager networkManager) + INetworkManager networkManager, + IStreamHelper streamHelper) : base(config, logger, jsonSerializer, fileSystem) { _httpClient = httpClient; _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; + _streamHelper = streamHelper; } public string Name => "HD Homerun"; - public override string Type => DeviceType; - - public static string DeviceType => "hdhomerun"; + public override string Type => "hdhomerun"; protected override string ChannelIdPrefix => "hdhr_"; private string GetChannelId(TunerHostInfo info, Channels i) - { - var id = ChannelIdPrefix + i.GuideNumber; - - return id; - } + => ChannelIdPrefix + i.GuideNumber; private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) { @@ -74,19 +72,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun CancellationToken = cancellationToken, BufferContent = false }; - using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) - { - using (var stream = response.Content) - { - var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - if (info.ImportFavoritesOnly) - { - lineup = lineup.Where(i => i.Favorite).ToList(); - } + using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false)) + using (var stream = response.Content) + { + var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - return lineup.Where(i => !i.DRM).ToList(); + if (info.ImportFavoritesOnly) + { + lineup = lineup.Where(i => i.Favorite).ToList(); } + + return lineup.Where(i => !i.DRM).ToList(); } } @@ -139,23 +136,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = string.Format("{0}/discover.json", GetApiUrl(info)), CancellationToken = cancellationToken, BufferContent = false - - }, "GET").ConfigureAwait(false)) + }, HttpMethod.Get).ConfigureAwait(false)) + using (var stream = response.Content) { - using (var stream = response.Content) - { - var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); + var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); - if (!string.IsNullOrEmpty(cacheKey)) + if (!string.IsNullOrEmpty(cacheKey)) + { + lock (_modelCache) { - lock (_modelCache) - { - _modelCache[cacheKey] = discoverResponse; - } + _modelCache[cacheKey] = discoverResponse; } - - return discoverResponse; } + + return discoverResponse; } } catch (HttpException ex) @@ -186,36 +180,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var stream = await _httpClient.Get(new HttpRequestOptions() + using (var response = await _httpClient.SendAsync(new HttpRequestOptions() { Url = string.Format("{0}/tuners.html", GetApiUrl(info)), CancellationToken = cancellationToken, BufferContent = false - })) + }, HttpMethod.Get)) + using (var stream = response.Content) + using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) { var tuners = new List<LiveTvTunerInfo>(); - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) + while (!sr.EndOfStream) { - while (!sr.EndOfStream) + string line = StripXML(sr.ReadLine()); + if (line.Contains("Channel")) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) + LiveTvTunerStatus status; + var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = line.Substring(0, index - 1); + var currentChannel = line.Substring(index + 7); + if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } + tuners.Add(new LiveTvTunerInfo { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); } } + return tuners; } } @@ -245,6 +239,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun bufferIndex++; } } + return new string(buffer, 0, bufferIndex); } @@ -256,7 +251,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var uri = new Uri(GetApiUrl(info)); - using (var manager = new HdHomerunManager(Logger)) + using (var manager = new HdHomerunManager()) { // Legacy HdHomeruns are IPv4 only var ipInfo = IPAddress.Parse(uri.Host); @@ -276,6 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } } + return tuners; } @@ -434,12 +430,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { videoCodec = channelInfo.VideoCodec; } + string audioCodec = channelInfo.AudioCodec; if (!videoBitrate.HasValue) { videoBitrate = isHd ? 15000000 : 2000000; } + int? audioBitrate = isHd ? 448000 : 192000; // normalize @@ -461,6 +459,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { id = "native"; } + id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N", CultureInfo.InvariantCulture); var mediaSource = new MediaSourceInfo @@ -527,29 +526,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } else { - try - { - var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - if (modelInfo != null && modelInfo.SupportsTranscoding) + if (modelInfo != null && modelInfo.SupportsTranscoding) + { + if (info.AllowHWTranscoding) { - if (info.AllowHWTranscoding) - { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); - } + list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); } - } - catch - { + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); } if (list.Count == 0) @@ -582,7 +574,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) { - return new HdHomerunUdpStream(mediaSource, info, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return new HdHomerunUdpStream( + mediaSource, + info, + streamId, + new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), + modelInfo.TunerCount, + FileSystem, + Logger, + Config.ApplicationPaths, + _appHost, + _socketFactory, + _networkManager, + _streamHelper); } var enableHttpStream = true; @@ -599,10 +603,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } mediaSource.Path = httpUrl; - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); - } - - return new HdHomerunUdpStream(mediaSource, info, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper); + } + + return new HdHomerunUdpStream( + mediaSource, + info, + streamId, + new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), + modelInfo.TunerCount, + FileSystem, + Logger, + Config.ApplicationPaths, + _appHost, + _socketFactory, + _networkManager, + _streamHelper); } public async Task Validate(TunerHostInfo info) @@ -701,9 +717,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun catch (OperationCanceledException) { } - catch + catch (Exception ex) { // Socket timeout indicates all messages have been received. + Logger.LogError(ex, "Error while sending discovery message"); } } @@ -718,21 +735,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = url }; - try - { - var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - - hostInfo.DeviceId = modelInfo.DeviceID; - hostInfo.FriendlyName = modelInfo.FriendlyName; + var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - return hostInfo; - } - catch - { - // logged at lower levels - } + hostInfo.DeviceId = modelInfo.DeviceID; + hostInfo.FriendlyName = modelInfo.FriendlyName; - return null; + return hostInfo; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index c19552428..3699b988c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.Net; using System.Net.Sockets; using System.Text; @@ -8,13 +9,12 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.LiveTv; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public interface IHdHomerunChannelCommands { - IEnumerable<Tuple<string, string>> GetCommands(); + IEnumerable<(string, string)> GetCommands(); } public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands @@ -33,16 +33,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public IEnumerable<Tuple<string, string>> GetCommands() + public IEnumerable<(string, string)> GetCommands() { - var commands = new List<Tuple<string, string>>(); - if (!string.IsNullOrEmpty(_channel)) - commands.Add(Tuple.Create("channel", _channel)); + { + yield return ("channel", _channel); + } if (!string.IsNullOrEmpty(_program)) - commands.Add(Tuple.Create("program", _program)); - return commands; + { + yield return ("program", _program); + } } } @@ -57,29 +58,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _profile = profile; } - public IEnumerable<Tuple<string, string>> GetCommands() + public IEnumerable<(string, string)> GetCommands() { - var commands = new List<Tuple<string, string>>(); - if (!string.IsNullOrEmpty(_channel)) { - if (!string.IsNullOrEmpty(_profile) && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(_profile) + && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) { - commands.Add(Tuple.Create("vchannel", string.Format("{0} transcode={1}", _channel, _profile))); + yield return ("vchannel", $"{_channel} transcode={_profile}"); } else { - commands.Add(Tuple.Create("vchannel", _channel)); + yield return ("vchannel", _channel); } } - - return commands; } } public class HdHomerunManager : IDisposable { public const int HdHomeRunPort = 65001; + // Message constants private const byte GetSetName = 3; private const byte GetSetValue = 4; @@ -87,19 +86,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private const ushort GetSetRequest = 4; private const ushort GetSetReply = 5; - private readonly ILogger _logger; - private uint? _lockkey = null; private int _activeTuner = -1; private IPEndPoint _remoteEndPoint; private TcpClient _tcpClient; - public HdHomerunManager(ILogger logger) - { - _logger = logger; - } - public void Dispose() { using (var socket = _tcpClient) @@ -108,8 +100,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { _tcpClient = null; - var task = StopStreaming(socket); - Task.WaitAll(task); + StopStreaming(socket).GetAwaiter().GetResult(); } } } @@ -173,20 +164,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out var returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { continue; } var commandList = commands.GetCommands(); - foreach (Tuple<string, string> command in commandList) + foreach (var command in commandList) { var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); continue; @@ -198,8 +191,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); continue; @@ -231,13 +225,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); try { - foreach (Tuple<string, string> command in commandList) + foreach (var command in commandList) { var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out string returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { return; } @@ -264,21 +259,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue) { - _logger.LogInformation("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue); - var stream = client.GetStream(); var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue); - await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length, CancellationToken.None).ConfigureAwait(false); + await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false); var buffer = ArrayPool<byte>.Shared.Rent(8192); try { - await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); _lockkey = null; - await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, CancellationToken.None).ConfigureAwait(false); - await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false); + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); } finally { @@ -288,7 +281,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static byte[] CreateGetMessage(int tuner, string name) { - var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name)); + var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length var message = new byte[messageLength]; @@ -311,12 +304,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey) { - var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name)); - var byteValue = Encoding.UTF8.GetBytes(string.Format("{0}\0", value)); + var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); + var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value)); int messageLength = byteName.Length + byteValue.Length + 12; if (lockkey.HasValue) + { messageLength += 6; + } var message = new byte[messageLength]; @@ -324,21 +319,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun bool flipEndian = BitConverter.IsLittleEndian; - message[offset] = GetSetValue; - offset++; - message[offset] = Convert.ToByte(byteValue.Length); - offset++; + message[offset++] = GetSetValue; + message[offset++] = Convert.ToByte(byteValue.Length); Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length); offset += byteValue.Length; if (lockkey.HasValue) { - message[offset] = GetSetLockkey; - offset++; - message[offset] = (byte)4; - offset++; + message[offset++] = GetSetLockkey; + message[offset++] = 4; var lockKeyBytes = BitConverter.GetBytes(lockkey.Value); if (flipEndian) + { Array.Reverse(lockKeyBytes); + } + Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4); offset += 4; } @@ -346,7 +340,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // calculate crc and insert at the end of the message var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); if (flipEndian) + { Array.Reverse(crcBytes); + } + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); return message; @@ -375,10 +372,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun offset += 2; // insert tag name and length - message[offset] = GetSetName; - offset++; - message[offset] = Convert.ToByte(byteName.Length); - offset++; + message[offset++] = GetSetName; + message[offset++] = Convert.ToByte(byteName.Length); // insert name string Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length); @@ -392,7 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun returnVal = string.Empty; if (numBytes < 4) + { return false; + } var flipEndian = BitConverter.IsLittleEndian; int offset = 0; @@ -400,45 +397,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length); if (flipEndian) + { Array.Reverse(msgTypeBytes); + } var msgType = BitConverter.ToUInt16(msgTypeBytes, 0); offset += 2; if (msgType != GetSetReply) + { return false; + } byte[] msgLengthBytes = new byte[2]; Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length); if (flipEndian) + { Array.Reverse(msgLengthBytes); + } var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0); offset += 2; if (numBytes < msgLength + 8) + { return false; + } - var nameTag = buf[offset]; - offset++; + var nameTag = buf[offset++]; - var nameLength = buf[offset]; - offset++; + var nameLength = buf[offset++]; // skip the name field to get to value for return offset += nameLength; - var valueTag = buf[offset]; - offset++; + var valueTag = buf[offset++]; - var valueLength = buf[offset]; - offset++; + var valueLength = buf[offset++]; returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator return true; } - private class HdHomerunCrc + private static class HdHomerunCrc { private static uint[] crc_table = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, @@ -510,15 +511,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var hash = 0xffffffff; for (var i = 0; i < numBytes; i++) + { hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff]; + } var tmp = ~hash & 0xffffffff; var b0 = tmp & 0xff; var b1 = (tmp >> 8) & 0xff; var b2 = (tmp >> 16) & 0xff; var b3 = (tmp >> 24) & 0xff; - hash = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; - return hash; + return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 1d79a5f96..fbbab07f8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Net; @@ -18,6 +19,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider { + private const int RtpHeaderBytes = 12; + private readonly IServerApplicationHost _appHost; private readonly MediaBrowser.Model.Net.ISocketFactory _socketFactory; @@ -32,13 +35,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHdHomerunChannelCommands channelCommands, int numTuners, IFileSystem fileSystem, - IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, MediaBrowser.Model.Net.ISocketFactory socketFactory, - INetworkManager networkManager) - : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) + INetworkManager networkManager, + IStreamHelper streamHelper) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper) { _appHost = appHost; _socketFactory = socketFactory; @@ -80,12 +83,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var udpClient = _socketFactory.CreateUdpSocket(localPort); - var hdHomerunManager = new HdHomerunManager(Logger); + var hdHomerunManager = new HdHomerunManager(); try { // send url to start streaming - await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false); + await hdHomerunManager.StartStreaming( + remoteAddress, + localAddress, + localPort, + _channelCommands, + _numTuners, + openCancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -103,7 +112,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var taskCompletionSource = new TaskCompletionSource<bool>(); - await StartStreaming(udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, LiveStreamCancellationTokenSource.Token); + await StartStreaming( + udpClient, + hdHomerunManager, + remoteAddress, + taskCompletionSource, + LiveStreamCancellationTokenSource.Token).ConfigureAwait(false); //OpenedMediaSource.Protocol = MediaProtocol.File; //OpenedMediaSource.Path = tempFile; @@ -148,50 +162,43 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } - private static void Resolve(TaskCompletionSource<bool> openTaskCompletionSource) - { - Task.Run(() => - { - openTaskCompletionSource.TrySetResult(true); - }); - } - - private const int RtpHeaderBytes = 12; - private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { - var bufferSize = 81920; - - byte[] buffer = new byte[bufferSize]; - int read; - var resolved = false; - - using (var source = _socketFactory.CreateNetworkStream(udpClient, false)) - using (var fileStream = FileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) + byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamDefaults.DefaultCopyToBufferSize); + try { - var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token; - - while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0) + using (var source = _socketFactory.CreateNetworkStream(udpClient, false)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) { - cancellationToken.ThrowIfCancellationRequested(); + var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token; + int read; + var resolved = false; + while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0) + { + cancellationToken.ThrowIfCancellationRequested(); - currentCancellationToken = cancellationToken; + currentCancellationToken = cancellationToken; - read -= RtpHeaderBytes; + read -= RtpHeaderBytes; - if (read > 0) - { - fileStream.Write(buffer, RtpHeaderBytes, read); - } + if (read > 0) + { + await fileStream.WriteAsync(buffer, RtpHeaderBytes, read).ConfigureAwait(false); + } - if (!resolved) - { - resolved = true; - DateOpened = DateTime.UtcNow; - Resolve(openTaskCompletionSource); + if (!resolved) + { + resolved = true; + DateOpened = DateTime.UtcNow; + openTaskCompletionSource.TrySetResult(true); + } } } } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index b4395e2e1..d12c96392 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -16,27 +16,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class LiveStream : ILiveStream { - public MediaSourceInfo OriginalMediaSource { get; set; } - public MediaSourceInfo MediaSource { get; set; } - - public int ConsumerCount { get; set; } - - public string OriginalStreamId { get; set; } - public bool EnableStreamSharing { get; set; } - public string UniqueId { get; } - protected readonly IFileSystem FileSystem; protected readonly IServerApplicationPaths AppPaths; + protected readonly IStreamHelper StreamHelper; protected string TempFilePath; protected readonly ILogger Logger; protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource(); - public string TunerHostId { get; } - - public DateTime DateOpened { get; protected set; } - - public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths) + public LiveStream( + MediaSourceInfo mediaSource, + TunerHostInfo tuner, + IFileSystem fileSystem, + ILogger logger, + IServerApplicationPaths appPaths, + IStreamHelper streamHelper) { OriginalMediaSource = mediaSource; FileSystem = fileSystem; @@ -51,11 +45,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } AppPaths = appPaths; + StreamHelper = streamHelper; ConsumerCount = 1; SetTempFilePath("ts"); } + protected virtual int EmptyReadLimit => 1000; + + public MediaSourceInfo OriginalMediaSource { get; set; } + public MediaSourceInfo MediaSource { get; set; } + + public int ConsumerCount { get; set; } + + public string OriginalStreamId { get; set; } + public bool EnableStreamSharing { get; set; } + public string UniqueId { get; } + + public string TunerHostId { get; } + + public DateTime DateOpened { get; protected set; } + protected void SetTempFilePath(string extension) { TempFilePath = Path.Combine(AppPaths.GetTranscodingTempPath(), UniqueId + "." + extension); @@ -71,24 +81,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { EnableStreamSharing = false; - Logger.LogInformation("Closing " + GetType().Name); + Logger.LogInformation("Closing {Type}", GetType().Name); LiveStreamCancellationTokenSource.Cancel(); return Task.CompletedTask; } - protected Stream GetInputStream(string path, bool allowAsyncFileRead) - { - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsyncFileRead) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions); - } + protected FileStream GetInputStream(string path, bool allowAsyncFileRead) + => new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + StreamDefaults.DefaultFileStreamBufferSize, + allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan); public Task DeleteTempFiles() { @@ -144,8 +151,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; var nextFileInfo = GetNextFile(null); - var nextFile = nextFileInfo.Item1; - var isLastFile = nextFileInfo.Item2; + var nextFile = nextFileInfo.file; + var isLastFile = nextFileInfo.isLastFile; while (!string.IsNullOrEmpty(nextFile)) { @@ -155,8 +162,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts seekFile = false; nextFileInfo = GetNextFile(nextFile); - nextFile = nextFileInfo.Item1; - isLastFile = nextFileInfo.Item2; + nextFile = nextFileInfo.file; + isLastFile = nextFileInfo.isLastFile; } Logger.LogInformation("Live Stream ended."); @@ -180,19 +187,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken) { - using (var inputStream = (FileStream)GetInputStream(path, allowAsync)) + using (var inputStream = GetInputStream(path, allowAsync)) { if (seekFile) { TrySeek(inputStream, -20000); } - await ApplicationHost.StreamHelper.CopyToAsync(inputStream, stream, 81920, emptyReadLimit, cancellationToken).ConfigureAwait(false); + await StreamHelper.CopyToAsync( + inputStream, + stream, + StreamDefaults.DefaultCopyToBufferSize, + emptyReadLimit, + cancellationToken).ConfigureAwait(false); } } - protected virtual int EmptyReadLimit => 1000; - private void TrySeek(FileStream stream, long offset) { if (!stream.CanSeek) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 6c5c80827..a02a9ade4 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -28,14 +28,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; private readonly IMediaSourceManager _mediaSourceManager; - - public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, INetworkManager networkManager) + private readonly IStreamHelper _streamHelper; + + public M3UTunerHost( + IServerConfigurationManager config, + IMediaSourceManager mediaSourceManager, + ILogger logger, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem, + IHttpClient httpClient, + IServerApplicationHost appHost, + INetworkManager networkManager, + IStreamHelper streamHelper) : base(config, logger, jsonSerializer, fileSystem) { _httpClient = httpClient; _appHost = appHost; _networkManager = networkManager; _mediaSourceManager = mediaSourceManager; + _streamHelper = streamHelper; } public override string Type => "m3u"; @@ -103,11 +114,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper); } } - return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths); + return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths, _streamHelper); } public async Task Validate(TunerHostInfo info) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 7de9931c7..c6e894560 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -19,8 +19,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IHttpClient _httpClient; private readonly IServerApplicationHost _appHost; - public SharedHttpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) - : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) + public SharedHttpStream( + MediaSourceInfo mediaSource, + TunerHostInfo tunerHostInfo, + string originalStreamId, + IFileSystem fileSystem, + IHttpClient httpClient, + ILogger logger, + IServerApplicationPaths appPaths, + IServerApplicationHost appHost, + IStreamHelper streamHelper) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper) { _httpClient = httpClient; _appHost = appHost; @@ -118,7 +127,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts using (var stream = response.Content) using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) { - await ApplicationHost.StreamHelper.CopyToAsync(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false); + await StreamHelper.CopyToAsync( + stream, + fileStream, + StreamDefaults.DefaultCopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -128,6 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { Logger.LogError(ex, "Error copying live stream."); } + EnableStreamSharing = false; await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); }); diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 8c49b6405..13cdc50ca 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -17,43 +17,49 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Localization { /// <summary> - /// Class LocalizationManager + /// Class LocalizationManager. /// </summary> public class LocalizationManager : ILocalizationManager { - /// <summary> - /// The _configuration manager - /// </summary> - private readonly IServerConfigurationManager _configurationManager; + private const string DefaultCulture = "en-US"; + private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; + private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; /// <summary> - /// The us culture + /// The _configuration manager. /// </summary> - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _configurationManager; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings = new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase); - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; - private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; + private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = + new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); + + private List<CultureDto> _cultures; /// <summary> /// Initializes a new instance of the <see cref="LocalizationManager" /> class. /// </summary> /// <param name="configurationManager">The configuration manager.</param> /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="loggerFactory">The logger factory</param> + /// <param name="logger">The logger.</param> public LocalizationManager( IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, - ILoggerFactory loggerFactory) + ILogger<LocalizationManager> logger) { _configurationManager = configurationManager; _jsonSerializer = jsonSerializer; - _logger = loggerFactory.CreateLogger(nameof(LocalizationManager)); + _logger = logger; } + /// <summary> + /// Loads all resources into memory. + /// </summary> + /// <returns><see cref="Task" />.</returns> public async Task LoadAll() { const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings."; @@ -82,9 +88,10 @@ namespace Emby.Server.Implementations.Localization string[] parts = line.Split(','); if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) + && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { - dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value }); + var name = parts[0]; + dict.Add(name, new ParentalRating(name, value)); } #if DEBUG else @@ -101,16 +108,11 @@ namespace Emby.Server.Implementations.Localization await LoadCultures().ConfigureAwait(false); } - public string NormalizeFormKD(string text) - => text.Normalize(NormalizationForm.FormKD); - - private CultureDto[] _cultures; - /// <summary> /// Gets the cultures. /// </summary> - /// <returns>IEnumerable{CultureDto}.</returns> - public CultureDto[] GetCultures() + /// <returns><see cref="IEnumerable{CultureDto}" />.</returns> + public IEnumerable<CultureDto> GetCultures() => _cultures; private async Task LoadCultures() @@ -168,9 +170,10 @@ namespace Emby.Server.Implementations.Localization } } - _cultures = list.ToArray(); + _cultures = list; } + /// <inheritdoc /> public CultureDto FindLanguageInfo(string language) => GetCultures() .FirstOrDefault(i => @@ -179,25 +182,19 @@ namespace Emby.Server.Implementations.Localization || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase) || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase)); - /// <summary> - /// Gets the countries. - /// </summary> - /// <returns>IEnumerable{CountryInfo}.</returns> - public Task<CountryInfo[]> GetCountries() - => _jsonSerializer.DeserializeFromStreamAsync<CountryInfo[]>( + /// <inheritdoc /> + public IEnumerable<CountryInfo> GetCountries() + => _jsonSerializer.DeserializeFromStream<IEnumerable<CountryInfo>>( _assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json")); - /// <summary> - /// Gets the parental ratings. - /// </summary> - /// <returns>IEnumerable{ParentalRating}.</returns> + /// <inheritdoc /> public IEnumerable<ParentalRating> GetParentalRatings() => GetParentalRatingsDictionary().Values; /// <summary> /// Gets the parental ratings dictionary. /// </summary> - /// <returns>Dictionary{System.StringParentalRating}.</returns> + /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns> private Dictionary<string, ParentalRating> GetParentalRatingsDictionary() { var countryCode = _configurationManager.Configuration.MetadataCountryCode; @@ -207,14 +204,14 @@ namespace Emby.Server.Implementations.Localization countryCode = "us"; } - return GetRatings(countryCode) ?? GetRatings("us"); + return GetRatings(countryCode) ?? GetRatings("us"); } /// <summary> /// Gets the ratings. /// </summary> /// <param name="countryCode">The country code.</param> - /// <returns>The ratings</returns> + /// <returns>The ratings.</returns> private Dictionary<string, ParentalRating> GetRatings(string countryCode) { _allParentalRatings.TryGetValue(countryCode, out var value); @@ -222,14 +219,7 @@ namespace Emby.Server.Implementations.Localization return value; } - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; - /// <inheritdoc /> - /// <summary> - /// Gets the rating level. - /// </summary> - /// <param name="rating">Rating field</param> - /// <returns>The rating level</returns>> public int? GetRatingLevel(string rating) { if (string.IsNullOrEmpty(rating)) @@ -277,6 +267,7 @@ namespace Emby.Server.Implementations.Localization return null; } + /// <inheritdoc /> public bool HasUnicodeCategory(string value, UnicodeCategory category) { foreach (var chr in value) @@ -290,11 +281,13 @@ namespace Emby.Server.Implementations.Localization return false; } + /// <inheritdoc /> public string GetLocalizedString(string phrase) { return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture); } + /// <inheritdoc /> public string GetLocalizedString(string phrase, string culture) { if (string.IsNullOrEmpty(culture)) @@ -317,12 +310,7 @@ namespace Emby.Server.Implementations.Localization return phrase; } - private const string DefaultCulture = "en-US"; - - private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = - new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); - - public Dictionary<string, string> GetLocalizationDictionary(string culture) + private Dictionary<string, string> GetLocalizationDictionary(string culture) { if (string.IsNullOrEmpty(culture)) { @@ -332,8 +320,9 @@ namespace Emby.Server.Implementations.Localization const string prefix = "Core"; var key = prefix + culture; - return _dictionaries.GetOrAdd(key, - f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); + return _dictionaries.GetOrAdd( + key, + f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); } private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) @@ -390,45 +379,45 @@ namespace Emby.Server.Implementations.Localization return culture + ".json"; } - public LocalizationOption[] GetLocalizationOptions() - => new LocalizationOption[] - { - new LocalizationOption("Arabic", "ar"), - new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"), - new LocalizationOption("Catalan", "ca"), - new LocalizationOption("Chinese Simplified", "zh-CN"), - new LocalizationOption("Chinese Traditional", "zh-TW"), - new LocalizationOption("Croatian", "hr"), - new LocalizationOption("Czech", "cs"), - new LocalizationOption("Danish", "da"), - new LocalizationOption("Dutch", "nl"), - new LocalizationOption("English (United Kingdom)", "en-GB"), - new LocalizationOption("English (United States)", "en-US"), - new LocalizationOption("French", "fr"), - new LocalizationOption("French (Canada)", "fr-CA"), - new LocalizationOption("German", "de"), - new LocalizationOption("Greek", "el"), - new LocalizationOption("Hebrew", "he"), - new LocalizationOption("Hungarian", "hu"), - new LocalizationOption("Italian", "it"), - new LocalizationOption("Kazakh", "kk"), - new LocalizationOption("Korean", "ko"), - new LocalizationOption("Lithuanian", "lt-LT"), - new LocalizationOption("Malay", "ms"), - new LocalizationOption("Norwegian Bokmål", "nb"), - new LocalizationOption("Persian", "fa"), - new LocalizationOption("Polish", "pl"), - new LocalizationOption("Portuguese (Brazil)", "pt-BR"), - new LocalizationOption("Portuguese (Portugal)", "pt-PT"), - new LocalizationOption("Russian", "ru"), - new LocalizationOption("Slovak", "sk"), - new LocalizationOption("Slovenian (Slovenia)", "sl-SI"), - new LocalizationOption("Spanish", "es"), - new LocalizationOption("Spanish (Argentina)", "es-AR"), - new LocalizationOption("Spanish (Mexico)", "es-MX"), - new LocalizationOption("Swedish", "sv"), - new LocalizationOption("Swiss German", "gsw"), - new LocalizationOption("Turkish", "tr") - }; + /// <inheritdoc /> + public IEnumerable<LocalizationOption> GetLocalizationOptions() + { + yield return new LocalizationOption("Arabic", "ar"); + yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"); + yield return new LocalizationOption("Catalan", "ca"); + yield return new LocalizationOption("Chinese Simplified", "zh-CN"); + yield return new LocalizationOption("Chinese Traditional", "zh-TW"); + yield return new LocalizationOption("Croatian", "hr"); + yield return new LocalizationOption("Czech", "cs"); + yield return new LocalizationOption("Danish", "da"); + yield return new LocalizationOption("Dutch", "nl"); + yield return new LocalizationOption("English (United Kingdom)", "en-GB"); + yield return new LocalizationOption("English (United States)", "en-US"); + yield return new LocalizationOption("French", "fr"); + yield return new LocalizationOption("French (Canada)", "fr-CA"); + yield return new LocalizationOption("German", "de"); + yield return new LocalizationOption("Greek", "el"); + yield return new LocalizationOption("Hebrew", "he"); + yield return new LocalizationOption("Hungarian", "hu"); + yield return new LocalizationOption("Italian", "it"); + yield return new LocalizationOption("Kazakh", "kk"); + yield return new LocalizationOption("Korean", "ko"); + yield return new LocalizationOption("Lithuanian", "lt-LT"); + yield return new LocalizationOption("Malay", "ms"); + yield return new LocalizationOption("Norwegian Bokmål", "nb"); + yield return new LocalizationOption("Persian", "fa"); + yield return new LocalizationOption("Polish", "pl"); + yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR"); + yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT"); + yield return new LocalizationOption("Russian", "ru"); + yield return new LocalizationOption("Slovak", "sk"); + yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI"); + yield return new LocalizationOption("Spanish", "es"); + yield return new LocalizationOption("Spanish (Argentina)", "es-AR"); + yield return new LocalizationOption("Spanish (Mexico)", "es-MX"); + yield return new LocalizationOption("Swedish", "sv"); + yield return new LocalizationOption("Swiss German", "gsw"); + yield return new LocalizationOption("Turkish", "tr"); + } } } diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 0b5ee5d03..1ef5c4b99 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -23,22 +23,22 @@ namespace Emby.Server.Implementations.Security public void Initialize() { - using (var connection = GetConnection()) + string[] queries = { - var tableNewlyCreated = !TableExists(connection, "Tokens"); + "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)", + "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)", + "drop index if exists idx_AccessTokens", + "drop index if exists Tokens1", + "drop index if exists Tokens2", - string[] queries = { - - "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)", - "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)", + "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)", + "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)", + "create index if not exists Devices1 on Devices (Id)" + }; - "drop index if exists idx_AccessTokens", - "drop index if exists Tokens1", - "drop index if exists Tokens2", - "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)", - "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)", - "create index if not exists Devices1 on Devices (Id)" - }; + using (var connection = GetConnection()) + { + var tableNewlyCreated = !TableExists(connection, "Tokens"); connection.RunQueries(queries); @@ -244,45 +244,46 @@ namespace Emby.Server.Implementations.Security } } - var list = new List<AuthenticationInfo>(); + var statementTexts = new[] + { + commandText, + "select count (Id) from Tokens" + whereTextWithoutPaging + }; + var list = new List<AuthenticationInfo>(); + var result = new QueryResult<AuthenticationInfo>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => - { - var result = new QueryResult<AuthenticationInfo>(); - - var statementTexts = new List<string>(); - statementTexts.Add(commandText); - statementTexts.Add("select count (Id) from Tokens" + whereTextWithoutPaging); - - var statements = PrepareAll(db, statementTexts) - .ToList(); - - using (var statement = statements[0]) + connection.RunInTransaction( + db => { - BindAuthenticationQueryParams(query, statement); + var statements = PrepareAll(db, statementTexts) + .ToList(); - foreach (var row in statement.ExecuteQuery()) + using (var statement = statements[0]) { - list.Add(Get(row)); - } + BindAuthenticationQueryParams(query, statement); - using (var totalCountStatement = statements[1]) - { - BindAuthenticationQueryParams(query, totalCountStatement); - - result.TotalRecordCount = totalCountStatement.ExecuteQuery() - .SelectScalarInt() - .First(); - } - } + foreach (var row in statement.ExecuteQuery()) + { + list.Add(Get(row)); + } - result.Items = list.ToArray(); - return result; + using (var totalCountStatement = statements[1]) + { + BindAuthenticationQueryParams(query, totalCountStatement); - }, ReadTransactionMode); + result.TotalRecordCount = totalCountStatement.ExecuteQuery() + .SelectScalarInt() + .First(); + } + } + }, + ReadTransactionMode); } + + result.Items = list.ToArray(); + return result; } private static AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 0347100a4..61329160a 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1375,16 +1375,14 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); User user = null; - if (!request.UserId.Equals(Guid.Empty)) + if (request.UserId != Guid.Empty) { - user = _userManager.Users - .FirstOrDefault(i => i.Id == request.UserId); + user = _userManager.GetUserById(request.UserId); } if (user == null) { - user = _userManager.Users - .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase)); + user = _userManager.GetUserByName(request.Username); } if (user != null) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index dd313b336..e93bff124 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp /// <summary> /// Releases the unmanaged resources and disposes of the managed resources used. /// </summary> - /// <param name="disposing">Whether or not the managed resources should be disposed</param> + /// <param name="disposing">Whether or not the managed resources should be disposed.</param> protected virtual void Dispose(bool disposing) { if (_disposed) |
